From e367cc3f64bb694791f210e5c1d87b72b8e26d5b Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Mon, 16 Dec 2024 12:51:11 +0100 Subject: [PATCH 01/16] feat: addd melos build_runner_repository to justfile --- catalyst_voices/justfile | 1 + 1 file changed, 1 insertion(+) diff --git a/catalyst_voices/justfile b/catalyst_voices/justfile index e09c03fa2602..a870a7d6a9b3 100755 --- a/catalyst_voices/justfile +++ b/catalyst_voices/justfile @@ -12,6 +12,7 @@ setup-code: generate-code: setup-code melos l10n melos build_runner + melos build_runner_repository just generate-gateway-services # Syntax sugar for linking packages and building generated code From 0080f722c3c6a7b6db00d11f155b3f1a6f4fd924 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Tue, 17 Dec 2024 13:01:11 +0100 Subject: [PATCH 02/16] feat: update cose sign to support multiple different signatures and algs --- .../packages/libs/catalyst_cose/README.md | 23 ++--- .../libs/catalyst_cose/example/main.dart | 23 ++--- .../catalyst_cose/lib/src/cose_constants.dart | 13 +++ .../libs/catalyst_cose/lib/src/cose_sign.dart | 97 ++++++++++--------- .../catalyst_cose/lib/src/cose_sign1.dart | 5 + .../lib/src/types/cose_headers.dart | 28 ++++++ .../catalyst_cose/test/cose_sign1_test.dart | 39 +++++--- .../catalyst_cose/test/cose_sign_test.dart | 54 +++++++---- 8 files changed, 180 insertions(+), 102 deletions(-) diff --git a/catalyst_voices/packages/libs/catalyst_cose/README.md b/catalyst_voices/packages/libs/catalyst_cose/README.md index ee107be4752f..d516a79d28ed 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/README.md +++ b/catalyst_voices/packages/libs/catalyst_cose/README.md @@ -47,14 +47,10 @@ Future main() async { Future _coseSign1() async { final algorithm = Ed25519(); final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); - final publicKey = await keyPair.extractPublicKey().then((e) => e.bytes); final signerVerifier = _SignerVerifier(algorithm, keyPair); final coseSign1 = await CoseSign1.sign( - protectedHeaders: CoseHeaders.protected( - alg: const IntValue(CoseValues.eddsaAlg), - kid: hex.encode(publicKey), - ), + protectedHeaders: const CoseHeaders.protected(), unprotectedHeaders: const CoseHeaders.unprotected(), signer: signerVerifier, payload: utf8.encode('This is the content.'), @@ -78,20 +74,16 @@ Future _coseSign1() async { Future _coseSign() async { final algorithm = Ed25519(); final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); - final publicKey = await keyPair.extractPublicKey().then((e) => e.bytes); final signerVerifier = _SignerVerifier(algorithm, keyPair); final coseSign = await CoseSign.sign( - protectedHeaders: CoseHeaders.protected( - alg: const IntValue(CoseValues.eddsaAlg), - kid: hex.encode(publicKey), - ), + protectedHeaders: const CoseHeaders.protected(), unprotectedHeaders: const CoseHeaders.unprotected(), signers: [signerVerifier], payload: utf8.encode('This is the content.'), ); - final verified = await coseSign.verify( + final verified = await coseSign.verifyAll( verifiers: [signerVerifier], ); @@ -113,6 +105,15 @@ final class _SignerVerifier const _SignerVerifier(this._algorithm, this._keyPair); + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async { + final pk = await _keyPair.extractPublicKey(); + return hex.encode(pk.bytes); + } + @override Future sign(Uint8List data) async { final signature = await _algorithm.sign(data, keyPair: _keyPair); diff --git a/catalyst_voices/packages/libs/catalyst_cose/example/main.dart b/catalyst_voices/packages/libs/catalyst_cose/example/main.dart index c7ea8131a662..4b0d2e550e62 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/example/main.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/example/main.dart @@ -16,14 +16,10 @@ Future main() async { Future _coseSign1() async { final algorithm = Ed25519(); final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); - final publicKey = await keyPair.extractPublicKey().then((e) => e.bytes); final signerVerifier = _SignerVerifier(algorithm, keyPair); final coseSign1 = await CoseSign1.sign( - protectedHeaders: CoseHeaders.protected( - alg: const IntValue(CoseValues.eddsaAlg), - kid: hex.encode(publicKey), - ), + protectedHeaders: const CoseHeaders.protected(), unprotectedHeaders: const CoseHeaders.unprotected(), signer: signerVerifier, payload: utf8.encode('This is the content.'), @@ -47,20 +43,16 @@ Future _coseSign1() async { Future _coseSign() async { final algorithm = Ed25519(); final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); - final publicKey = await keyPair.extractPublicKey().then((e) => e.bytes); final signerVerifier = _SignerVerifier(algorithm, keyPair); final coseSign = await CoseSign.sign( - protectedHeaders: CoseHeaders.protected( - alg: const IntValue(CoseValues.eddsaAlg), - kid: hex.encode(publicKey), - ), + protectedHeaders: const CoseHeaders.protected(), unprotectedHeaders: const CoseHeaders.unprotected(), signers: [signerVerifier], payload: utf8.encode('This is the content.'), ); - final verified = await coseSign.verify( + final verified = await coseSign.verifyAll( verifiers: [signerVerifier], ); @@ -82,6 +74,15 @@ final class _SignerVerifier const _SignerVerifier(this._algorithm, this._keyPair); + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async { + final pk = await _keyPair.extractPublicKey(); + return hex.encode(pk.bytes); + } + @override Future sign(Uint8List data) async { final signature = await _algorithm.sign(data, keyPair: _keyPair); diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart index feec000ce264..8aeb1254dc7e 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:catalyst_cose/src/types/string_or_int.dart'; import 'package:cbor/cbor.dart'; /// Holds commonly used tags in COSE. @@ -71,6 +72,14 @@ final class CoseValues { /// The interface for the data signer callback. // ignore: one_member_abstracts abstract interface class CatalystCoseSigner { + /// Returns the alg identifier that should refer + /// to the cryptographic algorithm used to [sign] the data. + StringOrInt? get alg; + + /// Returns a key identifier that typically should refer to the public key + /// of the private key used to sign the data. + Future get kid; + /// The [data] should be signed with a private key /// and the resulting signature returned as [Uint8List]. Future sign(Uint8List data); @@ -79,6 +88,10 @@ abstract interface class CatalystCoseSigner { /// The interface for the signature verifier callback. // ignore: one_member_abstracts abstract interface class CatalystCoseVerifier { + /// Returns a key identifier that typically should refer to the public key + /// of the private key used to sign the data. + Future get kid; + /// The [signature] should be verified against /// a known public/private key over the [data]. Future verify(Uint8List data, Uint8List signature); diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart index e525fe941d87..222f929b6e5e 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart @@ -72,21 +72,14 @@ final class CoseSign extends Equatable { final signatures = []; for (final signer in signers) { final signatureProtectedHeaders = CoseHeaders.protected( - alg: protectedHeaders.alg, + alg: signer.alg, + kid: await signer.kid, ); - final sigStructure = _createCoseSignSigStructure( - bodyProtectedHeaders: protectedHeaders.toCbor(), - signatureProtectedHeaders: signatureProtectedHeaders.toCbor(), - payload: CborBytes(payload), - ); - - final toBeSigned = Uint8List.fromList( - cbor.encode( - CborBytes( - cbor.encode(sigStructure), - ), - ), + final toBeSigned = _createCoseSignSigStructureBytes( + bodyProtectedHeaders: protectedHeaders, + signatureProtectedHeaders: signatureProtectedHeaders, + payload: payload, ); final signature = CoseSignature( @@ -106,46 +99,36 @@ final class CoseSign extends Equatable { ); } - /// Verifies whether the COSE_SIGN [signatures] are valid. + /// Verifies whether the COSE_SIGN signature is valid. /// - /// The [verifiers] are responsible for providing the verification algorithm. - /// The count of [verifiers] must match the number of [signatures]. + /// The signature is selected from the list of [signatures] based on the kid]. + /// The [verifier] is responsible for providing the verification algorithm. Future verify({ - required List verifiers, + required CatalystCoseVerifier verifier, }) async { - if (verifiers.length != signatures.length) { - throw ArgumentError( - 'Number of verifiers must match the number of signatures.', - ); + for (final signature in signatures) { + if (signature.protectedHeaders.kid == await verifier.kid) { + final toBeSigned = _createCoseSignSigStructureBytes( + bodyProtectedHeaders: protectedHeaders, + signatureProtectedHeaders: signature.protectedHeaders, + payload: payload, + ); + return verifier.verify(toBeSigned, signature.signature); + } } - for (var i = 0; i < verifiers.length; i++) { - final verifier = verifiers[i]; - final signature = signatures[i]; - - final signatureProtectedHeaders = CoseHeaders.protected( - alg: protectedHeaders.alg, - ); - - final sigStructure = _createCoseSignSigStructure( - bodyProtectedHeaders: protectedHeaders.toCbor(), - signatureProtectedHeaders: signatureProtectedHeaders.toCbor(), - payload: CborBytes(payload), - ); - - final toBeSigned = Uint8List.fromList( - cbor.encode( - CborBytes( - cbor.encode(sigStructure), - ), - ), - ); - - final isVerified = await verifier.verify( - Uint8List.fromList(toBeSigned), - Uint8List.fromList(signature.signature), - ); + // no eligible signature found that would match the kid + return false; + } + /// Verifies whether the COSE_SIGN [signatures] are valid. + /// + /// The [verifiers] are responsible for providing the verification algorithm. + Future verifyAll({ + required List verifiers, + }) async { + for (final verifier in verifiers) { + final isVerified = await verify(verifier: verifier); if (!isVerified) { return false; } @@ -177,6 +160,26 @@ final class CoseSign extends Equatable { signatures, ]; + static Uint8List _createCoseSignSigStructureBytes({ + required CoseHeaders bodyProtectedHeaders, + required CoseHeaders signatureProtectedHeaders, + required Uint8List payload, + }) { + final sigStructure = _createCoseSignSigStructure( + bodyProtectedHeaders: bodyProtectedHeaders.toCbor(), + signatureProtectedHeaders: signatureProtectedHeaders.toCbor(), + payload: CborBytes(payload), + ); + + return Uint8List.fromList( + cbor.encode( + CborBytes( + cbor.encode(sigStructure), + ), + ), + ); + } + static CborList _createCoseSignSigStructure({ required CborValue bodyProtectedHeaders, required CborValue signatureProtectedHeaders, diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart index 543494882424..7173d7a3fd64 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart @@ -67,6 +67,11 @@ final class CoseSign1 extends Equatable { required Uint8List payload, required CatalystCoseSigner signer, }) async { + protectedHeaders = protectedHeaders.copyWith( + alg: signer.alg, + kid: await signer.kid, + ); + final sigStructure = _createCoseSign1SigStructure( protectedHeader: protectedHeaders.toCbor(), payload: CborBytes(payload), diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart index dbc93956577f..55e1bb5b29fa 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart @@ -9,9 +9,15 @@ import 'package:equatable/equatable.dart'; /// can be used in protected/unprotected COSE headers. final class CoseHeaders extends Equatable { /// See [CoseHeaderKeys.alg]. + /// + /// Do not set the [alg] directly in the headers, + /// it will be auto-populated with [CatalystCoseSigner.alg] value. final StringOrInt? alg; /// See [CoseHeaderKeys.kid]. + /// + /// Do not set the [kid] directly in the headers, + /// it will be auto-populated with [CatalystCoseSigner.kid] value. final String? kid; /// See [CoseHeaderKeys.contentType]. @@ -161,6 +167,28 @@ final class CoseHeaders extends Equatable { } } + /// Returns a copy of the [CoseHeaders] with given [alg] and [kid]. + CoseHeaders copyWith({ + required StringOrInt? alg, + required String? kid, + }) { + return CoseHeaders( + alg: alg, + kid: kid, + contentType: contentType, + contentEncoding: contentEncoding, + type: type, + id: id, + ver: ver, + ref: ref, + template: template, + reply: reply, + section: section, + collabs: collabs, + encodeAsBytes: encodeAsBytes, + ); + } + @override List get props => [ alg, diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart index 2273a8c0d360..f7a61eb21434 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart @@ -9,38 +9,38 @@ import 'package:test/test.dart'; void main() { group(CoseSign1, () { const uuidV7 = '0193b535-7196-7cd1-84e6-ad9c316cf2d2'; - late SimplePublicKey publicKey; late _SignerVerifier signerVerifier; setUp(() async { // Initialize the Ed25519 algorithm and generate a key pair final algorithm = Ed25519(); final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); - publicKey = await keyPair.extractPublicKey(); signerVerifier = _SignerVerifier(algorithm, keyPair); }); test('sign generates a valid COSE_SIGN1 structure', () async { final coseSign1 = await CoseSign1.sign( - protectedHeaders: CoseHeaders.protected( - alg: const IntValue(CoseValues.eddsaAlg), - kid: hex.encode(publicKey.bytes), - contentType: const IntValue(CoseValues.jsonContentType), - contentEncoding: const StringValue(CoseValues.brotliContentEncoding), - type: const Uuid(uuidV7), - id: const Uuid(uuidV7), - ver: const Uuid(uuidV7), - ref: const SingleReferenceUuid(Uuid(uuidV7)), - template: const SingleReferenceUuid(Uuid(uuidV7)), - reply: const SingleReferenceUuid(Uuid(uuidV7)), + protectedHeaders: const CoseHeaders.protected( + contentType: IntValue(CoseValues.jsonContentType), + contentEncoding: StringValue(CoseValues.brotliContentEncoding), + type: Uuid(uuidV7), + id: Uuid(uuidV7), + ver: Uuid(uuidV7), + ref: SingleReferenceUuid(Uuid(uuidV7)), + template: SingleReferenceUuid(Uuid(uuidV7)), + reply: SingleReferenceUuid(Uuid(uuidV7)), section: 'section_name', - collabs: const ['test@domain.com'], + collabs: ['test@domain.com'], ), unprotectedHeaders: const CoseHeaders.unprotected(), signer: signerVerifier, payload: utf8.encode('Test payload'), ); + // verify whether alg & kid fields were added to protected headers + expect(coseSign1.protectedHeaders.alg, isNotNull); + expect(coseSign1.protectedHeaders.kid, isNotEmpty); + // test whether signature is valid final isValid = await coseSign1.verify(verifier: signerVerifier); expect(isValid, isTrue); @@ -59,7 +59,7 @@ void main() { signature: Uint8List(64), ); - // test whether signature is valid + // test whether signature is invalid final isValid = await coseSign1.verify(verifier: signerVerifier); expect(isValid, isFalse); }); @@ -73,6 +73,15 @@ final class _SignerVerifier const _SignerVerifier(this._algorithm, this._keyPair); + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async { + final pk = await _keyPair.extractPublicKey(); + return hex.encode(pk.bytes); + } + @override Future sign(Uint8List data) async { final signature = await _algorithm.sign(data, keyPair: _keyPair); diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart index 296415509338..2b4825f2aeed 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart @@ -9,40 +9,45 @@ import 'package:test/test.dart'; void main() { group(CoseSign, () { const uuidV7 = '0193b535-7196-7cd1-84e6-ad9c316cf2d2'; - late SimplePublicKey publicKey; late _SignerVerifier signerVerifier; setUp(() async { // Initialize the Ed25519 algorithm and generate a key pair final algorithm = Ed25519(); final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); - publicKey = await keyPair.extractPublicKey(); signerVerifier = _SignerVerifier(algorithm, keyPair); }); test('sign generates a valid COSE_SIGN structure', () async { final coseSign = await CoseSign.sign( - protectedHeaders: CoseHeaders.protected( - alg: const IntValue(CoseValues.eddsaAlg), - kid: hex.encode(publicKey.bytes), - contentType: const IntValue(CoseValues.jsonContentType), - contentEncoding: const StringValue(CoseValues.brotliContentEncoding), - type: const Uuid(uuidV7), - id: const Uuid(uuidV7), - ver: const Uuid(uuidV7), - ref: const SingleReferenceUuid(Uuid(uuidV7)), - template: const SingleReferenceUuid(Uuid(uuidV7)), - reply: const SingleReferenceUuid(Uuid(uuidV7)), + protectedHeaders: const CoseHeaders.protected( + contentType: IntValue(CoseValues.jsonContentType), + contentEncoding: StringValue(CoseValues.brotliContentEncoding), + type: Uuid(uuidV7), + id: Uuid(uuidV7), + ver: Uuid(uuidV7), + ref: SingleReferenceUuid(Uuid(uuidV7)), + template: SingleReferenceUuid(Uuid(uuidV7)), + reply: SingleReferenceUuid(Uuid(uuidV7)), section: 'section_name', - collabs: const ['test@domain.com'], + collabs: ['test@domain.com'], ), unprotectedHeaders: const CoseHeaders.unprotected(), signers: [signerVerifier], payload: utf8.encode('Test payload'), ); - // test whether signature is valid - final isValid = await coseSign.verify(verifiers: [signerVerifier]); + for (final signature in coseSign.signatures) { + // verify whether alg & kid fields were added to protected headers + expect(signature.protectedHeaders.alg, isNotNull); + expect(signature.protectedHeaders.kid, isNotEmpty); + } + + // test whether signatures are valid + final isValidAll = await coseSign.verifyAll(verifiers: [signerVerifier]); + expect(isValidAll, isTrue); + + final isValid = await coseSign.verify(verifier: signerVerifier); expect(isValid, isTrue); // test whether serialization/deserialization works @@ -65,8 +70,12 @@ void main() { ], ); - // test whether signature is valid - final isValid = await coseSign.verify(verifiers: [signerVerifier]); + // test whether all signatures are invalid + final isValidAll = await coseSign.verifyAll(verifiers: [signerVerifier]); + expect(isValidAll, isFalse); + + // test whether all signatures are invalid + final isValid = await coseSign.verify(verifier: signerVerifier); expect(isValid, isFalse); }); }); @@ -79,6 +88,15 @@ final class _SignerVerifier const _SignerVerifier(this._algorithm, this._keyPair); + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async { + final pk = await _keyPair.extractPublicKey(); + return hex.encode(pk.bytes); + } + @override Future sign(Uint8List data) async { final signature = await _algorithm.sign(data, keyPair: _keyPair); From 1dcd5c1a6937fdeac60d2ecc22bf19ce92e71f50 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Tue, 17 Dec 2024 13:01:50 +0100 Subject: [PATCH 03/16] feat: add document manager that handles signed documents (COSE_SIGN) --- .../lib/src/document/document_manager.dart | 75 ++++++++ .../src/document/document_manager_impl.dart | 117 ++++++++++++ .../catalyst_voices_shared/pubspec.yaml | 4 + .../src/document/document_manager_test.dart | 169 ++++++++++++++++++ 4 files changed, 365 insertions(+) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart new file mode 100644 index 000000000000..2f98cd1a271f --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart @@ -0,0 +1,75 @@ +import 'dart:typed_data'; + +import 'package:catalyst_compression/catalyst_compression.dart'; +import 'package:catalyst_cose/catalyst_cose.dart'; +import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; +import 'package:cbor/cbor.dart'; +import 'package:convert/convert.dart'; +import 'package:equatable/equatable.dart'; + +part 'document_manager_impl.dart'; + +/// Parses the document from the bytes obtained from [Document.toBytes]. +/// +/// Usually this would convert the [bytes] into a [String], +/// decode a [String] into a json and then parse the data class +/// from the json representation. +typedef DocumentParser = T Function(Uint8List bytes); + +/// Manages the [SignedDocument]s. +abstract interface class DocumentManager { + /// The default constructor for the [DocumentManager], + /// provides the default implementation of the interface. + const factory DocumentManager() = _DocumentManagerImpl; + + /// Parses the document from the [bytes] representation. + /// + /// The [parser] must be able to parse the document + /// from the bytes produced by [Document.toBytes]. + /// + /// The implementation of this method must be able to understand the [bytes] + /// that are obtained from the [SignedDocument.toBytes] method. + Future> parseDocument( + Uint8List bytes, { + required DocumentParser parser, + }); + + /// Signs the [document] with a single [privateKey]. + /// + /// The [publicKey] will be added as the metadata in the signed document + /// so that it's easier to identify who signed it. + Future> signDocument( + T document, { + required Uint8List publicKey, + required Uint8List privateKey, + }); +} + +/// Represents an abstract document that is protected +/// with cryptographic signature. +/// +/// The [document] payload can be UTF-8 encoded bytes, a binary data +/// or anything else that can be represented in binary format. +abstract interface class SignedDocument extends Equatable { + /// The default constructor for the [SignedDocument]. + const SignedDocument(); + + /// A getter that returns a parsed document. + T get document; + + /// Verifies if the [document] has been signed by a private key + /// that belongs to the given [publicKey]. + Future verifySignature(Uint8List publicKey); + + /// Converts the document into binary representation. + Uint8List toBytes(); +} + +/// Represents an abstract document that can be represented in binary format. +// ignore: one_member_abstracts +abstract interface class Document { + /// Converts the document into a binary representation. + /// + /// See [DocumentParser]. + Uint8List toBytes(); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart new file mode 100644 index 000000000000..103211012e6f --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart @@ -0,0 +1,117 @@ +part of 'document_manager.dart'; + +const _brotliEncoding = StringValue(CoseValues.brotliContentEncoding); + +final class _DocumentManagerImpl implements DocumentManager { + const _DocumentManagerImpl(); + + @override + Future> parseDocument( + Uint8List bytes, { + required DocumentParser parser, + }) async { + final coseSign = CoseSign.fromCbor(cbor.decode(bytes)); + final payload = await _brotliDecompressPayload(coseSign); + final document = parser(payload); + return _CoseSignedDocument(coseSign, document); + } + + @override + Future> signDocument( + T document, { + required Uint8List publicKey, + required Uint8List privateKey, + }) async { + final compressedPayload = await _brotliCompressPayload(document.toBytes()); + + final coseSign = await CoseSign.sign( + protectedHeaders: const CoseHeaders.protected( + contentEncoding: _brotliEncoding, + ), + unprotectedHeaders: const CoseHeaders.unprotected(), + payload: compressedPayload, + signers: [_Bip32Ed25519XSigner(publicKey, privateKey)], + ); + + return _CoseSignedDocument(coseSign, document); + } + + Future _brotliCompressPayload(Uint8List payload) async { + final compressor = CatalystCompression.instance.brotli; + final compressed = await compressor.compress(payload); + return Uint8List.fromList(compressed); + } + + Future _brotliDecompressPayload(CoseSign coseSign) async { + if (coseSign.protectedHeaders.contentEncoding == _brotliEncoding) { + final compressor = CatalystCompression.instance.brotli; + final decompressed = await compressor.decompress(coseSign.payload); + return Uint8List.fromList(decompressed); + } else { + return coseSign.payload; + } + } +} + +final class _CoseSignedDocument extends SignedDocument { + final CoseSign _coseSign; + final T _document; + + const _CoseSignedDocument(this._coseSign, this._document); + + @override + T get document => _document; + + @override + Future verifySignature(Uint8List publicKey) async { + return _coseSign.verify( + verifier: _Bip32Ed25519XVerifier(publicKey), + ); + } + + @override + Uint8List toBytes() { + final bytes = cbor.encode(_coseSign.toCbor()); + return Uint8List.fromList(bytes); + } + + @override + List get props => [_coseSign, _document]; +} + +final class _Bip32Ed25519XSigner implements CatalystCoseSigner { + final Uint8List publicKey; + final Uint8List privateKey; + + const _Bip32Ed25519XSigner(this.publicKey, this.privateKey); + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async => hex.encode(publicKey); + + @override + Future sign(Uint8List data) async { + final pk = Bip32Ed25519XPrivateKeyFactory.instance.fromBytes(privateKey); + final signature = await pk.sign(data); + return Uint8List.fromList(signature.bytes); + } +} + +final class _Bip32Ed25519XVerifier implements CatalystCoseVerifier { + final Uint8List publicKey; + + const _Bip32Ed25519XVerifier(this.publicKey); + + @override + Future get kid async => hex.encode(publicKey); + + @override + Future verify(Uint8List data, Uint8List signature) async { + final pk = Bip32Ed25519XPublicKeyFactory.instance.fromBytes(publicKey); + return pk.verify( + data, + signature: Bip32Ed25519XSignatureFactory.instance.fromBytes(signature), + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml index 66de050feead..14da899f7d65 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml @@ -9,12 +9,16 @@ environment: dependencies: catalyst_cardano_serialization: ^0.4.0 + catalyst_compression: ^0.3.0 + catalyst_cose: ^0.3.0 catalyst_key_derivation: ^0.1.0 catalyst_voices_models: path: ../catalyst_voices_models + cbor: ^6.2.0 collection: ^1.18.0 convert: ^3.1.1 cryptography: ^2.7.0 + equatable: ^2.0.7 flutter: sdk: flutter flutter_secure_storage: ^9.2.2 diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart new file mode 100644 index 000000000000..07c49260fd71 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart @@ -0,0 +1,169 @@ +import 'dart:convert'; + +import 'package:catalyst_compression/catalyst_compression.dart'; +import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; +import 'package:catalyst_voices_shared/src/document/document_manager.dart'; +import 'package:convert/convert.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +final _privateKey = Uint8List.fromList(List.filled(32, 0)); +final _publicKey = Uint8List.fromList(List.filled(32, 1)); +final _signature = Uint8List.fromList(List.filled(32, 2)); + +void main() { + group(DocumentManager, () { + const documentManager = DocumentManager(); + + setUpAll(() { + CatalystCompressionPlatform.instance = _FakeCatalystCompressionPlatform(); + + Bip32Ed25519XPublicKeyFactory.instance = + _FakeBip32Ed25519XPublicKeyFactory(); + + Bip32Ed25519XPrivateKeyFactory.instance = + _FakeBip32Ed25519XPrivateKeyFactory(); + + Bip32Ed25519XSignatureFactory.instance = + _FakeBip32Ed25519XSignatureFactory(); + }); + + test( + 'signDocument creates a signed document ' + 'that can be converted from/to bytes', () async { + const document = _JsonDocument('title'); + + final signedDocument = await documentManager.signDocument( + document, + publicKey: _publicKey, + privateKey: _privateKey, + ); + + expect(signedDocument.document, equals(document)); + + final isVerified = await signedDocument.verifySignature(_publicKey); + expect(isVerified, isTrue); + + final signedDocumentBytes = signedDocument.toBytes(); + final parsedDocument = await documentManager.parseDocument( + signedDocumentBytes, + parser: _JsonDocument.fromBytes, + ); + + expect(parsedDocument, equals(signedDocument)); + }); + }); +} + +final class _JsonDocument extends Equatable implements Document { + final String title; + + const _JsonDocument(this.title); + + factory _JsonDocument.fromBytes(Uint8List bytes) { + final string = utf8.decode(bytes); + final map = json.decode(string); + return _JsonDocument.fromJson(map as Map); + } + + factory _JsonDocument.fromJson(Map map) { + return _JsonDocument(map['title'] as String); + } + + Map toJson() { + return {'title': title}; + } + + @override + Uint8List toBytes() { + final jsonString = json.encode(toJson()); + return utf8.encode(jsonString); + } + + @override + List get props => [title]; +} + +class _FakeCatalystCompressionPlatform extends CatalystCompressionPlatform { + @override + CatalystCompressor get brotli => const _FakeCompressor(); +} + +final class _FakeCompressor implements CatalystCompressor { + const _FakeCompressor(); + + @override + Future> compress(List bytes) async => bytes; + + @override + Future> decompress(List bytes) async => bytes; +} + +class _FakeBip32Ed25519XPublicKeyFactory extends Bip32Ed25519XPublicKeyFactory { + @override + Bip32Ed25519XPublicKey fromBytes(List bytes) { + return _FakeBip32Ed22519XPublicKey(bytes: bytes); + } +} + +class _FakeBip32Ed25519XPrivateKeyFactory + extends Bip32Ed25519XPrivateKeyFactory { + @override + Bip32Ed25519XPrivateKey fromBytes(List bytes) { + return _FakeBip32Ed22519XPrivateKey(bytes: bytes); + } +} + +class _FakeBip32Ed25519XSignatureFactory extends Bip32Ed25519XSignatureFactory { + @override + Bip32Ed25519XSignature fromBytes(List bytes) { + return _FakeBip32Ed22519XSignature(bytes: bytes); + } +} + +class _FakeBip32Ed22519XPublicKey extends Fake + implements Bip32Ed25519XPublicKey { + @override + final List bytes; + + _FakeBip32Ed22519XPublicKey({required this.bytes}); + + @override + Future verify( + List message, { + required Bip32Ed25519XSignature signature, + }) async { + return listEquals(signature.bytes, _signature); + } + + @override + String toHex() => hex.encode(bytes); +} + +class _FakeBip32Ed22519XPrivateKey extends Fake + implements Bip32Ed25519XPrivateKey { + @override + final List bytes; + + _FakeBip32Ed22519XPrivateKey({required this.bytes}); + + @override + Future sign(List message) async { + return _FakeBip32Ed22519XSignature(bytes: _signature); + } + + @override + String toHex() => hex.encode(bytes); +} + +class _FakeBip32Ed22519XSignature extends Fake + implements Bip32Ed25519XSignature { + @override + final List bytes; + + _FakeBip32Ed22519XSignature({required this.bytes}); + + @override + String toHex() => hex.encode(bytes); +} From b5d0eea37e376c91e1ccdee906b0313b81d4a9f5 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Tue, 17 Dec 2024 13:45:21 +0100 Subject: [PATCH 04/16] fix: kid should be encoded as Uint8List, not as string --- .../lib/src/document/document_manager.dart | 1 - .../lib/src/document/document_manager_impl.dart | 4 ++-- .../packages/libs/catalyst_cose/README.md | 4 ++-- .../libs/catalyst_cose/example/main.dart | 4 ++-- .../catalyst_cose/lib/src/cose_constants.dart | 4 ++-- .../libs/catalyst_cose/lib/src/cose_sign.dart | 4 +--- .../libs/catalyst_cose/lib/src/cose_sign1.dart | 4 +--- .../lib/src/types/cose_headers.dart | 10 ++++++---- .../libs/catalyst_cose/lib/src/types/uuid.dart | 2 +- .../catalyst_cose/lib/src/utils/cbor_utils.dart | 16 ++++++++++++++++ .../libs/catalyst_cose/test/cose_sign1_test.dart | 5 ++--- .../libs/catalyst_cose/test/cose_sign_test.dart | 5 ++--- 12 files changed, 37 insertions(+), 26 deletions(-) diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart index 2f98cd1a271f..0db238fe9771 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart @@ -4,7 +4,6 @@ import 'package:catalyst_compression/catalyst_compression.dart'; import 'package:catalyst_cose/catalyst_cose.dart'; import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:cbor/cbor.dart'; -import 'package:convert/convert.dart'; import 'package:equatable/equatable.dart'; part 'document_manager_impl.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart index 103211012e6f..bf9560dbca2c 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart @@ -88,7 +88,7 @@ final class _Bip32Ed25519XSigner implements CatalystCoseSigner { StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); @override - Future get kid async => hex.encode(publicKey); + Future get kid async => publicKey; @override Future sign(Uint8List data) async { @@ -104,7 +104,7 @@ final class _Bip32Ed25519XVerifier implements CatalystCoseVerifier { const _Bip32Ed25519XVerifier(this.publicKey); @override - Future get kid async => hex.encode(publicKey); + Future get kid async => publicKey; @override Future verify(Uint8List data, Uint8List signature) async { diff --git a/catalyst_voices/packages/libs/catalyst_cose/README.md b/catalyst_voices/packages/libs/catalyst_cose/README.md index d516a79d28ed..90915e7da43b 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/README.md +++ b/catalyst_voices/packages/libs/catalyst_cose/README.md @@ -109,9 +109,9 @@ final class _SignerVerifier StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); @override - Future get kid async { + Future get kid async { final pk = await _keyPair.extractPublicKey(); - return hex.encode(pk.bytes); + return Uint8List.fromList(pk.bytes); } @override diff --git a/catalyst_voices/packages/libs/catalyst_cose/example/main.dart b/catalyst_voices/packages/libs/catalyst_cose/example/main.dart index 4b0d2e550e62..3bed84f27249 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/example/main.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/example/main.dart @@ -78,9 +78,9 @@ final class _SignerVerifier StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); @override - Future get kid async { + Future get kid async { final pk = await _keyPair.extractPublicKey(); - return hex.encode(pk.bytes); + return Uint8List.fromList(pk.bytes); } @override diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart index 8aeb1254dc7e..c555975b72d9 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart @@ -78,7 +78,7 @@ abstract interface class CatalystCoseSigner { /// Returns a key identifier that typically should refer to the public key /// of the private key used to sign the data. - Future get kid; + Future get kid; /// The [data] should be signed with a private key /// and the resulting signature returned as [Uint8List]. @@ -90,7 +90,7 @@ abstract interface class CatalystCoseSigner { abstract interface class CatalystCoseVerifier { /// Returns a key identifier that typically should refer to the public key /// of the private key used to sign the data. - Future get kid; + Future get kid; /// The [signature] should be verified against /// a known public/private key over the [data]. diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart index 222f929b6e5e..c1ff339d66ab 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart @@ -34,9 +34,7 @@ final class CoseSign extends Equatable { /// Deserializes the type from cbor. factory CoseSign.fromCbor(CborValue value) { - if (value is! CborList || - value.length != 4 || - !value.tags.contains(CoseTags.coseSign)) { + if (value is! CborList || value.length != 4) { throw FormatException('$value is not a valid COSE_SIGN structure'); } diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart index 7173d7a3fd64..00352f642b5c 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart @@ -32,9 +32,7 @@ final class CoseSign1 extends Equatable { /// Deserializes the type from cbor. factory CoseSign1.fromCbor(CborValue value) { - if (value is! CborList || - value.length != 4 || - !value.tags.contains(CoseTags.coseSign1)) { + if (value is! CborList || value.length != 4) { throw FormatException('$value is not a valid COSE_SIGN1 structure'); } diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart index 55e1bb5b29fa..563de6fc5b9f 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:catalyst_cose/src/cose_constants.dart'; import 'package:catalyst_cose/src/types/string_or_int.dart'; import 'package:catalyst_cose/src/types/uuid.dart'; @@ -18,7 +20,7 @@ final class CoseHeaders extends Equatable { /// /// Do not set the [kid] directly in the headers, /// it will be auto-populated with [CatalystCoseSigner.kid] value. - final String? kid; + final Uint8List? kid; /// See [CoseHeaderKeys.contentType]. final StringOrInt? contentType; @@ -122,7 +124,7 @@ final class CoseHeaders extends Equatable { return CoseHeaders( alg: CborUtils.deserializeStringOrInt(map[CoseHeaderKeys.alg]), - kid: CborUtils.deserializeString(map[CoseHeaderKeys.kid]), + kid: CborUtils.deserializeBytes(map[CoseHeaderKeys.kid]), contentType: CborUtils.deserializeStringOrInt(map[CoseHeaderKeys.contentType]), contentEncoding: @@ -144,7 +146,7 @@ final class CoseHeaders extends Equatable { CborValue toCbor() { final map = CborMap({ if (alg != null) CoseHeaderKeys.alg: alg!.toCbor(), - if (kid != null) CoseHeaderKeys.kid: CborString(kid!), + if (kid != null) CoseHeaderKeys.kid: CborBytes(kid!), if (contentType != null) CoseHeaderKeys.contentType: contentType!.toCbor(), if (contentEncoding != null) @@ -170,7 +172,7 @@ final class CoseHeaders extends Equatable { /// Returns a copy of the [CoseHeaders] with given [alg] and [kid]. CoseHeaders copyWith({ required StringOrInt? alg, - required String? kid, + required Uint8List? kid, }) { return CoseHeaders( alg: alg, diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart index 927a7ce35409..db329d7b6f60 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart @@ -10,7 +10,7 @@ import 'package:uuid/uuid.dart' as uuid; extension type const Uuid(String value) { /// Deserializes the type from cbor. factory Uuid.fromCbor(CborValue value) { - if (value is CborBytes && value.tags.contains(CborUtils.uuidTag)) { + if (value is CborBytes) { return Uuid(uuid.Uuid.unparse(value.bytes)); } else { throw FormatException('The $value is not a valid uuid!'); diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/utils/cbor_utils.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/utils/cbor_utils.dart index a3e5b9f80fe6..969b38f545c0 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/utils/cbor_utils.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/utils/cbor_utils.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; +import 'dart:typed_data'; + import 'package:catalyst_cose/src/types/string_or_int.dart'; import 'package:catalyst_cose/src/types/uuid.dart'; import 'package:cbor/cbor.dart'; @@ -36,6 +39,19 @@ final class CborUtils { return ReferenceUuid.fromCbor(value); } + /// Deserializes optional [Uint8List] type. + static Uint8List? deserializeBytes(CborValue? value) { + if (value == null) { + return null; + } + + if (value is CborString) { + return utf8.encode(value.toString()); + } + + return Uint8List.fromList((value as CborBytes).bytes); + } + /// Deserializes optional [String] type. static String? deserializeString(CborValue? value) { if (value == null) { diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart index f7a61eb21434..8e2876a17230 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:catalyst_cose/catalyst_cose.dart'; -import 'package:convert/convert.dart'; import 'package:cryptography/cryptography.dart'; import 'package:test/test.dart'; @@ -77,9 +76,9 @@ final class _SignerVerifier StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); @override - Future get kid async { + Future get kid async { final pk = await _keyPair.extractPublicKey(); - return hex.encode(pk.bytes); + return Uint8List.fromList(pk.bytes); } @override diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart index 2b4825f2aeed..28951dee16aa 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:catalyst_cose/catalyst_cose.dart'; -import 'package:convert/convert.dart'; import 'package:cryptography/cryptography.dart'; import 'package:test/test.dart'; @@ -92,9 +91,9 @@ final class _SignerVerifier StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); @override - Future get kid async { + Future get kid async { final pk = await _keyPair.extractPublicKey(); - return hex.encode(pk.bytes); + return Uint8List.fromList(pk.bytes); } @override From e9927170d9dc2991894f2fbaa9b7203e1b09a1f7 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Tue, 17 Dec 2024 13:51:11 +0100 Subject: [PATCH 05/16] style: typo --- .../lib/src/document/document_manager.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart index 0db238fe9771..719f029fafdb 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart @@ -35,7 +35,7 @@ abstract interface class DocumentManager { /// Signs the [document] with a single [privateKey]. /// - /// The [publicKey] will be added as the metadata in the signed document + /// The [publicKey] will be added as metadata in the signed document /// so that it's easier to identify who signed it. Future> signDocument( T document, { From 8886bda1a521b66ebb6d355c32ebfbb40f1cff7c Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Tue, 17 Dec 2024 13:55:35 +0100 Subject: [PATCH 06/16] fix: collection equality --- .../packages/libs/catalyst_cose/lib/src/cose_sign.dart | 4 +++- catalyst_voices/packages/libs/catalyst_cose/pubspec.yaml | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart index c1ff339d66ab..7d73a4eaceca 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:catalyst_cose/src/cose_constants.dart'; import 'package:catalyst_cose/src/types/cose_headers.dart'; import 'package:cbor/cbor.dart'; +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; /// The COSE_SIGN structure implementation, supporting multiple signatures. @@ -105,7 +106,8 @@ final class CoseSign extends Equatable { required CatalystCoseVerifier verifier, }) async { for (final signature in signatures) { - if (signature.protectedHeaders.kid == await verifier.kid) { + if (const DeepCollectionEquality() + .equals(signature.protectedHeaders.kid, await verifier.kid)) { final toBeSigned = _createCoseSignSigStructureBytes( bodyProtectedHeaders: protectedHeaders, signatureProtectedHeaders: signature.protectedHeaders, diff --git a/catalyst_voices/packages/libs/catalyst_cose/pubspec.yaml b/catalyst_voices/packages/libs/catalyst_cose/pubspec.yaml index 9f4b358988e6..6bebbcbdc825 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/pubspec.yaml +++ b/catalyst_voices/packages/libs/catalyst_cose/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: cbor: ^6.2.0 + collection: ^1.18.0 convert: ^3.1.1 cryptography: ^2.7.0 equatable: ^2.0.7 From bfc83fc1d6947a54c720bdc3fc5540f5598b4eaa Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Wed, 18 Dec 2024 09:32:57 +0100 Subject: [PATCH 07/16] chore: review feedback --- .../lib/src/document/document_manager.dart | 2 +- .../lib/src/document/document_manager_impl.dart | 10 +++++----- .../libs/catalyst_cose/test/cose_sign_test.dart | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart index 719f029fafdb..cfd3fed8d80f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart @@ -49,7 +49,7 @@ abstract interface class DocumentManager { /// /// The [document] payload can be UTF-8 encoded bytes, a binary data /// or anything else that can be represented in binary format. -abstract interface class SignedDocument extends Equatable { +abstract base class SignedDocument extends Equatable { /// The default constructor for the [SignedDocument]. const SignedDocument(); diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart index bf9560dbca2c..4dd705d3c827 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart @@ -55,12 +55,11 @@ final class _DocumentManagerImpl implements DocumentManager { final class _CoseSignedDocument extends SignedDocument { final CoseSign _coseSign; - final T _document; - - const _CoseSignedDocument(this._coseSign, this._document); @override - T get document => _document; + final T document; + + const _CoseSignedDocument(this._coseSign, this.document); @override Future verifySignature(Uint8List publicKey) async { @@ -76,7 +75,7 @@ final class _CoseSignedDocument extends SignedDocument { } @override - List get props => [_coseSign, _document]; + List get props => [_coseSign, document]; } final class _Bip32Ed25519XSigner implements CatalystCoseSigner { @@ -84,6 +83,7 @@ final class _Bip32Ed25519XSigner implements CatalystCoseSigner { final Uint8List privateKey; const _Bip32Ed25519XSigner(this.publicKey, this.privateKey); + @override StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart index 28951dee16aa..7f9ff3890163 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart @@ -7,6 +7,7 @@ import 'package:test/test.dart'; void main() { group(CoseSign, () { + const uuidV4 = 'e9aba14f-d05b-49b2-b5b5-100595853384'; const uuidV7 = '0193b535-7196-7cd1-84e6-ad9c316cf2d2'; late _SignerVerifier signerVerifier; @@ -22,7 +23,7 @@ void main() { protectedHeaders: const CoseHeaders.protected( contentType: IntValue(CoseValues.jsonContentType), contentEncoding: StringValue(CoseValues.brotliContentEncoding), - type: Uuid(uuidV7), + type: Uuid(uuidV4), id: Uuid(uuidV7), ver: Uuid(uuidV7), ref: SingleReferenceUuid(Uuid(uuidV7)), From 10ca57466ba3bca5021a5a414bd8b56543952046 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Wed, 18 Dec 2024 09:41:40 +0100 Subject: [PATCH 08/16] feat: add content type --- .../lib/src/document/document_manager.dart | 9 +++++++++ .../lib/src/document/document_manager_impl.dart | 13 ++++++++++++- .../test/src/document/document_manager_test.dart | 3 +++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart index cfd3fed8d80f..d47a157c03dc 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart @@ -71,4 +71,13 @@ abstract interface class Document { /// /// See [DocumentParser]. Uint8List toBytes(); + + /// Returns the document content type. + DocumentContentType get contentType; +} + +/// Defines the content type of the [Document]. +enum DocumentContentType { + /// The document's content type is JSON. + json, } diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart index 4dd705d3c827..c610de4ed450 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart @@ -25,8 +25,9 @@ final class _DocumentManagerImpl implements DocumentManager { final compressedPayload = await _brotliCompressPayload(document.toBytes()); final coseSign = await CoseSign.sign( - protectedHeaders: const CoseHeaders.protected( + protectedHeaders: CoseHeaders.protected( contentEncoding: _brotliEncoding, + contentType: document.contentType.asCose, ), unprotectedHeaders: const CoseHeaders.unprotected(), payload: compressedPayload, @@ -78,6 +79,16 @@ final class _CoseSignedDocument extends SignedDocument { List get props => [_coseSign, document]; } +extension _CoseDocumentContentType on DocumentContentType { + /// Maps the [DocumentContentType] into COSE representation. + StringOrInt get asCose { + switch (this) { + case DocumentContentType.json: + return const IntValue(CoseValues.jsonContentType); + } + } +} + final class _Bip32Ed25519XSigner implements CatalystCoseSigner { final Uint8List publicKey; final Uint8List privateKey; diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart index 07c49260fd71..c830e152d400 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart @@ -81,6 +81,9 @@ final class _JsonDocument extends Equatable implements Document { return utf8.encode(jsonString); } + @override + DocumentContentType get contentType => DocumentContentType.json; + @override List get props => [title]; } From 27e1a0d3f45ef7606cad2e16b9f59f1e24f56a95 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Wed, 18 Dec 2024 10:01:14 +0100 Subject: [PATCH 09/16] chore: rename document to binary document, export document manager --- .../lib/src/catalyst_voices_shared.dart | 1 + .../lib/src/document/document_manager.dart | 16 ++++++++-------- .../lib/src/document/document_manager_impl.dart | 7 ++++--- .../test/src/document/document_manager_test.dart | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart index a2b7d04c7c37..d42c6829d2b6 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart @@ -7,6 +7,7 @@ export 'crypto/crypto_service.dart'; export 'crypto/key_derivation.dart'; export 'crypto/local_crypto_service.dart'; export 'dependency/dependency_provider.dart'; +export 'document/document_manager.dart'; export 'formatter/cryptocurrency_formatter.dart'; export 'formatter/wallet_address_formatter.dart'; export 'keychain/keychain.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart index d47a157c03dc..8f536100f070 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart @@ -8,12 +8,12 @@ import 'package:equatable/equatable.dart'; part 'document_manager_impl.dart'; -/// Parses the document from the bytes obtained from [Document.toBytes]. +/// Parses the document from the bytes obtained from [BinaryDocument.toBytes]. /// /// Usually this would convert the [bytes] into a [String], /// decode a [String] into a json and then parse the data class /// from the json representation. -typedef DocumentParser = T Function(Uint8List bytes); +typedef DocumentParser = T Function(Uint8List bytes); /// Manages the [SignedDocument]s. abstract interface class DocumentManager { @@ -24,11 +24,11 @@ abstract interface class DocumentManager { /// Parses the document from the [bytes] representation. /// /// The [parser] must be able to parse the document - /// from the bytes produced by [Document.toBytes]. + /// from the bytes produced by [BinaryDocument.toBytes]. /// /// The implementation of this method must be able to understand the [bytes] /// that are obtained from the [SignedDocument.toBytes] method. - Future> parseDocument( + Future> parseDocument( Uint8List bytes, { required DocumentParser parser, }); @@ -37,7 +37,7 @@ abstract interface class DocumentManager { /// /// The [publicKey] will be added as metadata in the signed document /// so that it's easier to identify who signed it. - Future> signDocument( + Future> signDocument( T document, { required Uint8List publicKey, required Uint8List privateKey, @@ -49,7 +49,7 @@ abstract interface class DocumentManager { /// /// The [document] payload can be UTF-8 encoded bytes, a binary data /// or anything else that can be represented in binary format. -abstract base class SignedDocument extends Equatable { +abstract base class SignedDocument extends Equatable { /// The default constructor for the [SignedDocument]. const SignedDocument(); @@ -66,7 +66,7 @@ abstract base class SignedDocument extends Equatable { /// Represents an abstract document that can be represented in binary format. // ignore: one_member_abstracts -abstract interface class Document { +abstract interface class BinaryDocument { /// Converts the document into a binary representation. /// /// See [DocumentParser]. @@ -76,7 +76,7 @@ abstract interface class Document { DocumentContentType get contentType; } -/// Defines the content type of the [Document]. +/// Defines the content type of the [BinaryDocument]. enum DocumentContentType { /// The document's content type is JSON. json, diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart index c610de4ed450..74ebb4b67352 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart @@ -6,7 +6,7 @@ final class _DocumentManagerImpl implements DocumentManager { const _DocumentManagerImpl(); @override - Future> parseDocument( + Future> parseDocument( Uint8List bytes, { required DocumentParser parser, }) async { @@ -17,7 +17,7 @@ final class _DocumentManagerImpl implements DocumentManager { } @override - Future> signDocument( + Future> signDocument( T document, { required Uint8List publicKey, required Uint8List privateKey, @@ -54,7 +54,8 @@ final class _DocumentManagerImpl implements DocumentManager { } } -final class _CoseSignedDocument extends SignedDocument { +final class _CoseSignedDocument + extends SignedDocument { final CoseSign _coseSign; @override diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart index c830e152d400..40b1e7ff4c6d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart @@ -56,7 +56,7 @@ void main() { }); } -final class _JsonDocument extends Equatable implements Document { +final class _JsonDocument extends Equatable implements BinaryDocument { final String title; const _JsonDocument(this.title); From f2f1ba9c23952a80c5aaa3c508b899aa70f3ed9a Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Wed, 18 Dec 2024 11:33:59 +0100 Subject: [PATCH 10/16] chore: copyWith fix --- .../catalyst_cose/lib/src/cose_sign1.dart | 6 ++- .../lib/src/types/cose_headers.dart | 51 +++++++++++++------ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart index 00352f642b5c..842fa57ef5f9 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart @@ -65,9 +65,11 @@ final class CoseSign1 extends Equatable { required Uint8List payload, required CatalystCoseSigner signer, }) async { + final kid = await signer.kid; + protectedHeaders = protectedHeaders.copyWith( - alg: signer.alg, - kid: await signer.kid, + alg: () => signer.alg, + kid: () => kid, ); final sigStructure = _createCoseSign1SigStructure( diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart index 563de6fc5b9f..ff1da58c1133 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart @@ -7,6 +7,13 @@ import 'package:catalyst_cose/src/utils/cbor_utils.dart'; import 'package:cbor/cbor.dart'; import 'package:equatable/equatable.dart'; +/// A callback to get an optional value. +/// Helps to distinguish whether a method argument +/// has been passed as null or not passed at all. +/// +/// See [CoseHeaders.copyWith]. +typedef OptionalValueGetter = T? Function(); + /// A class that specifies headers that /// can be used in protected/unprotected COSE headers. final class CoseHeaders extends Equatable { @@ -169,25 +176,37 @@ final class CoseHeaders extends Equatable { } } - /// Returns a copy of the [CoseHeaders] with given [alg] and [kid]. + /// Returns a copy of the [CoseHeaders] with overwritten properties. CoseHeaders copyWith({ - required StringOrInt? alg, - required Uint8List? kid, + OptionalValueGetter? alg, + OptionalValueGetter? kid, + OptionalValueGetter? contentType, + OptionalValueGetter? contentEncoding, + OptionalValueGetter? type, + OptionalValueGetter? id, + OptionalValueGetter? ver, + OptionalValueGetter? ref, + OptionalValueGetter? template, + OptionalValueGetter? reply, + OptionalValueGetter? section, + OptionalValueGetter?>? collabs, + bool? encodeAsBytes, }) { return CoseHeaders( - alg: alg, - kid: kid, - contentType: contentType, - contentEncoding: contentEncoding, - type: type, - id: id, - ver: ver, - ref: ref, - template: template, - reply: reply, - section: section, - collabs: collabs, - encodeAsBytes: encodeAsBytes, + alg: alg != null ? alg() : this.alg, + kid: kid != null ? kid() : this.kid, + contentType: contentType != null ? contentType() : this.contentType, + contentEncoding: + contentEncoding != null ? contentEncoding() : this.contentEncoding, + type: type != null ? type() : this.type, + id: id != null ? id() : this.id, + ver: ver != null ? ver() : this.ver, + ref: ref != null ? ref() : this.ref, + template: template != null ? template() : this.template, + reply: reply != null ? reply() : this.reply, + section: section != null ? section() : this.section, + collabs: collabs != null ? collabs() : this.collabs, + encodeAsBytes: encodeAsBytes ?? this.encodeAsBytes, ); } From 878926d5bc8842afc81627783c3a8ad547d636a5 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Thu, 19 Dec 2024 19:12:16 +0100 Subject: [PATCH 11/16] fix: json content type --- .../packages/libs/catalyst_cose/lib/src/cose_constants.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart index c555975b72d9..a914fd495292 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart @@ -63,7 +63,7 @@ final class CoseValues { static const eddsaAlg = -8; /// The json content type value. - static const jsonContentType = 30; + static const jsonContentType = 50; /// The brotli compression content encoding. static const brotliContentEncoding = 'br'; From 4fac543d79850be31a1c4c141f95bf1c5b7209dd Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Thu, 19 Dec 2024 19:12:57 +0100 Subject: [PATCH 12/16] fix: put default alg in top-level headers if all signatures use the same alg --- .../packages/libs/catalyst_cose/lib/src/cose_sign.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart index 7d73a4eaceca..31357c007a32 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart @@ -68,6 +68,15 @@ final class CoseSign extends Equatable { required Uint8List payload, required List signers, }) async { + final algTheSame = signers.map((e) => e.alg).toSet().length == 1; + if (algTheSame) { + // if alg is the same put it on + // top-level headers as the default one + protectedHeaders = protectedHeaders.copyWith( + alg: () => signers.first.alg, + ); + } + final signatures = []; for (final signer in signers) { final signatureProtectedHeaders = CoseHeaders.protected( From 46a91e0821c1e80cc1f19fd5036ec86dc7003393 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Fri, 20 Dec 2024 09:22:41 +0100 Subject: [PATCH 13/16] chore: refactor reference uuid to give it a more meaningful name --- .../catalyst_cose/lib/src/types/uuid.dart | 87 +++++++------------ .../catalyst_cose/test/cose_sign1_test.dart | 6 +- .../catalyst_cose/test/cose_sign_test.dart | 6 +- 3 files changed, 35 insertions(+), 64 deletions(-) diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart index db329d7b6f60..a7d8f1ebe832 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart @@ -24,80 +24,51 @@ extension type const Uuid(String value) { } } -/// Either a single [Uuid] or two [Uuid]'s. +/// A reference to an entity represented by the [uuid]. +/// Optionally the version of the entity may be specified by the [ver]. /// /// What this uuid means depends where and how the class is used. /// In CDDL it is defined as (UUID / [UUID, UUID]). -sealed class ReferenceUuid extends Equatable { +final class ReferenceUuid extends Equatable { + /// The referenced entity uuid. + final Uuid uuid; + + /// The version of the referenced entity. + final Uuid? ver; + /// The default constructor for the [ReferenceUuid]. - const ReferenceUuid(); + const ReferenceUuid({ + required this.uuid, + this.ver, + }); /// Deserializes the type from cbor. factory ReferenceUuid.fromCbor(CborValue value) { if (value is CborList) { - return DoubleReferenceUuid.fromCbor(value); + return ReferenceUuid( + uuid: Uuid.fromCbor(value[0]), + ver: Uuid.fromCbor(value[1]), + ); } else { - return SingleReferenceUuid.fromCbor(value); + return ReferenceUuid( + uuid: Uuid.fromCbor(value), + ); } } /// Serializes the type as cbor. - CborValue toCbor(); -} - -/// A single [Uuid] reference. -final class SingleReferenceUuid extends ReferenceUuid { - /// A single reference uuid. - final Uuid uuid; - - /// A default constructor for the [SingleReferenceUuid]. - const SingleReferenceUuid(this.uuid); - - /// Deserializes the type from cbor. - factory SingleReferenceUuid.fromCbor(CborValue value) { - return SingleReferenceUuid(Uuid.fromCbor(value)); - } - - @override CborValue toCbor() { - return uuid.toCbor(); - } - - @override - List get props => [uuid]; -} - -/// A single [Uuid] reference. -final class DoubleReferenceUuid extends ReferenceUuid { - /// The first referenced uuid. - final Uuid first; - - /// The second reference uuid. - final Uuid second; - - /// A default constructor for the [DoubleReferenceUuid]. - const DoubleReferenceUuid(this.first, this.second); - - /// Deserializes the type from cbor. - factory DoubleReferenceUuid.fromCbor(CborValue value) { - if (value is! CborList || value.length != 2) { - throw FormatException('Wrong format of the DoubleReferenceUuid: $value'); + final ver = this.ver; + if (ver != null) { + return CborList([ + uuid.toCbor(), + ver.toCbor(), + ]); + } else { + return uuid.toCbor(); } - - return DoubleReferenceUuid( - Uuid.fromCbor(value[0]), - Uuid.fromCbor(value[1]), - ); - } - - @override - CborValue toCbor() { - return CborList([ - first.toCbor(), - second.toCbor(), - ]); } @override - List get props => [first, second]; + List get props => [uuid, ver]; } diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart index 8e2876a17230..2b51d8fbf118 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart @@ -25,9 +25,9 @@ void main() { type: Uuid(uuidV7), id: Uuid(uuidV7), ver: Uuid(uuidV7), - ref: SingleReferenceUuid(Uuid(uuidV7)), - template: SingleReferenceUuid(Uuid(uuidV7)), - reply: SingleReferenceUuid(Uuid(uuidV7)), + ref: ReferenceUuid(uuid: Uuid(uuidV7)), + template: ReferenceUuid(uuid: Uuid(uuidV7)), + reply: ReferenceUuid(uuid: Uuid(uuidV7)), section: 'section_name', collabs: ['test@domain.com'], ), diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart index 7f9ff3890163..22d200c77e9f 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart @@ -26,9 +26,9 @@ void main() { type: Uuid(uuidV4), id: Uuid(uuidV7), ver: Uuid(uuidV7), - ref: SingleReferenceUuid(Uuid(uuidV7)), - template: SingleReferenceUuid(Uuid(uuidV7)), - reply: SingleReferenceUuid(Uuid(uuidV7)), + ref: ReferenceUuid(uuid: Uuid(uuidV7)), + template: ReferenceUuid(uuid: Uuid(uuidV7)), + reply: ReferenceUuid(uuid: Uuid(uuidV7)), section: 'section_name', collabs: ['test@domain.com'], ), From 5e25bcf3a15b4ae1aba7d487b14afbd53c6da8d6 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Fri, 20 Dec 2024 09:24:33 +0100 Subject: [PATCH 14/16] chore: get rid of equatable from SignedDocument interface --- .../lib/src/document/document_manager.dart | 2 +- .../lib/src/document/document_manager_impl.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart index 8f536100f070..b5582446d4a2 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart @@ -49,7 +49,7 @@ abstract interface class DocumentManager { /// /// The [document] payload can be UTF-8 encoded bytes, a binary data /// or anything else that can be represented in binary format. -abstract base class SignedDocument extends Equatable { +abstract interface class SignedDocument { /// The default constructor for the [SignedDocument]. const SignedDocument(); diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart index 74ebb4b67352..38682442e327 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart @@ -55,7 +55,7 @@ final class _DocumentManagerImpl implements DocumentManager { } final class _CoseSignedDocument - extends SignedDocument { + extends SignedDocument with EquatableMixin { final CoseSign _coseSign; @override From 81c548d616726244dac45741cc95e6cae0b6cc48 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Fri, 20 Dec 2024 09:26:17 +0100 Subject: [PATCH 15/16] fix: do not put alg in top-level protected headers for COSE_SIGN --- .../packages/libs/catalyst_cose/lib/src/cose_sign.dart | 9 --------- 1 file changed, 9 deletions(-) diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart index 31357c007a32..7d73a4eaceca 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart @@ -68,15 +68,6 @@ final class CoseSign extends Equatable { required Uint8List payload, required List signers, }) async { - final algTheSame = signers.map((e) => e.alg).toSet().length == 1; - if (algTheSame) { - // if alg is the same put it on - // top-level headers as the default one - protectedHeaders = protectedHeaders.copyWith( - alg: () => signers.first.alg, - ); - } - final signatures = []; for (final signer in signers) { final signatureProtectedHeaders = CoseHeaders.protected( From b3b6fc267adab2a88b10035623dd0d419ccae6c7 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Fri, 20 Dec 2024 09:52:53 +0100 Subject: [PATCH 16/16] chore: update field name --- .../libs/catalyst_cose/lib/src/types/uuid.dart | 16 ++++++++-------- .../libs/catalyst_cose/test/cose_sign1_test.dart | 6 +++--- .../libs/catalyst_cose/test/cose_sign_test.dart | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart index a7d8f1ebe832..14ee6c340978 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart @@ -24,21 +24,21 @@ extension type const Uuid(String value) { } } -/// A reference to an entity represented by the [uuid]. +/// A reference to an entity represented by the [id]. /// Optionally the version of the entity may be specified by the [ver]. /// /// What this uuid means depends where and how the class is used. /// In CDDL it is defined as (UUID / [UUID, UUID]). final class ReferenceUuid extends Equatable { /// The referenced entity uuid. - final Uuid uuid; + final Uuid id; /// The version of the referenced entity. final Uuid? ver; /// The default constructor for the [ReferenceUuid]. const ReferenceUuid({ - required this.uuid, + required this.id, this.ver, }); @@ -46,12 +46,12 @@ final class ReferenceUuid extends Equatable { factory ReferenceUuid.fromCbor(CborValue value) { if (value is CborList) { return ReferenceUuid( - uuid: Uuid.fromCbor(value[0]), + id: Uuid.fromCbor(value[0]), ver: Uuid.fromCbor(value[1]), ); } else { return ReferenceUuid( - uuid: Uuid.fromCbor(value), + id: Uuid.fromCbor(value), ); } } @@ -61,14 +61,14 @@ final class ReferenceUuid extends Equatable { final ver = this.ver; if (ver != null) { return CborList([ - uuid.toCbor(), + id.toCbor(), ver.toCbor(), ]); } else { - return uuid.toCbor(); + return id.toCbor(); } } @override - List get props => [uuid, ver]; + List get props => [id, ver]; } diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart index 2b51d8fbf118..83d879725751 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart @@ -25,9 +25,9 @@ void main() { type: Uuid(uuidV7), id: Uuid(uuidV7), ver: Uuid(uuidV7), - ref: ReferenceUuid(uuid: Uuid(uuidV7)), - template: ReferenceUuid(uuid: Uuid(uuidV7)), - reply: ReferenceUuid(uuid: Uuid(uuidV7)), + ref: ReferenceUuid(id: Uuid(uuidV7)), + template: ReferenceUuid(id: Uuid(uuidV7)), + reply: ReferenceUuid(id: Uuid(uuidV7)), section: 'section_name', collabs: ['test@domain.com'], ), diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart index 22d200c77e9f..2acf3925f78d 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart @@ -26,9 +26,9 @@ void main() { type: Uuid(uuidV4), id: Uuid(uuidV7), ver: Uuid(uuidV7), - ref: ReferenceUuid(uuid: Uuid(uuidV7)), - template: ReferenceUuid(uuid: Uuid(uuidV7)), - reply: ReferenceUuid(uuid: Uuid(uuidV7)), + ref: ReferenceUuid(id: Uuid(uuidV7)), + template: ReferenceUuid(id: Uuid(uuidV7)), + reply: ReferenceUuid(id: Uuid(uuidV7)), section: 'section_name', collabs: ['test@domain.com'], ),