@vanaheimr/cose 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/LICENSE.md +201 -0
  3. package/NOTICE +31 -0
  4. package/README.md +537 -0
  5. package/dist/aes.d.ts +45 -0
  6. package/dist/aes.d.ts.map +1 -0
  7. package/dist/algorithm.d.ts +137 -0
  8. package/dist/algorithm.d.ts.map +1 -0
  9. package/dist/asn1.d.ts +114 -0
  10. package/dist/asn1.d.ts.map +1 -0
  11. package/dist/cbor.d.ts +65 -0
  12. package/dist/cbor.d.ts.map +1 -0
  13. package/dist/curve.d.ts +65 -0
  14. package/dist/curve.d.ts.map +1 -0
  15. package/dist/ecdsa.d.ts +56 -0
  16. package/dist/ecdsa.d.ts.map +1 -0
  17. package/dist/eddsa.d.ts +14 -0
  18. package/dist/eddsa.d.ts.map +1 -0
  19. package/dist/encrypt.d.ts +140 -0
  20. package/dist/encrypt.d.ts.map +1 -0
  21. package/dist/errors.d.ts +64 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/headers.d.ts +77 -0
  24. package/dist/headers.d.ts.map +1 -0
  25. package/dist/hmac.d.ts +32 -0
  26. package/dist/hmac.d.ts.map +1 -0
  27. package/dist/index.d.ts +87 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +4159 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/key.d.ts +201 -0
  32. package/dist/key.d.ts.map +1 -0
  33. package/dist/labels.d.ts +51 -0
  34. package/dist/labels.d.ts.map +1 -0
  35. package/dist/mac.d.ts +93 -0
  36. package/dist/mac.d.ts.map +1 -0
  37. package/dist/mac0.d.ts +132 -0
  38. package/dist/mac0.d.ts.map +1 -0
  39. package/dist/mldsa.d.ts +29 -0
  40. package/dist/mldsa.d.ts.map +1 -0
  41. package/dist/payload.d.ts +25 -0
  42. package/dist/payload.d.ts.map +1 -0
  43. package/dist/recipient.d.ts +65 -0
  44. package/dist/recipient.d.ts.map +1 -0
  45. package/dist/resolve.d.ts +29 -0
  46. package/dist/resolve.d.ts.map +1 -0
  47. package/dist/sign.d.ts +82 -0
  48. package/dist/sign.d.ts.map +1 -0
  49. package/dist/sign1.d.ts +273 -0
  50. package/dist/sign1.d.ts.map +1 -0
  51. package/dist/signature.d.ts +43 -0
  52. package/dist/signature.d.ts.map +1 -0
  53. package/dist/x509.d.ts +144 -0
  54. package/dist/x509.d.ts.map +1 -0
  55. package/dist/x5chain.d.ts +98 -0
  56. package/dist/x5chain.d.ts.map +1 -0
  57. package/package.json +97 -0
