diff --git a/doc/doc.md b/doc/doc.md new file mode 100644 index 0000000..426f440 --- /dev/null +++ b/doc/doc.md @@ -0,0 +1,12 @@ +# Developer Documentation + +## OpenSSL the reference implementation +For the sake of development testing OpenSSL has been used as the +reference implementation so any test certificates have been generated +using OpenSSL. +* [Creating a self-signed certificate](https://devopscube.com/create-self-signed-certificates-openssl/) + +## Object identifiers OID's +Certificates use OID's +* [Wikipedia Object Identifier](https://en.wikipedia.org/wiki/Object_identifier) +* [List of OID's](http://www.oid-info.com/faq.htm) \ No newline at end of file diff --git a/lib/src/asn1_bit_string.dart b/lib/src/asn1_bit_string.dart new file mode 100644 index 0000000..1e75600 --- /dev/null +++ b/lib/src/asn1_bit_string.dart @@ -0,0 +1,29 @@ +import 'package:asn1lib/asn1lib.dart'; + +extension ASN1BitStringExtension on ASN1BitString{ + static ASN1BitString fromBitArray(List bits){ + var firstBit = int.parse('80', radix: 16); + var unusedBits = 0; + var stringValue = []; + bits = bits.sublist(0, bits.lastIndexOf(true) + 1); + var numBitsLastByte = bits.length % 8; + unusedBits = 8 - numBitsLastByte; + var bytesCount = bits.length / 8; + var bitCount = 0; + for(var byteIndex = 0; byteIndex < bytesCount; byteIndex++){ + var byteValue = 0; + var lastBit = 8; + if(byteIndex > (bytesCount - 1)){ + lastBit = numBitsLastByte; + } + for(var bitIndex = 0; bitIndex < lastBit; bitIndex++){ + if(bits[bitCount]){ + byteValue |= firstBit >> bitIndex; + } + bitCount++; + } + stringValue.add(byteValue); + } + return ASN1BitString(stringValue, unusedbits: unusedBits); + } +} \ No newline at end of file diff --git a/lib/src/asn1_util.dart b/lib/src/asn1_util.dart new file mode 100644 index 0000000..857c072 --- /dev/null +++ b/lib/src/asn1_util.dart @@ -0,0 +1,193 @@ +import 'dart:io'; + +import 'package:asn1lib/asn1lib.dart'; +import 'package:basic_utils/basic_utils.dart'; +import 'package:collection/collection.dart'; +import 'package:x509/x509.dart'; + +/// This is a utility class for comparing 2 ASN.1 Objects +/// This is useful for finding the difference between 2 objects +class ASN1CompareUtil{ + static ASN1CompareResult compareTreeBranch(List branch1, List branch2){ + var object1 = branch1.last; + var object2 = branch2.last; + if(object1.runtimeType != object2.runtimeType){ + return ASN1CompareResult.different(branch1, branch2); + } + if(object1 is ASN1Sequence){ + object2 = object2 as ASN1Sequence; + if(object1.elements.length != object2.elements.length){ + return ASN1CompareResult.different(branch1, branch2); + } + for(var i = 0; i < object1.elements.length; i++){ + var result = compareTreeBranch( + [...branch1,object1.elements[i]], + [...branch2,object2.elements[i]] + ); + if(result.isDifferent){ + return result; + } + } + }else if(object1 is ASN1Set){ + object2 = object2 as ASN1Set; + if(object1.elements.length != object2.elements.length){ + return ASN1CompareResult.different(branch1, branch2); + } + // TODO: This is probably incorrect as a set is not ordered. Will need to look at + // the specification to see what are the rules for a unique element in ASN.1 Set + var object1List = object1.elements.toList(); + var object2List = object2.elements.toList(); + for(var i = 0; i < object1List.length; i++){ + var result = compareTreeBranch( + [...branch1,object1List[i]], + [...branch2,object2List[i]] + ); + if(result.isDifferent){ + return result; + } + } + }else{ + var bytes1 = object1.encodedBytes; + var bytes2 = object2.encodedBytes; + var equal = ListEquality().equals(bytes1, bytes2); + if(!equal){ + // See if the content is a ASN1 Object. + try{ + var object1List = ASN1Parser(object1.contentBytes()).toList(); + var object2List = ASN1Parser(object2.contentBytes()).toList(); + if(object1List.length != object2List.length){ + return ASN1CompareResult.different(branch1, branch2); + } + for(var i = 0; i < object1List.length; i++){ + var result = compareTreeBranch( + [...branch1,object1List[i]], + [...branch2,object2List[i]] + ); + if(result.isDifferent){ + return result; + } + } + }catch(_){ + return ASN1CompareResult.different(branch1, branch2); + } + } + } + return ASN1CompareResult.same(); + } + + static ASN1CompareResult compareTree(ASN1Object object1, ASN1Object object2){ + return compareTreeBranch([object1], [object2]); + } + + static String listToBinary(List list) { + var b = StringBuffer('['); + var doComma = false; + for (var v in list) { + doComma ? b.write(', ') : doComma = true; + b.write(v.toRadixString(2).padLeft(8,'0')); + } + b.write(']'); + return b.toString(); + } +} + +class ASN1CompareResult{ + final bool same; + final List branch1; + final List branch2; + + ASN1CompareResult.same() : same = true, branch1 = [], branch2 = []; + ASN1CompareResult.different(this.branch1, this.branch2): same = false; + + bool get isDifferent => !same; + + void printDiff(){ + if(same){ + print('No difference'); + } + _printDiffLevel(0); + } + + void _printDiffLevel(int level){ + if(level == branch1.length - 1){ + stdout.write(' | '.repeat(level)); + stdout.write('${branch1[level].tag} ${branch1[level].runtimeType} ${branch1[level]}\n'); + stdout.write('Bytes from object 1\n'); + stdout.write('${ASN1Util.listToString(branch1[level].encodedBytes)}\n'); + stdout.write('${branch1[level].encodedBytes}\n'); + stdout.write('${ASN1CompareUtil.listToBinary(branch1[level].encodedBytes)}\n'); + stdout.write('${String.fromCharCodes(branch1[level].valueBytes())}\n'); + stdout.write('Bytes from object 2\n'); + stdout.write('${ASN1Util.listToString(branch2[level].encodedBytes)}\n'); + stdout.write('${branch2[level].encodedBytes}\n'); + stdout.write('${ASN1CompareUtil.listToBinary(branch2[level].encodedBytes)}\n'); + stdout.write('${String.fromCharCodes(branch2[level].valueBytes())}\n'); + }else{ + var currentObject = branch1[level]; + stdout.write(' | '.repeat(level)); + if(branch1[level] is ASN1Sequence){ + stdout.write('${branch1[level].tag} ${branch1[level].runtimeType}\n'); + var objectList = (branch1[level] as ASN1Sequence).elements; + for(var i = 0; i < objectList.length; i++){ + if(objectList[i] == branch1[level + 1]){ + _printDiffLevel(level + 1); + }else{ + stdout.write(' | '.repeat(level + 1)); + stdout.write('${_objectToString(objectList[i])}\n'); + } + } + }else{ + stdout.write('${_objectToString(currentObject)}\n'); + _printDiffLevel(level + 1); + } + } + } + + String _objectToString(ASN1Object object){ + var leader = '${object.tag} ${object.runtimeType}'; + String stringValue; + if(object is ASN1ObjectIdentifier){ + var oidEntry = OIDDatabase.getEntryByIdentifierString(object.identifier); + stringValue = '$leader ${object.identifier} ${oidEntry.fullName}'; + }else if(object is ASN1Sequence){ + stringValue = leader; + }else if(object is ASN1Integer){ + stringValue = '$leader ${object.valueAsBigInteger.toString()}'; + } + else{ + if(!object.isEncoded){ + object.encodedBytes; + } + stringValue = '$leader ${object.toString()}'; + } + return stringValue.truncate(100); + } +} + +extension on ASN1Parser{ + List toList(){ + var objectList = []; + while(hasNext()){ + objectList.add(nextObject()); + } + return objectList; + } +} + +extension on String{ + String repeat(int count){ + var outputString = ''; + while(count-- > 0){ + outputString += this; + } + return outputString; + } + + String truncate(int length, {String symbol = '...'}){ + if(this.length > length){ + return substring(0,length) + symbol; + }else{ + return this; + } + } +} \ No newline at end of file diff --git a/lib/src/certificate.dart b/lib/src/certificate.dart index 0e8440d..ab62510 100644 --- a/lib/src/certificate.dart +++ b/lib/src/certificate.dart @@ -1,7 +1,11 @@ -part of x509; +part of 'x509_base.dart'; + +const beginCert = '-----BEGIN CERTIFICATE-----'; +const endCert = '-----END CERTIFICATE-----'; /// A Certificate. abstract class Certificate { + /// The public key from this certificate. PublicKey get publicKey; } @@ -22,6 +26,108 @@ class X509Certificate implements Certificate { const X509Certificate( this.tbsCertificate, this.signatureAlgorithm, this.signatureValue); + factory X509Certificate.generateCA({BigInt? serialNumber, String signatureType = 'sha256WithRSAEncryption', + required Name name, + int days = 30, required KeyPair keyPair, DateTime? from}){ + serialNumber ??= BigInt.from(1); + if(keyPair.publicKey == null || keyPair.privateKey == null){ + throw ArgumentError('The keyPair must have a valid public and private key that is not null', 'keyPair'); + } + var now = from ?? DateTime.now(); + var validity = Validity(notBefore: now, notAfter: now.add(Duration(days: days))); + var subjectPublicKeyInfo = SubjectPublicKeyInfo.fromPublicKey(keyPair.publicKey!); + var extensions = [ + Extension(extnId: ObjectIdentifier.fromOiReadableName('subjectKeyIdentifier'), + extnValue: SubjectKeyIdentifier.fromPublicKeyBytes(subjectPublicKeyInfo.publicKeyBytes)), + Extension(extnId: ObjectIdentifier.fromOiReadableName('authorityKeyIdentifier'), + extnValue: AuthorityKeyIdentifier.fromPublicKeyBytes(subjectPublicKeyInfo.publicKeyBytes)), + Extension(extnId: ObjectIdentifier.fromOiReadableName('basicConstraints'), + extnValue: BasicConstraints(cA: true), + isCritical: true + ), + // Extension(extnId: ObjectIdentifier.fromOiReadableName('keyUsage'), + // extnValue: KeyUsage.optional(keyCertSign: true), + // isCritical: true) + ]; + var tbsCertificate = TbsCertificate( + version: 3, + serialNumber: serialNumber, + signature: AlgorithmIdentifier.fromOiReadableName(signatureType), + issuer: name, + validity: validity, + subject: name, + subjectPublicKeyInfo: SubjectPublicKeyInfo.fromPublicKey(keyPair.publicKey!), + extensions: extensions); + + var signer = keyPair.privateKey!.createSigner(algorithms.signing.rsa.sha256); + var data = tbsCertificate.toAsn1(); + var signature = signer.sign(data.encodedBytes); + + var cert = X509Certificate(tbsCertificate, + AlgorithmIdentifier.fromOiReadableName(signatureType), + signature.data); + return cert; + } + + factory X509Certificate.selfSigned(PrivateKey privateKey, CertificationRequest certificationRequest, {int days = 30, + BigInt? serialNumber, Name? issuer, X509Certificate? caCert, DateTime? from, List? extensions}){ + var now = from ?? DateTime.now(); + var validity = Validity(notBefore: now, notAfter: now.add(Duration(days: days))); + var extensionsMap = Map.fromEntries(extensions?.map((e) => MapEntry(e.extnId, e)).toList() ?? >[]); + serialNumber ??= BigInt.from(1); + + for(var attribute in (certificationRequest.certificationRequestInfo.attributes?.attributes ?? [])){ + if(attribute is ExtensionRequestAttribute){ + for (var element in attribute.extensions) { + if(!extensionsMap.containsKey(element.extnId)){ + extensionsMap[element.extnId] = element; + } + } + } + } + + caCert?.tbsCertificate.extensions?.forEach((element) { + var value = element.extnValue; + if(value is SubjectKeyIdentifier){ + var newExtension = Extension(extnId: ObjectIdentifier.fromOiReadableName('authorityKeyIdentifier'), + extnValue: AuthorityKeyIdentifier.fromSubjectKeyIdentifier(value)); + extensionsMap[newExtension.extnId] = newExtension; + } + }); + + var ski = Extension(extnId: ObjectIdentifier.fromOiReadableName('subjectKeyIdentifier'), + extnValue: SubjectKeyIdentifier.fromPublicKeyBytes(certificationRequest.certificationRequestInfo.subjectPublicKeyInfo.publicKeyBytes)); + + extensionsMap[ski.extnId] = ski; + + var extensionsList = extensionsMap.values.toList(); + extensionsList.sort((a, b) => a.extnId.name.compareTo(b.extnId.name)); + + var issuerToUse = issuer; + issuerToUse ??= caCert?.tbsCertificate.subject; + issuerToUse ??= certificationRequest.certificationRequestInfo.subject; + + var tbsCertificate = TbsCertificate( + version: 3, + serialNumber: serialNumber, + signature: AlgorithmIdentifier.fromOiReadableName('sha256WithRSAEncryption'), + issuer: issuerToUse, + validity: validity, + subject: certificationRequest.certificationRequestInfo.subject, + subjectPublicKeyInfo: certificationRequest.certificationRequestInfo.subjectPublicKeyInfo, + extensions: extensionsList); + + var signer = privateKey.createSigner(algorithms.signing.rsa.sha256); + var data = tbsCertificate.toAsn1(); + var signature = signer.sign(data.encodedBytes); + + var cert = X509Certificate(tbsCertificate, + AlgorithmIdentifier.fromOiReadableName('sha256WithRSAEncryption'), + signature.data); + return cert; + + } + /// Creates a certificate from an [ASN1Sequence]. /// /// The ASN.1 definition is: @@ -46,6 +152,15 @@ class X509Certificate implements Certificate { ..add(fromDart(signatureValue)); } + String toPem(){ + var asn1 = toAsn1(); + var bytes = asn1.encodedBytes; + var stringValue = base64.encode(bytes); + var chunks = StringUtils.chunk(stringValue, 64); + var pem = '$beginCert\n${chunks.join('\n')}\n$endCert'; + return pem; + } + @override String toString([String prefix = '']) { var buffer = StringBuffer(); @@ -64,7 +179,7 @@ class TbsCertificate { final int? version; /// The serial number of the certificate. - final int? serialNumber; + final BigInt? serialNumber; /// The signature of the certificate. final AlgorithmIdentifier? signature; @@ -161,7 +276,7 @@ class TbsCertificate { return TbsCertificate( version: version, - serialNumber: (elements[0] as ASN1Integer).valueAsBigInteger.toInt(), + serialNumber: (elements[0] as ASN1Integer).valueAsBigInteger, signature: AlgorithmIdentifier.fromAsn1(elements[1] as ASN1Sequence), issuer: Name.fromAsn1(elements[2] as ASN1Sequence), validity: Validity.fromAsn1(elements[3] as ASN1Sequence), @@ -193,6 +308,14 @@ class TbsCertificate { ..add(subject!.toAsn1()) ..add(subjectPublicKeyInfo!.toAsn1()); if (version! > 1) { + if(extensions?.isNotEmpty ?? false){ + var extensionsSequence = ASN1Sequence(); + for(var extension in extensions!){ + extensionsSequence.add(extension.toAsn1()); + } + var bytes = extensionsSequence.encodedBytes; + seq.add(ASN1Object.preEncoded(0xA3, bytes)); + } if (issuerUniqueID != null) { // TODO // var iuid = ASN1BitString.fromBytes(issuerUniqueID); diff --git a/lib/src/extension.dart b/lib/src/extension.dart index 72e591a..b3db3ad 100644 --- a/lib/src/extension.dart +++ b/lib/src/extension.dart @@ -1,4 +1,4 @@ -part of x509; +part of 'x509_base.dart'; /// An X.509 extension class Extension { @@ -53,6 +53,27 @@ class Extension { return Extension(extnId: id, isCritical: critical, extnValue: value); } + ASN1Sequence toAsn1(){ + if(extnValue is ToAsn1){ + var asn1ExtValue = extnValue as ToAsn1; + var topSequence = _getExtensionInstance(); + var asn1 = asn1ExtValue.toAsn1(); + var o = ASN1OctetString(asn1.encodedBytes); + topSequence.add(o); + return topSequence; + }else{ + throw UnimplementedError('Conversion from ${extnValue.runtimeType} to ASN.1 has not yet been implemented' ); + } + } + + ASN1Sequence _getExtensionInstance(){ + var topSequence = ASN1Sequence(); + topSequence.add(extnId.toAsn1()); + if(isCritical) topSequence.add(fromDart(isCritical)); + return topSequence; + } + + @override String toString([String prefix = '']) { var buffer = StringBuffer(); @@ -62,11 +83,63 @@ class Extension { } } +class BasicConstraintsExtension extends Extension{ + BasicConstraintsExtension({super.isCritical, bool cA = false, int? pathLenConstraint}) + : super(extnId: ObjectIdentifier.fromOiReadableName('basicConstraints'), + extnValue: BasicConstraints(cA: cA, pathLenConstraint: pathLenConstraint)); +} + +class KeyUsageExtension extends Extension{ + + KeyUsageExtension({super.isCritical, + required bool digitalSignature, + required bool nonRepudiation, + required bool keyEncipherment, + required bool dataEncipherment, + required bool keyAgreement, + required bool keyCertSign, + required bool cRLSign, + required bool encipherOnly, + required bool decipherOnly}) + : super(extnId: ObjectIdentifier.keyUsage, extnValue: KeyUsage(digitalSignature: digitalSignature, nonRepudiation: nonRepudiation, keyEncipherment: keyEncipherment, dataEncipherment: dataEncipherment, keyAgreement: keyAgreement, keyCertSign: keyCertSign, cRLSign: cRLSign, encipherOnly: encipherOnly, decipherOnly: decipherOnly)); + + factory KeyUsageExtension.optional({ + bool isCritical = false, + bool digitalSignature = false, + bool nonRepudiation = false, + bool keyEncipherment = false, + bool dataEncipherment = false, + bool keyAgreement = false, + bool keyCertSign = false, + bool cRLSign = false, + bool encipherOnly = false, + bool decipherOnly = false}){ + return KeyUsageExtension( + isCritical : isCritical, + digitalSignature: digitalSignature, + nonRepudiation: nonRepudiation, + keyEncipherment: keyEncipherment, + dataEncipherment: dataEncipherment, + keyAgreement: keyAgreement, + keyCertSign: keyCertSign, + cRLSign: cRLSign, + encipherOnly: encipherOnly, + decipherOnly: decipherOnly + ); + } +} + +class SubjectAltNameExtension extends Extension{ + + SubjectAltNameExtension({super.isCritical, required List names}) : super(extnId: ObjectIdentifier.subjectAltName, + extnValue: GeneralNames(names)); +} + /// The base class for extension values. abstract class ExtensionValue { static const ceId = ObjectIdentifier([2, 5, 29]); static const peId = ObjectIdentifier([1, 3, 6, 1, 5, 5, 7, 1]); - static const goog24Id = ObjectIdentifier([1, 3, 6, 1, 4, 1, 11129, 2, 4]); + static const google24Id = ObjectIdentifier([1, 3, 6, 1, 4, 1, 11129, 2, 4]); const ExtensionValue(); @@ -113,7 +186,7 @@ abstract class ExtensionValue { return ProxyCertInfo.fromAsn1(obj as ASN1Sequence); } } - if (id.parent == goog24Id) { + if (id.parent == google24Id) { switch (id.nodes.last) { case 2: return SctList.fromAsn1(obj as ASN1OctetString); @@ -127,7 +200,7 @@ abstract class ExtensionValue { /// /// The authority key identifier extension provides a means of identifying the /// public key corresponding to the private key used to sign a certificate. -class AuthorityKeyIdentifier extends ExtensionValue { +class AuthorityKeyIdentifier extends ExtensionValue implements ToAsn1{ final List? keyIdentifier; final GeneralNames? authorityCertIssuer; final BigInt? authorityCertSerialNumber; @@ -135,6 +208,14 @@ class AuthorityKeyIdentifier extends ExtensionValue { AuthorityKeyIdentifier(this.keyIdentifier, this.authorityCertIssuer, this.authorityCertSerialNumber); + factory AuthorityKeyIdentifier.fromPublicKeyBytes(Uint8List bytes){ + return AuthorityKeyIdentifier(sha1.convert(bytes).bytes, null, null); + } + + factory AuthorityKeyIdentifier.fromSubjectKeyIdentifier(SubjectKeyIdentifier ski){ + return AuthorityKeyIdentifier(ski.keyIdentifier, null, null); + } + /// Creates an authority key identifier extension value from an [ASN1Sequence]. /// /// The ASN.1 definition is: @@ -165,23 +246,46 @@ class AuthorityKeyIdentifier extends ExtensionValue { } return AuthorityKeyIdentifier(keyId, issuer, number); } + + @override + ASN1Sequence toAsn1() { + var sequence = ASN1Sequence(); + if(keyIdentifier != null) sequence.add(ASN1Object.preEncoded(0x80, Uint8List.fromList(keyIdentifier!))); + if(authorityCertIssuer != null) sequence.add(ASN1Object.preEncoded(0x81, authorityCertIssuer!.toAsn1().encodedBytes)); + if(authorityCertSerialNumber != null) sequence.add(ASN1Object.preEncoded(0x82, fromDart(authorityCertSerialNumber).encodedBytes)); + return sequence; + } + + } /// The subject key identifier extension provides a means of identifying /// certificates that contain a particular public key. -class SubjectKeyIdentifier extends ExtensionValue { +class SubjectKeyIdentifier extends ExtensionValue implements ToAsn1{ final List? keyIdentifier; SubjectKeyIdentifier(this.keyIdentifier); + factory SubjectKeyIdentifier.fromPublicKeyBytes(Uint8List bytes){ + return SubjectKeyIdentifier(sha1.convert(bytes).bytes); + } + factory SubjectKeyIdentifier.fromAsn1(ASN1Object obj) { return SubjectKeyIdentifier(obj.contentBytes()); } + + @override + ASN1Object toAsn1() { + if(keyIdentifier == null){ + throw StateError('A subject key identifier should not be converted to ASN1 if there is no value'); + } + return ASN1OctetString(Uint8List.fromList(keyIdentifier!)); + } } /// The key usage extension defines the purpose (e.g., encipherment, signature, /// certificate signing) of the key contained in the certificate. -class KeyUsage extends ExtensionValue { +class KeyUsage extends ExtensionValue implements ToAsn1{ /// True when the subject public key is used for verifying digital signatures, /// other than signatures on certificates and CRLs, such as those used in an /// entity authentication service, a data origin authentication service, @@ -250,6 +354,30 @@ class KeyUsage extends ExtensionValue { required this.encipherOnly, required this.decipherOnly}); + + factory KeyUsage.optional({ + bool digitalSignature = false, + bool nonRepudiation = false, + bool keyEncipherment = false, + bool dataEncipherment = false, + bool keyAgreement = false, + bool keyCertSign = false, + bool cRLSign = false, + bool encipherOnly = false, + bool decipherOnly = false}){ + return KeyUsage( + digitalSignature: digitalSignature, + nonRepudiation: nonRepudiation, + keyEncipherment: keyEncipherment, + dataEncipherment: dataEncipherment, + keyAgreement: keyAgreement, + keyCertSign: keyCertSign, + cRLSign: cRLSign, + encipherOnly: encipherOnly, + decipherOnly: decipherOnly + ); + } + /// Creates a key usage extension from an [ASN1BitString]. /// /// The ASN.1 definition is: @@ -287,6 +415,22 @@ class KeyUsage extends ExtensionValue { decipherOnly: bits[8]); } + @override + ASN1BitString toAsn1(){ + var bits = ASN1BitStringExtension.fromBitArray([ + digitalSignature, + nonRepudiation, + keyEncipherment, + dataEncipherment, + keyAgreement, + keyCertSign, + cRLSign, + encipherOnly, + decipherOnly + ]); + return bits; + } + @override String toString() => [ digitalSignature ? 'Digital Signature' : null @@ -297,7 +441,7 @@ class KeyUsage extends ExtensionValue { /// This extension indicates one or more purposes for which the certified /// public key may be used, in addition to or in place of the basic purposes /// indicated in the key usage extension. -class ExtendedKeyUsage extends ExtensionValue { +class ExtendedKeyUsage extends ExtensionValue implements ToAsn1{ final List ids; const ExtendedKeyUsage(this.ids); @@ -306,6 +450,15 @@ class ExtendedKeyUsage extends ExtensionValue { return ExtendedKeyUsage((toDart(sequence) as List).cast()); } + @override + ASN1Sequence toAsn1(){ + var sequence = ASN1Sequence(); + for(var id in ids){ + sequence.add(id.toAsn1()); + } + return sequence; + } + @override String toString() => ids.join(', '); } @@ -344,7 +497,7 @@ class PrivateKeyUsagePeriod extends ExtensionValue { /// The basic constraints extension identifies whether the subject of the /// certificate is a CA and the maximum depth of valid certification paths /// that include this certificate. -class BasicConstraints extends ExtensionValue { +class BasicConstraints extends ExtensionValue implements ToAsn1{ final bool cA; final int? pathLenConstraint; @@ -371,6 +524,16 @@ class BasicConstraints extends ExtensionValue { return BasicConstraints(cA: cA, pathLenConstraint: len); } + @override + ASN1Sequence toAsn1(){ + var sequence = ASN1Sequence(); + if(cA) sequence.add(fromDart(cA)); + if(pathLenConstraint != null){ + sequence.add(fromDart(pathLenConstraint)); + } + return sequence; + } + @override String toString() => [ "CA:${"$cA".toUpperCase()}" @@ -783,6 +946,9 @@ class GeneralName { 'registeredID', ]; + static final choiceDnsName = 2; + static final choiceIpAddress = 7; + factory GeneralName.fromAsn1(ASN1Object obj) { var tag = obj.tag; var isConstructed = (0xA0 & tag) == 0xA0; @@ -818,6 +984,13 @@ class GeneralName { isConstructed: isConstructed, choice: choice, contents: contents!); } + ASN1Object toAsn1(){ + var tag = isConstructed ? 0xA0 : 0x80; + tag = tag | choice; + var object = ASN1Object.preEncoded(tag, contents.valueBytes()); + return object; + } + @override String toString() { String contentsString; @@ -832,7 +1005,15 @@ class GeneralName { } } -class GeneralNames extends ExtensionValue { +class DNSName extends GeneralName{ + DNSName(String dnsName) : super(isConstructed: false, choice: GeneralName.choiceDnsName, contents: ASN1IA5String(dnsName)); +} + +class IPAddressName extends GeneralName{ + IPAddressName(int octet1, int octet2, int octet3, int octet4) : super(isConstructed: false, choice: GeneralName.choiceIpAddress, contents: ASN1OctetString([octet1, octet2, octet3, octet4])); +} + +class GeneralNames extends ExtensionValue implements ToAsn1{ List names; GeneralNames(this.names); @@ -850,6 +1031,15 @@ class GeneralNames extends ExtensionValue { } } + @override + ASN1Object toAsn1(){ + var sequence = ASN1Sequence(); + for(var name in names){ + sequence.add(name.toAsn1()); + } + return sequence; + } + @override String toString() { return names.map((n) => n.toString()).join(', '); diff --git a/lib/src/objectidentifier.dart b/lib/src/objectidentifier.dart index ddccfaa..c88de2c 100644 --- a/lib/src/objectidentifier.dart +++ b/lib/src/objectidentifier.dart @@ -1,52 +1,33 @@ -part of x509; +part of 'x509_base.dart'; class ObjectIdentifier { final List nodes; const ObjectIdentifier(this.nodes); + static ObjectIdentifier get keyUsage => ObjectIdentifier.fromOiReadableName('keyUsage'); + static ObjectIdentifier get subjectAltName => ObjectIdentifier.fromOiReadableName('subjectAltName'); + ObjectIdentifier? get parent => nodes.length > 1 ? ObjectIdentifier(nodes.take(nodes.length - 1).toList()) : null; factory ObjectIdentifier.fromAsn1(ASN1ObjectIdentifier id) { - var bytes = id.valueBytes(); - var nodes = []; - var v = bytes.first; - nodes.add(v ~/ 40); - nodes.add(v % 40); + // ASN1ObjectIdentifier parameter 'oi' is the same as ObjectIdentifier parameter nodes + return ObjectIdentifier(id.oi); + } - var w = 0; - for (var v in bytes.skip(1)) { - if (v >= 128) { - w += v - 128; - w *= 128; - } else { - w += v; - nodes.add(w); - w = 0; - } + factory ObjectIdentifier.fromOiReadableName(String readableName) { + var oiEntry = _getOidEntryFromName(readableName); + if(oiEntry == null){ + throw ArgumentError('Could not find Object Identifier with name $readableName'); } - - return ObjectIdentifier(nodes); + return ObjectIdentifier(oiEntry.identifier); } ASN1ObjectIdentifier toAsn1() { - var bytes = []; - bytes.add(nodes.first * 40 + nodes[1]); - for (var v in nodes.skip(2)) { - var w = []; - while (v > 128) { - var u = v % 128; - v -= u; - v ~/= 128; - w.add(u); - } - w.add(v); - bytes.addAll(w.skip(1).toList().reversed.map((v) => v + 128)); - bytes.add(w.first); - } - return ASN1ObjectIdentifier(bytes); + // ObjectIdentifier parameter 'oi' is the same as ASN1ObjectIdentifier parameter nodes + return ASN1ObjectIdentifier(nodes); } @override @@ -202,7 +183,8 @@ class ObjectIdentifier { 6: 'countersignature', 7: 'challengePassword', 8: 'unstructuredAddress', - 9: 'extendedCertificateAttributes' + 9: 'extendedCertificateAttributes', + 14: 'extensionRequest', }, 3: {null: 'pkcs-3', 1: 'dhKeyAgreement'}, 7: { @@ -471,8 +453,8 @@ class ObjectIdentifier { 12: 'title', 35: 'userPassword', 36: 'userCertificate', - 37: 'cAcertificate', - 38: 'authorityRecovationList', + 37: 'cACertificate', + 38: 'authorityRevocationList', 39: 'certificateRevocationList', 40: 'crossCertificatePair', 41: 'name', @@ -524,7 +506,7 @@ class ObjectIdentifier { 2: 'netscape-base-url', 3: 'netscape-revocation-url', 4: 'netscape-ca-revocation-url', - 7: 'netcape-cert-renewal-url', + 7: 'netscape-cert-renewal-url', 8: 'netscape-policy-url', 12: 'netscape-ssl-server-name', 13: 'netscape-comment' @@ -558,10 +540,86 @@ class ObjectIdentifier { } } }; + + static List oidDatabase = _buildOidDatabase(); + + static OidEntry? _getOidEntryFromName(String oidReadableName){ + for(var entry in oidDatabase){ + if(entry.readableName == oidReadableName){ + return entry; + } + } + return null; + } + + static List _buildOidDatabase(){ + var database = []; + for(var entry in _tree.entries){ + addEntry(database, entry, null); + } + return database; + } + + static void addEntry(List database, MapEntry entry, OidEntry? parent) { + if(entry.key != null){ + var current = List.from(parent?.identifier ?? [])..add(entry.key!); + var value = entry.value; + if(value is Map && value[null] is String){ + var name = value[null] as String; + var fullName = [if(parent != null) parent.fullName, name].join('.'); + var newOidEntry = OidEntry(current.join('.'), name, fullName, current); + database.add(newOidEntry); + for(var newEntry in value.entries){ + if(newEntry.key != null){ + addEntry(database, newEntry as MapEntry, newOidEntry); + } + } + }else if(value is String){ + var name = value; + var fullName = [if(parent != null) parent.fullName, name].join('.'); + database.add(OidEntry(current.join('.'), name, fullName, current)); + } + } + } +} + +class OidEntry{ + String identifierString; + String readableName; + String fullName; + List identifier; + + OidEntry(this.identifierString, this.readableName, this.fullName, this.identifier); + + @override + String toString() { + // TODO: implement toString + return ''' +{ + $identifierString + $readableName + $fullName + $identifier + } + '''; + } } // Throw when There OID name is unknown. // For example, be defined unique extension. class UnknownOIDNameError extends StateError { - UnknownOIDNameError(String message) : super(message); + UnknownOIDNameError(super.message); +} + +class OIDDatabase{ + static List oidDatabase = ObjectIdentifier._buildOidDatabase(); + static OidEntry missingEntry = OidEntry('not found', 'not found', 'not found', []); + static OidEntry getEntryByIdentifierString(String? identifierString){ + for(var entry in oidDatabase){ + if(entry.identifierString == identifierString){ + return entry; + } + } + return missingEntry; + } } diff --git a/lib/src/request.dart b/lib/src/request.dart index e536ab9..3da7e52 100644 --- a/lib/src/request.dart +++ b/lib/src/request.dart @@ -1,4 +1,4 @@ -part of x509; +part of 'x509_base.dart'; /// https://tools.ietf.org/html/rfc2986 class CertificationRequest { @@ -9,6 +9,16 @@ class CertificationRequest { CertificationRequest( this.certificationRequestInfo, this.signatureAlgorithm, this.signature); + factory CertificationRequest.generate(CertificationRequestInfo certificationRequestInfo, PrivateKey privateKey) { + var bytes = certificationRequestInfo.toAsn1().encodedBytes; + if(privateKey is RsaPrivateKey){ + var signature = privateKey.createSigner(algorithms.signing.rsa.sha256).sign(bytes).data; + return CertificationRequest(certificationRequestInfo, AlgorithmIdentifier.fromOiReadableName('sha256WithRSAEncryption'), signature); + } + throw UnimplementedError('Keys of type ${privateKey.runtimeType} are currently not supported'); + } + + /// CertificationRequest ::= SEQUENCE { /// certificationRequestInfo CertificationRequestInfo, /// signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }}, @@ -21,17 +31,29 @@ class CertificationRequest { algorithm, (sequence.elements[2] as ASN1BitString).contentBytes()); } + + ASN1Sequence toAsn1(){ + var sequence = ASN1Sequence(); + sequence.add(certificationRequestInfo.toAsn1()); + sequence.add(signatureAlgorithm.toAsn1()); + sequence.add(fromDart(signature)); + return sequence; + } } class CertificationRequestInfo { final int? version; final Name subject; final SubjectPublicKeyInfo subjectPublicKeyInfo; - final Map>? attributes; + final Attributes? attributes; - CertificationRequestInfo( + CertificationRequestInfo._( this.version, this.subject, this.subjectPublicKeyInfo, this.attributes); + factory CertificationRequestInfo(Name subject, SubjectPublicKeyInfo subjectPublicKeyInfo, {int version = 1, Attributes? attributes}){ + return CertificationRequestInfo._(version, subject, subjectPublicKeyInfo, attributes); + } + /// CertificationRequestInfo ::= SEQUENCE { /// version INTEGER { v1(0) } (v1,...), /// subject Name, @@ -39,10 +61,112 @@ class CertificationRequestInfo { /// attributes [0] Attributes{{ CRIAttributes }} /// } factory CertificationRequestInfo.fromAsn1(ASN1Sequence sequence) { - return CertificationRequestInfo( + return CertificationRequestInfo._( toDart(sequence.elements[0]).toInt() + 1, Name.fromAsn1(sequence.elements[1] as ASN1Sequence), SubjectPublicKeyInfo.fromAsn1(sequence.elements[2] as ASN1Sequence), - null /*TODO*/); + Attributes.fromAsn1(sequence.elements[3])); + } + + ASN1Sequence toAsn1(){ + var sequence = ASN1Sequence(); + sequence.add(fromDart((version ?? 1) - 1)); + sequence.add(subject.toAsn1()); + sequence.add(subjectPublicKeyInfo.toAsn1()); + if(attributes != null) sequence.add(attributes!.toAsn1()); + return sequence; + } +} + +/// Attributes { ATTRIBUTE:IOSet } ::= SET OF Attribute{{ IOSet }} +/// +/// CRIAttributes ATTRIBUTE ::= { +/// ... -- add any locally defined attributes here -- } +/// +class Attributes{ + + final List attributes; + + Attributes(this.attributes); + + ASN1Object toAsn1(){ + // It is not really a set but it is convenient to use set + var set = ASN1Set(tag: 0xA0); + for(var attribute in attributes){ + set.add(attribute.toAsn1()); + } + return set; + } + + factory Attributes.fromAsn1(ASN1Object element) { + if(element.tag != 0xA0){ + throw BadAttributesError('The tag from the Attributes ASN1Octet String does not equal 0xA0'); + } + var parser = ASN1Parser(element.valueBytes()); + var attributes = []; + while(parser.hasNext()){ + attributes.add(Attribute.fromAsn1(parser.nextObject())); + } + return Attributes(attributes); + } +} + +class BadAttributesError extends StateError{ + BadAttributesError(super.message); +} + +/// Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE { +/// type ATTRIBUTE.&id({IOSet}), +/// values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type}) +/// } +abstract class Attribute { + final ObjectIdentifier oi; + + Attribute(this.oi); + + ASN1Object toAsn1(); + + factory Attribute.fromAsn1(ASN1Object object) { + if(object is ASN1Sequence){ + var oi = ObjectIdentifier.fromAsn1(object.elements[0] as ASN1ObjectIdentifier); + if(oi.name == 'extensionRequest'){ + return ExtensionRequestAttribute.fromAsn1(object); + } + } + throw BadAttributesError('It is expected that an Attribute would be an ASN1Sequence'); + } +} + +class ExtensionRequestAttribute extends Attribute{ + List extensions; + + ExtensionRequestAttribute(this.extensions) : super(ObjectIdentifier.fromOiReadableName('extensionRequest')); + + factory ExtensionRequestAttribute.fromAsn1(ASN1Sequence object) { + var set = object.elements[1] as ASN1Set; + var sequence = set.elements.first as ASN1Sequence; + var extensions = []; + for(var extSeq in sequence.elements){ + if(extSeq is! ASN1Sequence){ + throw BadAttributesError('It was expected that an extension would be a sequence'); + } + extensions.add(Extension.fromAsn1(extSeq)); + } + return ExtensionRequestAttribute(extensions); + } + + @override + ASN1Sequence toAsn1(){ + var outer = ASN1Sequence(); + outer.add(oi.toAsn1()); + var set = ASN1Set(); + outer.add(set); + var inner = ASN1Sequence(); + set.add(inner); + for(var extension in extensions){ + inner.add(extension.toAsn1()); + } + + return outer; } } diff --git a/lib/src/util.dart b/lib/src/util.dart index a11aac8..133a04b 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -146,7 +146,7 @@ KeyPair keyPairFromAsn1(ASN1BitString data, ObjectIdentifier algorithm) { return ecKeyPairFromAsn1(sequence); case 'sha1WithRSAEncryption': } - throw UnimplementedError('Unknown algoritmh $algorithm'); + throw UnimplementedError('Unknown algorithm $algorithm'); } PublicKey publicKeyFromAsn1(ASN1BitString data, AlgorithmIdentifier algorithm) { @@ -159,7 +159,7 @@ PublicKey publicKeyFromAsn1(ASN1BitString data, AlgorithmIdentifier algorithm) { curve: _curveObjectIdentifierToIdentifier(algorithm.parameters)); case 'sha1WithRSAEncryption': } - throw UnimplementedError('Unknown algoritmh $algorithm'); + throw UnimplementedError('Unknown algorithm $algorithm'); } String keyToString(Key key, [String prefix = '']) { @@ -257,7 +257,7 @@ dynamic toDart(ASN1Object obj) { // // The Class type is below: // 0 0(0): Universal - // 0 1(1): Applicaation + // 0 1(1): Application // 1 0(2): Context-Specific // 1 1(3): Private // diff --git a/lib/src/x509_base.dart b/lib/src/x509_base.dart index 40b32a2..0f3d2f6 100644 --- a/lib/src/x509_base.dart +++ b/lib/src/x509_base.dart @@ -10,9 +10,12 @@ import 'dart:convert'; import 'package:quiver/core.dart'; import 'package:quiver/collection.dart'; import 'dart:typed_data'; +import 'package:basic_utils/basic_utils.dart' show StringUtils; +import 'package:crypto/crypto.dart'; import 'package:crypto_keys/crypto_keys.dart'; export 'package:crypto_keys/crypto_keys.dart'; +import 'asn1_bit_string.dart'; import 'util.dart'; part 'certificate.dart'; @@ -25,6 +28,28 @@ class Name { const Name(this.names); + /// Creates a Name from a map of Object Identifier readable names and values + /// eg. + /// { + /// 'commonName':certificateName, + /// 'countryName':countryCode, + /// 'stateOrProvinceName':state, + /// 'localityName':locality, + /// 'organizationName':certificateName, + /// } + /// + factory Name.fromMap(Map nameMap){ + var names = nameMap.entries.map((cnEntry) { + var objectIdentifier = ObjectIdentifier.fromOiReadableName(cnEntry.key); + if(objectIdentifier == null){ + throw ArgumentError('Object Identifier ${cnEntry.key} not supported!'); + } + return {objectIdentifier: cnEntry.value}; + }).toList(); + return Name(names); + } + + /// Name ::= CHOICE { -- only one possibility for now -- /// rdnSequence RDNSequence } /// @@ -54,9 +79,16 @@ class Name { for (var n in names) { var set = ASN1Set(); n.forEach((k, v) { + ASN1Object value; + //TODO: Make this more sensible + if(k?.name == 'commonName' || k?.name == 'localityName' || k?.name == 'stateOrProvinceName' || k?.name == 'organizationName' || k?.name == 'organizationUnitName'){ + value = ASN1UTF8String(v); + }else{ + value = fromDart(v); + } set.add(ASN1Sequence() ..add(fromDart(k)) - ..add(fromDart(v))); + ..add(value)); }); seq.add(set); } @@ -100,6 +132,14 @@ class SubjectPublicKeyInfo { SubjectPublicKeyInfo(this.algorithm, this.subjectPublicKey); + factory SubjectPublicKeyInfo.fromPublicKey(PublicKey subjectPublicKey){ + if(subjectPublicKey is RsaPublicKey){ + var ai = AlgorithmIdentifier.fromOiReadableName('rsaEncryption'); + return SubjectPublicKeyInfo(ai, subjectPublicKey); + } + throw ArgumentError('PublicKey type ${subjectPublicKey.runtimeType} not supported'); + } + factory SubjectPublicKeyInfo.fromAsn1(ASN1Sequence sequence) { final algorithm = AlgorithmIdentifier.fromAsn1(sequence.elements[0] as ASN1Sequence); @@ -121,6 +161,12 @@ class SubjectPublicKeyInfo { ..add(algorithm.toAsn1()) ..add(keyToAsn1(subjectPublicKey)); } + + /// This function returns the bytes that represent the public key + /// this is useful for generating the subjectKeyIdentifier + Uint8List get publicKeyBytes{ + return keyToAsn1(subjectPublicKey).contentBytes(); + } } class AlgorithmIdentifier { @@ -129,6 +175,11 @@ class AlgorithmIdentifier { AlgorithmIdentifier(this.algorithm, this.parameters); + factory AlgorithmIdentifier.fromOiReadableName(String readableName){ + var algorithm = ObjectIdentifier.fromOiReadableName(readableName); + return AlgorithmIdentifier(algorithm, null); + } + /// AlgorithmIdentifier ::= SEQUENCE { /// algorithm OBJECT IDENTIFIER, /// parameters ANY DEFINED BY algorithm OPTIONAL } @@ -277,3 +328,7 @@ Iterable parsePem(String pem) sync* { yield _parseDer(bytes, type); } } + +abstract class ToAsn1{ + T toAsn1(); +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index a532ddb..0f501fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,10 +12,13 @@ dependencies: asn1lib: ^1.0.0 quiver: ^3.0.0 crypto_keys: ">=0.2.0 <0.4.0" + basic_utils: ^5.0.0 + crypto: ^3.0.0 + collection: ^1.0.0 dev_dependencies: test: ^1.0.0 - lints: ^2.0.0 + lints: ^3.0.0 false_secrets: - test/files/* diff --git a/test/compare_util_test.dart b/test/compare_util_test.dart new file mode 100644 index 0000000..6b24a00 --- /dev/null +++ b/test/compare_util_test.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:asn1lib/asn1lib.dart'; +import 'package:test/test.dart'; +import 'package:x509/src/asn1_util.dart'; +import 'package:x509/x509.dart'; + +void main() { + group('ASN1 compare utility', () { + test('compare 2 ASN1 objects with differences', () { + var serverPem = File('test/files/self-signed/server.crt').readAsStringSync(); + var caPem = File('test/files/self-signed/rootCA.crt').readAsStringSync(); + var serverKeyPem = File('test/files/self-signed/server.key').readAsStringSync(); + var caKeyPem = File('test/files/self-signed/rootCA.key').readAsStringSync(); + var csrPem = File('test/files/self-signed/csr.pem').readAsStringSync(); + var serverCert = parsePem(serverPem).single as X509Certificate; + var caCert = parsePem(caPem).single as X509Certificate; + var serverKey = parsePem(serverKeyPem).single as PrivateKeyInfo; + var caKey = parsePem(caKeyPem).single as PrivateKeyInfo; + var csr = parsePem(csrPem).single as CertificationRequest; + + var extensions = [ + BasicConstraintsExtension(), + KeyUsageExtension.optional(digitalSignature: true, nonRepudiation: true, keyEncipherment: true, dataEncipherment: true), + // The sample is 'demo.mlopshub.com' + SubjectAltNameExtension(names: [DNSName('demo1.mlopshub.com')]), + ]; + + var serverCert2 = X509Certificate.selfSigned(caKey.keyPair.privateKey!, + csr, caCert: caCert, serialNumber: BigInt.parse('302062104447233620017396921218438266574345124003'), + from: DateTime.utc(2024,1,4,3,16,44), days: 365, extensions: extensions); + + var result = ASN1CompareUtil.compareTree(serverCert.toAsn1(), serverCert2.toAsn1()); + if(result.isDifferent){ + result.printDiff(); + } + expect(result.isDifferent, true); + }); + }); +} \ No newline at end of file diff --git a/test/files/self-signed/csr.pem b/test/files/self-signed/csr.pem new file mode 100644 index 0000000..a02f529 --- /dev/null +++ b/test/files/self-signed/csr.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIDGDCCAgACAQAwgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh +MRYwFAYDVQQHDA1TYW4gRnJhbnNpc2NvMREwDwYDVQQKDAhNTG9wc0h1YjEVMBMG +A1UECwwMTWxvcHNIdWIgRGV2MRowGAYDVQQDDBFkZW1vLm1sb3BzaHViLmNvbTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbkO5WOjZNj6kLunObLbp+n +RQbDUeeXlSxdlc0dJMsaHbpBi0JjqEXdiLjGftVBBXz8fKPjr6UrEeGmj6H1jOGx +qmjM+0fwAEs76N8tKy0T29ElUecNtjAQ8Cm746hMpemNv2VWgXtFywo1WR/zJDAh +hkIab5Lc8mfx3Nao5FNkF4B8CF6XrOZFU9eO4C3Lh7mLYDvyPnBr/m6be/rB+0QK +KbwwpbBQy/1VHkm/CSiYQ3JLSzDnIq50cE4FN8f4QP7X6X/smt8UAQA3df56tRkb +dnTRieTJghOFDnL+CmeTGESOv+X6IY2JR8r6vmUNmzHm8E9oAjl/u8MxZt+J53kC +AwEAAaBSMFAGCSqGSIb3DQEJDjFDMEEwPwYDVR0RBDgwNoIRZGVtby5tbG9wc2h1 +Yi5jb22CFXd3dy5kZW1vLm1sb3BzaHViLmNvbYcEwKgBBYcEwKgBBjANBgkqhkiG +9w0BAQsFAAOCAQEAb0H0x0zXJFa8DQM+SB6ZjiU0gZ6evLwoJwnQAqjuw6OclZyw +XXRfIBZfNZq8wXGR9f9CP65naUeLrMmJPzZueCCFQfwVD6GnmHuoZUsqv88WvCr7 +eJzILiEQaaDYGJobkVqKpocwdpgnIrqJIr47MGOp99b0/52mjJZoJBPuL926X3rx ++AHA0PNW4jwOcbClI99sK2aswHs4uM0Jpw+cRLpI5yP+g9yovNwmx4N0tpeqfMGw +hp2Ddnw/iiRsoqoiPnSgad5sPUBjfEVH2u8o3QfooBX+XtOiuoEBir9HdBfI0s8S +lmTZ+1/wJicnGuVtgpxlm+kqVJdOX5bh23V0BA== +-----END CERTIFICATE REQUEST----- diff --git a/test/files/self-signed/rootCA.crt b/test/files/self-signed/rootCA.crt new file mode 100644 index 0000000..ad52554 --- /dev/null +++ b/test/files/self-signed/rootCA.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIUGAMZwtcAFLHCTIhFnvkTndFo0Z4wDQYJKoZIhvcNAQEL +BQAwQTEaMBgGA1UEAwwRZGVtby5tbG9wc2h1Yi5jb20xCzAJBgNVBAYTAlVTMRYw +FAYDVQQHDA1TYW4gRnJhbnNpc2NvMB4XDTI0MDEwNDAwNTM0NFoXDTI0MTIyNTAw +NTM0NFowQTEaMBgGA1UEAwwRZGVtby5tbG9wc2h1Yi5jb20xCzAJBgNVBAYTAlVT +MRYwFAYDVQQHDA1TYW4gRnJhbnNpc2NvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxVBMwjcx2+KMq5n1JaSFsp7epTPRNKh1MOxnKr61dCT/ZyiUdoBE +0D7r0hqZMAlbz6a+OiVebA4ABPvvMkTtdS3eFGKPSaA5Seem7qNn7w0zMWIptexJ +QO2cBbMp8wB4qpRd9I5ZQQqAcOPTm3YZ+ooRaWjOPXBh0nM6dw83SgqBL0IkB9XD +yJNq076quCDxnK3JtCtaumYy/bTVU0rhgrfiYoLG3N5xxHiaI5NqG8TypccZPB2J +vUq6+PCt9Tt0sPhOrtbalSQOeCkp3BL97t52pwAmBVnAGdpkDU7ddyTca01lZLnp +ss6hUdKwWV2+YcJmYElIXr/spVIqPoks1wIDAQABo1MwUTAdBgNVHQ4EFgQUnh1+ +egyhYezEn9u5HjO9+KwEXDEwHwYDVR0jBBgwFoAUnh1+egyhYezEn9u5HjO9+KwE +XDEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAsanzk+ihLEie +nJvTzQgHUda8BebpTj2G/1A+L7UeTH3lAKOMcd+mjNojHrSrUln8juKWdN+3a02G +IcWZ+KWDQKhyCAfPINESPK61BT/fzRfXbdcnU6SQpZXruQKCtl4InJYjSgeQEGhT +ISQCbtIPAkqHPdcJePHGpCSM/PCGAPTBh/itkLheVW0ymoGvjIkpa4urbHYyJeir +khQPwEjLKivruRehLRk5Mpgr75DIRMz/d44zMFsi5HvRev1WZHjINKvX6i2yHDKS +Ow8foaQkVLLFOSyRN8N250XOah9iFjRik5mOdqVI/j5nkC3h6aw3SsKwee7/5WDS +bZhN1cTgeQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/files/self-signed/rootCA.key b/test/files/self-signed/rootCA.key new file mode 100644 index 0000000..facbc6c --- /dev/null +++ b/test/files/self-signed/rootCA.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDFUEzCNzHb4oyr +mfUlpIWynt6lM9E0qHUw7GcqvrV0JP9nKJR2gETQPuvSGpkwCVvPpr46JV5sDgAE +++8yRO11Ld4UYo9JoDlJ56buo2fvDTMxYim17ElA7ZwFsynzAHiqlF30jllBCoBw +49Obdhn6ihFpaM49cGHSczp3DzdKCoEvQiQH1cPIk2rTvqq4IPGcrcm0K1q6ZjL9 +tNVTSuGCt+Jigsbc3nHEeJojk2obxPKlxxk8HYm9Srr48K31O3Sw+E6u1tqVJA54 +KSncEv3u3nanACYFWcAZ2mQNTt13JNxrTWVkuemyzqFR0rBZXb5hwmZgSUhev+yl +Uio+iSzXAgMBAAECggEARZY5S/F9UwgGBmsIqxrn0AP5j5fDu2eB1Rw6kjep9Okr +be1A1r4MBsl6D6Izzl4ulABBbG5La55QYXGcfUUT3dSsUtIjqJqqOadKFzVn7UXh +fRQak79eYliqQtRaAgpzg+1JaXtefPLAM5AVy+kN5m8VDvDYc5CT302mG+Ew5ZpJ +XWLMoq3l8X3D+7nloaE8dvAs+N1kor8xxO8HsWm7GdmQ+3dCpoVXSBLGmh/hWIO3 +ja4Qoa6UNXQHOvPWAkDpH+3P27uat0VU5n2Y9tezniwM8Fp/Fuv3u+pxbdbbJe3I +cQix2j9oMI4P6zPq6iAs5GmrP1c9X+eGmikjllbAEQKBgQDJ1771JCvp/laBgwsH ++14vE1yyG2etUUE93Jzu+jJy8dkVczfzbPW0+M0B/a2IlIRftS44ck5b4xr6Nkq1 +g/2THD2tnOtedI4NH1zZDFhoUllHi+nHsQ+EYyj+B/RSdDXe069Cx+6kTZdhttCy +x4E+mrRVR2V4oshsK4GKRn9uTwKBgQD6QXVQ1P4wDokvyVfPwWoHZOFyaJc5Scdq +M4xYmnNIZDoAl8n/3Y/SR0oB009Rx4aS25+ZB5PyHnTcGJ6gpEq/UETTtu+zKlfp +RQLFezj3vw8XHFWKikNFL1eRdTq4xNTCK+QPMmUZ3fa5tF+ouhxO0KbR5blwEa9p +3LGD3/R++QKBgB90TKDdKy7C4O474qsyxAGoDcj/tk9vGzCtwZMIUHZTNBZAp1Z7 +A2tZXnFR/AoNwvc7P+GyBn4RTTHy5f+Vex3Cx+XXT2Kf3Uc0PP7iCqDvPFSG/D6P +XDCwV0IHMU7sJzz7VhOdHpZiNRYYLDvAFWcRKssjXi/Hhl49BWnsBI3HAoGAWDRY +IBxiVxfYfJJPs/cs4txIpeV9X4DEm4b9sYEGnv5Mf0cAuIEkHu5nhEsxStazdPGZ +x/smxxC7CZRX3LDrc5DcIW75/0EuaRacynQK+S9LJ08iS0k+OpVcHPWfs94USzfj +EwQlJD/apUuQ58xpC9J46cQ5Xums1PgnTR7TcykCgYEAw6izAeUg3yAuS7VOFPaD +KY0Hkkgg7P0LHlpjM3XTaGdacUWKhViGlHraOGs0C3dAV+2aav7gW58R3oHzkkSS +134Jt9nJ9aJqqIrE324xDg7l6YCY4w25zGg0PTrO8yvJHM4BZxNB2odNTXsY7rbT +xrbQM6zqbJnyG1/Ot0XaEjw= +-----END PRIVATE KEY----- diff --git a/test/files/self-signed/server.crt b/test/files/self-signed/server.crt new file mode 100644 index 0000000..c4b2be4 --- /dev/null +++ b/test/files/self-signed/server.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDyDCCArCgAwIBAgIUNOju53AjNRSKCAs96cA2+IWMuKMwDQYJKoZIhvcNAQEL +BQAwQTEaMBgGA1UEAwwRZGVtby5tbG9wc2h1Yi5jb20xCzAJBgNVBAYTAlVTMRYw +FAYDVQQHDA1TYW4gRnJhbnNpc2NvMB4XDTI0MDEwNDAzMTY0NFoXDTI1MDEwMzAz +MTY0NFowgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYD +VQQHDA1TYW4gRnJhbnNpc2NvMREwDwYDVQQKDAhNTG9wc0h1YjEVMBMGA1UECwwM +TWxvcHNIdWIgRGV2MRowGAYDVQQDDBFkZW1vLm1sb3BzaHViLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbkO5WOjZNj6kLunObLbp+nRQbDUeeX +lSxdlc0dJMsaHbpBi0JjqEXdiLjGftVBBXz8fKPjr6UrEeGmj6H1jOGxqmjM+0fw +AEs76N8tKy0T29ElUecNtjAQ8Cm746hMpemNv2VWgXtFywo1WR/zJDAhhkIab5Lc +8mfx3Nao5FNkF4B8CF6XrOZFU9eO4C3Lh7mLYDvyPnBr/m6be/rB+0QKKbwwpbBQ +y/1VHkm/CSiYQ3JLSzDnIq50cE4FN8f4QP7X6X/smt8UAQA3df56tRkbdnTRieTJ +ghOFDnL+CmeTGESOv+X6IY2JR8r6vmUNmzHm8E9oAjl/u8MxZt+J53kCAwEAAaN4 +MHYwHwYDVR0jBBgwFoAUnh1+egyhYezEn9u5HjO9+KwEXDEwCQYDVR0TBAIwADAL +BgNVHQ8EBAMCBPAwHAYDVR0RBBUwE4IRZGVtby5tbG9wc2h1Yi5jb20wHQYDVR0O +BBYEFC0qi/0OLLOAvbPsQxw00gTWG7WBMA0GCSqGSIb3DQEBCwUAA4IBAQBY+1EI +VzinT9lpBrXSIxRAxzCLBa1cHGPDEJ5MPBWsrFyrO+StI04JU/N2PF/rKQ7NJu3R +sBVrDoDvH9wOhu6/qP4UZltBD938+MO6NpWokAKtDSI9Y5ae+2KhZqvPKGe8CO3C +Wyn8PQf/i/mjRJTHDd1cqudzI2kpIfx0SC47YSdCc8i5loK8ptuyyE0ufzhBej8P +6Rnort3K9VlVjmXFHxIKgO0/+n+CQx+0VbUxG7xulh1kpyoZ9Uo8djN/aBjl6kv1 +yRff1Emtn4Yjr+MOkOwzs92PiuXqGRvAer0QAvk9VDqiPvUjKQFFHgfCBS9X2L6/ +65OOB1enxUFYCaXe +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/files/self-signed/server.key b/test/files/self-signed/server.key new file mode 100644 index 0000000..cec195d --- /dev/null +++ b/test/files/self-signed/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCm5DuVjo2TY+pC +7pzmy26fp0UGw1Hnl5UsXZXNHSTLGh26QYtCY6hF3Yi4xn7VQQV8/Hyj46+lKxHh +po+h9YzhsapozPtH8ABLO+jfLSstE9vRJVHnDbYwEPApu+OoTKXpjb9lVoF7RcsK +NVkf8yQwIYZCGm+S3PJn8dzWqORTZBeAfAhel6zmRVPXjuAty4e5i2A78j5wa/5u +m3v6wftECim8MKWwUMv9VR5JvwkomENyS0sw5yKudHBOBTfH+ED+1+l/7JrfFAEA +N3X+erUZG3Z00YnkyYIThQ5y/gpnkxhEjr/l+iGNiUfK+r5lDZsx5vBPaAI5f7vD +MWbfied5AgMBAAECggEAArGO06JeHxme+48R2bjBU5LVzp7i6Me9yN5Gz21vvVZx +0eKCbqMgoWYKsZ7Eh8kZn1OM6HFBkbsg+gEf8td9e1wMec4LJTVWr+Aq6UU3m+3k +35qIWKAUR3DSi33klgAMsnkO9IIBq31sIkFLq7QXKq6z1cfK8rYdtHdHHvDRZJYj +hbmO7RsxC6ctt5/OlF8Hr1u4xvSbrdYrc4w64mt1Bsfaf0o+p4u+FmLzzKuq95BS +yr4pRteYLgmICSSovHRmK6VcHapvxapXLebV8FdB7Qg1AzLQjpn44McRqogou41T +uCCX+JvRCSh5l5rG51HOAOF5mRmD54H3gouzG4l9QQKBgQC7drKkcfQvDk3jNo68 +ijsq9/jIbQ5IX9OH2Mx5i8GM7ZuZmBaSwk/WG4UtPujhXI3skAlbK7ZieN6vHlE2 +y4oASWCzD0EiGCoHmsqSBf/ZtwpTZoaP8WoY7mpJFq7ohopYpOn1AksLXd/L3f+B +68hrbz9RIymQO2rHvome5pA/WQKBgQDj6CA9dp+0GCtio6gsyu05oRqESEr4USGe +49E0QQ49RTd++6+Y4TOSUZ4KLthr3tkP5P3dJELWM5kBGUTSyud54Dxl7lPf2qwN +ydyAdNURKcOtwglyf544f5LT+oN3YhQeU4Mgozx0KGYUn4FQqMcDqtIaGj8Mo8cj +c6H++b0FIQKBgASmq0P9N8u7FR+gCOaQn2svf9KpMgOFrR/ftyME3qZ6drPW3CiD +/asYP7OhrfF5dGP8Jt9GNF45FX1OyUEMx72+FFIc/Ma1xsUth/0bfP+P1QfAsXH8 +0V8Q+z4Y+/n07JXKcauMhQQhLh5GwcIdcXmI5w9CShO0BbAzAAMbQVTxAoGAUmEQ +bkcXmRh3bjMr0e0T7JXQKOqctr9UwMMmVpYBWKJRWgQNx9v3MTdxQcsHDY7CtR0X +qjy2MAj8kEoa93rCSuqDynBoPu0i7eT+YCxa69ZF1ePiWFHK1i8+2oKdzKRWE1Qq +fLykWHRV2bSCIK6xKSEwyqCcE6yLicP9VVXePiECgYEAir8rtHikjLeOEY9C1QaU +92IP+4r9yf6qp/5r+I/T5iyqEb0Q/dMoL/cxY1UwDf2MsuIHHoDPG8Z+nbqSgfuN +bt2DiH7g8yxEUSsaxbmzQAkvdMpkBUdKEXoXre5Jq3bdh1C9VoziX8ig/DcxLwJc +VHvgxI7NIqY6K7pexPtCbMQ= +-----END PRIVATE KEY----- diff --git a/test/objectidentifier_test.dart b/test/objectidentifier_test.dart index c7342b9..e37a22e 100644 --- a/test/objectidentifier_test.dart +++ b/test/objectidentifier_test.dart @@ -1,7 +1,20 @@ +import 'package:asn1lib/asn1lib.dart'; import 'package:test/test.dart'; import 'package:x509/x509.dart'; void main() { + group('equality', (){ + test('equals',(){ + expect(ObjectIdentifier([2, 5, 4, 3]) == ObjectIdentifier([2, 5, 4, 3]), true); + }); + test('not equals last node different',(){ + expect(ObjectIdentifier([2, 5, 4, 3]) != ObjectIdentifier([2, 5, 4, 4]), true); + }); + test('not equals last node same',(){ + expect(ObjectIdentifier([2, 5, 4, 3]) != ObjectIdentifier([2, 5, 8, 3]), true); + }); + }); + group('name', () { test('return correct name', () { var oid = ObjectIdentifier([2, 5, 4, 3]); @@ -12,5 +25,28 @@ void main() { var oid = ObjectIdentifier([1, 2, 3, 4, 5, 6]); expect(() => oid.name, throwsA(TypeMatcher())); }); + + test('from name', (){ + var oid = ObjectIdentifier.fromOiReadableName('commonName'); + print(oid); + expect(oid.name, 'commonName'); + }); + }); + + group('asn.1 conversion', (){ + test('convert to asn.1', (){ + var oidOriginal = ObjectIdentifier([2, 5, 4, 3]); + var asn1Original = oidOriginal.toAsn1(); + var bytes = asn1Original.encodedBytes; + var asn1Restored = ASN1ObjectIdentifier.fromBytes(bytes); + var oidRestored = ObjectIdentifier.fromAsn1(asn1Restored); + expect(oidOriginal == oidRestored, true); + }); + test('convert from asn.1', (){ + var oidOriginal = ObjectIdentifier([2, 5, 4, 3]); + var asn1Original = ASN1ObjectIdentifier([2, 5, 4, 3]); + var oidFromAsn1 = ObjectIdentifier.fromAsn1(asn1Original); + expect(oidOriginal == oidFromAsn1, true); + }); }); } \ No newline at end of file diff --git a/test/x509_test.dart b/test/x509_test.dart index 27d4aff..3321000 100644 --- a/test/x509_test.dart +++ b/test/x509_test.dart @@ -1,7 +1,10 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; +import 'package:crypto/crypto.dart'; import 'package:test/test.dart'; +import 'package:x509/src/asn1_util.dart'; import 'package:x509/x509.dart'; import 'dart:io'; import 'package:asn1lib/asn1lib.dart'; @@ -167,7 +170,24 @@ void main() { group('csr', () { test('parse csr', () { var pem = File('test/files/csr.pem').readAsStringSync(); - parsePem(pem).single as CertificationRequest; + var csr = parsePem(pem).single as CertificationRequest; + expect(csr is CertificationRequest, true); + }); + test('parse csr with extensions', () { + var pem = File('test/files/self-signed/csr.pem').readAsStringSync(); + var csr = parsePem(pem).single as CertificationRequest; + expect(csr is CertificationRequest, true); + }); + test('generate csr', () { + var pem = File('test/files/self-signed/csr.pem').readAsStringSync(); + var csr = parsePem(pem).single as CertificationRequest; + var bytes = csr.toAsn1().encodedBytes; + var p = ASN1Parser(bytes); + var newAsn1 = p.nextObject() as ASN1Sequence; + var newCsr = CertificationRequest.fromAsn1(newAsn1); + var newBytes = newCsr.toAsn1().encodedBytes; + + expect(ListEquality().equals(bytes, newBytes), true); }); }); @@ -458,4 +478,144 @@ MIIIHzCCB8WgAwIBAgIJf35N0O0if7S5MAoGCCqGSM49BAMCMIGwMT8wPQYDVQQDDDZFQURUcnVzdCBF expect(c, isA()); }); }); + + group('self signed', (){ + + test('Parse root CA cert generated through openssl', (){ + var pem = File('test/files/self-signed/rootCA.crt').readAsStringSync(); + var cert = parsePem(pem).single as X509Certificate; + var pem2 = cert.toPem(); + expect(pem2, pem); + }); + + test('Generate CA same as sample generated through openssl', (){ + var pem = File('test/files/self-signed/rootCA.crt').readAsStringSync(); + var privateKeyPem = File('test/files/self-signed/rootCA.key').readAsStringSync(); + var cert = parsePem(pem).single as X509Certificate; + var privateKey = parsePem(privateKeyPem).single as PrivateKeyInfo; + var subject = Name.fromMap({ + 'commonName' : 'demo.mlopshub.com', + 'countryName' : 'US', + 'localityName' : 'San Fransisco', + }); + var from = DateTime.utc(2024, 1, 4, 0, 53, 44); + var cert2 = X509Certificate.generateCA( + name: subject, + keyPair: privateKey.keyPair, + serialNumber: BigInt.parse('137084924843079655944714502240399509422757958046'), + from: from, + days: 356, + ); + ASN1CompareUtil.compareTree(cert.tbsCertificate.toAsn1(), cert2.tbsCertificate.toAsn1()); + var bytes1 = cert.tbsCertificate.toAsn1().valueBytes(); + var bytes2 = cert2.tbsCertificate.toAsn1().valueBytes(); + expect(ListEquality().equals(bytes1, bytes2), true); + var pem2 = cert2.toPem(); + expect(pem2, pem); + }); + + test('Validate keyIdentifier for CA cert', (){ + var pem = File('test/files/self-signed/rootCA.crt').readAsStringSync(); + var cert = parsePem(pem).single as X509Certificate; + var publicKeyBytes = cert.tbsCertificate.subjectPublicKeyInfo?.publicKeyBytes; + var sha1Value = sha1.convert(publicKeyBytes!).bytes; + var ski = (cert.tbsCertificate.extensions?[0].extnValue as SubjectKeyIdentifier).keyIdentifier; + var aki = (cert.tbsCertificate.extensions?[1].extnValue as AuthorityKeyIdentifier).keyIdentifier; + expect(ListEquality().equals(sha1Value, ski), true); + expect(ListEquality().equals(sha1Value, aki), true); + }); + + test('Parse self signed certificate generated through openssl', (){ + var pem = File('test/files/self-signed/server.crt').readAsStringSync(); + var cert = parsePem(pem).single as X509Certificate; + var pem2 = cert.toPem(); + expect(pem, pem2); + }); + + test('Parse self signed certificate generated through openssl same asn', (){ + var pem = File('test/files/self-signed/server.crt').readAsStringSync(); + var lines = pem + .split('\n') + .map((line) => line.trim()) + .where((line) => line.isNotEmpty) + .toList(); + var coded = lines.sublist(1, lines.length - 1).join(''); + var der = base64.decode(coded); + var originalParser = ASN1Parser(der); + var originalAsn1 = originalParser.nextObject() as ASN1Sequence; + var originalAttributesAsn = ASN1Parser((originalAsn1.elements[0] as ASN1Sequence).elements[7].valueBytes()).nextObject(); + + var cert = parsePem(pem).single as X509Certificate; + var generatedAsn1 = cert.toAsn1(); + var generatedCoded = generatedAsn1.encodedBytes; + var originalCoded = originalAsn1.encodedBytes; + var generatedAttributesAsn = ASN1Parser((generatedAsn1.elements[0] as ASN1Sequence).elements[7].valueBytes()).nextObject(); + expect(originalCoded.length, generatedCoded.length); + ASN1CompareUtil.compareTree(originalAsn1, generatedAsn1); + ASN1CompareUtil.compareTree(originalAttributesAsn, generatedAttributesAsn); + expect(ListEquality().equals(originalAsn1.encodedBytes, generatedAsn1.encodedBytes), true); + }); + + test('Generate csr same as sample', (){ + var pem = File('test/files/self-signed/csr.pem').readAsStringSync(); + var privateKeyPem = File('test/files/self-signed/server.key').readAsStringSync(); + var csr = parsePem(pem).single as CertificationRequest; + var privateKey = parsePem(privateKeyPem).single as PrivateKeyInfo; + + var subjectPublicKeyInfo = SubjectPublicKeyInfo.fromPublicKey(privateKey.keyPair.publicKey!); + var subject = Name.fromMap({ + 'countryName' : 'US', + 'stateOrProvinceName' : 'California', + 'localityName' : 'San Fransisco', + 'organizationName' : 'MLopsHub', + 'organizationUnitName' : 'MlopsHub Dev', + 'commonName' : 'demo.mlopshub.com', + }); + var serverSubjectAlternateNames = SubjectAltNameExtension(names: [ + DNSName('demo.mlopshub.com'), + DNSName('www.demo.mlopshub.com'), + IPAddressName(192, 168, 1, 5), + IPAddressName(192, 168, 1, 6), + ]); + var attributes = Attributes([ExtensionRequestAttribute([ + serverSubjectAlternateNames + ])]); + var csri = CertificationRequestInfo(subject, subjectPublicKeyInfo, attributes: attributes); + var csr2 = CertificationRequest.generate(csri, privateKey.keyPair.privateKey!); + ASN1CompareUtil.compareTree(csr.toAsn1(), csr2.toAsn1()); + var bytes1 = csr.toAsn1().valueBytes(); + var bytes2 = csr.toAsn1().valueBytes(); + expect(ListEquality().equals(bytes1, bytes2), true); + }); + + test('Generate server certificate same as sample', (){ + var serverPem = File('test/files/self-signed/server.crt').readAsStringSync(); + var caPem = File('test/files/self-signed/rootCA.crt').readAsStringSync(); + var serverKeyPem = File('test/files/self-signed/server.key').readAsStringSync(); + var caKeyPem = File('test/files/self-signed/rootCA.key').readAsStringSync(); + var csrPem = File('test/files/self-signed/csr.pem').readAsStringSync(); + var serverCert = parsePem(serverPem).single as X509Certificate; + var caCert = parsePem(caPem).single as X509Certificate; + var serverKey = parsePem(serverKeyPem).single as PrivateKeyInfo; + var caKey = parsePem(caKeyPem).single as PrivateKeyInfo; + var csr = parsePem(csrPem).single as CertificationRequest; + + var extensions = [ + BasicConstraintsExtension(), + KeyUsageExtension.optional(digitalSignature: true, nonRepudiation: true, keyEncipherment: true, dataEncipherment: true), + SubjectAltNameExtension(names: [DNSName('demo.mlopshub.com')]), + ]; + + var serverCert2 = X509Certificate.selfSigned(caKey.keyPair.privateKey!, + csr, caCert: caCert, serialNumber: BigInt.parse('302062104447233620017396921218438266574345124003'), + from: DateTime.utc(2024,1,4,3,16,44), days: 365, extensions: extensions); + + ASN1CompareUtil.compareTree(serverCert.toAsn1(), serverCert2.toAsn1()); + var bytes1 = serverCert.toAsn1().valueBytes(); + var bytes2 = serverCert2.toAsn1().valueBytes(); + expect(ListEquality().equals(bytes1, bytes2), true); + var serverPem2 = serverCert2.toPem(); + expect(serverPem2, serverPem); + }); + }); }