cose library
COSE wrappers for xDSA and xHPKE.
https://datatracker.ietf.org/doc/html/rfc9052 https://datatracker.ietf.org/doc/html/draft-ietf-cose-hpke
Signatures are COSE_Sign1 envelopes carrying the signer's fingerprint and a
timestamp in the protected header. Encryption is COSE_Encrypt0 around a
signed envelope, so every message created by seal is also signed.
Signing, verification, encryption and decryption use an application domain,
prefixed with dark-bio-v1:, which both sides must agree on.
Payloads and authenticated messages are plain Dart values. They are encoded
with the cbor package and must fit the CBOR subset that this package's
cbor library lists. Decoded payloads come back the way the cbor package
decodes them, so collections are untyped List and Map values. Byte
strings come back as List<int>, which needs Uint8List.fromList before
it is encoded again. Integers wider than 53 bits come back as BigInt,
even when they fit an int.
import 'dart:convert';
import 'package:darkbio_crypto/cose.dart' as cose;
import 'package:darkbio_crypto/xdsa.dart' as xdsa;
import 'package:darkbio_crypto/xhpke.dart' as xhpke;
void example() {
final signer = xdsa.SecretKey.generate();
final recipient = xhpke.SecretKey.generate();
final domain = utf8.encode('example');
// Sign a payload, binding a second message supplied separately
final signed = cose.sign(msgToEmbed: 'hello', msgToAuth: 'context', signer: signer, domain: domain);
final payload = cose.verify<String>(msgToCheck: signed, msgToAuth: 'context', verifier: signer.publicKey(), domain: domain, maxDriftSecs: 60);
assert(payload == 'hello');
// Sign and encrypt to a recipient in one step, then open and verify it back
final sealed = cose.seal(msgToSeal: 'secret', msgToAuth: 'context', signer: signer, recipient: recipient.publicKey(), domain: domain);
final opened = cose.open<String>(msgToOpen: sealed, msgToAuth: 'context', recipient: recipient, sender: signer.publicKey(), domain: domain, maxDriftSecs: 60);
assert(opened == 'secret');
}
Domain separation and freshness
Choose distinct domains for distinct application operations. Domains
prevent a message for one purpose from being accepted for another; they do
not stop repeated use within the same domain. Verification accepts a
signature whose timestamp is at most maxDriftSecs seconds in the past or
future. null skips this timestamp check. Applications that require
one-time acceptance must also track a message identifier, nonce, or
challenge to reject replays.
Wire profile
Interoperating implementations must match these Dark Bio conventions:
- Envelopes are untagged COSE_Sign1 and COSE_Encrypt0 arrays. CBOR tags are not accepted. Headers use deterministic integer-key maps.
- The private algorithm IDs are
-70000for xDSA and-70001for xHPKE. The protectedkidis the appropriate public key's fingerprint. Signatures require the private timestamp header-70002and name it incrit. - For signatures, the Sig_structure
external_aadis the CBOR encoding of[bstr("dark-bio-v1:" || domain), msgToAuth]. An embedded payload is the CBOR encoding of the caller's value. - For signDetached, the caller's message is authenticated in that
external_aad, while the Sig_structure payload is an empty byte string and the envelope payload is null. A generic COSE detached-payload API that puts the caller's message in the Sig_structure payload must be adapted to this convention. - For encryption, the Enc_structure
external_aadis the CBOR encoding ofmsgToAuth; the complete encoded Enc_structure is passed as HPKE AAD. HPKE key derivation uses"dark-bio-v1:" || domainas its info. The X-Wing encapsulated key is carried in unprotected header-4.
Here bstr denotes a CBOR byte string and || denotes byte concatenation.
The domain and msgToAuth are not included in the returned envelope; both
parties must know them or transmit them separately.
Functions
-
decrypt(
{required Uint8List msgToOpen, required Object? msgToAuth, required SecretKey recipient, required Uint8List domain}) → Uint8List - Decrypts a sealed message without verifying the signature.
-
encrypt(
{required Uint8List sign1, required Object? msgToAuth, required PublicKey recipient, required Uint8List domain}) → Uint8List - Encrypts an already-signed COSE_Sign1 to a recipient.
-
open<
T> ({required Uint8List msgToOpen, required Object? msgToAuth, required SecretKey recipient, required PublicKey sender, required Uint8List domain, int? maxDriftSecs}) → T - Decrypts and verifies a sealed message.
-
peek<
T> ({required Uint8List signature}) → T - Extracts the embedded payload from a COSE_Sign1 signature without verifying it.
-
recipient(
{required Uint8List ciphertext}) → Fingerprint - Extracts the recipient's fingerprint from a COSE_Encrypt0 message without decrypting it.
-
seal(
{required Object? msgToSeal, required Object? msgToAuth, required SecretKey signer, required PublicKey recipient, required Uint8List domain}) → Uint8List - Signs a message then encrypts it to a recipient.
-
sign(
{required Object? msgToEmbed, required Object? msgToAuth, required SecretKey signer, required Uint8List domain}) → Uint8List - Creates a COSE_Sign1 digital signature with an embedded payload.
-
signDetached(
{required Object? msgToAuth, required SecretKey signer, required Uint8List domain}) → Uint8List - Creates a COSE_Sign1 digital signature without an embedded payload (the envelope payload is null).
-
signer(
{required Uint8List signature}) → Fingerprint - Extracts the signer's fingerprint from a COSE_Sign1 signature without verifying it.
-
verify<
T> ({required Uint8List msgToCheck, required Object? msgToAuth, required PublicKey verifier, required Uint8List domain, int? maxDriftSecs}) → T - Validates a COSE_Sign1 digital signature and returns the embedded payload.
-
verifyDetached(
{required Uint8List msgToCheck, required Object? msgToAuth, required PublicKey verifier, required Uint8List domain, int? maxDriftSecs}) → void - Validates a COSE_Sign1 digital signature with a detached payload.