diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/main.dart b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/main.dart index e946097b18c..a3fd2f5c2cc 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/main.dart +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:catalyst_cardano/catalyst_cardano.dart'; import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/sign_and_submit_rbac_tx.dart b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/sign_and_submit_rbac_tx.dart index 9a40fffac96..5ad2c02bd6f 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/sign_and_submit_rbac_tx.dart +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/sign_and_submit_rbac_tx.dart @@ -2,63 +2,6 @@ part of 'main.dart'; -final _derCert = X509DerCertificate.fromHex( - ''' -308202343082019DA00302010202145A -FC371DAF301793CF0B1835A118C2F903 -63D5D9300D06092A864886F70D01010B -05003045310B30090603550406130241 -553113301106035504080C0A536F6D65 -2D53746174653121301F060355040A0C -18496E7465726E657420576964676974 -7320507479204C7464301E170D323430 -3731313038353733365A170D32353037 -31313038353733365A3045310B300906 -03550406130241553113301106035504 -080C0A536F6D652D5374617465312130 -1F060355040A0C18496E7465726E6574 -205769646769747320507479204C7464 -30819F300D06092A864886F70D010101 -050003818D0030818902818100CD28E2 -0B157CA70C85433C1689B1D5890EC479 -BDD1FFDCC5647AE12BE9BADF4AF20764 -CD24BD64130831A57506DFBBDD3E924C -96B259C6CCEDF24D6A25618F0819643C -739F145B733C3C94333E5937B499ADA9 -A4FFC127457C7CB557F2F5623DCADEA1 -E06F09129DB9584B0AEE949244B3252B -52AFDE5D385C65E563A6EFB07F020301 -0001A321301F301D0603551D0E041604 -1492EB169818B833588321957A846077 -AA239CF3A0300D06092A864886F70D01 -010B0500038181002E5F73333CE667E4 -172B252416EAA1D2E9681F59943724B4 -F366A8B930443CA6B69B12DD9DEBEE9C -8A6307695EE1884DA4B00136195D1D82 -23D1C253FF408EDFC8ED03AF1819244C -35D3843855FB9AF86E84FB7636FA3F4A -0FC396F6FB6FD16D3BCEBDE68A8BD81B -E61E8EE7D77E9F7F9804E03EBC31B458 -1313C955A667658B -''' - .replaceAll('\n', ''), -); - -final _c509Cert = C509Certificate.fromHex( - ''' -004301F50D6B524643207465737420 -43411A63B0CD001A6955B90047010123 -456789AB01582102B1216AB96E5B3B33 -40F5BDF02E693F16213A04525ED44450 -B1019C2DFD3838AB010058406FC90301 -5259A38C0800A3D0B2969CA21977E8ED -6EC344964D4E1C6B37C8FB541274C3BB -81B2F53073C5F101A5AC2A92886583B6 -A2679B6E682D2A26945ED0B2 -''' - .replaceAll('\n', ''), -); - const _catalystUserRoleRegistrationPurpose = 'ca7a1457-ef9f-4c7f-9c74-7f8c4a4cfa6c'; @@ -73,6 +16,7 @@ Future _signAndSubmitRbacTx({ var result = ''; try { final changeAddress = await api.getChangeAddress(); + final rewardAddresses = await api.getRewardAddresses(); final utxos = await api.getUtxos( amount: const Balance( @@ -86,6 +30,7 @@ Future _signAndSubmitRbacTx({ final x509Envelope = await _buildMetadataEnvelope( utxos: utxos, + rewardAddress: rewardAddresses.first, ); final auxiliaryData = AuxiliaryData.fromCbor( @@ -95,11 +40,16 @@ Future _signAndSubmitRbacTx({ final unsignedTx = _buildUnsignedRbacTx( inputs: utxos, changeAddress: changeAddress, + rewardAddress: rewardAddresses.first, auxiliaryData: auxiliaryData, ); + print('unsigned tx: ${hex.encode(cbor.encode(unsignedTx.toCbor()))}'); + final witnessSet = await api.signTx(transaction: unsignedTx); + print('Witness set: $witnessSet'); + final signedTx = Transaction( body: unsignedTx.body, isValid: true, @@ -107,6 +57,8 @@ Future _signAndSubmitRbacTx({ auxiliaryData: unsignedTx.auxiliaryData, ); + print('signed tx: ${hex.encode(cbor.encode(signedTx.toCbor()))}'); + final txHash = await api.submitTx(transaction: signedTx); result = 'Tx hash: ${txHash.toHex()}'; } catch (error) { @@ -124,22 +76,29 @@ Future _signAndSubmitRbacTx({ Future> _buildMetadataEnvelope({ required List utxos, + required ShelleyAddress rewardAddress, }) async { - final keyPair = - await Ed25519KeyPair.fromSeed(Ed25519PrivateKey.seeded(0).bytes); + final seed = List.generate( + Ed25519PrivateKey.length, + (i) => Random().nextInt(256), + ); + + final keyPair = await Ed25519KeyPair.fromSeed(seed); + + final cert = await generateX509Certificate( + keyPair: keyPair, + stakeAddress: rewardAddress, + ); + + final derCert = cert.toDer(); final x509Envelope = X509MetadataEnvelope.unsigned( purpose: UuidV4.fromString(_catalystUserRoleRegistrationPurpose), txInputsHash: TransactionInputsHash.fromTransactionInputs(utxos), previousTransactionId: _transactionHash, chunkedData: RegistrationData( - derCerts: [_derCert], - cborCerts: [_c509Cert], + derCerts: [derCert], publicKeys: [keyPair.publicKey], - revocationSet: [ - CertificateHash.fromX509DerCertificate(_derCert), - CertificateHash.fromC509Certificate(_c509Cert), - ], roleDataSet: { RoleData( roleNumber: 0, @@ -150,7 +109,7 @@ Future> _buildMetadataEnvelope({ ), ), roleEncryptionKey: KeyReference( - hash: CertificateHash.fromX509DerCertificate(_derCert), + hash: CertificateHash.fromX509DerCertificate(derCert), ), paymentKey: 0, roleSpecificData: { @@ -195,13 +154,23 @@ Future> _buildMetadataEnvelope({ Transaction _buildUnsignedRbacTx({ required List inputs, required ShelleyAddress changeAddress, + required ShelleyAddress rewardAddress, required AuxiliaryData auxiliaryData, }) { final txBuilder = TransactionBuilder( + requiredSigners: { + rewardAddress.publicKeyHash, + }, config: _buildTransactionBuilderConfig(), inputs: inputs, networkId: NetworkId.testnet, auxiliaryData: auxiliaryData, + witnessBuilder: const TransactionWitnessSetBuilder( + vkeys: {}, + // TODO(dtscalac): investigate if vkeyCount can be found out + // in a better way, count = reward addresses vkeys + payment address vkey + vkeysCount: 2, + ), ); final txBody = txBuilder.withChangeAddressIfNeeded(changeAddress).buildBody(); @@ -213,3 +182,44 @@ Transaction _buildUnsignedRbacTx({ auxiliaryData: auxiliaryData, ); } + +Future generateX509Certificate({ + required Ed25519KeyPair keyPair, + required ShelleyAddress stakeAddress, +}) async { + const maxInt = 4294967296; + + /* cSpell:disable */ + const issuer = X509DistinguishedName( + countryName: 'US', + stateOrProvinceName: 'California', + localityName: 'San Francisco', + organizationName: 'MyCompany', + organizationalUnitName: 'MyDepartment', + commonName: 'mydomain.com', + ); + + final tbs = X509TBSCertificate( + serialNumber: Random().nextInt(maxInt), + subjectPublicKey: keyPair.publicKey, + issuer: issuer, + validityNotBefore: DateTime.now().toUtc(), + validityNotAfter: X509TBSCertificate.foreverValid, + subject: issuer, + extensions: X509CertificateExtensions( + subjectAltName: [ + 'mydomain.com', + 'www.mydomain.com', + 'example.com', + 'www.example.com', + 'web+cardano://addr/${stakeAddress.toBech32()}', + ], + ), + ); + /* cSpell:enable */ + + return X509Certificate.generateSelfSigned( + tbsCertificate: tbs, + keyPair: keyPair, + ); +} diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/catalyst_cardano_serialization.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/catalyst_cardano_serialization.dart index 0a57c968881..d8012ad1c97 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/catalyst_cardano_serialization.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/catalyst_cardano_serialization.dart @@ -9,6 +9,7 @@ export 'src/fees.dart'; export 'src/hashes.dart'; export 'src/rbac/auth_token.dart'; export 'src/rbac/registration_data.dart'; +export 'src/rbac/x509_certificate.dart'; export 'src/rbac/x509_metadata_envelope.dart'; export 'src/redeemer.dart'; export 'src/scripts.dart'; diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/address.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/address.dart index 34f1b453983..03d6f60476b 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/address.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/address.dart @@ -5,6 +5,7 @@ import 'package:bip32_ed25519/bip32_ed25519.dart'; import 'package:catalyst_cardano_serialization/src/exceptions.dart'; +import 'package:catalyst_cardano_serialization/src/hashes.dart'; import 'package:catalyst_cardano_serialization/src/types.dart'; import 'package:cbor/cbor.dart'; import 'package:equatable/equatable.dart'; @@ -78,6 +79,11 @@ class ShelleyAddress extends Equatable implements CborEncodable { /// Returns the [NetworkId] related to this address. NetworkId get network => _extractNetworkId(bytes); + /// Returns the [Ed25519PublicKeyHash] contained in this address. + /// + /// Practically it's the address without the header byte. + Ed25519PublicKeyHash get publicKeyHash => _extractPublicKeyHash(bytes); + /// Encodes the address in bech32 format. String toBech32() { final prefix = _computeHrp(network, hrp); @@ -150,4 +156,11 @@ class ShelleyAddress extends Equatable implements CborEncodable { ? NetworkId.testnet : NetworkId.mainnet; } + + /// Extracts the payload from [bytes]. + /// + /// Format [ 8 bit header | payload ] + static Ed25519PublicKeyHash _extractPublicKeyHash(List bytes) { + return Ed25519PublicKeyHash.fromBytes(bytes: bytes.sublist(1)); + } } diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/exceptions.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/exceptions.dart index d4901aa3062..fb27c133625 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/exceptions.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/exceptions.dart @@ -116,14 +116,17 @@ final class TxValueSizeExceededException extends Equatable /// Exception thrown when parsing a hash that has incorrect length. final class HashFormatException extends Equatable implements Exception { + /// Exception details. + final String? message; + /// The default constructor for [HashFormatException]. - const HashFormatException(); + const HashFormatException([this.message]); @override - String toString() => 'HashFormatException'; + String toString() => 'HashFormatException: $message'; @override - List get props => []; + List get props => [message]; } /// Exception thrown if the address doesn't match the bech32 specification diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/hashes.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/hashes.dart index d90c4a8f679..dd469357520 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/hashes.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/hashes.dart @@ -22,7 +22,7 @@ abstract base class BaseHash extends Equatable implements CborEncodable { /// Constructs the [BaseHash] from raw [bytes]. BaseHash.fromBytes({required this.bytes}) { if (bytes.length != length) { - throw const HashFormatException(); + throw HashFormatException('Expected $length bytes, got: ${bytes.length}'); } } diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/x509_certificate.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/x509_certificate.dart new file mode 100644 index 00000000000..c80e37fdeb7 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/x509_certificate.dart @@ -0,0 +1,352 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:asn1lib/asn1lib.dart'; +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:equatable/equatable.dart'; + +bool _registeredASN1Names = false; + +/// Before we can interact with the asn1lib we must register frequent names, +/// however there's no single entry point to catalyst_cardano_serialization +/// and we must expect different starting points. Therefore we call the method +/// to register these names each time we need to interact with the asn1lib. +void _ensureASN1FrequentNamesRegistered() { + if (!_registeredASN1Names) { + _registerASN1FrequentNames(); + } +} + +void _registerASN1FrequentNames() { + ASN1ObjectIdentifier.registerFrequentNames(); + _registeredASN1Names = true; +} + +/// Represents the X509 encoded certificate. +class X509Certificate with EquatableMixin { + /// The ToBeSigned certificate with details. + final X509TBSCertificate tbsCertificate; + + /// The signature of a signed [tbsCertificate]. + final Uint8List signature; + + /// The default constructor for [X509Certificate]. + const X509Certificate({ + required this.tbsCertificate, + required this.signature, + }); + + /// Generates a self-signed [X509Certificate] from [tbsCertificate] + /// that is signed using the private key of [keyPair]. + static Future generateSelfSigned({ + required X509TBSCertificate tbsCertificate, + required Ed25519KeyPair keyPair, + }) async { + final encodedTbsCertificate = tbsCertificate.toASN1(); + final signature = + await keyPair.privateKey.sign(encodedTbsCertificate.encodedBytes); + + return X509Certificate( + tbsCertificate: tbsCertificate, + signature: Uint8List.fromList(signature.bytes), + ); + } + + /// Encodes the data in ASN1 format. + ASN1Object toASN1() { + _ensureASN1FrequentNamesRegistered(); + + return ASN1Sequence() + ..add(tbsCertificate.toASN1()) + ..add(tbsCertificate._createSignatureAlgorithm()) + ..add(ASN1BitString(signature)); + } + + /// Encodes the data in DER format. + X509DerCertificate toDer() { + return X509DerCertificate.fromBytes(bytes: toASN1().encodedBytes); + } + + /// Converts the certificate to PEM format, uses [label] + /// as the certificate header. + String toPem([String label = 'CERTIFICATE']) { + final derBytes = toASN1().encodedBytes; + final base64 = base64Encode(derBytes); + final chunks = + RegExp('.{1,64}').allMatches(base64).map((m) => m.group(0)).join('\n'); + + return '-----BEGIN $label-----\n' + '$chunks\n' + '-----END $label-----'; + } + + @override + List get props => [tbsCertificate, signature]; +} + +/// The ToBeSigned X509 certificate, contains the details about +/// the [subject] and [issuer] of the certificate. +/// +/// In case of a self-signed certificate the [issuer] will +/// be the same as [subject]. +class X509TBSCertificate with EquatableMixin { + /// A [DateTime] that can be used as [validityNotAfter] + /// to represent a certificate which never expires. + static final DateTime foreverValid = DateTime.utc(9999, 12, 31, 23, 59, 59); + + /// The version of the certificate, the latest one is v3 + /// (int value = 2). + final int version; + + /// The unique serial number of the certificate. + /// A simple integer consisting of up to 20 bytes. + final int serialNumber; + + /// The issuer of the certificate. + /// In case of self-signed it's the same as [subject]. + final X509DistinguishedName issuer; + + /// The timestamp which specifies from when the certificate is valid. + final DateTime validityNotBefore; + + /// The timestamp which specifies until when the certificate is valid. + final DateTime validityNotAfter; + + /// The subject of the certificate. + /// + /// In case of self-signed it's the same as [issuer]. + final X509DistinguishedName subject; + + /// The public key of the [subject]. + final Ed25519PublicKey subjectPublicKey; + + /// Extra extensions of the certificate. + final X509CertificateExtensions? extensions; + + /// The default constructor for [X509TBSCertificate]. + const X509TBSCertificate({ + this.version = 2, + required this.serialNumber, + required this.subjectPublicKey, + required this.issuer, + required this.validityNotBefore, + required this.validityNotAfter, + required this.subject, + this.extensions, + }); + + /// Encodes the data in ASN1 format. + ASN1Object toASN1() { + _ensureASN1FrequentNamesRegistered(); + + final sequence = ASN1Sequence() + ..add(_createVersion(version)) + ..add(_createSerialNumber(serialNumber)) + ..add(_createSignatureAlgorithm()) + ..add(issuer.toASN1()) + ..add(_createValidity()) + ..add(subject.toASN1()) + ..add(_createSubjectPublicKeyInfo(subjectPublicKey)); + + final extensions = this.extensions; + if (extensions != null) { + sequence.add(extensions.toASN1()); + } + + return sequence; + } + + ASN1Object _createVersion(int version) { + return ASN1Sequence(tag: 0xA0)..add(ASN1Integer(BigInt.from(version))); + } + + ASN1Object _createSerialNumber(int serialNumber) { + return ASN1Integer(BigInt.from(serialNumber)); + } + + ASN1Object _createSignatureAlgorithm() { + /* cSpell:disable */ + ASN1ObjectIdentifier.registerObjectIdentiferName( + 'Ed25519', + ASN1ObjectIdentifier.fromComponentString('1.3.101.112'), + ); + /* cSpell:enable */ + + return ASN1Sequence()..add(ASN1ObjectIdentifier.fromName('Ed25519')); + } + + ASN1Object _createValidity() { + // Validity (not before and not after) + return ASN1Sequence() + ..add(ASN1UtcTime(validityNotBefore.toUtc())) + ..add(ASN1UtcTime(validityNotAfter.toUtc())); + } + + ASN1Object _createSubjectPublicKeyInfo(Ed25519PublicKey publicKey) { + return ASN1Sequence() + ..add( + ASN1Sequence() + ..add( + ASN1ObjectIdentifier.fromName('Ed25519'), + ), + ) + ..add(ASN1BitString(publicKey.bytes)); + } + + @override + List get props => [ + version, + serialNumber, + subjectPublicKey, + issuer, + validityNotBefore, + validityNotAfter, + subject, + extensions, + ]; +} + +/// The structure describing the [X509TBSCertificate.issuer] / [X509TBSCertificate.subject]. +class X509DistinguishedName with EquatableMixin { + /// The country name as country code, i.e. "US". + final String? countryName; + + /// The state or province, i.e. California. + final String? stateOrProvinceName; + + /// The location (city), i.e. San Francisco. + final String? localityName; + + /// The organization (foundation, company, etc). + final String? organizationName; + + /// The unit name in the organization (i.e. department of a company/foundation). + final String? organizationalUnitName; + + /// The entity name (domain name or individual's name). + final String? commonName; + + /// The default constructor for [X509DistinguishedName]. + const X509DistinguishedName({ + this.countryName, + this.stateOrProvinceName, + this.localityName, + this.organizationName, + this.organizationalUnitName, + this.commonName, + }); + + /// Encodes the data in ASN1 format. + ASN1Object toASN1() { + _ensureASN1FrequentNamesRegistered(); + + final sequence = ASN1Sequence(); + + final countryName = this.countryName; + if (countryName != null) { + sequence.add( + ASN1Sequence() + ..add(ASN1ObjectIdentifier.fromName('c')) + ..add(ASN1PrintableString(countryName)), + ); + } + + final stateOrProvinceName = this.stateOrProvinceName; + if (stateOrProvinceName != null) { + sequence.add( + ASN1Sequence() + ..add(ASN1ObjectIdentifier.fromName('st')) + ..add(ASN1PrintableString(stateOrProvinceName)), + ); + } + + final localityName = this.localityName; + if (localityName != null) { + sequence.add( + ASN1Sequence() + ..add(ASN1ObjectIdentifier.fromName('l')) + ..add(ASN1PrintableString(localityName)), + ); + } + + final organizationName = this.organizationName; + if (organizationName != null) { + sequence.add( + ASN1Sequence() + ..add(ASN1ObjectIdentifier.fromName('o')) + ..add(ASN1PrintableString(organizationName)), + ); + } + + final organizationalUnitName = this.organizationalUnitName; + if (organizationalUnitName != null) { + sequence.add( + ASN1Sequence() + ..add(ASN1ObjectIdentifier.fromName('ou')) + ..add(ASN1PrintableString(organizationalUnitName)), + ); + } + + final commonName = this.commonName; + if (commonName != null) { + sequence.add( + ASN1Sequence() + ..add(ASN1ObjectIdentifier.fromName('cn')) + ..add(ASN1PrintableString(commonName)), + ); + } + + return sequence; + } + + @override + List get props => [ + countryName, + stateOrProvinceName, + localityName, + organizationName, + organizationalUnitName, + commonName, + ]; +} + +/// Extra extensions of the certificate. +class X509CertificateExtensions with EquatableMixin { + /// List of alternative subject names. + final List? subjectAltName; + + /// The default constructor for the [X509CertificateExtensions]. + const X509CertificateExtensions({this.subjectAltName}); + + /// Encodes the data in ASN1 format. + ASN1Object toASN1() { + _ensureASN1FrequentNamesRegistered(); + + final sequence = ASN1Sequence(tag: 0xA3); + final extensionsSequence = ASN1Sequence(); + sequence.add(extensionsSequence); + + final subjectAltName = this.subjectAltName; + if (subjectAltName != null) { + final subjectAltNameSequence = ASN1Sequence(); + for (final name in subjectAltName) { + subjectAltNameSequence.add(ASN1OctetString(name)); + } + + extensionsSequence.add( + ASN1Sequence() + ..add(ASN1ObjectIdentifier.fromName('subjectAltName')) + ..add(ASN1OctetString(subjectAltNameSequence.encodedBytes)), + ); + } + + if (extensionsSequence.elements.isEmpty) { + return ASN1Null(); + } + + return sequence; + } + + @override + List get props => [subjectAltName]; +} diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/pubspec.yaml b/catalyst_voices_packages/catalyst_cardano_serialization/pubspec.yaml index 5a4c26478e1..b020ae19153 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/pubspec.yaml +++ b/catalyst_voices_packages/catalyst_cardano_serialization/pubspec.yaml @@ -10,6 +10,7 @@ environment: flutter: ">=3.24.1" dependencies: + asn1lib: ^1.5.3 bech32: ^0.2.2 bip32_ed25519: ^0.6.0 catalyst_compression: ^0.3.0 diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/test/address_test.dart b/catalyst_voices_packages/catalyst_cardano_serialization/test/address_test.dart index d124a3667a1..3ad4fc1f628 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/test/address_test.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/test/address_test.dart @@ -1,5 +1,4 @@ -import 'package:catalyst_cardano_serialization/src/address.dart'; -import 'package:catalyst_cardano_serialization/src/types.dart'; +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; import 'package:test/test.dart'; import 'test_utils/test_data.dart'; @@ -64,6 +63,25 @@ void main() { expect(testnetStakeAddr.network, equals(NetworkId.testnet)); }); + test('publicKeyHash from stake address', () { + expect( + mainnetStakeAddr.publicKeyHash, + equals( + Ed25519PublicKeyHash.fromHex( + '337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251', + ), + ), + ); + expect( + testnetStakeAddr.publicKeyHash, + equals( + Ed25519PublicKeyHash.fromHex( + '337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251', + ), + ), + ); + }); + test('toString returns bech32', () { expect(mainnetAddr.toString(), equals(mainnetAddr.toBech32())); expect(testnetAddr.toString(), equals(testnetAddr.toBech32())); diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/test/rbac/x509_certificate_test.dart b/catalyst_voices_packages/catalyst_cardano_serialization/test/rbac/x509_certificate_test.dart new file mode 100644 index 00000000000..7bc9408dc32 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano_serialization/test/rbac/x509_certificate_test.dart @@ -0,0 +1,51 @@ +import 'package:catalyst_cardano_serialization/src/rbac/x509_certificate.dart'; +import 'package:catalyst_cardano_serialization/src/signature.dart'; +import 'package:test/test.dart'; + +void main() { + group(X509Certificate, () { + test('generateSelfSigned X509 certificate', () async { + final seed = List.filled(Ed25519PrivateKey.length, 0); + final keyPair = await Ed25519KeyPair.fromSeed(seed); + + /* cSpell:disable */ + const issuer = X509DistinguishedName( + countryName: 'US', + stateOrProvinceName: 'California', + localityName: 'San Francisco', + organizationName: 'MyCompany', + organizationalUnitName: 'MyDepartment', + commonName: 'mydomain.com', + ); + + final tbs = X509TBSCertificate( + serialNumber: 1, + subjectPublicKey: keyPair.publicKey, + issuer: issuer, + validityNotBefore: DateTime.now().toUtc(), + validityNotAfter: X509TBSCertificate.foreverValid, + subject: issuer, + extensions: const X509CertificateExtensions( + subjectAltName: [ + 'mydomain.com', + 'www.mydomain.com', + 'example.com', + 'www.example.com', + ], + ), + ); + /* cSpell:enable */ + + final certificate = await X509Certificate.generateSelfSigned( + tbsCertificate: tbs, + keyPair: keyPair, + ); + + expect(certificate.tbsCertificate, equals(tbs)); + expect(certificate.signature, isNotEmpty); + + expect(certificate.toPem(), isNotEmpty); + expect(certificate.toDer().bytes, isNotEmpty); + }); + }); +} diff --git a/melos.yaml b/melos.yaml index 23d2f51ef82..7378b9e2027 100644 --- a/melos.yaml +++ b/melos.yaml @@ -18,6 +18,7 @@ command: sdk: ">=3.5.0 <4.0.0" flutter: ">=3.24.1" dependencies: + asn1lib: ^1.5.3 bloc_concurrency: ^0.2.2 collection: ^1.18.0 cryptography: ^2.7.0