diff --git a/lib/src/X509Utils.dart b/lib/src/X509Utils.dart index db3b18d..788049a 100644 --- a/lib/src/X509Utils.dart +++ b/lib/src/X509Utils.dart @@ -290,6 +290,8 @@ class X509Utils { /// * [sans] = Subject alternative names to place within the certificate /// * [keyUsage] = The key usage definition extension /// * [extKeyUsage] = The extended key usage definition + /// * [cA] = The cA boolean of the basic constraints extension, which indicates whether the certificate is a CA + /// * [pathLenConstraint] = The pathLenConstraint field of the basic constraints extension. This is ignored if cA is null or false, or if pathLenConstraint is less than 0. /// * [serialNumber] = The serialnumber. If not set the default will be 1. /// * [issuer] = The issuer. If null, the issuer will be the subject of the given csr. /// * [notBefore] = The Timestamp after when the certificate is valid. If null, this will be [DateTime.now]. @@ -302,6 +304,8 @@ class X509Utils { List? sans, List? keyUsage, List? extKeyUsage, + bool? cA, + int? pathLenConstraint, String serialNumber = '1', Map? issuer, DateTime? notBefore, @@ -397,7 +401,8 @@ class X509Utils { // Add Extensions if (IterableUtils.isNotNullOrEmpty(sans) || IterableUtils.isNotNullOrEmpty(keyUsage) || - IterableUtils.isNotNullOrEmpty(extKeyUsage)) { + IterableUtils.isNotNullOrEmpty(extKeyUsage) || + cA != null) { var extensionTopSequence = ASN1Sequence(); if (IterableUtils.isNotNullOrEmpty(keyUsage)) { @@ -484,6 +489,30 @@ class X509Utils { extensionTopSequence.add(sanSequence); } + if (cA != null) { + var basicConstraintsSequence = ASN1Sequence(); + + basicConstraintsSequence + .add(ASN1ObjectIdentifier.fromIdentifierString("2.5.29.19")); + basicConstraintsSequence.add(ASN1Boolean(true)); + + var basicConstraintsList = ASN1Sequence(); + + if (cA) { + basicConstraintsList.add(ASN1Boolean(cA)); + } + + // check if CA to allow pathLenConstraint + if (pathLenConstraint != null && cA && pathLenConstraint >= 0) { + basicConstraintsList.add(ASN1Integer.fromtInt(pathLenConstraint)); + } + + basicConstraintsSequence + .add(ASN1OctetString(octets: basicConstraintsList.encode())); + + extensionTopSequence.add(basicConstraintsSequence); + } + var extObj = ASN1Object(tag: 0xA3); extObj.valueBytes = extensionTopSequence.encode(); @@ -1417,6 +1446,28 @@ class X509Utils { return extKeyUsage; } + /// + /// Parses the given ASN1Object to the two basic constraint + /// fields cA and pathLenConstraint. Returns a list of types [bool, int] if + /// cA is true and a valid pathLenConstraint is specified, else the + /// corresponding element will be null. + /// + static List _fetchBasicConstraintsFromExtension(ASN1Object extData) { + var basicConstraints = [null, null]; + var octet = extData as ASN1OctetString; + var constraintParser = ASN1Parser(octet.valueBytes); + var constraintSeq = constraintParser.nextObject() as ASN1Sequence; + constraintSeq.elements!.forEach((ASN1Object obj) { + if (obj is ASN1Boolean) { + basicConstraints[0] = obj.boolValue; + } + if (obj is ASN1Integer) { + basicConstraints[1] = obj.integer!.toInt(); + } + }); + return basicConstraints; + } + /// /// Parses the given object identifier values to the internal enum /// @@ -1926,6 +1977,7 @@ class X509Utils { List? sans; List? keyUsage; List? extKeyUsage; + List basicConstraints; var extensions = X509CertificateDataExtensions(); extSequence.elements!.forEach( (ASN1Object subseq) { @@ -1962,6 +2014,17 @@ class X509Utils { } extensions.extKeyUsage = extKeyUsage; } + 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]; + } if (oi.objectIdentifierAsString == '1.3.6.1.5.5.7.1.12') { var vmcData = _fetchVmcLogo(seq.elements!.elementAt(1)); extensions.vmc = vmcData; diff --git a/lib/src/model/x509/X509CertificateDataExtensions.dart b/lib/src/model/x509/X509CertificateDataExtensions.dart index 21b5db2..2b35000 100644 --- a/lib/src/model/x509/X509CertificateDataExtensions.dart +++ b/lib/src/model/x509/X509CertificateDataExtensions.dart @@ -19,6 +19,12 @@ class X509CertificateDataExtensions { /// The key usage extension List? keyUsage; + /// The cA field of the basic constraints extension + bool? cA; + + /// The pathLenConstraint field of the basic constraints extension + int? pathLenConstraint; + /// The base64 encoded VMC logo VmcData? vmc; @@ -29,6 +35,8 @@ class X509CertificateDataExtensions { this.subjectAlternativNames, this.extKeyUsage, this.keyUsage, + this.cA, + this.pathLenConstraint, this.vmc, this.cRLDistributionPoints, }); diff --git a/lib/src/model/x509/X509CertificateDataExtensions.g.dart b/lib/src/model/x509/X509CertificateDataExtensions.g.dart index 49c87d8..56584bf 100644 --- a/lib/src/model/x509/X509CertificateDataExtensions.g.dart +++ b/lib/src/model/x509/X509CertificateDataExtensions.g.dart @@ -18,6 +18,8 @@ X509CertificateDataExtensions _$X509CertificateDataExtensionsFromJson( keyUsage: (json['keyUsage'] as List?) ?.map((e) => $enumDecode(_$KeyUsageEnumMap, e)) .toList(), + cA: json['cA'] as bool?, + pathLenConstraint: json['pathLenConstraint'] as int?, vmc: json['vmc'] == null ? null : VmcData.fromJson(json['vmc'] as Map), @@ -41,6 +43,8 @@ Map _$X509CertificateDataExtensionsToJson( instance.extKeyUsage?.map((e) => _$ExtendedKeyUsageEnumMap[e]!).toList()); writeNotNull('keyUsage', instance.keyUsage?.map((e) => _$KeyUsageEnumMap[e]!).toList()); + writeNotNull('cA', instance.cA); + writeNotNull('pathLenConstraint', instance.pathLenConstraint); writeNotNull('vmc', instance.vmc?.toJson()); writeNotNull('cRLDistributionPoints', instance.cRLDistributionPoints); return val; diff --git a/test/x509_utils_test.dart b/test/x509_utils_test.dart index f267e35..2eb2e56 100644 --- a/test/x509_utils_test.dart +++ b/test/x509_utils_test.dart @@ -1652,10 +1652,16 @@ SEQUENCE (1 elem) notBefore: notBefore, ); var x509 = X509Utils.x509CertificateFromPem(pem); - expect(x509.tbsCertificate?.validity.notBefore.toIso8601String().substring(0, 10), + expect( + x509.tbsCertificate?.validity.notBefore + .toIso8601String() + .substring(0, 10), notBefore.toUtc().toIso8601String().substring(0, 10), reason: "notBefore match except milliseconds as utc"); - expect(x509.tbsCertificate?.validity.notAfter.toIso8601String().substring(0, 10), + expect( + x509.tbsCertificate?.validity.notAfter + .toIso8601String() + .substring(0, 10), notAfter.toUtc().toIso8601String().substring(0, 10), reason: "notAfter match except milliseconds as utc"); }); @@ -1707,6 +1713,93 @@ SEQUENCE (1 elem) KeyUsage.DECIPHER_ONLY); }); + test('Test generateSelfSignedCertificate with cA true and valid pathlen', () { + var pair = CryptoUtils.generateEcKeyPair(); + var dn = { + 'CN': 'basic-utils.dev', + 'O': 'Magic Company', + 'L': 'Fakecity', + 'S': 'FakeState', + 'C': 'DE', + }; + var csr = X509Utils.generateEccCsrPem( + dn, pair.privateKey as ECPrivateKey, pair.publicKey as ECPublicKey, + san: ['san1.basic-utils.dev', 'san2.basic-utils.dev']); + + var pem = X509Utils.generateSelfSignedCertificate(pair.privateKey, csr, 365, + cA: true, pathLenConstraint: 10); + var x509 = X509Utils.x509CertificateFromPem(pem); + + expect(x509.tbsCertificate?.extensions?.cA, true); + expect(x509.tbsCertificate?.extensions?.pathLenConstraint, 10); + }); + + test('Test generateSelfSignedCertificate with cA false and valid pathLen', + () { + var pair = CryptoUtils.generateEcKeyPair(); + var dn = { + 'CN': 'basic-utils.dev', + 'O': 'Magic Company', + 'L': 'Fakecity', + 'S': 'FakeState', + 'C': 'DE', + }; + var csr = X509Utils.generateEccCsrPem( + dn, pair.privateKey as ECPrivateKey, pair.publicKey as ECPublicKey, + san: ['san1.basic-utils.dev', 'san2.basic-utils.dev']); + + var pem = X509Utils.generateSelfSignedCertificate(pair.privateKey, csr, 365, + cA: false, pathLenConstraint: 10); + var x509 = X509Utils.x509CertificateFromPem(pem); + + expect(x509.tbsCertificate?.extensions?.cA, null); + expect(x509.tbsCertificate?.extensions?.pathLenConstraint, null); + }); + + test('Test generateSelfSignedCertificate with cA true and invalid pathLen', + () { + var pair = CryptoUtils.generateEcKeyPair(); + var dn = { + 'CN': 'basic-utils.dev', + 'O': 'Magic Company', + 'L': 'Fakecity', + 'S': 'FakeState', + 'C': 'DE', + }; + var csr = X509Utils.generateEccCsrPem( + dn, pair.privateKey as ECPrivateKey, pair.publicKey as ECPublicKey, + san: ['san1.basic-utils.dev', 'san2.basic-utils.dev']); + + var pem = X509Utils.generateSelfSignedCertificate(pair.privateKey, csr, 365, + cA: true, pathLenConstraint: -10); + var x509 = X509Utils.x509CertificateFromPem(pem); + + expect(x509.tbsCertificate?.extensions?.cA, true); + expect(x509.tbsCertificate?.extensions?.pathLenConstraint, null); + }); + + test('Test generateSelfSignedCertificate with cA false and invalid pathLen', + () { + var pair = CryptoUtils.generateEcKeyPair(); + var dn = { + 'CN': 'basic-utils.dev', + 'O': 'Magic Company', + 'L': 'Fakecity', + 'S': 'FakeState', + 'C': 'DE', + }; + var csr = X509Utils.generateEccCsrPem( + dn, pair.privateKey as ECPrivateKey, pair.publicKey as ECPublicKey, + san: ['san1.basic-utils.dev', 'san2.basic-utils.dev']); + + var pem = X509Utils.generateSelfSignedCertificate(pair.privateKey, csr, 365, + cA: false, pathLenConstraint: -10); + var x509 = X509Utils.x509CertificateFromPem(pem); + + expect(x509.tbsCertificate?.extensions?.cA, null); + expect(x509.tbsCertificate?.extensions?.pathLenConstraint, null); + }); + test('Test x509CertificateFromPem with vmc', () { var data = X509Utils.x509CertificateFromPem(vmc);