11import 'dart:collection' ;
2+ import 'dart:typed_data' ;
3+
4+ import 'package:cryptography/cryptography.dart' as crypto_api;
5+ import 'package:pointycastle/export.dart' as pc;
6+ import 'package:asn1lib/asn1lib.dart' as asn1;
27
38import 'package:cbor/cbor.dart' ;
9+ import 'package:convert/convert.dart' ;
410
511/// Represents a key as specified by RFC8152:
612/// [CBOR Object Signing and Encryption (COSE)] (https://tools.ietf.org/html/rfc8152)
713///
814/// Extended this class to support more COSE key types.
915sealed class CoseKey extends MapView <int , dynamic > {
16+ // Key Objects (RFC8152 Section 7.1)
17+ static const int ktyIdx = 1 ;
18+ static const int algIdx = 3 ;
19+
20+ // Key Types (RFC8152 Section 13)
21+ static const int ktyOKP = 1 ;
22+ static const int ktyEC2 = 2 ;
23+
24+ // EC2 Keys (RFC8152 Section 13.1.1)
25+ static const int ec2CrvIdx = - 1 ;
26+ static const int ec2XIdx = - 2 ;
27+ static const int ec2YIdx = - 3 ;
28+
29+ // EC2 Curves (RFC8152 Section 13.1)
30+ static const int ec2CrvP256 = 1 ;
31+
32+ // OKP Keys (RFC8152 Section 13.2)
33+ static const int okpCrvIdx = - 1 ;
34+ static const int okpXIdx = - 2 ;
35+
36+ // OKP Curves (RFC8152 Section 13.1)
37+ static const int okpCrvEd25519 = 6 ;
38+
1039 static const int ? algorithm = null ;
1140
1241 /// Constructor with optional parameters
1342 CoseKey (super .coseKeyParams);
1443
15- /// Method to verify a signature
16- void verify (List <int > message, List <int > signature) {
44+ /// Verifies a signature for the provided message using this COSE public key.
45+ ///
46+ /// Throws an [Exception] if verification fails or the algorithm is unsupported.
47+ Future <void > verify (List <int > message, List <int > signature) async {
1748 throw UnimplementedError ('Signature verification not supported.' );
1849 }
1950
51+ /// Convert to a CBOR Map representation suitable for passing through APIs
52+ /// that expect a [CborMap] (e.g., server verification inputs).
53+ CborMap toCborMap () {
54+ throw UnimplementedError ('toCborMap not supported.' );
55+ }
56+
2057 /// Convert to standard CBOR encoded format
2158 CborValue toCbor () {
22- throw UnimplementedError ( 'toCbor not supported.' );
59+ return CborValue ( toCborMap () );
2360 }
2461
2562 /// Static method to parse a COSE key
2663 static CoseKey parse (Map <int , dynamic > cose) {
27- int ? alg = cose[3 ];
64+ int ? alg = cose[algIdx ];
2865 if (alg == null ) {
2966 throw ArgumentError ('COSE alg identifier must be provided.' );
3067 }
3168 switch (alg) {
3269 case ES256 .algorithm:
3370 return ES256 (cose);
71+ case EdDSA .algorithm:
72+ return EdDSA (cose);
73+ case EcdhEsHkdf256 .algorithm:
74+ return EcdhEsHkdf256 (cose);
3475 }
3576 return UnsupportedKey (cose);
3677 }
3778
3879 /// Static method to get all algorithms supported by fido2 library
3980 static List <int > supportedAlgorithms () {
40- return [ES256 .algorithm];
81+ return [ES256 .algorithm, EdDSA .algorithm, EcdhEsHkdf256 .algorithm];
82+ }
83+
84+ @override
85+ String toString () {
86+ final alg = this [algIdx];
87+ final buffer = StringBuffer ();
88+ buffer.writeln ('CoseKey(' );
89+ buffer.writeln (' algorithm: $alg ,' );
90+ buffer.writeln (' params: ${Map <int , dynamic >.from (this )}' );
91+ buffer.write (')' );
92+ return buffer.toString ();
4193 }
4294}
4395
4496/// Represents a currently unsupported COSE key type
4597class UnsupportedKey extends CoseKey {
4698 UnsupportedKey (super .coseKeyParams);
99+
100+ @override
101+ String toString () {
102+ final alg = this [CoseKey .algIdx];
103+ final buffer = StringBuffer ();
104+ buffer.writeln ('UnsupportedKey(' );
105+ buffer.writeln (' algorithm: $alg ,' );
106+ buffer.writeln (' params: ${Map <int , dynamic >.from (this )}' );
107+ buffer.write (')' );
108+ return buffer.toString ();
109+ }
110+ }
111+
112+ /// Represents a COSE key of type EdDSA (Ed25519, see RFC8152 8.2)
113+ class EdDSA extends CoseKey {
114+ static const int algorithm = - 8 ;
115+
116+ EdDSA (super .coseKeyParams);
117+
118+ /// Static method to create a new instance from public key coordinates
119+ static EdDSA fromPublicKey (List <int > x) {
120+ return EdDSA ({
121+ CoseKey .ktyIdx: CoseKey .ktyOKP,
122+ CoseKey .algIdx: EdDSA .algorithm,
123+ CoseKey .okpCrvIdx: CoseKey .okpCrvEd25519,
124+ CoseKey .okpXIdx: x,
125+ });
126+ }
127+
128+ @override
129+ CborMap toCborMap () {
130+ return CborMap ({
131+ CborInt (BigInt .from (CoseKey .ktyIdx)):
132+ CborInt (BigInt .from (CoseKey .ktyOKP)),
133+ CborInt (BigInt .from (CoseKey .algIdx)):
134+ CborInt (BigInt .from (EdDSA .algorithm)),
135+ CborInt (BigInt .from (CoseKey .okpCrvIdx)):
136+ CborInt (BigInt .from (CoseKey .okpCrvEd25519)),
137+ CborInt (BigInt .from (CoseKey .okpXIdx)): CborBytes (this [CoseKey .okpXIdx]),
138+ });
139+ }
140+
141+ @override
142+ Future <void > verify (List <int > message, List <int > signature) async {
143+ final xBytes = this [CoseKey .okpXIdx] as List <int >? ;
144+ if (xBytes == null ) {
145+ throw Exception ('Ed25519 verification failed: missing public key (x).' );
146+ }
147+ // Ed25519 signatures must be exactly 64 bytes (R || S)
148+ if (signature.length != 64 ) {
149+ throw Exception (
150+ 'Assertion signature verification failed (Ed25519): invalid signature length ${signature .length }, expected 64 bytes.' );
151+ }
152+ final pubKey = crypto_api.SimplePublicKey (
153+ xBytes,
154+ type: crypto_api.KeyPairType .ed25519,
155+ );
156+ final verified = await crypto_api.Ed25519 ().verify (
157+ message,
158+ signature: crypto_api.Signature (signature, publicKey: pubKey),
159+ );
160+ if (! verified) {
161+ throw Exception ('Assertion signature verification failed (Ed25519).' );
162+ }
163+ }
47164}
48165
49166/// Represents a COSE key of type ES256 (ECDSA w/ SHA-256, see RFC8152 8.1)
@@ -55,24 +172,95 @@ class ES256 extends CoseKey {
55172 /// Static method to create a new instance from public key coordinates
56173 static ES256 fromPublicKey (List <int > x, List <int > y) {
57174 return ES256 ({
58- 1 : 2 ,
59- 3 : ES256 .algorithm,
60- - 1 : 1 ,
61- - 2 : x,
62- - 3 : y,
175+ CoseKey .ktyIdx : CoseKey .ktyEC2 ,
176+ CoseKey .algIdx : ES256 .algorithm,
177+ CoseKey .ec2CrvIdx : CoseKey .ec2CrvP256 ,
178+ CoseKey .ec2XIdx : x,
179+ CoseKey .ec2YIdx : y,
63180 });
64181 }
65182
66183 @override
67- CborValue toCbor () {
68- return CborValue ({
69- 1 : 2 ,
70- 3 : ES256 .algorithm,
71- - 1 : 1 ,
72- - 2 : CborBytes (this [- 2 ]),
73- - 3 : CborBytes (this [- 3 ]),
184+ CborMap toCborMap () {
185+ return CborMap ({
186+ CborInt (BigInt .from (CoseKey .ktyIdx)):
187+ CborInt (BigInt .from (CoseKey .ktyEC2)),
188+ CborInt (BigInt .from (CoseKey .algIdx)):
189+ CborInt (BigInt .from (ES256 .algorithm)),
190+ CborInt (BigInt .from (CoseKey .ec2CrvIdx)):
191+ CborInt (BigInt .from (CoseKey .ec2CrvP256)),
192+ CborInt (BigInt .from (CoseKey .ec2XIdx)): CborBytes (this [CoseKey .ec2XIdx]),
193+ CborInt (BigInt .from (CoseKey .ec2YIdx)): CborBytes (this [CoseKey .ec2YIdx]),
74194 });
75195 }
196+
197+ @override
198+ String toString () {
199+ final x = this [CoseKey .ec2XIdx] as List <int >? ;
200+ final y = this [CoseKey .ec2YIdx] as List <int >? ;
201+ final buffer = StringBuffer ();
202+ buffer.writeln ('ES256(' );
203+ buffer.writeln (' algorithm: $algorithm ,' );
204+ buffer.writeln (' x: ${x != null ? hex .encode (x ) : null },' );
205+ buffer.writeln (' y: ${y != null ? hex .encode (y ) : null }' );
206+ buffer.write (')' );
207+ return buffer.toString ();
208+ }
209+
210+ @override
211+ Future <void > verify (List <int > message, List <int > signature) async {
212+ final xBytes = this [CoseKey .ec2XIdx] as List <int >? ;
213+ final yBytes = this [CoseKey .ec2YIdx] as List <int >? ;
214+ if (xBytes == null || yBytes == null ) {
215+ throw Exception ('ES256 verification failed: missing public key x/y.' );
216+ }
217+
218+ // Parse ASN.1 DER ECDSA signature: SEQUENCE(INTEGER r, INTEGER s)
219+ BigInt bytesToInt (List <int > bytes) =>
220+ bytes.fold <BigInt >(BigInt .zero, (a, b) => (a << 8 ) | BigInt .from (b));
221+ final parser = asn1.ASN1Parser (Uint8List .fromList (signature));
222+ final obj = parser.nextObject ();
223+ if (obj is ! asn1.ASN1Sequence || obj.elements.length != 2 ) {
224+ throw Exception ('ES256 verification failed: malformed DER signature.' );
225+ }
226+ // Reject trailing bytes beyond the DER SEQUENCE
227+ // If the parser can continue, the signature contains extra data.
228+ if (parser.hasNext ()) {
229+ throw Exception (
230+ 'ES256 verification failed: trailing bytes present in DER signature.' );
231+ }
232+ final rObj = obj.elements[0 ];
233+ final sObj = obj.elements[1 ];
234+ if (rObj is ! asn1.ASN1Integer || sObj is ! asn1.ASN1Integer ) {
235+ throw Exception (
236+ 'ES256 verification failed: DER must contain two integers.' );
237+ }
238+ final rBytes = rObj.valueBytes ();
239+ final sBytes = sObj.valueBytes ();
240+ final r = bytesToInt (rBytes);
241+ var s = bytesToInt (sBytes);
242+
243+ // Build PointyCastle public key
244+ final domain = pc.ECDomainParameters ('secp256r1' );
245+ final q = domain.curve.createPoint (bytesToInt (xBytes), bytesToInt (yBytes));
246+ final pubKey = pc.ECPublicKey (q, domain);
247+
248+ // Low-S normalization to prevent malleability: use s = min(s, n - s)
249+ final n = domain.n;
250+ final halfN = n >> 1 ;
251+ if (s > halfN) {
252+ s = n - s;
253+ }
254+
255+ // Verify using SHA-256/ECDSA
256+ final verifier = pc.Signer ('SHA-256/ECDSA' );
257+ verifier.init (false , pc.PublicKeyParameter <pc.ECPublicKey >(pubKey));
258+ final ok = verifier.verifySignature (
259+ Uint8List .fromList (message), pc.ECSignature (r, s));
260+ if (! ok) {
261+ throw Exception ('Assertion signature verification failed (ES256).' );
262+ }
263+ }
76264}
77265
78266/// Represents a COSE key of type ECDH-ES+HKDF-256 (see RFC8152 11.1)
@@ -84,22 +272,38 @@ class EcdhEsHkdf256 extends CoseKey {
84272 /// Static method to create a new instance from public key coordinates
85273 static EcdhEsHkdf256 fromPublicKey (List <int > x, List <int > y) {
86274 return EcdhEsHkdf256 ({
87- 1 : 2 ,
88- 3 : EcdhEsHkdf256 .algorithm,
89- - 1 : 1 ,
90- - 2 : x,
91- - 3 : y,
275+ CoseKey .ktyIdx : CoseKey .ktyEC2 ,
276+ CoseKey .algIdx : EcdhEsHkdf256 .algorithm,
277+ CoseKey .ec2CrvIdx : CoseKey .ec2CrvP256 ,
278+ CoseKey .ec2XIdx : x,
279+ CoseKey .ec2YIdx : y,
92280 });
93281 }
94282
95283 @override
96- CborValue toCbor () {
97- return CborValue ({
98- 1 : 2 ,
99- 3 : EcdhEsHkdf256 .algorithm,
100- - 1 : 1 ,
101- - 2 : CborBytes (this [- 2 ]),
102- - 3 : CborBytes (this [- 3 ]),
284+ CborMap toCborMap () {
285+ return CborMap ({
286+ CborInt (BigInt .from (CoseKey .ktyIdx)):
287+ CborInt (BigInt .from (CoseKey .ktyEC2)),
288+ CborInt (BigInt .from (CoseKey .algIdx)):
289+ CborInt (BigInt .from (EcdhEsHkdf256 .algorithm)),
290+ CborInt (BigInt .from (CoseKey .ec2CrvIdx)):
291+ CborInt (BigInt .from (CoseKey .ec2CrvP256)),
292+ CborInt (BigInt .from (CoseKey .ec2XIdx)): CborBytes (this [CoseKey .ec2XIdx]),
293+ CborInt (BigInt .from (CoseKey .ec2YIdx)): CborBytes (this [CoseKey .ec2YIdx]),
103294 });
104295 }
296+
297+ @override
298+ String toString () {
299+ final x = this [CoseKey .ec2XIdx] as List <int >? ;
300+ final y = this [CoseKey .ec2YIdx] as List <int >? ;
301+ final buffer = StringBuffer ();
302+ buffer.writeln ('EcdhEsHkdf256(' );
303+ buffer.writeln (' algorithm: $algorithm ,' );
304+ buffer.writeln (' x: ${x != null ? hex .encode (x ) : null },' );
305+ buffer.writeln (' y: ${y != null ? hex .encode (y ) : null }' );
306+ buffer.write (')' );
307+ return buffer.toString ();
308+ }
105309}
0 commit comments