Skip to content

COSE_Sign1 (RFC 9964, the COSE half) ​

cose.sign1 and cose.verify1 sign and verify COSE_Sign1 messages with ML-DSA: the CBOR counterpart of a compact JWS, used by CBOR Web Tokens, WebAuthn and many IoT and attestation formats. StandardJwt is the JOSE half of the same RFC.

Use it when a CBOR/COSE system must verify your signature (or you must verify theirs). Do not use it between two quantum-safe services that only need to exchange signed data: Sign with a context is simpler.

A complete example ​

ts
import { Sign, VerificationError, cose, utf8 } from 'quantum-safe-ts';

const signer = new Sign('ML-DSA-65');                 // ML-DSA-44, -65 or -87 (COSE algorithms -48, -49, -50)
using pair = signer.generateStandardKeyPair();        // a 32-byte seed and the FIPS 204 public key

const message = cose.sign1(utf8('hello'), pair, { kid: utf8('key-1'), externalAad: utf8('myapp-v1') });
const payload = cose.verify1(message, pair.publicKey, { externalAad: utf8('myapp-v1') });
if (new TextDecoder().decode(payload) !== 'hello') throw new Error('payload changed');

try {
  cose.verify1(message, pair.publicKey);              // the external AAD is part of what was signed
  throw new Error('should have been refused');
} catch (e) {
  if (!(e instanceof VerificationError)) throw e;
}

const info = cose.inspectSign1(message);              // facts without a key
if (info.algorithm !== 'ML-DSA-65' || info.coseAlg !== -49) throw new Error('unexpected header');

What it signs ​

Sig_structure = ["Signature1", protected, external_aad, payload] (RFC 9052), signed with plain FIPS 204 ML-DSA.Sign and the empty context, the same operation as Sign.signRaw. Any compliant COSE library verifies the result. RFC 9964's own Appendix A examples (all three levels) are in this library's tests: its signatures verify, its signed bytes are reproduced byte for byte, and its all-zero seed derives its public key.

Options ​

OptionEffect
kidKey identifier, placed in the protected header
externalAadApplication data that is signed but not carried; the verifier must pass the same value
tagged: falseOmit CBOR tag 18 (the default is tagged)
detached: trueLeave the payload out; the verifier passes detachedPayload

Keys ​

cose.encodeKey(algorithm, publicKey, { kid }) writes an AKP COSE_Key; cose.encodePrivateKey(pair) also writes the 32-byte seed (label -2), which you must store as a secret; cose.decodeKey(bytes) and cose.keyPairFromCoseKey(bytes) read them back.

Strictness ​

Verification refuses: an algorithm that is not in the protected header or not ML-DSA; a crit header; non-minimal CBOR, indefinite lengths, duplicate map labels and trailing bytes; a COSE_Sign (tag 98) where a COSE_Sign1 is expected.

Seed keys sign; expanded keys from quantum-safe-py only verify

quantum-safe-py's liboqs-generated secret keys are expanded keys with no recoverable seed, so TypeScript cannot sign with them (see Signatures). It verifies everything they sign.

Apache-2.0.