@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.
- package/CHANGELOG.md +81 -0
- package/LICENSE.md +201 -0
- package/NOTICE +31 -0
- package/README.md +537 -0
- package/dist/aes.d.ts +45 -0
- package/dist/aes.d.ts.map +1 -0
- package/dist/algorithm.d.ts +137 -0
- package/dist/algorithm.d.ts.map +1 -0
- package/dist/asn1.d.ts +114 -0
- package/dist/asn1.d.ts.map +1 -0
- package/dist/cbor.d.ts +65 -0
- package/dist/cbor.d.ts.map +1 -0
- package/dist/curve.d.ts +65 -0
- package/dist/curve.d.ts.map +1 -0
- package/dist/ecdsa.d.ts +56 -0
- package/dist/ecdsa.d.ts.map +1 -0
- package/dist/eddsa.d.ts +14 -0
- package/dist/eddsa.d.ts.map +1 -0
- package/dist/encrypt.d.ts +140 -0
- package/dist/encrypt.d.ts.map +1 -0
- package/dist/errors.d.ts +64 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/headers.d.ts +77 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/hmac.d.ts +32 -0
- package/dist/hmac.d.ts.map +1 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4159 -0
- package/dist/index.js.map +1 -0
- package/dist/key.d.ts +201 -0
- package/dist/key.d.ts.map +1 -0
- package/dist/labels.d.ts +51 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/mac.d.ts +93 -0
- package/dist/mac.d.ts.map +1 -0
- package/dist/mac0.d.ts +132 -0
- package/dist/mac0.d.ts.map +1 -0
- package/dist/mldsa.d.ts +29 -0
- package/dist/mldsa.d.ts.map +1 -0
- package/dist/payload.d.ts +25 -0
- package/dist/payload.d.ts.map +1 -0
- package/dist/recipient.d.ts +65 -0
- package/dist/recipient.d.ts.map +1 -0
- package/dist/resolve.d.ts +29 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/sign.d.ts +82 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign1.d.ts +273 -0
- package/dist/sign1.d.ts.map +1 -0
- package/dist/signature.d.ts +43 -0
- package/dist/signature.d.ts.map +1 -0
- package/dist/x509.d.ts +144 -0
- package/dist/x509.d.ts.map +1 -0
- package/dist/x5chain.d.ts +98 -0
- package/dist/x5chain.d.ts.map +1 -0
- package/package.json +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# Vanaheimr COSE (TypeScript)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Vanaheimr/COSE.TS/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/Vanaheimr/COSE.TS/actions/workflows/nightly.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@vanaheimr/cose)
|
|
6
|
+
[](https://github.com/Vanaheimr/MCBORConformanceTests/actions/workflows/ci.yml)
|
|
7
|
+
[](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"}
|