diff --git a/lib/src/X509Utils.dart b/lib/src/X509Utils.dart index fdb6854..398ccac 100644 --- a/lib/src/X509Utils.dart +++ b/lib/src/X509Utils.dart @@ -34,10 +34,11 @@ import 'package:basic_utils/src/model/x509/X509CertificateData.dart'; import 'package:basic_utils/src/model/x509/X509CertificateDataExtensions.dart'; import 'package:basic_utils/src/model/x509/X509CertificatePublicKeyData.dart'; import 'package:basic_utils/src/model/x509/X509CertificateValidity.dart'; -import 'package:pointycastle/asn1/unsupported_object_identifier_exception.dart'; import 'package:pointycastle/export.dart'; import 'package:pointycastle/pointycastle.dart'; +import 'package:pointycastle/asn1/object_identifiers.dart'; +import 'package:pointycastle/asn1/unsupported_object_identifier_exception.dart'; /// /// Helper class for certificate operations. @@ -554,9 +555,21 @@ class X509Utils { /// * SHA-384 /// * SHA-512 /// - static String generateEccCsrPem(Map attributes, - ECPrivateKey privateKey, ECPublicKey publicKey, - {List? san, String signingAlgorithm = 'SHA-256'}) { + /// [extensions] defines custom extensions to be placed within the CSR. + /// Each extension should have a valid OID. Valid types for an extension value are: + /// * int + /// * String + /// * List + /// * bool + /// + static String generateEccCsrPem( + Map attributes, + ECPrivateKey privateKey, + ECPublicKey publicKey, { + List? san, + String signingAlgorithm = 'SHA-256', + Map? extensions, + }) { if (!_validRsaSigner.contains(signingAlgorithm)) { ArgumentError('Signingalgorithm $signingAlgorithm not supported!'); } @@ -567,32 +580,70 @@ class X509Utils { blockDN.add(ASN1Integer(BigInt.from(0))); blockDN.add(encodedDN); blockDN.add(publicKeySequence); - // Check if extensions are needed - if (san != null && san.isNotEmpty) { - var outerBlockExt = ASN1Sequence(); - outerBlockExt.add(ASN1ObjectIdentifier.fromName('extensionRequest')); - var setExt = ASN1Set(); - - var innerBlockExt = ASN1Sequence(); + // Compatibility with old code (subjectAltName) + if (san != null && san.isNotEmpty) { + extensions ??= {}; + extensions['2.5.29.17'] = san; + } - var sanExtSeq = ASN1Sequence(); - sanExtSeq.add(ASN1ObjectIdentifier.fromName('subjectAltName')); - var sanSeq = ASN1Sequence(); - for (var s in san) { - var sanIa5 = ASN1IA5String(stringValue: s, tag: 0x82); - sanSeq.add(sanIa5); + if (extensions != null && extensions.isNotEmpty) { + // Extensions -> Sequence(Extensions' OID, Set) + // Set -> Sequence(Extension[]) + // Extension -> Sequence(custom OID, Critical?, Value) + // Value -> OctetString(ASN1Object) + var extensionsSeq = ASN1Sequence(); + extensionsSeq.add(ASN1ObjectIdentifier.fromName('extensionRequest')); + + var extensionsSet = ASN1Set(); + var setSeq = ASN1Sequence(); + + for (var ext in extensions.entries) { + var extension = ASN1Sequence(); + ASN1ObjectIdentifier obj = ASN1ObjectIdentifier.fromIdentifierString( + ext.key, + ); + + extension.add(obj); + var critical = ASN1Boolean(false); + extension.add(critical); + + // Creates the (subclass of) ASN1Object based on the type of the value + if (ext.value is int) { + var integerValue = ASN1Integer(BigInt.from(ext.value)); + var octet = ASN1OctetString(octets: integerValue.encode()); + extension.add(octet); + setSeq.add(extension); + } else if (ext.value is String) { + var stringValue = ASN1OctetString( + octets: Uint8List.fromList(ext.value.codeUnits), + ); + var octet = ASN1OctetString(octets: stringValue.encode()); + extension.add(octet); + setSeq.add(extension); + } else if (ext.value is List) { + var listValue = ASN1Sequence(); + for (var s in ext.value) { + var stringValue = ASN1IA5String(stringValue: s, tag: 0x82); + listValue.add(stringValue); + } + var octet = ASN1OctetString(octets: listValue.encode()); + extension.add(octet); + setSeq.add(extension); + } else if (ext.value is bool) { + var booleanValue = ASN1Boolean(ext.value); + var octet = ASN1OctetString(octets: booleanValue.encode()); + extension.add(octet); + setSeq.add(extension); + } else { + throw ArgumentError('Unsupported type for extension value'); + } } - var octet = ASN1OctetString(octets: sanSeq.encode()); - sanExtSeq.add(octet); - innerBlockExt.add(sanExtSeq); - - setExt.add(innerBlockExt); + extensionsSet.add(setSeq); + extensionsSeq.add(extensionsSet); - outerBlockExt.add(setExt); - - var asn1Null = ASN1OctetString(tag: 0xA0, octets: outerBlockExt.encode()); + var asn1Null = ASN1OctetString(tag: 0xA0, octets: extensionsSeq.encode()); //asn1Null.valueBytes = outerBlockExt.encode(); blockDN.add(asn1Null); } else { @@ -988,6 +1039,9 @@ class X509Utils { /// /// Parses the given CSR [pem] to [CertificateSigningRequestData] object /// + /// Note: When parsing the CSR extensions, if the extension value is invalid + /// the value won't be parsed, staying raw. + /// static CertificateSigningRequestData csrFromPem(String pem) { var bytes = CryptoUtils.getBytesFromPEMString(pem); var asn1Parser = ASN1Parser(bytes); @@ -1841,6 +1895,7 @@ class X509Utils { tbsCertificateSeq.elements!.elementAt(element + 6) as ASN1Sequence; var subjectPublicKeyInfo = _getSubjectPublicKeyInfoFromSeq(pubKeySequence); + // Extensions var extensions; if (version > 1 && tbsCertificateSeq.elements!.length > element + 7) { var extensionObject = tbsCertificateSeq.elements!.elementAt(element + 7); @@ -1973,70 +2028,135 @@ class X509Utils { ); } + static _fetchSubjectKeyIdentifier(ASN1Object extData) { + var octet = extData as ASN1OctetString; + var parser = ASN1Parser(octet.valueBytes); + var keyIdentifier = parser.nextObject() as ASN1OctetString; + var hexKeyIdentifier = keyIdentifier.valueBytes! + .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + .join(''); + + return hexKeyIdentifier; + } + + // TODO: RFC defines a different structure for the authority key identifier + static _fetchAuthorityKeyIdentifier(ASN1Object extData) { + try { + var octet = extData as ASN1OctetString; + var parser = ASN1Parser(octet.valueBytes); + var extSeq = parser.nextObject() as ASN1Sequence; + var keyIdentifier = extSeq.elements!.elementAt(0); + var hexKeyIdentifier = keyIdentifier.valueBytes! + .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + .join(''); + + return hexKeyIdentifier; + } catch (e) { + return null; + } + } + + static _fetchAuthorityInfoAccess(ASN1Object extData) { + var authorityInfoAccess = []; + var octet = extData as ASN1OctetString; + var parser = ASN1Parser(octet.valueBytes); + var extSeq = parser.nextObject() as ASN1Sequence; + extSeq.elements!.forEach((ASN1Object subseq) { + var seq = subseq as ASN1Sequence; + var accessMethod = seq.elements!.elementAt(0) as ASN1ObjectIdentifier; + var accessLocationParser = ASN1Parser( + seq.elements!.elementAt(1).encodedBytes, + ); + var accessLocation = + utf8.decode(accessLocationParser.nextObject().valueBytes!); + + authorityInfoAccess.add( + '${accessMethod.objectIdentifierAsString}::${accessLocation}', + ); + }); + return authorityInfoAccess; + } + static X509CertificateDataExtensions _getExtensionsFromSeq( - ASN1Sequence extSequence) { + ASN1Sequence extSequence, + ) { List? sans; List? keyUsage; List? extKeyUsage; List basicConstraints; var extensions = X509CertificateDataExtensions(); - extSequence.elements!.forEach( - (ASN1Object subseq) { - var seq = subseq as ASN1Sequence; - var oi = seq.elements!.elementAt(0) as ASN1ObjectIdentifier; - if (oi.objectIdentifierAsString == '2.5.29.17') { - if (seq.elements!.length == 3) { - sans = _fetchSansFromExtension(seq.elements!.elementAt(2)); - } else { - sans = _fetchSansFromExtension(seq.elements!.elementAt(1)); - } - extensions.subjectAlternativNames = sans; - } - - var keyUsageSequence = ASN1Sequence(); - keyUsageSequence - .add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.15')); - - if (oi.objectIdentifierAsString == '2.5.29.15') { - if (seq.elements!.length == 3) { - keyUsage = _fetchKeyUsageFromExtension(seq.elements!.elementAt(2)); - } else { - keyUsage = _fetchKeyUsageFromExtension(seq.elements!.elementAt(1)); - } - extensions.keyUsage = keyUsage; - } - if (oi.objectIdentifierAsString == '2.5.29.37') { - if (seq.elements!.length == 3) { - extKeyUsage = - _fetchExtendedKeyUsageFromExtension(seq.elements!.elementAt(2)); - } else { - extKeyUsage = - _fetchExtendedKeyUsageFromExtension(seq.elements!.elementAt(1)); - } - extensions.extKeyUsage = extKeyUsage; + extSequence.elements!.forEach((ASN1Object subseq) { + var seq = subseq as ASN1Sequence; + var oi = seq.elements!.elementAt(0) as ASN1ObjectIdentifier; + if (oi.objectIdentifierAsString == '2.5.29.17') { + if (seq.elements!.length == 3) { + sans = _fetchSansFromExtension(seq.elements!.elementAt(2)); + } else { + sans = _fetchSansFromExtension(seq.elements!.elementAt(1)); } - if (oi.objectIdentifierAsString == '2.5.29.19') { - if (seq.elements!.length == 3) { - basicConstraints = - _fetchBasicConstraintsFromExtension(seq.elements!.elementAt(2)); - } else { - basicConstraints = [null, null]; - } - - extensions.cA = basicConstraints[0]; - extensions.pathLenConstraint = basicConstraints[1]; + extensions.subjectAlternativNames = sans; + } else if (oi.objectIdentifierAsString == '2.5.29.15') { + if (seq.elements!.length == 3) { + keyUsage = _fetchKeyUsageFromExtension(seq.elements!.elementAt(2)); + } else { + keyUsage = _fetchKeyUsageFromExtension(seq.elements!.elementAt(1)); } - if (oi.objectIdentifierAsString == '1.3.6.1.5.5.7.1.12') { - var vmcData = _fetchVmcLogo(seq.elements!.elementAt(1)); - extensions.vmc = vmcData; + extensions.keyUsage = keyUsage; + } else if (oi.objectIdentifierAsString == '2.5.29.37') { + if (seq.elements!.length == 3) { + extKeyUsage = _fetchExtendedKeyUsageFromExtension( + seq.elements!.elementAt(2), + ); + } else { + extKeyUsage = _fetchExtendedKeyUsageFromExtension( + seq.elements!.elementAt(1), + ); } - if (oi.objectIdentifierAsString == '2.5.29.31') { - var cRLDistributionPoints = - _fetchCrlDistributionPoints(seq.elements!.elementAt(1)); - extensions.cRLDistributionPoints = cRLDistributionPoints; + extensions.extKeyUsage = extKeyUsage; + } else if (oi.objectIdentifierAsString == '2.5.29.19') { + if (seq.elements!.length == 3) { + // Basic Constraints' critical is always present ? + basicConstraints = _fetchBasicConstraintsFromExtension( + seq.elements!.elementAt(2), + ); + } else { + basicConstraints = [null, null]; } - }, - ); + + extensions.cA = basicConstraints[0]; + extensions.pathLenConstraint = basicConstraints[1]; + } else if (oi.objectIdentifierAsString == '1.3.6.1.5.5.7.1.12') { + var vmcData = _fetchVmcLogo(seq.elements!.elementAt(1)); + extensions.vmc = vmcData; + } else if (oi.objectIdentifierAsString == '2.5.29.31') { + var cRLDistributionPoints = _fetchCrlDistributionPoints( + seq.elements!.elementAt(1), + ); + extensions.cRLDistributionPoints = cRLDistributionPoints; + } else if (oi.objectIdentifierAsString == '2.5.29.35') { + var authorityKeyIdentifier = + _fetchAuthorityKeyIdentifier(seq.elements!.elementAt(1)); + extensions.authorityKeyIdentifier = authorityKeyIdentifier; + } else if (oi.objectIdentifierAsString == "2.5.29.14") { + var subjectKeyIdentifier = + _fetchSubjectKeyIdentifier(seq.elements!.elementAt(1)); + extensions.subjectKeyIdentifier = subjectKeyIdentifier; + } else if (oi.objectIdentifierAsString == "1.3.6.1.5.5.7.1.1") { + var authorityInfoAccess = + _fetchAuthorityInfoAccess(seq.elements!.elementAt(1)); + extensions.authorityInfoAccess = authorityInfoAccess; + } else if (ObjectIdentifiers.getIdentifierByIdentifier( + oi.objectIdentifierAsString!) == + null) { + var extValue = _fetchCustomExtensionFromSeq(seq); + + // Add the extension value to the custom extensions map + extensions.customExtensions ??= {}; + extensions.customExtensions![oi.objectIdentifierAsString!] = extValue; + } else { + print("Parser for ${oi.objectIdentifierAsString} not implemented."); + } + }); return extensions; } @@ -2054,12 +2174,12 @@ class X509Utils { var pubInfo = _getSubjectPublicKeyInfoFromSeq(pubSeq); // Extensions - var extensions; + var extensions = CertificateSigningRequestExtensions(); + if (infoSeq.elements!.length == 4) { var extensionObject = infoSeq.elements!.elementAt(3); if (extensionObject.valueByteLength != 0) { List? sans; - extensions = CertificateSigningRequestExtensions(); var extParser = ASN1Parser(extensionObject.valueBytes); while (extParser.hasNext()) { var outerExtSequence = extParser.nextObject() as ASN1Sequence; @@ -2072,7 +2192,9 @@ class X509Utils { extSequence.elements!.forEach((ASN1Object subseq) { var seq = subseq as ASN1Sequence; var oi = seq.elements!.elementAt(0) as ASN1ObjectIdentifier; + // Verifies if SAN is present if (oi.objectIdentifierAsString == '2.5.29.17') { + // Verifies if CRITICAL is present if (seq.elements!.length == 3) { sans = _fetchSansFromExtension(seq.elements!.elementAt(2)); } else { @@ -2080,11 +2202,28 @@ class X509Utils { } extensions.subjectAlternativNames = sans; } + // Parse custom extensions + // Each extension value is encoded as an ASN1OctetString + else if (ObjectIdentifiers.getIdentifierByIdentifier( + oi.objectIdentifierAsString!) == + null) { + extensions.customExtensions ??= {}; + + var extValue = _fetchCustomExtensionFromSeq(seq); + + // Add the extension value to the custom extensions map + extensions.customExtensions![oi.objectIdentifierAsString!] = + extValue; + } else { + print( + "Parser for ${oi.objectIdentifierAsString} not implemented."); + } }); } } } } + return bu.CertificationRequestInfo( version: asn1Version.integer!.toInt(), subject: subject, @@ -2093,6 +2232,44 @@ class X509Utils { ); } + static _fetchCustomExtensionFromSeq(ASN1Sequence seq) { + var extValue; + + // Verifies if CRITICAL is present + var octet = (seq.elements!.length == 3 + ? seq.elements!.elementAt(2) + : seq.elements!.elementAt(1)) as ASN1OctetString; + + var extParser = ASN1Parser(octet.valueBytes); + var extElement = extParser.nextObject(); + + // Extension value is a List + if (extElement is ASN1Sequence) { + extValue = []; + extElement.elements!.forEach((ASN1Object asn) { + var octectString = ASN1OctetString.fromBytes(asn.encodedBytes!); + extValue.add(utf8.decode(octectString.valueBytes!)); + }); + } else if (extElement is ASN1Integer) { + extValue = extElement.integer?.toInt(); + } else if (extElement is ASN1OctetString || extElement is ASN1BitString) { + try { + extValue = utf8.decode(extElement.valueBytes!); + } catch (e) { + extValue = extElement.valueBytes; + } + } else if (extElement is ASN1UTF8String) { + extValue = utf8.decode(extElement.valueBytes!); + } else if (extElement is ASN1Boolean) { + extValue = extElement.boolValue; + } else { + // Unsupported types -> return the raw value + extValue = extElement.valueBytes; + } + + return extValue; + } + /// /// Trys to fix the given [pem] string. /// * Removes whitespaces diff --git a/lib/src/model/csr/CertificateSigningRequestExtensions.dart b/lib/src/model/csr/CertificateSigningRequestExtensions.dart index 319c552..4037816 100644 --- a/lib/src/model/csr/CertificateSigningRequestExtensions.dart +++ b/lib/src/model/csr/CertificateSigningRequestExtensions.dart @@ -18,9 +18,11 @@ class CertificateSigningRequestExtensions { // certificatePolicies // authorityInfoAccess => OCSP und caIssuers - CertificateSigningRequestExtensions({ - this.subjectAlternativNames, - }); + /// Custom user defined extensions + Map? customExtensions; + + CertificateSigningRequestExtensions( + {this.subjectAlternativNames, this.customExtensions}); /* * Json to CertificateSigningRequestExtensions object diff --git a/lib/src/model/csr/CertificateSigningRequestExtensions.g.dart b/lib/src/model/csr/CertificateSigningRequestExtensions.g.dart index d252ecf..18040b0 100644 --- a/lib/src/model/csr/CertificateSigningRequestExtensions.g.dart +++ b/lib/src/model/csr/CertificateSigningRequestExtensions.g.dart @@ -13,6 +13,7 @@ CertificateSigningRequestExtensions (json['subjectAlternativNames'] as List?) ?.map((e) => e as String) .toList(), + customExtensions: json['customExtensions'] as Map?, ); Map _$CertificateSigningRequestExtensionsToJson( @@ -26,5 +27,6 @@ Map _$CertificateSigningRequestExtensionsToJson( } writeNotNull('subjectAlternativNames', instance.subjectAlternativNames); + writeNotNull('customExtensions', instance.customExtensions); return val; } diff --git a/lib/src/model/x509/X509CertificateDataExtensions.dart b/lib/src/model/x509/X509CertificateDataExtensions.dart index 8049fad..3a34c3d 100644 --- a/lib/src/model/x509/X509CertificateDataExtensions.dart +++ b/lib/src/model/x509/X509CertificateDataExtensions.dart @@ -31,6 +31,20 @@ class X509CertificateDataExtensions { /// The distribution points for the crl files. Normally a url. List? cRLDistributionPoints; + /// The Suject Key Identifier Base64 encoded + String? subjectKeyIdentifier; + + /// The Authority Key Identifier Base64 encoded + String? authorityKeyIdentifier; + + /// The Authority Info Access extension. Contains OCSP and CA Issuers, for example. + /// + /// After parsing the X509 the format is as follows: "accessMethod::accessLocation" + List? authorityInfoAccess; + + /// Custom user defined extensions + Map? customExtensions; + X509CertificateDataExtensions({ this.subjectAlternativNames, this.extKeyUsage, @@ -39,6 +53,10 @@ class X509CertificateDataExtensions { this.pathLenConstraint, this.vmc, this.cRLDistributionPoints, + this.subjectKeyIdentifier, + this.authorityKeyIdentifier, + this.authorityInfoAccess, + this.customExtensions, }); /* diff --git a/lib/src/model/x509/X509CertificateDataExtensions.g.dart b/lib/src/model/x509/X509CertificateDataExtensions.g.dart index 56584bf..2f55c88 100644 --- a/lib/src/model/x509/X509CertificateDataExtensions.g.dart +++ b/lib/src/model/x509/X509CertificateDataExtensions.g.dart @@ -26,6 +26,12 @@ X509CertificateDataExtensions _$X509CertificateDataExtensionsFromJson( cRLDistributionPoints: (json['cRLDistributionPoints'] as List?) ?.map((e) => e as String) .toList(), + subjectKeyIdentifier: json['subjectKeyIdentifier'] as String?, + authorityKeyIdentifier: json['authorityKeyIdentifier'] as String?, + authorityInfoAccess: (json['authorityInfoAccess'] as List?) + ?.map((e) => e as String) + .toList(), + customExtensions: json['customExtensions'] as Map?, ); Map _$X509CertificateDataExtensionsToJson( @@ -47,6 +53,10 @@ Map _$X509CertificateDataExtensionsToJson( writeNotNull('pathLenConstraint', instance.pathLenConstraint); writeNotNull('vmc', instance.vmc?.toJson()); writeNotNull('cRLDistributionPoints', instance.cRLDistributionPoints); + writeNotNull('subjectKeyIdentifier', instance.subjectKeyIdentifier); + writeNotNull('authorityKeyIdentifier', instance.authorityKeyIdentifier); + writeNotNull('authorityInfoAccess', instance.authorityInfoAccess); + writeNotNull('customExtensions', instance.customExtensions); return val; }