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 -70000 for xDSA and -70001 for xHPKE. The protected kid is the appropriate public key's fingerprint. Signatures require the private timestamp header -70002 and name it in crit.
  • For signatures, the Sig_structure external_aad is 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_aad is the CBOR encoding of msgToAuth; the complete encoded Enc_structure is passed as HPKE AAD. HPKE key derivation uses "dark-bio-v1:" || domain as 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.