package/README.md ADDED
@@ -0,0 +1,537 @@
1
+ # Vanaheimr COSE (TypeScript)
2
+
3
+ [![CI](https://github.com/Vanaheimr/COSE.TS/actions/workflows/ci.yml/badge.svg)](https://github.com/Vanaheimr/COSE.TS/actions/workflows/ci.yml)
4
+ [![Nightly](https://github.com/Vanaheimr/COSE.TS/actions/workflows/nightly.yml/badge.svg)](https://github.com/Vanaheimr/COSE.TS/actions/workflows/nightly.yml)
5
+ [![npm](https://img.shields.io/npm/v/@vanaheimr/cose.svg)](https://www.npmjs.com/package/@vanaheimr/cose)
6
+ [![Cross-signing](https://img.shields.io/github/actions/workflow/status/Vanaheimr/MCBORConformanceTests/ci.yml?branch=master&label=cross-signing%20vs.%20Styx)](https://github.com/Vanaheimr/MCBORConformanceTests/actions/workflows/ci.yml)
7
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE.md)
8
+
9
+ **CBOR Object Signing and Encryption** ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052))
10
+ in TypeScript, built on the CBOR codec of
11
+ [MetrologicalCBOR.TS](https://github.com/Vanaheimr/MetrologicalCBOR.TS) — the
12
+ counterpart of [Vanaheimr Styx](https://github.com/Vanaheimr/Styx)'s
13
+ [`Illias/COSE`](https://github.com/Vanaheimr/Styx/blob/master/Styx/Illias/COSE/README.md),
14
+ and the second implementation the
15
+ [COSE cross-signing conformance suite](https://github.com/Vanaheimr/MCBORConformanceTests)
16
+ needs in order to have anything to compare.
17
+
18
+ Signing is what turns the [metrological value extension](https://github.com/OpenChargingTechnology/Whitepapers/blob/master/MetrologicalCBOR/README.md)
19
+ into something a third party can check: the encoding of a reading is a pure
20
+ function of its value, scale, unit, prefix and uncertainty, so the same reading
21
+ always produces the same bytes — and therefore the same signature. Two codecs
22
+ that disagree about one byte of one reading produce signatures that fail at the
23
+ other, which is a conformance failure worth catching before a meter ships.
24
+
25
+ ## Why it is a repository of its own
26
+
27
+ `@vanaheimr/metrological-cbor` has zero runtime dependencies and a documented
28
+ rule that it will never do cryptography: a data format that also carried a
29
+ crypto stack would be unusable as the leaf of somebody else's schema. That rule
30
+ stays intact by keeping the two apart — exactly as Styx keeps `Illias/COSE`
31
+ beside `Illias/CBOR` rather than inside it. Whoever wants the format gets no
32
+ crypto; whoever wants signatures adds this and gets the `@noble` family with it.
33
+
34
+ ## Where it runs
35
+
36
+ Under Node and in a browser alike. The cryptography is the `@noble` family end
37
+ to end — [curves](https://github.com/paulmillr/noble-curves),
38
+ [hashes](https://github.com/paulmillr/noble-hashes),
39
+ [ciphers](https://github.com/paulmillr/noble-ciphers),
40
+ [post-quantum](https://github.com/paulmillr/noble-post-quantum) — and nothing
41
+ touches `node:crypto` or WebCrypto, so both platforms run the same
42
+ implementation, byte for byte. That is a choice with reasons beyond
43
+ portability: WebCrypto is asynchronous end to end, which would put an `await`
44
+ into every signature of this API, and it speaks neither brainpool nor ML-DSA,
45
+ so the platform's own crypto could not carry the algorithm registry anyway.
46
+
47
+ The claim is enforced rather than intended:
48
+ [`tsconfig.browser.json`](https://github.com/Vanaheimr/COSE.TS/blob/master/tsconfig.browser.json) type-checks `src/` a second
49
+ time against a browser's view of the world, where `node:*` does not resolve
50
+ and `Buffer` is not a name. A Node-only import is a build failure here, not a
51
+ bug report from somebody's bundler.
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ npm install @vanaheimr/cose
57
+ ```
58
+
59
+ The package is ESM, with one bundled `dist/index.js` and per-module type
60
+ declarations. There is deliberately no `.cjs` twin: every runtime dependency
61
+ ships as ESM only, so a CommonJS build would promise a compatibility its own
62
+ imports cannot honour. `require()` works where Node supports `require(esm)`,
63
+ which is 20.19 and later.
64
+
65
+ [`src/cbor.ts`](https://github.com/Vanaheimr/COSE.TS/blob/master/src/cbor.ts) is the only module that knows where the codec
66
+ comes from, and it names
67
+ [`@vanaheimr/metrological-cbor`](https://www.npmjs.com/package/@vanaheimr/metrological-cbor)
68
+ — an ordinary dependency, pinned by the lock file like everything else. The
69
+ place that still builds both implementations from source is the cross-signing
70
+ conformance suite, whose job is exactly that: what it signs has to be what the
71
+ codec produces *today*, and it has its own repository and checkout layout
72
+ for it.
73
+
74
+ ## What is implemented
75
+
76
+ - **`CoseSign1`** — a payload signed by a single signer (CBOR tag 18): sign,
77
+ verify, detached payloads, external additional authenticated data, and the
78
+ `crit` header parameter.
79
+ - **`CoseSign` / `CoseSignature`** — one payload, several signers (CBOR tag
80
+ 98). Each signature carries its own header buckets, so every party signs with
81
+ its own algorithm and its own key.
82
+ - **Countersignatures** ([RFC 9338](https://www.rfc-editor.org/rfc/rfc9338),
83
+ header parameter 11) on a `CoseSign1` — a signature *of a signature*, in the
84
+ version 2 form that actually covers the signature it countersigns.
85
+ - **`CoseKey`** — COSE keys of key type EC2 ([RFC 9052 §7](https://www.rfc-editor.org/rfc/rfc9052#section-7),
86
+ including compressed `y`), OKP, and AKP
87
+ ([RFC 9964](https://www.rfc-editor.org/rfc/rfc9964)), with the COSE Key
88
+ Thumbprints of [RFC 9679](https://www.rfc-editor.org/rfc/rfc9679).
89
+ - **The algorithm and curve registries**, including the fully-specified
90
+ algorithms of [RFC 9864](https://www.rfc-editor.org/rfc/rfc9864) and the
91
+ brainpool curves registered by ISO/IEC 18013-5.
92
+ - **`CoseMac0` / `CoseMac`** — a payload authenticated with a shared key
93
+ (CBOR tags 17 and 97), with the HMAC algorithms of
94
+ [RFC 9053 §3.1](https://www.rfc-editor.org/rfc/rfc9053#section-3.1). Not a
95
+ small signature. See below.
96
+ - **`CoseEncrypt0` / `CoseEncrypt`** — content encrypted with AES-GCM (CBOR
97
+ tags 16 and 96).
98
+ - **`CoseRecipient`** — how a content key reaches a party: `direct` and AES key
99
+ wrap ([RFC 3394](https://www.rfc-editor.org/rfc/rfc3394)). This is what the
100
+ enveloped forms have and the bare ones do not.
101
+ - **X.509 certificate chains** ([RFC 9360](https://www.rfc-editor.org/rfc/rfc9360),
102
+ header parameters `x5chain` and `x5t`) — parsed, walked to a trust anchor,
103
+ and bound to the key that signed. See below.
104
+
105
+ | Algorithm | Id | Curve | Digest |
106
+ |-----------|---:|-------|--------|
107
+ | `ES256` / `ES384` / `ES512` | −7 / −35 / −36 | any (deprecated by RFC 9864) | SHA-256 / 384 / 512 |
108
+ | `ESP256` / `ESP384` / `ESP512` | −9 / −51 / −52 | P-256 / P-384 / P-521 | SHA-256 / 384 / 512 |
109
+ | `ESB256` / `ESB320` / `ESB384` / `ESB512` | −265 / −266 / −267 / −268 | brainpoolP256r1 / P320r1 / P384r1 / P512r1 | SHA-256 / 384 / 384 / 512 |
110
+ | `ES256K` | −47 | secp256k1 | SHA-256 |
111
+ | `Ed25519` / `Ed448` | −19 / −53 | Ed25519 / Ed448 | *(none — pure)* |
112
+ | `ML-DSA-44` / `-65` / `-87` | −48 / −49 / −50 | *(none — an algorithm key pair)* | *(none — pure)* |
113
+
114
+ **Two of those three families are pure**: EdDSA ([RFC 8032](https://www.rfc-editor.org/rfc/rfc8032))
115
+ and ML-DSA ([FIPS 204](https://doi.org/10.6028/NIST.FIPS.204), registered for
116
+ COSE by [RFC 9964](https://www.rfc-editor.org/rfc/rfc9964)) sign the
117
+ `Sig_structure` itself rather than a digest of it. Handing a pure signer a hash
118
+ yields a signature that is valid for the hash and that nobody else accepts —
119
+ which is why the family is a property of the algorithm here and not an
120
+ afterthought.
121
+
122
+ They also bring two more key types. EdDSA uses **OKP**, where the public key is
123
+ the whole of `x` and there is no `y`. ML-DSA uses **AKP**, a key pair belonging
124
+ to an algorithm rather than to a curve — and there the labels shift underfoot:
125
+ `−1` is the public key and `−2` the private one, where an EC2 or OKP key has
126
+ the curve and the x coordinate. Parsing therefore establishes the key type
127
+ before it reads anything else. Two further RFC 9964 particulars: `priv` is the
128
+ **32-byte seed**, not the expanded secret key, and the thumbprint covers `alg`,
129
+ because an ML-DSA public key does not say which parameter set produced it.
130
+
131
+ EdDSA is deterministic by construction; ML-DSA is not, and RFC 9964 does not
132
+ choose. This library always takes the deterministic variant of FIPS 204, where
133
+ the per-signature randomness is 32 zero bytes — the choice that decides whether
134
+ two implementations can be compared byte for byte or only asked whether each
135
+ accepts the other.
136
+
137
+ Every EC2 curve in the COSE registry is computable here. **brainpoolP320r1 is
138
+ the one this package defines itself**, in [`src/ecdsa.ts`](https://github.com/Vanaheimr/COSE.TS/blob/master/src/ecdsa.ts), from
139
+ the domain parameters of [RFC 5639 §3.4](https://www.rfc-editor.org/rfc/rfc5639#section-3.4):
140
+ the underlying library ships the three other brainpool curves and not that one.
141
+ Transcribing 320-bit constants is the kind of task that fails silently, so they
142
+ are checked three times over — the curve constructor refuses a generator that
143
+ is not on the curve, [`tests/brainpool.test.ts`](https://github.com/Vanaheimr/COSE.TS/blob/master/tests/brainpool.test.ts)
144
+ checks that the order really is the order, and the conformance suite signs with
145
+ them and compares the bytes against Bouncy Castle's own brainpoolP320r1. A
146
+ single wrong digit survives none of the three.
147
+
148
+ ### Message authentication is not signing
149
+
150
+ `CoseMac0` is the structural twin of `CoseSign1`: four elements in the same
151
+ order, CBOR tag 17 against 18, and a MAC_structure differing from the
152
+ Sig_structure in one string — `"MAC0"` where the other says `"Signature1"`.
153
+ Everything the signature code learned applies unchanged: the protected bucket
154
+ kept verbatim, the CBOR tag not covered, detached payloads, external additional
155
+ authenticated data.
156
+
157
+ **What is not the same is what a verified message means**, and that difference
158
+ is the whole reason this section exists rather than a line in the table above.
159
+
160
+ A signature says *"the holder of that private key produced this"*, to anybody
161
+ who cares to check. A tag says *"someone holding the shared key produced this"*
162
+ — and it says it only to someone who holds that key too, because verifying one
163
+ requires the very key that creates one. Between two parties that is still
164
+ useful: each knows the other made it, having not made it themselves. Towards a
165
+ third party it is worth nothing, and a party who later denies having produced a
166
+ message cannot be contradicted with a tag.
167
+
168
+ That is why a metrological record is **signed**. The customer, the operator and
169
+ the regulator all have to be able to check a reading, and none of them may be
170
+ able to manufacture one. What a MAC buys instead is size and speed: eight bytes
171
+ and one pass of a hash, against sixty-four bytes and a curve multiplication for
172
+ the smallest signature here — or 4627 bytes post-quantum. It belongs where the
173
+ two ends of a link already share a secret and want cheap tamper detection, with
174
+ the durable evidence carried by a signature underneath. COSE nests, so both can
175
+ travel at once.
176
+
177
+ The API keeps them apart deliberately: `signWith` refuses an HMAC algorithm and
178
+ `macWith` refuses a signature one, so neither can stand in for the other by
179
+ accident.
180
+
181
+ #### Only HMAC, and why
182
+
183
+ [RFC 9053 §3.2](https://www.rfc-editor.org/rfc/rfc9053#section-3.2) also
184
+ registers AES-CBC-MAC (algorithms 14, 15, 25, 26). It is deliberately absent
185
+ here. Raw CBC-MAC is secure only for messages of a **fixed** length: given the
186
+ tag `T` of a one-block message `M`, the two-block message `M ‖ (T ⊕ M)` has the
187
+ very same tag, which is a forgery constructed without the key. §3.2.1 says so
188
+ itself, and names what saves it inside COSE — *"the current structure mitigates
189
+ this problem, as a specific encoding structure that includes lengths is built
190
+ and signed"*. Its safety there rests on the MAC_structure, not on the
191
+ primitive. HMAC needs no such argument, and it is what a device without an AES
192
+ accelerator would reach for anyway.
193
+
194
+ Worth knowing if you ever read the two RFCs side by side: RFC 9052's own
195
+ Appendix C.6.1 describes algorithm 15 as *"AES-CMAC"*, while RFC 9053 §3.2
196
+ states outright that AES-CBC-MAC **is not** AES-CMAC ([RFC 4493](https://www.rfc-editor.org/rfc/rfc4493)),
197
+ which is a different construction that fixes exactly the length problem above.
198
+ The identifier is CBC-MAC; the prose of the other RFC is wrong.
199
+
200
+ #### Three details that are easy to get backwards
201
+
202
+ - **Truncation applies to the output, never to the key.** `HMAC 256/64` is the
203
+ leftmost eight bytes of the full HMAC-SHA-256. An implementation that
204
+ shortened the key instead would produce tags nobody else accepts, and would
205
+ verify its own perfectly.
206
+ - **The comparison is constant time.** A byte-by-byte compare returning early
207
+ tells an attacker how many leading bytes of a guessed tag were right, which
208
+ turns forging a 32-byte tag from 2^256 work into 32 × 256. Signature
209
+ verification has no equivalent exposure, because everything it compares is
210
+ public — this is a requirement a MAC has and a signature does not.
211
+ - **A key issued for one algorithm is not talked into another.** Using an
212
+ `HMAC 256/256` key to produce a 64-bit tag is a downgrade its holder never
213
+ agreed to, so `CoseMac0` refuses it — one of the key checks RFC 9053 §3.1
214
+ asks for.
215
+
216
+ #### Recipient structures, and what they cost
217
+
218
+ `CoseMac` (tag 97) and `CoseEncrypt` (tag 96) differ from their bare
219
+ counterparts in one element: a list of **recipient structures**, each
220
+ delivering the one content key to one party by a route only that party can
221
+ walk. `CoseMac0` and `CoseEncrypt0` assume both sides already hold the key;
222
+ these solve the distribution problem inside the message.
223
+
224
+ Two routes are implemented, and they are the two reachable from a pre-shared
225
+ secret. **`direct`** transports nothing — the recipient's key *is* the content
226
+ key, and the structure carries an empty protected bucket, an empty ciphertext
227
+ and a key identifier. That makes a one-`direct`-recipient `CoseMac` a
228
+ `CoseMac0` with ceremony, which is exactly why the bare forms exist. **AES key
229
+ wrap** carries the content key encrypted under a key-encryption key; note that
230
+ the algorithm follows the width of the *key-encryption* key, so `A256KW` wraps
231
+ a 128-bit content key perfectly well. Key wrap is deterministic — no nonce, no
232
+ salt — which is safe only because what it wraps is a uniformly random key
233
+ rather than a message.
234
+
235
+ **A recipient list costs more than bytes.** Every recipient holds the same
236
+ content key afterwards, so with more than one of them the tag stops
237
+ distinguishing them at all: any recipient can produce a message the others will
238
+ accept as coming from the sender. A `CoseMac0` between two parties at least
239
+ tells each of them that the other made it, on the grounds that they did not
240
+ make it themselves; a `CoseMac` to three parties tells nobody that. RFC 9052
241
+ §8.2 is blunt about it — a MAC *"cannot be used to prove the identity of the
242
+ sender to a third party"*.
243
+
244
+ Not implemented: ECDH key agreement and the HKDF-based key derivations. Both
245
+ need `COSE_KDF_Context` ([RFC 9053 §5.2](https://www.rfc-editor.org/rfc/rfc9053#section-5.2)),
246
+ a structure of its own carrying PartyU and PartyV information and the
247
+ supplementary public info — and one whose fields, got subtly wrong, derive a
248
+ key that agrees only with an implementation making the same mistake. It is a
249
+ piece of work in its own right rather than a variation on this one.
250
+
251
+ ### Encryption
252
+
253
+ `CoseEncrypt0.encrypt(plaintext, key, { iv })` and
254
+ `CoseEncrypt.encrypt(plaintext, contentKey, recipients, { iv })`, with AES-GCM
255
+ in all three key widths. Three things here differ from everything else in this
256
+ package, and all three catch people out.
257
+
258
+ **The `Enc_structure` has three elements, not four.** It is
259
+ `[context, protected, external_aad]` — no payload. The payload is what is being
260
+ *encrypted*; the `Enc_structure` is what is merely *authenticated* alongside
261
+ it, as the AEAD's additional data, and the recipient rebuilds it from the
262
+ message rather than receiving it.
263
+
264
+ **The authentication tag is not a field.** AES-GCM's 16-byte tag is appended to
265
+ the ciphertext and travels inside the same byte string.
266
+
267
+ **The nonce is public and must never repeat.** It travels in the `iv` header
268
+ parameter in the clear, and that is fine; using one twice with the same key is
269
+ not. GCM fails catastrophically on nonce reuse — two messages under one nonce
270
+ leak the XOR of their plaintexts *and* the authentication subkey, which lets an
271
+ attacker forge afterwards. `iv` is therefore a required option with no default:
272
+ only the caller knows which nonces it has spent.
273
+
274
+ And the point to keep in view: an encrypted message says nothing about *who*
275
+ sent it. AEAD integrity means "whoever holds this key wrote this". RFC 9052
276
+ §8.3 calls it *"either no or very limited data origination"*. A signed payload
277
+ inside an encrypted envelope is how one gets both — and COSE nests, so both can
278
+ travel at once.
279
+
280
+ Not implemented: AES-CCM and ChaCha20/Poly1305 — scope rather than judgement,
281
+ in both cases. CCM is eight registered variants of nonce and tag width (64-bit
282
+ tags included, a constrained-radio economy), serving a world this library does
283
+ not live in — and since the crypto moved to `@noble/ciphers`, a primitive
284
+ without a supplier: noble deliberately ships no CCM, and `node:crypto`, which
285
+ did, is exactly what the browser support removed. ChaCha20/Poly1305 earns its
286
+ keep where there is no AES hardware, which nothing here asked for. And Styx
287
+ carries neither, so the cross-signing suite would have nothing to compare
288
+ them against. Also absent: the `COSE_Encrypt` recipient routes listed above.
289
+
290
+ #### The symmetric key
291
+
292
+ Key type 4 [[RFC 9053 §7.3](https://www.rfc-editor.org/rfc/rfc9053#section-7.3)],
293
+ carrying its value under label `−1`. That label now means a **third** thing:
294
+ the curve on an EC2 or OKP key, the public key on an algorithm key pair, and
295
+ the shared secret here — which is why the key type is established in a pass of
296
+ its own before anything else is read.
297
+
298
+ `publicKey()` throws on one rather than returning it unchanged. RFC 9053 states
299
+ outright that the structure *"does not have a form that contains only public
300
+ members"*, so stripping the private fields would hand a caller the shared
301
+ secret under a name promising the opposite.
302
+
303
+ Its thumbprint [[RFC 9679 §4.4](https://www.rfc-editor.org/rfc/rfc9679#section-4.4)]
304
+ covers `kty` and `k` — and is a hash **of the secret**. §7 of the same RFC warns
305
+ about it plainly: a low-entropy key can simply be looked up in a precomputed
306
+ table, so thumbprints MUST NOT be used with passwords or anything resembling
307
+ one.
308
+
309
+ No key length is enforced. RFC 9053 says a key SHOULD be as wide as the hash
310
+ output, which is advice about key management rather than a rule about the
311
+ primitive — RFC 2104 accepts any width, and the published vectors of RFC 4231
312
+ include a four-byte key. What a caller must not do is derive one from a
313
+ password, and no length check would catch that.
314
+
315
+ ### Certificate chains
316
+
317
+ `CoseSign1.verifyWithCertificateChain(trustAnchors, { at })` verifies a message
318
+ against the chain it carries rather than against a public key somebody handed
319
+ over: the chain is walked to one of the given anchors, and the key of its
320
+ end-entity certificate is then the key the signature has to verify with. The
321
+ two are never answered apart from one another, because a chain that validates
322
+ beautifully says nothing about the message it arrived with unless the key it
323
+ ends in is the key that signed — and that is a failure no chain check alone
324
+ catches.
325
+
326
+ Checked: the validity periods (at an instant the caller may name, which an
327
+ archived message needs), that each certificate was signed by the next, that
328
+ every issuer is actually a certification authority allowed to sign
329
+ certificates, that the chain ends at an anchor or at something an anchor
330
+ issued, and that the end-entity certificate is allowed to create signatures at
331
+ all. `x5t`, when present, must name the certificate that travelled.
332
+
333
+ Not checked, and the same list as Styx so that the two agree about what a pass
334
+ means: revocation, name constraints, certificate policies, and path length
335
+ beyond the CA flag itself.
336
+
337
+ The DER is read here rather than by a library, in
338
+ [`src/asn1.ts`](https://github.com/Vanaheimr/COSE.TS/blob/master/src/asn1.ts) and [`src/x509.ts`](https://github.com/Vanaheimr/COSE.TS/blob/master/src/x509.ts), and the reason
339
+ is the same one that made brainpoolP320r1 a local definition. A certificate
340
+ chain has to be verifiable with every algorithm this package signs with — which
341
+ includes the four brainpool curves and the three ML-DSA parameter sets — and
342
+ the TypeScript X.509 libraries verify through WebCrypto, which supports
343
+ neither. A meter certificate on brainpoolP256r1 is exactly the certificate they
344
+ cannot check. Verification here goes through the same path as every other
345
+ signature this package verifies, so whatever COSE can verify, a certificate can
346
+ be signed with.
347
+
348
+ The certificates the test suite reads are minted by Bouncy Castle rather than
349
+ written here, which is not a convenience: a DER parser checked against
350
+ certificates its own package produced would agree with itself about any
351
+ misreading whatsoever.
352
+
353
+ Not implemented, deliberately in every case: `COSE_Countersignature0`, the
354
+ abbreviated countersignature of RFC 9338 — a bare signature with no header
355
+ buckets of its own, so which algorithm made it and which key checks it travel
356
+ outside the message, by agreement, which is the opposite direction from
357
+ everything else here; AES-CBC-MAC, argued in the MAC chapter above; AES-CCM
358
+ and ChaCha20/Poly1305, argued in the encryption chapter above; ECDH-based key
359
+ agreement and the HKDF derivations, which need `COSE_KDF_Context`, a structure
360
+ of its own; and the `x5bag` and `x5u` header parameters — a bag is an
361
+ unordered heap with no path to follow, and a URI is a fetch, which a signature
362
+ library has no business performing.
363
+
364
+ ## Signing and verifying
365
+
366
+ ```typescript
367
+ import { CoseAlgorithms, CoseCurves, CoseKey, CoseSign1 } from './src/index.ts';
368
+
369
+ const key = CoseKey.fromPrivateScalar(CoseCurves.P256, privateScalar,
370
+ { algorithm: CoseAlgorithms.ES256 });
371
+
372
+ const signed = CoseSign1.sign(payload, key);
373
+ const bytes = signed.toBytes();
374
+
375
+ const message = CoseSign1.parse(bytes);
376
+ const result = message.verify(key.publicKey());
377
+
378
+ if (!result.verified)
379
+ console.log(result.reason);
380
+ ```
381
+
382
+ A failed verification is not an exception — it is the expected outcome of
383
+ checking untrusted data — so it comes back as a result carrying the reason.
384
+ Malformed input, which is a different kind of wrong, throws `CoseError`.
385
+
386
+ Signing is **deterministic** ([RFC 6979](https://www.rfc-editor.org/rfc/rfc6979)):
387
+ the nonce is derived from the private key and the message rather than drawn at
388
+ random. Signing the same data twice yields the same bytes, which makes a
389
+ published example recomputable — and it matters rather more for a device with
390
+ no dependable source of randomness, since a repeated nonce hands over the
391
+ private key.
392
+
393
+ The **payload** is rewritten in the deterministic encoding of
394
+ [RFC 8949](https://www.rfc-editor.org/rfc/rfc8949) §4.2.1 before it is
395
+ signed, unless `canonicalizePayload: false` says otherwise. A COSE payload is
396
+ an opaque byte string, so a signature covers one *spelling* of a record rather
397
+ than the record — and a receiver that decodes it and encodes it again
398
+ produces the deterministic spelling. Where the signer wrote a different one,
399
+ that receiver forwards a signature that no longer verifies, having altered
400
+ nothing, and the failure looks exactly like tampering. A payload that is not
401
+ CBOR is signed as it is; a *detached* payload that canonicalizing would change
402
+ is refused, because the message does not carry it and the verifier would be
403
+ handed the caller's own bytes. `CoseMac0` and `CoseMac` do the same, for the
404
+ same reason: an authentication tag covers bytes exactly as a signature does.
405
+
406
+ Whenever the signing key does not live in this process, `toBeSigned()` hands
407
+ out exactly the byte string that has to be signed.
408
+
409
+ ## Five things that are easy to get wrong
410
+
411
+ 1. **The signature never covers the message.** It covers the `Sig_structure`
412
+ `["Signature1", protected, external_aad, payload]` (RFC 9052 §4.4). The CBOR
413
+ tag is therefore *not* signed: the same message with and without tag 18
414
+ carries the very same signature bytes.
415
+ 2. **The protected bucket is kept verbatim.** A re-serialization differing in a
416
+ single byte — a non-preferred integer head, a different map order —
417
+ invalidates every signature made over the original bytes. Nothing here
418
+ re-encodes it, and the CBOR codec is driven in `preserve` mode throughout
419
+ for the same reason.
420
+ 3. **An empty protected bucket is `h''`, not `h'A0'`.** A zero-length byte
421
+ string, not an encoded empty map, and the parser never "repairs" it.
422
+ 4. **ECDSA signatures are `r ‖ s`, not DER**, each component zero-padded to the
423
+ width of the group order (RFC 9053 §2.1) — 64 bytes on P-256, 132 on P-521.
424
+ A DER signature produces an error message that says so.
425
+ 5. **Low-S normalization is a policy, not a rule.** `@noble/curves` normalizes
426
+ `s` by default; COSE does not, and RFC 6979 publishes the un-normalized
427
+ values. Signing here passes `lowS: false` so that the bytes match the C#
428
+ reference implementation, and verification passes it too so that a meter
429
+ which signs without normalizing is not refused.
430
+
431
+ Key material has its own version of the same trap: coordinates and private keys
432
+ are fixed-width byte strings whose **leading zeroes must be preserved**
433
+ (RFC 9053 §7.1.1). A plain big-integer serialization shortens them roughly one
434
+ time in 256, and the resulting keys are rejected elsewhere. `CoseKey` always
435
+ pads, and checks the width whenever a key is actually used.
436
+
437
+ ## Golden vectors
438
+
439
+ ```bash
440
+ npm install && npm test
441
+ ```
442
+
443
+ The tests are pinned against the same published vectors the C# implementation
444
+ uses — same RFCs, same appendices, same transcription:
445
+
446
+ - **RFC 9052 C.2.1** — `COSE_Sign1`, including the external-AAD variant and the
447
+ untagged form.
448
+ - **RFC 9052 C.1.1 / C.1.2** — `COSE_Sign` with one and with two signatures,
449
+ the latter on P-256 and P-521 at once.
450
+ - **RFC 9338 A.2.1** — the countersignature the RFC prints in diagnostic
451
+ notation only, assembled here from its documented parts. That both its body
452
+ signature and its countersignature then verify is what proves the assembly.
453
+ - **RFC 6979 A.2.5** — the deterministic P-256 signatures, reproduced exactly.
454
+ - **RFC 8032 §7.1 and §7.4** — Ed25519 and Ed448, also reproduced exactly, and
455
+ that is a stronger check than any ECDSA vector allows: EdDSA has no nonce to
456
+ draw, so a published signature is not merely verifiable but *recomputable*.
457
+ - **RFC 4231** — the canonical HMAC-SHA-2 vectors, all three digests, including
458
+ the four-byte key and the one longer than the block size. RFC 9052's only
459
+ `COSE_Mac0` example uses AES-CBC-MAC rather than HMAC, so no published
460
+ message pins both the structure and the primitive at once; the structure is
461
+ pinned against that example all the same — its 37 bytes are parsed, checked
462
+ field by field, re-encoded identically and its MAC_structure asserted — and
463
+ the primitive against these.
464
+ - **RFC 9052 Appendix C.5.4 and the COSE working group examples** — for the
465
+ encrypted and enveloped structures. C.5.4 is a `COSE_Mac` whose recipient
466
+ wraps the content key under a published 256-bit key: unwrapping it and
467
+ recomputing the tag reproduces the RFC's published value byte for byte, which
468
+ pins the key wrap, the recipient structure, the `"MAC"` context and HMAC in
469
+ one chain. The working group's examples carry whole AES-GCM messages
470
+ *together with their intermediates* — the `Enc_structure` as hex, the content
471
+ key, the nonce — and every one of those is checked, not merely the final
472
+ bytes: a message that comes out right by way of a wrong additional-data
473
+ structure stops coming out right the moment anything changes.
474
+ - **A certificate corpus minted by Bouncy Castle** — fifteen certificates and
475
+ the hierarchies they form, read back here field by field and walked to their
476
+ anchors at a fixed instant. They cover an ECDSA root signing a brainpool
477
+ authority, a chain signed with Ed25519 and one signed with ML-DSA-65, expired
478
+ and not-yet-valid certificates, an issuer that is not an authority, and a
479
+ chain that certifies the wrong key.
480
+ - **The worked signed record of the specification** — 713 bytes produced by the
481
+ C# implementation: the station's signature verifies *and is reproduced byte
482
+ for byte*, both meter readings verify and are reproduced, the operator's
483
+ countersignature verifies, the two metrological readings decode to the values
484
+ the document prints, and all three key identifiers recompute as RFC 9679
485
+ thumbprints.
486
+
487
+ ECDSA is randomized, so published signature bytes generally cannot be
488
+ reproduced by signing — but they can be *verified*, which is the stronger
489
+ statement: a single wrong byte anywhere in the `Sig_structure`, the header
490
+ buckets or the key would make the verification fail. Where the signer used
491
+ RFC 6979, reproduction is available too, and the tests take it.
492
+
493
+ RFC 5639 publishes brainpoolP320r1's domain parameters and no ECDSA vector for
494
+ them, so that curve is checked by arithmetic identity instead — the generator
495
+ lies on the curve, and the order really is the order of the generator — and
496
+ then, decisively, by the conformance suite comparing its signatures against a
497
+ second implementation of the same curve. ML-DSA is checked the same way, by the
498
+ properties RFC 9964 pins down (the sizes, the seed-derived key pair, the
499
+ label-shifting AKP parameters) and then by that same comparison.
500
+
501
+ ## Continuous integration
502
+
503
+ `ci.yml` installs everything from the lock file — the CBOR codec included,
504
+ which since its first release arrives here the way it arrives everywhere —
505
+ then type-checks both views of `src/`, assembles the same artifact a release
506
+ would publish, and runs the vectors above on Node 20, 22 and 24 on Linux, and
507
+ on Node 22 on Windows and macOS.
508
+
509
+ `nightly.yml` is a real drift detector rather than the same run on a timer.
510
+ The foundations here — the CBOR codec and the `@noble` libraries — are
511
+ versioned releases, which pull requests install from the lock file and the
512
+ nightly installs without one. A release cut yesterday can break these tests
513
+ overnight — the curve library has already done it once, by normalising ECDSA's
514
+ `s` where COSE does not. Drift in the codec's *unreleased* master belongs to
515
+ the conformance suite below, which builds both implementations from source.
516
+
517
+ The third badge is the one that matters most and belongs to another repository:
518
+ [MCBORConformanceTests](https://github.com/Vanaheimr/MCBORConformanceTests)
519
+ signs every case with **both** this implementation and the C# one and hands
520
+ each message to the other to verify. Nothing in this repository can catch two
521
+ implementations that quietly disagree about a byte; that suite is what does.
522
+
523
+ ## A note on German calibration law
524
+
525
+ COSE does not make a data format legally usable in the regulated part of the
526
+ charging infrastructure. That follows from the type approval of the measuring
527
+ instrument and from the data being checkable with the verification software the
528
+ conformity assessment covers — in practice OCMF or a signed meter format today,
529
+ not a free-form CBOR structure.
530
+
531
+ What this is for is the integrity of measurement data along the rest of the
532
+ chain, and for the conversation about what a digital, signed SI quantity should
533
+ look like.
534
+
535
+ ## License
536
+
537
+ [Apache License 2.0](LICENSE.md), matching both reference implementations.
package/dist/aes.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ /** The nonce width AES-GCM is fixed to in COSE: 96 bits [RFC 9053, Section 4.1]. */
2
+ export declare const GCM_NONCE_SIZE = 12;
3
+ /** The authentication tag width AES-GCM is fixed to in COSE: 128 bits. */
4
+ export declare const GCM_TAG_SIZE = 16;
5
+ /**
6
+ * Encrypt with AES-GCM.
7
+ *
8
+ * The returned byte string is `ciphertext ‖ tag`, which is how COSE carries it
9
+ * — the authentication tag is not a field of its own, it is the last sixteen
10
+ * bytes of the ciphertext. An implementation keeping them apart interoperates
11
+ * with nothing.
12
+ *
13
+ * `additionalData` is the encoded `Enc_structure`: authenticated, never
14
+ * encrypted, and never transported, because the recipient rebuilds it from the
15
+ * message.
16
+ */
17
+ export declare function aesGcmEncrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, additionalData: Uint8Array): Uint8Array;
18
+ /**
19
+ * Decrypt with AES-GCM, or say why not.
20
+ *
21
+ * A failed decryption is not an exception here for the same reason a failed
22
+ * signature verification is not: it is the expected outcome of processing
23
+ * untrusted data. And an AEAD failure means the *whole* message is
24
+ * unauthenticated, so there is no partial plaintext to hand back — anything
25
+ * decrypted before the tag was checked must be discarded, which is precisely
26
+ * what makes releasing unverified plaintext the classic AEAD mistake.
27
+ */
28
+ export declare function aesGcmDecrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, additionalData: Uint8Array): Uint8Array | null;
29
+ /**
30
+ * Wrap a content key under a key-encryption key [RFC 3394].
31
+ *
32
+ * The result is eight bytes longer than the key: the integrity check value of
33
+ * RFC 3394 travels with it, which is what makes an unwrap able to fail rather
34
+ * than silently return rubbish.
35
+ */
36
+ export declare function aesKeyWrap(keyEncryptionKey: Uint8Array, contentKey: Uint8Array): Uint8Array;
37
+ /**
38
+ * Unwrap a content key, or return null when the integrity check fails.
39
+ *
40
+ * Failing is the useful behaviour: RFC 3394's check value is what tells a
41
+ * recipient that this wrapped key was not meant for them, rather than handing
42
+ * them a plausible-looking key that decrypts nothing.
43
+ */
44
+ export declare function aesKeyUnwrap(keyEncryptionKey: Uint8Array, wrapped: Uint8Array): Uint8Array | null;
45
+ //# sourceMappingURL=aes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aes.d.ts","sourceRoot":"","sources":["../src/aes.ts"],"names":[],"mappings":"AAoCA,oFAAoF;AACpF,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,0EAA0E;AAC1E,eAAO,MAAM,YAAY,KAAK,CAAC;AAY/B;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAc,UAAU,EAC3B,KAAK,EAAY,UAAU,EAC3B,SAAS,EAAQ,UAAU,EAC3B,cAAc,EAAG,UAAU,GAAG,UAAU,CASrE;AAGD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAc,UAAU,EAC3B,KAAK,EAAY,UAAU,EAC3B,UAAU,EAAO,UAAU,EAC3B,cAAc,EAAG,UAAU,GAAG,UAAU,GAAG,IAAI,CAqB5E;AAGD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,gBAAgB,EAAE,UAAU,EAC5B,UAAU,EAAQ,UAAU,GAAG,UAAU,CASnE;AAGD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,gBAAgB,EAAE,UAAU,EAC5B,OAAO,EAAW,UAAU,GAAG,UAAU,GAAG,IAAI,CAe5E"}