@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/dist/index.js
ADDED
|
@@ -0,0 +1,4159 @@
|
|
|
1
|
+
import { cbor } from '@vanaheimr/metrological-cbor';
|
|
2
|
+
export { cbor } from '@vanaheimr/metrological-cbor';
|
|
3
|
+
import { ecdsa, weierstrass } from '@noble/curves/abstract/weierstrass.js';
|
|
4
|
+
import { sha384, sha512, sha256 } from '@noble/hashes/sha2.js';
|
|
5
|
+
import { p521, p384, p256 } from '@noble/curves/nist.js';
|
|
6
|
+
import { brainpoolP512r1, brainpoolP384r1, brainpoolP256r1 } from '@noble/curves/misc.js';
|
|
7
|
+
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
8
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
9
|
+
import { ed448 } from '@noble/curves/ed448.js';
|
|
10
|
+
import { ml_dsa87, ml_dsa65, ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
11
|
+
import { hmac as hmac$1 } from '@noble/hashes/hmac.js';
|
|
12
|
+
import { gcm, aeskw } from '@noble/ciphers/aes.js';
|
|
13
|
+
|
|
14
|
+
// src/errors.ts
|
|
15
|
+
var CoseError = class extends Error {
|
|
16
|
+
name = "CoseError";
|
|
17
|
+
constructor(message, options) {
|
|
18
|
+
super(message, options);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var VERIFIED = { verified: true };
|
|
22
|
+
var notVerified = (reason) => ({ verified: false, reason });
|
|
23
|
+
var notDecrypted = (reason) => ({ decrypted: false, reason });
|
|
24
|
+
var PRESERVE = { mapKeys: "preserve", floats: "preserve" };
|
|
25
|
+
var DETERMINISTIC = { mapKeys: "sorted", floats: "shortest" };
|
|
26
|
+
var encode = (value) => cbor.encode(value, PRESERVE);
|
|
27
|
+
var decode = (data) => cbor.decode(data, { strict: false });
|
|
28
|
+
function bytesEqual(left, right) {
|
|
29
|
+
if (left.length !== right.length)
|
|
30
|
+
return false;
|
|
31
|
+
for (let index = 0; index < left.length; index++) {
|
|
32
|
+
if (left[index] !== right[index])
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
var NO_BYTES = new Uint8Array();
|
|
38
|
+
|
|
39
|
+
// src/payload.ts
|
|
40
|
+
function canonicalizePayload(payload) {
|
|
41
|
+
try {
|
|
42
|
+
return cbor.encode(cbor.decode(payload, { strict: false }), DETERMINISTIC);
|
|
43
|
+
} catch {
|
|
44
|
+
return payload;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function isCanonicalPayload(payload) {
|
|
48
|
+
try {
|
|
49
|
+
return bytesEqual(
|
|
50
|
+
cbor.encode(cbor.decode(payload, { strict: false }), DETERMINISTIC),
|
|
51
|
+
payload
|
|
52
|
+
);
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/labels.ts
|
|
59
|
+
var HeaderLabel = {
|
|
60
|
+
/** The cryptographic algorithm [RFC 9052]. */
|
|
61
|
+
algorithm: 1,
|
|
62
|
+
/** The labels a recipient is required to understand [RFC 9052]. */
|
|
63
|
+
critical: 2,
|
|
64
|
+
/** The content type of the payload [RFC 9052]. */
|
|
65
|
+
contentType: 3,
|
|
66
|
+
/** The key identifier [RFC 9052]. */
|
|
67
|
+
keyIdentifier: 4,
|
|
68
|
+
/** The initialization vector [RFC 9052]. */
|
|
69
|
+
iv: 5,
|
|
70
|
+
/** The partial initialization vector [RFC 9052]. */
|
|
71
|
+
partialIv: 6,
|
|
72
|
+
/** The countersignature of RFC 8152, superseded by label 11 [RFC 9338]. */
|
|
73
|
+
counterSignature: 7,
|
|
74
|
+
/** The countersignature that also covers the signature it signs [RFC 9338]. */
|
|
75
|
+
counterSignatureV2: 11,
|
|
76
|
+
/** The abbreviated countersignature, not implemented here [RFC 9338]. */
|
|
77
|
+
counterSignature0V2: 12,
|
|
78
|
+
/** An unordered bag of X.509 certificates [RFC 9360]. */
|
|
79
|
+
x5Bag: 32,
|
|
80
|
+
/** An ordered X.509 certificate chain [RFC 9360]. */
|
|
81
|
+
x5Chain: 33,
|
|
82
|
+
/** The hash of an X.509 certificate [RFC 9360]. */
|
|
83
|
+
x5T: 34,
|
|
84
|
+
/** A URI naming an X.509 certificate [RFC 9360]. */
|
|
85
|
+
x5U: 35
|
|
86
|
+
};
|
|
87
|
+
var label = (value) => cbor.int(value);
|
|
88
|
+
function headerLabelName(value) {
|
|
89
|
+
if (value.type === "int") {
|
|
90
|
+
switch (value.value) {
|
|
91
|
+
case 1n:
|
|
92
|
+
return "alg";
|
|
93
|
+
case 2n:
|
|
94
|
+
return "crit";
|
|
95
|
+
case 3n:
|
|
96
|
+
return "content type";
|
|
97
|
+
case 4n:
|
|
98
|
+
return "kid";
|
|
99
|
+
case 5n:
|
|
100
|
+
return "IV";
|
|
101
|
+
case 6n:
|
|
102
|
+
return "Partial IV";
|
|
103
|
+
case 7n:
|
|
104
|
+
return "counter signature";
|
|
105
|
+
case 11n:
|
|
106
|
+
return "counter signature version 2";
|
|
107
|
+
case 12n:
|
|
108
|
+
return "counter signature 0 version 2";
|
|
109
|
+
case 32n:
|
|
110
|
+
return "x5bag";
|
|
111
|
+
case 33n:
|
|
112
|
+
return "x5chain";
|
|
113
|
+
case 34n:
|
|
114
|
+
return "x5t";
|
|
115
|
+
case 35n:
|
|
116
|
+
return "x5u";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (value.type === "text")
|
|
120
|
+
return value.value;
|
|
121
|
+
return cbor.diagnostic(value);
|
|
122
|
+
}
|
|
123
|
+
function isUnderstood(value) {
|
|
124
|
+
return value.type === "int" && (value.value === 1n || value.value === 2n || value.value === 3n || value.value === 4n);
|
|
125
|
+
}
|
|
126
|
+
function sameLabel(left, right) {
|
|
127
|
+
if (left.type === "int" && right.type === "int")
|
|
128
|
+
return left.value === right.value;
|
|
129
|
+
if (left.type === "text" && right.type === "text")
|
|
130
|
+
return left.value === right.value;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/curve.ts
|
|
135
|
+
var curve = (id, name, keyType, fieldSize, orderSize) => ({ id, name, keyType, fieldSize, orderSize });
|
|
136
|
+
var KEY_TYPE_EC2 = 2;
|
|
137
|
+
var KEY_TYPE_OKP = 1;
|
|
138
|
+
var CoseCurves = {
|
|
139
|
+
P256: curve(1, "P-256", KEY_TYPE_EC2, 32, 32),
|
|
140
|
+
P384: curve(2, "P-384", KEY_TYPE_EC2, 48, 48),
|
|
141
|
+
P521: curve(3, "P-521", KEY_TYPE_EC2, 66, 66),
|
|
142
|
+
// The two key-agreement curves carry no signature algorithm, so their
|
|
143
|
+
// widths are recorded as unknown rather than guessed at.
|
|
144
|
+
X25519: curve(4, "X25519", KEY_TYPE_OKP, null, null),
|
|
145
|
+
X448: curve(5, "X448", KEY_TYPE_OKP, null, null),
|
|
146
|
+
Ed25519: curve(6, "Ed25519", KEY_TYPE_OKP, 32, 32),
|
|
147
|
+
Ed448: curve(7, "Ed448", KEY_TYPE_OKP, 57, 57),
|
|
148
|
+
secp256k1: curve(8, "secp256k1", KEY_TYPE_EC2, 32, 32),
|
|
149
|
+
// Registered by ISO/IEC 18013-5 for the mobile driving licence.
|
|
150
|
+
brainpoolP256r1: curve(256, "brainpoolP256r1", KEY_TYPE_EC2, 32, 32),
|
|
151
|
+
brainpoolP320r1: curve(257, "brainpoolP320r1", KEY_TYPE_EC2, 40, 40),
|
|
152
|
+
brainpoolP384r1: curve(258, "brainpoolP384r1", KEY_TYPE_EC2, 48, 48),
|
|
153
|
+
brainpoolP512r1: curve(259, "brainpoolP512r1", KEY_TYPE_EC2, 64, 64)
|
|
154
|
+
};
|
|
155
|
+
var ALL_CURVES = Object.values(CoseCurves);
|
|
156
|
+
function curveById(id) {
|
|
157
|
+
return ALL_CURVES.find((each) => each.id === id) ?? null;
|
|
158
|
+
}
|
|
159
|
+
function curveByName(name) {
|
|
160
|
+
return ALL_CURVES.find((each) => each.name === name) ?? null;
|
|
161
|
+
}
|
|
162
|
+
var brainpoolP320r1 = /* @__PURE__ */ ecdsa(weierstrass({
|
|
163
|
+
p: BigInt("0xd35e472036bc4fb7e13c785ed201e065f98fcfa6f6f40def4f92b9ec7893ec28fcd412b1f1b32e27"),
|
|
164
|
+
a: BigInt("0x3ee30b568fbab0f883ccebd46d3f3bb8a2a73513f5eb79da66190eb085ffa9f492f375a97d860eb4"),
|
|
165
|
+
b: BigInt("0x520883949dfdbc42d3ad198640688a6fe13f41349554b49acc31dccd884539816f5eb4ac8fb1f1a6"),
|
|
166
|
+
n: BigInt("0xd35e472036bc4fb7e13c785ed201e065f98fcfa5b68f12a32d482ec7ee8658e98691555b44c59311"),
|
|
167
|
+
Gx: BigInt("0x43bd7e9afb53d8b85289bcc48ee5bfe6f20137d10a087eb6e7871e2a10a599c710af8d0d39e20611"),
|
|
168
|
+
Gy: BigInt("0x14fdd05545ec1cc8ab4093247f77275e0743ffed117182eaa9c77877aaac6ac7d35245d1692e8ee1"),
|
|
169
|
+
h: BigInt(1)
|
|
170
|
+
}), sha384);
|
|
171
|
+
var HASHES = { sha256, sha384, sha512 };
|
|
172
|
+
var IMPLEMENTED = {
|
|
173
|
+
"P-256": p256,
|
|
174
|
+
"P-384": p384,
|
|
175
|
+
"P-521": p521,
|
|
176
|
+
"secp256k1": secp256k1,
|
|
177
|
+
"brainpoolP256r1": brainpoolP256r1,
|
|
178
|
+
"brainpoolP320r1": brainpoolP320r1,
|
|
179
|
+
"brainpoolP384r1": brainpoolP384r1,
|
|
180
|
+
"brainpoolP512r1": brainpoolP512r1
|
|
181
|
+
};
|
|
182
|
+
var isImplemented = (curve2) => curve2.name in IMPLEMENTED;
|
|
183
|
+
function implementationOf(curve2) {
|
|
184
|
+
const implementation = IMPLEMENTED[curve2.name];
|
|
185
|
+
if (implementation === void 0)
|
|
186
|
+
throw new CoseError(`The elliptic curve '${curve2.name}' is registered by COSE, but this build does not implement it!`);
|
|
187
|
+
return implementation;
|
|
188
|
+
}
|
|
189
|
+
var digest = (algorithm2, data) => HASHES[algorithm2](data);
|
|
190
|
+
function sign(curve2, messageHash, privateKey) {
|
|
191
|
+
return implementationOf(curve2).sign(
|
|
192
|
+
messageHash,
|
|
193
|
+
privateKey,
|
|
194
|
+
{ prehash: false, lowS: false }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
function verify(curve2, signature, messageHash, publicKey) {
|
|
198
|
+
const expected = (curve2.orderSize ?? 0) * 2;
|
|
199
|
+
if (signature.length !== expected)
|
|
200
|
+
throw new CoseError(`An ECDSA signature on the curve '${curve2.name}' must be ${expected} bytes wide \u2014 the concatenated components r and s, each zero-padded to the width of the group order [RFC 9053, Section 2.1] \u2014 but was ${signature.length} bytes wide. A signature of a length such as this one is usually a DER encoding that was never converted!`);
|
|
201
|
+
try {
|
|
202
|
+
return implementationOf(curve2).verify(
|
|
203
|
+
signature,
|
|
204
|
+
messageHash,
|
|
205
|
+
publicKey,
|
|
206
|
+
{ prehash: false, lowS: false }
|
|
207
|
+
);
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function publicKeyFor(curve2, privateKey) {
|
|
213
|
+
return implementationOf(curve2).getPublicKey(privateKey, false);
|
|
214
|
+
}
|
|
215
|
+
function decompressY(curve2, x, oddY) {
|
|
216
|
+
const fieldSize = curve2.fieldSize ?? 0;
|
|
217
|
+
const compressed = new Uint8Array(1 + x.length);
|
|
218
|
+
compressed[0] = oddY ? 3 : 2;
|
|
219
|
+
compressed.set(x, 1);
|
|
220
|
+
const uncompressed = implementationOf(curve2).Point.fromBytes(compressed).toBytes(false);
|
|
221
|
+
return uncompressed.slice(1 + fieldSize);
|
|
222
|
+
}
|
|
223
|
+
function isOnCurve(curve2, uncompressed) {
|
|
224
|
+
try {
|
|
225
|
+
implementationOf(curve2).Point.fromBytes(uncompressed);
|
|
226
|
+
return true;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
var IMPLEMENTED2 = {
|
|
232
|
+
"Ed25519": ed25519,
|
|
233
|
+
"Ed448": ed448
|
|
234
|
+
};
|
|
235
|
+
var isEddsaCurve = (curve2) => curve2.name in IMPLEMENTED2;
|
|
236
|
+
function implementationOf2(curve2) {
|
|
237
|
+
const implementation = IMPLEMENTED2[curve2.name];
|
|
238
|
+
if (implementation === void 0)
|
|
239
|
+
throw new CoseError(`The curve '${curve2.name}' is not an EdDSA signature curve this implementation supports!`);
|
|
240
|
+
return implementation;
|
|
241
|
+
}
|
|
242
|
+
function eddsaSignatureSize(curve2) {
|
|
243
|
+
if (curve2.id === CoseCurves.Ed25519.id) return 64;
|
|
244
|
+
if (curve2.id === CoseCurves.Ed448.id) return 114;
|
|
245
|
+
throw new CoseError(`The curve '${curve2.name}' has no EdDSA signature size!`);
|
|
246
|
+
}
|
|
247
|
+
function eddsaSign(curve2, message, privateKey) {
|
|
248
|
+
return implementationOf2(curve2).sign(message, privateKey);
|
|
249
|
+
}
|
|
250
|
+
function eddsaVerify(curve2, signature, message, publicKey) {
|
|
251
|
+
const expected = eddsaSignatureSize(curve2);
|
|
252
|
+
if (signature.length !== expected)
|
|
253
|
+
throw new CoseError(`An EdDSA signature on the curve '${curve2.name}' must be ${String(expected)} bytes wide, but was ${String(signature.length)} bytes wide!`);
|
|
254
|
+
try {
|
|
255
|
+
return implementationOf2(curve2).verify(signature, message, publicKey);
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function eddsaPublicKeyFor(curve2, privateKey) {
|
|
261
|
+
return implementationOf2(curve2).getPublicKey(privateKey);
|
|
262
|
+
}
|
|
263
|
+
var MLDSA_PARAMETER_SETS = ["ML-DSA-44", "ML-DSA-65", "ML-DSA-87"];
|
|
264
|
+
var IMPLEMENTED3 = {
|
|
265
|
+
"ML-DSA-44": ml_dsa44,
|
|
266
|
+
"ML-DSA-65": ml_dsa65,
|
|
267
|
+
"ML-DSA-87": ml_dsa87
|
|
268
|
+
};
|
|
269
|
+
var MLDSA_SIZES = {
|
|
270
|
+
"ML-DSA-44": { publicKey: 1312, signature: 2420 },
|
|
271
|
+
"ML-DSA-65": { publicKey: 1952, signature: 3309 },
|
|
272
|
+
"ML-DSA-87": { publicKey: 2592, signature: 4627 }
|
|
273
|
+
};
|
|
274
|
+
var MLDSA_SEED_SIZE = 32;
|
|
275
|
+
function schemeOf(parameterSet) {
|
|
276
|
+
const scheme = IMPLEMENTED3[parameterSet];
|
|
277
|
+
if (scheme === void 0)
|
|
278
|
+
throw new CoseError(`'${parameterSet}' is not an ML-DSA parameter set this implementation supports!`);
|
|
279
|
+
return scheme;
|
|
280
|
+
}
|
|
281
|
+
var isMldsaParameterSet = (parameterSet) => parameterSet in IMPLEMENTED3;
|
|
282
|
+
function mldsaPublicKeyFor(parameterSet, seed) {
|
|
283
|
+
requireSeed(seed);
|
|
284
|
+
return schemeOf(parameterSet).keygen(seed).publicKey;
|
|
285
|
+
}
|
|
286
|
+
function mldsaSign(parameterSet, message, seed) {
|
|
287
|
+
requireSeed(seed);
|
|
288
|
+
const scheme = schemeOf(parameterSet);
|
|
289
|
+
return scheme.sign(message, scheme.keygen(seed).secretKey, { extraEntropy: false });
|
|
290
|
+
}
|
|
291
|
+
function mldsaVerify(parameterSet, signature, message, publicKey) {
|
|
292
|
+
const sizes = isMldsaParameterSet(parameterSet) ? MLDSA_SIZES[parameterSet] : void 0;
|
|
293
|
+
if (sizes !== void 0 && signature.length !== sizes.signature)
|
|
294
|
+
throw new CoseError(`An ${parameterSet} signature must be ${String(sizes.signature)} bytes wide, but was ${String(signature.length)} bytes wide!`);
|
|
295
|
+
try {
|
|
296
|
+
return schemeOf(parameterSet).verify(signature, message, publicKey);
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function requireSeed(seed) {
|
|
302
|
+
if (seed.length !== MLDSA_SEED_SIZE)
|
|
303
|
+
throw new CoseError(`The private key of an ML-DSA COSE key is the seed and must be ${String(MLDSA_SEED_SIZE)} bytes long [RFC 9964], but was ${String(seed.length)} bytes long!`);
|
|
304
|
+
}
|
|
305
|
+
var hmac = (hash, key, message) => hmac$1(HASHES[hash], key, message);
|
|
306
|
+
function macTag(hash, tagSize, key, message) {
|
|
307
|
+
const full = hmac(hash, key, message);
|
|
308
|
+
if (tagSize > full.length)
|
|
309
|
+
throw new CoseError(`An HMAC over ${hash} is ${String(full.length)} bytes long and can not be truncated to ${String(tagSize)}!`);
|
|
310
|
+
return tagSize === full.length ? full : full.subarray(0, tagSize);
|
|
311
|
+
}
|
|
312
|
+
function tagsEqual(left, right) {
|
|
313
|
+
if (left.length !== right.length)
|
|
314
|
+
return false;
|
|
315
|
+
let difference = 0;
|
|
316
|
+
for (let index = 0; index < left.length; index++)
|
|
317
|
+
difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
|
|
318
|
+
return difference === 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/algorithm.ts
|
|
322
|
+
var algorithm = (id, name, description, spec = {}) => ({
|
|
323
|
+
id,
|
|
324
|
+
name,
|
|
325
|
+
description,
|
|
326
|
+
family: spec.family ?? "none",
|
|
327
|
+
hash: spec.hash ?? null,
|
|
328
|
+
curve: spec.curve ?? null,
|
|
329
|
+
parameterSet: spec.parameterSet ?? null,
|
|
330
|
+
tagSize: spec.tagSize ?? null,
|
|
331
|
+
keySize: spec.keySize ?? null,
|
|
332
|
+
signing: spec.signing ?? false,
|
|
333
|
+
deprecated: spec.deprecated ?? false
|
|
334
|
+
});
|
|
335
|
+
var CoseAlgorithms = {
|
|
336
|
+
// ECDSA, curve taken from the key [RFC 9053]. Deprecated by RFC 9864.
|
|
337
|
+
ES256: algorithm(
|
|
338
|
+
-7,
|
|
339
|
+
"ES256",
|
|
340
|
+
"ECDSA w/ SHA-256",
|
|
341
|
+
{ family: "ecdsa", hash: "sha256", signing: true, deprecated: true }
|
|
342
|
+
),
|
|
343
|
+
ES384: algorithm(
|
|
344
|
+
-35,
|
|
345
|
+
"ES384",
|
|
346
|
+
"ECDSA w/ SHA-384",
|
|
347
|
+
{ family: "ecdsa", hash: "sha384", signing: true, deprecated: true }
|
|
348
|
+
),
|
|
349
|
+
ES512: algorithm(
|
|
350
|
+
-36,
|
|
351
|
+
"ES512",
|
|
352
|
+
"ECDSA w/ SHA-512",
|
|
353
|
+
{ family: "ecdsa", hash: "sha512", signing: true, deprecated: true }
|
|
354
|
+
),
|
|
355
|
+
ES256K: algorithm(
|
|
356
|
+
-47,
|
|
357
|
+
"ES256K",
|
|
358
|
+
"ECDSA using secp256k1 curve and SHA-256",
|
|
359
|
+
{ family: "ecdsa", hash: "sha256", curve: CoseCurves.secp256k1, signing: true }
|
|
360
|
+
),
|
|
361
|
+
// Fully-specified ECDSA [RFC 9864].
|
|
362
|
+
ESP256: algorithm(
|
|
363
|
+
-9,
|
|
364
|
+
"ESP256",
|
|
365
|
+
"ECDSA using P-256 curve and SHA-256",
|
|
366
|
+
{ family: "ecdsa", hash: "sha256", curve: CoseCurves.P256, signing: true }
|
|
367
|
+
),
|
|
368
|
+
ESP384: algorithm(
|
|
369
|
+
-51,
|
|
370
|
+
"ESP384",
|
|
371
|
+
"ECDSA using P-384 curve and SHA-384",
|
|
372
|
+
{ family: "ecdsa", hash: "sha384", curve: CoseCurves.P384, signing: true }
|
|
373
|
+
),
|
|
374
|
+
ESP512: algorithm(
|
|
375
|
+
-52,
|
|
376
|
+
"ESP512",
|
|
377
|
+
"ECDSA using P-521 curve and SHA-512",
|
|
378
|
+
{ family: "ecdsa", hash: "sha512", curve: CoseCurves.P521, signing: true }
|
|
379
|
+
),
|
|
380
|
+
ESB256: algorithm(
|
|
381
|
+
-265,
|
|
382
|
+
"ESB256",
|
|
383
|
+
"ECDSA using BrainpoolP256r1 curve and SHA-256",
|
|
384
|
+
{ family: "ecdsa", hash: "sha256", curve: CoseCurves.brainpoolP256r1, signing: true }
|
|
385
|
+
),
|
|
386
|
+
ESB320: algorithm(
|
|
387
|
+
-266,
|
|
388
|
+
"ESB320",
|
|
389
|
+
"ECDSA using BrainpoolP320r1 curve and SHA-384",
|
|
390
|
+
{ family: "ecdsa", hash: "sha384", curve: CoseCurves.brainpoolP320r1, signing: true }
|
|
391
|
+
),
|
|
392
|
+
ESB384: algorithm(
|
|
393
|
+
-267,
|
|
394
|
+
"ESB384",
|
|
395
|
+
"ECDSA using BrainpoolP384r1 curve and SHA-384",
|
|
396
|
+
{ family: "ecdsa", hash: "sha384", curve: CoseCurves.brainpoolP384r1, signing: true }
|
|
397
|
+
),
|
|
398
|
+
ESB512: algorithm(
|
|
399
|
+
-268,
|
|
400
|
+
"ESB512",
|
|
401
|
+
"ECDSA using BrainpoolP512r1 curve and SHA-512",
|
|
402
|
+
{ family: "ecdsa", hash: "sha512", curve: CoseCurves.brainpoolP512r1, signing: true }
|
|
403
|
+
),
|
|
404
|
+
// EdDSA [RFC 8032]. No digest of its own: the message is signed whole.
|
|
405
|
+
// The un-suffixed identifier leaves the curve to the key, which is what
|
|
406
|
+
// RFC 9864 deprecates it for.
|
|
407
|
+
EdDSA: algorithm(
|
|
408
|
+
-8,
|
|
409
|
+
"EdDSA",
|
|
410
|
+
"EdDSA",
|
|
411
|
+
{ family: "eddsa", signing: true, deprecated: true }
|
|
412
|
+
),
|
|
413
|
+
Ed25519: algorithm(
|
|
414
|
+
-19,
|
|
415
|
+
"Ed25519",
|
|
416
|
+
"EdDSA using the Ed25519 parameter set",
|
|
417
|
+
{ family: "eddsa", curve: CoseCurves.Ed25519, signing: true }
|
|
418
|
+
),
|
|
419
|
+
Ed448: algorithm(
|
|
420
|
+
-53,
|
|
421
|
+
"Ed448",
|
|
422
|
+
"EdDSA using the Ed448 parameter set",
|
|
423
|
+
{ family: "eddsa", curve: CoseCurves.Ed448, signing: true }
|
|
424
|
+
),
|
|
425
|
+
// ML-DSA [FIPS 204, RFC 9964]. Also pure, and also without a curve: an
|
|
426
|
+
// ML-DSA key is a key pair of an algorithm rather than a point on
|
|
427
|
+
// something, which is why RFC 9964 gives it a key type of its own.
|
|
428
|
+
MLDSA44: algorithm(
|
|
429
|
+
-48,
|
|
430
|
+
"ML-DSA-44",
|
|
431
|
+
"CBOR Object Signing Algorithm for ML-DSA-44",
|
|
432
|
+
{ family: "mldsa", parameterSet: "ML-DSA-44", signing: true }
|
|
433
|
+
),
|
|
434
|
+
MLDSA65: algorithm(
|
|
435
|
+
-49,
|
|
436
|
+
"ML-DSA-65",
|
|
437
|
+
"CBOR Object Signing Algorithm for ML-DSA-65",
|
|
438
|
+
{ family: "mldsa", parameterSet: "ML-DSA-65", signing: true }
|
|
439
|
+
),
|
|
440
|
+
MLDSA87: algorithm(
|
|
441
|
+
-50,
|
|
442
|
+
"ML-DSA-87",
|
|
443
|
+
"CBOR Object Signing Algorithm for ML-DSA-87",
|
|
444
|
+
{ family: "mldsa", parameterSet: "ML-DSA-87", signing: true }
|
|
445
|
+
),
|
|
446
|
+
// HMAC [RFC 9053, Section 3.1]. A message authentication code rather than
|
|
447
|
+
// a signature: symmetric, so whoever verifies one can produce one, and
|
|
448
|
+
// `signing` is therefore false for all four. The name is "hash size /
|
|
449
|
+
// tag size", and the tag is the leftmost bits of the full HMAC.
|
|
450
|
+
HMAC256_64: algorithm(
|
|
451
|
+
4,
|
|
452
|
+
"HMAC 256/64",
|
|
453
|
+
"HMAC w/ SHA-256 truncated to 64 bits",
|
|
454
|
+
{ family: "hmac", hash: "sha256", tagSize: 8 }
|
|
455
|
+
),
|
|
456
|
+
HMAC256_256: algorithm(
|
|
457
|
+
5,
|
|
458
|
+
"HMAC 256/256",
|
|
459
|
+
"HMAC w/ SHA-256",
|
|
460
|
+
{ family: "hmac", hash: "sha256", tagSize: 32 }
|
|
461
|
+
),
|
|
462
|
+
HMAC384_384: algorithm(
|
|
463
|
+
6,
|
|
464
|
+
"HMAC 384/384",
|
|
465
|
+
"HMAC w/ SHA-384",
|
|
466
|
+
{ family: "hmac", hash: "sha384", tagSize: 48 }
|
|
467
|
+
),
|
|
468
|
+
HMAC512_512: algorithm(
|
|
469
|
+
7,
|
|
470
|
+
"HMAC 512/512",
|
|
471
|
+
"HMAC w/ SHA-512",
|
|
472
|
+
{ family: "hmac", hash: "sha512", tagSize: 64 }
|
|
473
|
+
),
|
|
474
|
+
// AES-GCM [RFC 9053, Section 4.1], the content encryption algorithms.
|
|
475
|
+
// COSE fixes the nonce at 96 bits and the authentication tag at 128, so the
|
|
476
|
+
// key width is the only thing left for the identifier to name.
|
|
477
|
+
A128GCM: algorithm(
|
|
478
|
+
1,
|
|
479
|
+
"A128GCM",
|
|
480
|
+
"AES-GCM mode w/ 128-bit key, 128-bit tag",
|
|
481
|
+
{ family: "aesgcm", keySize: 16, tagSize: 16 }
|
|
482
|
+
),
|
|
483
|
+
A192GCM: algorithm(
|
|
484
|
+
2,
|
|
485
|
+
"A192GCM",
|
|
486
|
+
"AES-GCM mode w/ 192-bit key, 128-bit tag",
|
|
487
|
+
{ family: "aesgcm", keySize: 24, tagSize: 16 }
|
|
488
|
+
),
|
|
489
|
+
A256GCM: algorithm(
|
|
490
|
+
3,
|
|
491
|
+
"A256GCM",
|
|
492
|
+
"AES-GCM mode w/ 256-bit key, 128-bit tag",
|
|
493
|
+
{ family: "aesgcm", keySize: 32, tagSize: 16 }
|
|
494
|
+
),
|
|
495
|
+
// AES key wrap [RFC 9053, Section 6.2.1, RFC 3394]. A recipient algorithm:
|
|
496
|
+
// it carries a content key rather than content, and the width named here is
|
|
497
|
+
// that of the KEY-ENCRYPTION key, not of the key being wrapped.
|
|
498
|
+
A128KW: algorithm(
|
|
499
|
+
-3,
|
|
500
|
+
"A128KW",
|
|
501
|
+
"AES Key Wrap w/ 128-bit key",
|
|
502
|
+
{ family: "keywrap", keySize: 16 }
|
|
503
|
+
),
|
|
504
|
+
A192KW: algorithm(
|
|
505
|
+
-4,
|
|
506
|
+
"A192KW",
|
|
507
|
+
"AES Key Wrap w/ 192-bit key",
|
|
508
|
+
{ family: "keywrap", keySize: 24 }
|
|
509
|
+
),
|
|
510
|
+
A256KW: algorithm(
|
|
511
|
+
-5,
|
|
512
|
+
"A256KW",
|
|
513
|
+
"AES Key Wrap w/ 256-bit key",
|
|
514
|
+
{ family: "keywrap", keySize: 32 }
|
|
515
|
+
),
|
|
516
|
+
// The recipient algorithm that transports nothing [RFC 9053, Section
|
|
517
|
+
// 6.1.1]: the recipient's key IS the content key. Its protected bucket and
|
|
518
|
+
// its ciphertext must both be empty, which is what makes a COSE_Mac with
|
|
519
|
+
// one direct recipient a COSE_Mac0 with extra ceremony.
|
|
520
|
+
direct: algorithm(
|
|
521
|
+
-6,
|
|
522
|
+
"direct",
|
|
523
|
+
"Direct use of content encryption key (CEK)",
|
|
524
|
+
{ family: "direct" }
|
|
525
|
+
),
|
|
526
|
+
// Digests, which are algorithms in the same registry but never sign.
|
|
527
|
+
SHA256: algorithm(-16, "SHA-256", "SHA-2 256-bit Hash", { hash: "sha256" }),
|
|
528
|
+
SHA384: algorithm(-43, "SHA-384", "SHA-2 384-bit Hash", { hash: "sha384" }),
|
|
529
|
+
SHA512: algorithm(-44, "SHA-512", "SHA-2 512-bit Hash", { hash: "sha512" }),
|
|
530
|
+
SHA1: algorithm(-14, "SHA-1", "SHA-1 Hash", { deprecated: true })
|
|
531
|
+
};
|
|
532
|
+
var ALL_ALGORITHMS = Object.values(CoseAlgorithms);
|
|
533
|
+
function algorithmById(id) {
|
|
534
|
+
return ALL_ALGORITHMS.find((each) => each.id === id) ?? null;
|
|
535
|
+
}
|
|
536
|
+
function algorithmByName(name) {
|
|
537
|
+
return ALL_ALGORITHMS.find((each) => each.name === name) ?? null;
|
|
538
|
+
}
|
|
539
|
+
function algorithmFromCbor(value) {
|
|
540
|
+
if (value.type !== "int")
|
|
541
|
+
throw new CoseError("The COSE algorithm identifier must be an integer!");
|
|
542
|
+
const id = Number(value.value);
|
|
543
|
+
return algorithmById(id) ?? algorithm(id, `unregistered(${String(id)})`, "An algorithm this implementation does not know");
|
|
544
|
+
}
|
|
545
|
+
var algorithmToCbor = (value) => cbor.int(value.id);
|
|
546
|
+
var sameAlgorithm = (left, right) => left.id === right.id;
|
|
547
|
+
function resolveCurve(value, keyCurve) {
|
|
548
|
+
if (!value.signing)
|
|
549
|
+
throw new CoseError(`The COSE algorithm '${value.name}' is not a signature algorithm this implementation supports!`);
|
|
550
|
+
if (value.curve !== null) {
|
|
551
|
+
if (keyCurve !== null && keyCurve.id !== value.curve.id)
|
|
552
|
+
throw new CoseError(`The COSE algorithm '${value.name}' is defined on the curve '${value.curve.name}', but the key is on the curve '${keyCurve.name}'!`);
|
|
553
|
+
return value.curve;
|
|
554
|
+
}
|
|
555
|
+
if (keyCurve === null)
|
|
556
|
+
throw new CoseError(`The COSE algorithm '${value.name}' does not name a curve, therefore the key has to!`);
|
|
557
|
+
return keyCurve;
|
|
558
|
+
}
|
|
559
|
+
function hashOf(value) {
|
|
560
|
+
if (value.hash === null)
|
|
561
|
+
throw new CoseError(`The COSE algorithm '${value.name}' does not define a separate message digest!`);
|
|
562
|
+
return value.hash;
|
|
563
|
+
}
|
|
564
|
+
function parameterSetOf(value) {
|
|
565
|
+
if (value.parameterSet === null)
|
|
566
|
+
throw new CoseError(`The COSE algorithm '${value.name}' does not name an ML-DSA parameter set!`);
|
|
567
|
+
return value.parameterSet;
|
|
568
|
+
}
|
|
569
|
+
function signWith(value, keyCurve, toBeSigned, privateKey) {
|
|
570
|
+
switch (value.family) {
|
|
571
|
+
case "ecdsa":
|
|
572
|
+
return sign(
|
|
573
|
+
resolveCurve(value, keyCurve),
|
|
574
|
+
digest(hashOf(value), toBeSigned),
|
|
575
|
+
privateKey
|
|
576
|
+
);
|
|
577
|
+
case "eddsa":
|
|
578
|
+
return eddsaSign(resolveCurve(value, keyCurve), toBeSigned, privateKey);
|
|
579
|
+
case "mldsa":
|
|
580
|
+
return mldsaSign(parameterSetOf(value), toBeSigned, privateKey);
|
|
581
|
+
default:
|
|
582
|
+
throw new CoseError(`The COSE algorithm '${value.name}' is not a signature algorithm this implementation supports!`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function macWith(value, toBeMaced, key) {
|
|
586
|
+
if (value.family !== "hmac")
|
|
587
|
+
throw new CoseError(`The COSE algorithm '${value.name}' is not a message authentication algorithm this implementation supports!`);
|
|
588
|
+
return macTag(hashOf(value), tagSizeOf(value), key, toBeMaced);
|
|
589
|
+
}
|
|
590
|
+
function verifyMacWith(value, toBeMaced, tag, key) {
|
|
591
|
+
return tagsEqual(macWith(value, toBeMaced, key), tag);
|
|
592
|
+
}
|
|
593
|
+
function tagSizeOf(value) {
|
|
594
|
+
if (value.tagSize === null)
|
|
595
|
+
throw new CoseError(`The COSE algorithm '${value.name}' does not define an authentication tag width!`);
|
|
596
|
+
return value.tagSize;
|
|
597
|
+
}
|
|
598
|
+
function verifyWith(value, keyCurve, toBeSigned, signature, publicKey) {
|
|
599
|
+
switch (value.family) {
|
|
600
|
+
case "ecdsa":
|
|
601
|
+
return verify(
|
|
602
|
+
resolveCurve(value, keyCurve),
|
|
603
|
+
signature,
|
|
604
|
+
digest(hashOf(value), toBeSigned),
|
|
605
|
+
publicKey
|
|
606
|
+
);
|
|
607
|
+
case "eddsa":
|
|
608
|
+
return eddsaVerify(resolveCurve(value, keyCurve), signature, toBeSigned, publicKey);
|
|
609
|
+
case "mldsa":
|
|
610
|
+
return mldsaVerify(parameterSetOf(value), signature, toBeSigned, publicKey);
|
|
611
|
+
default:
|
|
612
|
+
throw new CoseError(`The COSE algorithm '${value.name}' is not a signature algorithm this implementation supports!`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/headers.ts
|
|
617
|
+
var CoseHeaders = class _CoseHeaders {
|
|
618
|
+
/** The header parameters, in the order they were given or read. */
|
|
619
|
+
parameters;
|
|
620
|
+
constructor(parameters = []) {
|
|
621
|
+
this.parameters = [...parameters];
|
|
622
|
+
for (let index = 0; index < this.parameters.length; index++) {
|
|
623
|
+
for (let other = 0; other < index; other++) {
|
|
624
|
+
if (sameLabel(this.parameters[index][0], this.parameters[other][0]))
|
|
625
|
+
throw new CoseError(`The COSE header parameter '${headerLabelName(this.parameters[index][0])}' was given more than once!`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* A bucket without any header parameters. As a protected bucket it is
|
|
631
|
+
* serialized as a zero-length byte string.
|
|
632
|
+
*/
|
|
633
|
+
static empty = new _CoseHeaders();
|
|
634
|
+
/** How many header parameters this bucket holds. */
|
|
635
|
+
get count() {
|
|
636
|
+
return this.parameters.length;
|
|
637
|
+
}
|
|
638
|
+
/** Whether this bucket has no header parameters at all. */
|
|
639
|
+
get isEmpty() {
|
|
640
|
+
return this.parameters.length === 0;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* The bucket holding the two header parameters almost every signed COSE
|
|
644
|
+
* message uses.
|
|
645
|
+
*/
|
|
646
|
+
static create(algorithm2, keyIdentifier) {
|
|
647
|
+
const parameters = [];
|
|
648
|
+
if (algorithm2 !== void 0 && algorithm2 !== null)
|
|
649
|
+
parameters.push([label(HeaderLabel.algorithm), algorithmToCbor(algorithm2)]);
|
|
650
|
+
if (keyIdentifier !== void 0 && keyIdentifier !== null)
|
|
651
|
+
parameters.push([label(HeaderLabel.keyIdentifier), cbor.bytes(keyIdentifier)]);
|
|
652
|
+
return new _CoseHeaders(parameters);
|
|
653
|
+
}
|
|
654
|
+
/** Parse the given CBOR map as a bucket of header parameters. */
|
|
655
|
+
static parse(value) {
|
|
656
|
+
if (value.type !== "map")
|
|
657
|
+
throw new CoseError(`A bucket of COSE header parameters must be a CBOR map, but was a CBOR ${value.type}!`);
|
|
658
|
+
return new _CoseHeaders(value.entries);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Parse the serialized protected bucket of a COSE message.
|
|
662
|
+
*
|
|
663
|
+
* A zero-length byte string is an empty bucket and *not* an encoded empty
|
|
664
|
+
* map. It is the one encoding that must never be "repaired" into `A0`,
|
|
665
|
+
* because the signature covers these exact bytes [RFC 9052, Section 3].
|
|
666
|
+
*
|
|
667
|
+
* This is deliberately a separate function rather than an overload: a byte
|
|
668
|
+
* array is also a perfectly good CBOR byte string, and the two readings of
|
|
669
|
+
* the same argument mean very different things here.
|
|
670
|
+
*/
|
|
671
|
+
static parseProtected(protectedBytes) {
|
|
672
|
+
if (protectedBytes.length === 0)
|
|
673
|
+
return _CoseHeaders.empty;
|
|
674
|
+
return _CoseHeaders.parse(decode(protectedBytes));
|
|
675
|
+
}
|
|
676
|
+
/** The value of the given header parameter, or null. */
|
|
677
|
+
get(labelValue) {
|
|
678
|
+
for (const [key, value] of this.parameters) {
|
|
679
|
+
if (sameLabel(key, labelValue))
|
|
680
|
+
return value;
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
/** Whether the given header parameter is present within this bucket. */
|
|
685
|
+
has(labelValue) {
|
|
686
|
+
return this.get(labelValue) !== null;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Return a copy in which the given header parameter is set, replacing it
|
|
690
|
+
* in place when it is already present and appending it otherwise.
|
|
691
|
+
*/
|
|
692
|
+
set(labelValue, value) {
|
|
693
|
+
const updated = [];
|
|
694
|
+
let replaced = false;
|
|
695
|
+
for (const entry of this.parameters) {
|
|
696
|
+
if (sameLabel(entry[0], labelValue)) {
|
|
697
|
+
updated.push([labelValue, value]);
|
|
698
|
+
replaced = true;
|
|
699
|
+
} else
|
|
700
|
+
updated.push(entry);
|
|
701
|
+
}
|
|
702
|
+
if (!replaced)
|
|
703
|
+
updated.push([labelValue, value]);
|
|
704
|
+
return new _CoseHeaders(updated);
|
|
705
|
+
}
|
|
706
|
+
/** The cryptographic algorithm (label 1), or null when absent. */
|
|
707
|
+
get algorithm() {
|
|
708
|
+
const value = this.get(label(HeaderLabel.algorithm));
|
|
709
|
+
if (value?.type !== "int")
|
|
710
|
+
return null;
|
|
711
|
+
return algorithmFromCbor(value);
|
|
712
|
+
}
|
|
713
|
+
/** The key identifier (label 4), or null when absent or not a byte string. */
|
|
714
|
+
get keyIdentifier() {
|
|
715
|
+
const value = this.get(label(HeaderLabel.keyIdentifier));
|
|
716
|
+
return value !== null && value.type === "bytes" ? value.value : null;
|
|
717
|
+
}
|
|
718
|
+
/** The content type (label 3), or null when absent. */
|
|
719
|
+
get contentType() {
|
|
720
|
+
return this.get(label(HeaderLabel.contentType));
|
|
721
|
+
}
|
|
722
|
+
/** The labels a recipient is required to understand (label 2), or null. */
|
|
723
|
+
get critical() {
|
|
724
|
+
const value = this.get(label(HeaderLabel.critical));
|
|
725
|
+
return value !== null && value.type === "array" ? value.items : null;
|
|
726
|
+
}
|
|
727
|
+
/** A CBOR map holding these header parameters, in their own order. */
|
|
728
|
+
toCbor() {
|
|
729
|
+
return cbor.map(this.parameters);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* The serialization of this bucket for use as the PROTECTED bucket of a
|
|
733
|
+
* COSE message: a zero-length byte string when the bucket is empty, and
|
|
734
|
+
* the encoded map otherwise [RFC 9052, Section 3].
|
|
735
|
+
*
|
|
736
|
+
* These are the exact bytes a signature is computed over, so they have to
|
|
737
|
+
* be transmitted and stored verbatim.
|
|
738
|
+
*/
|
|
739
|
+
toProtectedBytes() {
|
|
740
|
+
return this.isEmpty ? NO_BYTES : encode(this.toCbor());
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
function verifyCriticalHeaderParameters(protectedHeader, unprotectedHeader, alsoUnderstood = []) {
|
|
744
|
+
if (unprotectedHeader.has(label(HeaderLabel.critical)))
|
|
745
|
+
return notVerified('The "crit" header parameter must be placed within the protected header bucket!');
|
|
746
|
+
const critical = protectedHeader.critical;
|
|
747
|
+
if (critical === null) {
|
|
748
|
+
if (protectedHeader.has(label(HeaderLabel.critical)))
|
|
749
|
+
return notVerified('The "crit" header parameter must be an array of header parameter labels!');
|
|
750
|
+
return VERIFIED;
|
|
751
|
+
}
|
|
752
|
+
if (critical.length === 0)
|
|
753
|
+
return notVerified('The "crit" header parameter must list at least one header parameter label!');
|
|
754
|
+
for (const each of critical) {
|
|
755
|
+
if (!protectedHeader.has(each))
|
|
756
|
+
return notVerified(`The "crit" header parameter lists '${headerLabelName(each)}', which is not present within the protected header bucket!`);
|
|
757
|
+
if (!isUnderstood(each) && !alsoUnderstood.some((other) => sameLabel(other, each)))
|
|
758
|
+
return notVerified(`The "crit" header parameter demands that '${headerLabelName(each)}' be understood, which this implementation does not!`);
|
|
759
|
+
}
|
|
760
|
+
return VERIFIED;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/key.ts
|
|
764
|
+
var KEY_TYPE_AKP = 7;
|
|
765
|
+
var KEY_TYPE_SYMMETRIC = 4;
|
|
766
|
+
var KeyLabel = {
|
|
767
|
+
keyType: 1,
|
|
768
|
+
keyIdentifier: 2,
|
|
769
|
+
algorithm: 3,
|
|
770
|
+
keyOperations: 4,
|
|
771
|
+
// EC2 and OKP.
|
|
772
|
+
curve: -1,
|
|
773
|
+
x: -2,
|
|
774
|
+
y: -3,
|
|
775
|
+
d: -4,
|
|
776
|
+
// AKP [RFC 9964] — note the collision with the two above.
|
|
777
|
+
pub: -1,
|
|
778
|
+
priv: -2,
|
|
779
|
+
// Symmetric [RFC 9053, Section 7.3] — and that is label −1 meaning a
|
|
780
|
+
// third thing: the curve on an EC2 or OKP key, the public key on an AKP
|
|
781
|
+
// one, and here the shared secret itself.
|
|
782
|
+
k: -1
|
|
783
|
+
};
|
|
784
|
+
function padded(value, width, what) {
|
|
785
|
+
if (value.length === width)
|
|
786
|
+
return value;
|
|
787
|
+
if (value.length > width) {
|
|
788
|
+
const excess = value.length - width;
|
|
789
|
+
for (let index = 0; index < excess; index++) {
|
|
790
|
+
if (value[index] !== 0)
|
|
791
|
+
throw new CoseError(`The ${what} of a COSE key must be ${String(width)} bytes wide, but a ${String(value.length)}-byte value was given that does not fit!`);
|
|
792
|
+
}
|
|
793
|
+
return value.slice(excess);
|
|
794
|
+
}
|
|
795
|
+
const result = new Uint8Array(width);
|
|
796
|
+
result.set(value, width - value.length);
|
|
797
|
+
return result;
|
|
798
|
+
}
|
|
799
|
+
var CoseKey = class _CoseKey {
|
|
800
|
+
keyType;
|
|
801
|
+
keyIdentifier;
|
|
802
|
+
algorithm;
|
|
803
|
+
keyOperations;
|
|
804
|
+
/** The curve identifier as it travels, also when it is not registered. */
|
|
805
|
+
curveId;
|
|
806
|
+
/** EC2 and OKP: the coordinates and the private scalar. */
|
|
807
|
+
x;
|
|
808
|
+
y;
|
|
809
|
+
d;
|
|
810
|
+
/** AKP: the public key, and the private key — which is always the seed. */
|
|
811
|
+
pub;
|
|
812
|
+
priv;
|
|
813
|
+
/** Symmetric: the shared secret. There is no public half of one. */
|
|
814
|
+
k;
|
|
815
|
+
/** Header parameters this implementation does not know, kept for the round trip. */
|
|
816
|
+
additional;
|
|
817
|
+
constructor(fields) {
|
|
818
|
+
this.keyType = fields.keyType;
|
|
819
|
+
this.keyIdentifier = fields.keyIdentifier;
|
|
820
|
+
this.algorithm = fields.algorithm;
|
|
821
|
+
this.keyOperations = fields.keyOperations;
|
|
822
|
+
this.curveId = fields.curveId;
|
|
823
|
+
this.x = fields.x;
|
|
824
|
+
this.y = fields.y;
|
|
825
|
+
this.d = fields.d;
|
|
826
|
+
this.pub = fields.pub;
|
|
827
|
+
this.priv = fields.priv;
|
|
828
|
+
this.k = fields.k;
|
|
829
|
+
this.additional = fields.additional;
|
|
830
|
+
}
|
|
831
|
+
get fields() {
|
|
832
|
+
return {
|
|
833
|
+
keyType: this.keyType,
|
|
834
|
+
keyIdentifier: this.keyIdentifier,
|
|
835
|
+
algorithm: this.algorithm,
|
|
836
|
+
keyOperations: this.keyOperations,
|
|
837
|
+
curveId: this.curveId,
|
|
838
|
+
x: this.x,
|
|
839
|
+
y: this.y,
|
|
840
|
+
d: this.d,
|
|
841
|
+
pub: this.pub,
|
|
842
|
+
priv: this.priv,
|
|
843
|
+
k: this.k,
|
|
844
|
+
additional: this.additional
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
copy(overrides) {
|
|
848
|
+
return new _CoseKey({ ...this.fields, ...overrides });
|
|
849
|
+
}
|
|
850
|
+
/** The curve of this key, or null when it names none or an unregistered one. */
|
|
851
|
+
get curve() {
|
|
852
|
+
return this.curveId === null ? null : curveById(this.curveId);
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Whether this key carries secret key material.
|
|
856
|
+
*
|
|
857
|
+
* A symmetric key is *always* secret — it has nothing else — which is why
|
|
858
|
+
* `k` counts here beside the two private halves.
|
|
859
|
+
*/
|
|
860
|
+
get isPrivate() {
|
|
861
|
+
return this.d !== null || this.priv !== null || this.k !== null;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* An elliptic curve key pair from its private scalar.
|
|
865
|
+
*
|
|
866
|
+
* The public point is recomputed rather than asked for, which is what the
|
|
867
|
+
* examples of RFC 9052 do as well: a private COSE key always carries the
|
|
868
|
+
* full pair, and the two halves cannot disagree.
|
|
869
|
+
*/
|
|
870
|
+
static fromPrivateScalar(curve2, d, parts = {}) {
|
|
871
|
+
if (curve2.keyType === KEY_TYPE_OKP)
|
|
872
|
+
return _CoseKey.fromOkpPrivateKey(curve2, d, parts);
|
|
873
|
+
const scalar = padded(d, curve2.orderSize ?? d.length, "private key");
|
|
874
|
+
const uncompressed = publicKeyFor(curve2, scalar);
|
|
875
|
+
const fieldSize = curve2.fieldSize ?? 0;
|
|
876
|
+
return new _CoseKey({
|
|
877
|
+
...empty(curve2.keyType, parts),
|
|
878
|
+
curveId: curve2.id,
|
|
879
|
+
x: uncompressed.slice(1, 1 + fieldSize),
|
|
880
|
+
y: uncompressed.slice(1 + fieldSize),
|
|
881
|
+
d: scalar
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* An octet key pair from its private key.
|
|
886
|
+
*
|
|
887
|
+
* There is no `y`: an EdDSA public key is the whole of `x`.
|
|
888
|
+
*/
|
|
889
|
+
static fromOkpPrivateKey(curve2, d, parts = {}) {
|
|
890
|
+
const width = curve2.orderSize ?? d.length;
|
|
891
|
+
if (d.length !== width)
|
|
892
|
+
throw new CoseError(`The private key of a COSE key on the curve '${curve2.name}' must be ${String(width)} bytes long, but was ${String(d.length)} bytes long!`);
|
|
893
|
+
return new _CoseKey({
|
|
894
|
+
...empty(KEY_TYPE_OKP, parts),
|
|
895
|
+
curveId: curve2.id,
|
|
896
|
+
x: eddsaPublicKeyFor(curve2, d),
|
|
897
|
+
d
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* An algorithm key pair from its seed.
|
|
902
|
+
*
|
|
903
|
+
* The private key of an ML-DSA COSE key is the 32-byte seed and nothing
|
|
904
|
+
* else [RFC 9964] — not the expanded secret key, which is up to 4896 bytes
|
|
905
|
+
* and derivable from it. The algorithm is not optional here: an ML-DSA
|
|
906
|
+
* public key does not say which parameter set produced it, so a key that
|
|
907
|
+
* did not name one could not be used, and could not even be identified,
|
|
908
|
+
* since its thumbprint covers the algorithm.
|
|
909
|
+
*/
|
|
910
|
+
static fromAkpSeed(algorithm2, seed, parts = {}) {
|
|
911
|
+
if (algorithm2.parameterSet === null)
|
|
912
|
+
throw new CoseError(`The COSE algorithm '${algorithm2.name}' is not an algorithm key pair algorithm!`);
|
|
913
|
+
if (seed.length !== MLDSA_SEED_SIZE)
|
|
914
|
+
throw new CoseError(`The private key of an algorithm key pair is the seed and must be ${String(MLDSA_SEED_SIZE)} bytes long [RFC 9964], but was ${String(seed.length)} bytes long!`);
|
|
915
|
+
return new _CoseKey({
|
|
916
|
+
...empty(KEY_TYPE_AKP, { ...parts, algorithm: algorithm2 }),
|
|
917
|
+
pub: mldsaPublicKeyFor(algorithm2.parameterSet, seed),
|
|
918
|
+
priv: seed
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* A public algorithm key pair, from the public key alone.
|
|
923
|
+
*
|
|
924
|
+
* This is the form a certificate hands over: a `SubjectPublicKeyInfo`
|
|
925
|
+
* carries the public key and names the parameter set in its algorithm
|
|
926
|
+
* identifier, and there is no seed to be had. The algorithm is required
|
|
927
|
+
* for the same reason as in {@link fromAkpSeed} — the bytes do not say
|
|
928
|
+
* which parameter set produced them, and the thumbprint covers `alg`.
|
|
929
|
+
*/
|
|
930
|
+
static fromAkpPublicKey(algorithm2, pub, parts = {}) {
|
|
931
|
+
if (algorithm2.parameterSet === null)
|
|
932
|
+
throw new CoseError(`The COSE algorithm '${algorithm2.name}' is not an algorithm key pair algorithm!`);
|
|
933
|
+
const expected = MLDSA_SIZES[algorithm2.parameterSet].publicKey;
|
|
934
|
+
if (pub.length !== expected)
|
|
935
|
+
throw new CoseError(`The public key of an '${algorithm2.name}' algorithm key pair must be ${String(expected)} bytes long, but was ${String(pub.length)} bytes long!`);
|
|
936
|
+
return new _CoseKey({
|
|
937
|
+
...empty(KEY_TYPE_AKP, { ...parts, algorithm: algorithm2 }),
|
|
938
|
+
pub
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* A symmetric key, which is a shared secret and nothing else.
|
|
943
|
+
*
|
|
944
|
+
* No length is enforced. RFC 9053 says an HMAC key SHOULD be as wide as
|
|
945
|
+
* the hash output, which is advice about key management rather than a rule
|
|
946
|
+
* about the primitive — RFC 2104 accepts any width and the published test
|
|
947
|
+
* vectors of RFC 4231 include a four-byte key. What a caller must not do
|
|
948
|
+
* is derive one from a password, and no length check would catch that.
|
|
949
|
+
*/
|
|
950
|
+
static fromSymmetricKey(k, parts = {}) {
|
|
951
|
+
if (k.length === 0)
|
|
952
|
+
throw new CoseError("A symmetric COSE key must carry a key value!");
|
|
953
|
+
return new _CoseKey({
|
|
954
|
+
...empty(KEY_TYPE_SYMMETRIC, parts),
|
|
955
|
+
k
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
/** A public octet key pair, which is the whole of `x` and nothing else. */
|
|
959
|
+
static fromOkpPublicKey(curve2, x, parts = {}) {
|
|
960
|
+
return new _CoseKey({
|
|
961
|
+
...empty(KEY_TYPE_OKP, parts),
|
|
962
|
+
curveId: curve2.id,
|
|
963
|
+
x
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
/** A public elliptic curve key from its coordinates. */
|
|
967
|
+
static fromCoordinates(curve2, x, y, parts = {}) {
|
|
968
|
+
const width = curve2.fieldSize ?? x.length;
|
|
969
|
+
return new _CoseKey({
|
|
970
|
+
...empty(curve2.keyType, parts),
|
|
971
|
+
curveId: curve2.id,
|
|
972
|
+
x: padded(x, width, "x coordinate"),
|
|
973
|
+
y: padded(y, width, "y coordinate")
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Parse a COSE key.
|
|
978
|
+
*
|
|
979
|
+
* Widths are deliberately not checked here — only that the CBOR types are
|
|
980
|
+
* what they claim to be. A key with a shortened coordinate is a real key
|
|
981
|
+
* that a real implementation produced, and reading it is how one finds out
|
|
982
|
+
* that it is wrong; the refusal belongs at the point where the key would
|
|
983
|
+
* be used, where the error can say what was expected.
|
|
984
|
+
*
|
|
985
|
+
* A duplicate label takes its last value, which is what a lenient CBOR map
|
|
986
|
+
* does everywhere else in COSE.
|
|
987
|
+
*/
|
|
988
|
+
static parse(input) {
|
|
989
|
+
const value = input instanceof Uint8Array ? decode(input) : input;
|
|
990
|
+
if (value.type !== "map")
|
|
991
|
+
throw new CoseError(`A COSE key must be a CBOR map, but was a CBOR ${value.type}!`);
|
|
992
|
+
const keyType = _CoseKey.readKeyType(value.entries);
|
|
993
|
+
if (keyType === KEY_TYPE_AKP)
|
|
994
|
+
return _CoseKey.parseAkp(value.entries, keyType);
|
|
995
|
+
if (keyType === KEY_TYPE_SYMMETRIC)
|
|
996
|
+
return _CoseKey.parseSymmetric(value.entries, keyType);
|
|
997
|
+
return _CoseKey.parseCurveKey(value.entries, keyType);
|
|
998
|
+
}
|
|
999
|
+
static readKeyType(entries) {
|
|
1000
|
+
for (const [key, item] of entries) {
|
|
1001
|
+
if (key.type === "int" && Number(key.value) === KeyLabel.keyType) {
|
|
1002
|
+
if (item.type !== "int")
|
|
1003
|
+
throw new CoseError("The key type of a COSE key must be an integer!");
|
|
1004
|
+
return Number(item.value);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
throw new CoseError("A COSE key must have a key type!");
|
|
1008
|
+
}
|
|
1009
|
+
static parseCurveKey(entries, keyType) {
|
|
1010
|
+
let keyIdentifier = null;
|
|
1011
|
+
let algorithm2 = null;
|
|
1012
|
+
let keyOperations = null;
|
|
1013
|
+
let curveId = null;
|
|
1014
|
+
let x = null;
|
|
1015
|
+
let yValue = null;
|
|
1016
|
+
let d = null;
|
|
1017
|
+
const additional = [];
|
|
1018
|
+
for (const [key, item] of entries) {
|
|
1019
|
+
if (key.type !== "int") {
|
|
1020
|
+
additional.push([key, item]);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
switch (Number(key.value)) {
|
|
1024
|
+
case KeyLabel.keyType:
|
|
1025
|
+
break;
|
|
1026
|
+
case KeyLabel.keyIdentifier:
|
|
1027
|
+
keyIdentifier = bytesOf(item, "The key identifier of a COSE key");
|
|
1028
|
+
break;
|
|
1029
|
+
case KeyLabel.algorithm:
|
|
1030
|
+
algorithm2 = algorithmFromCbor(item);
|
|
1031
|
+
break;
|
|
1032
|
+
case KeyLabel.keyOperations:
|
|
1033
|
+
if (item.type !== "array")
|
|
1034
|
+
throw new CoseError("The key operations of a COSE key must be an array!");
|
|
1035
|
+
keyOperations = item;
|
|
1036
|
+
break;
|
|
1037
|
+
case KeyLabel.curve:
|
|
1038
|
+
if (item.type !== "int")
|
|
1039
|
+
throw new CoseError("The curve of a COSE key must be an integer!");
|
|
1040
|
+
curveId = Number(item.value);
|
|
1041
|
+
break;
|
|
1042
|
+
case KeyLabel.x:
|
|
1043
|
+
x = bytesOf(item, "The x coordinate of a COSE key");
|
|
1044
|
+
break;
|
|
1045
|
+
case KeyLabel.y:
|
|
1046
|
+
yValue = item;
|
|
1047
|
+
break;
|
|
1048
|
+
case KeyLabel.d:
|
|
1049
|
+
d = bytesOf(item, "The private key of a COSE key");
|
|
1050
|
+
break;
|
|
1051
|
+
default:
|
|
1052
|
+
additional.push([key, item]);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return new _CoseKey({
|
|
1056
|
+
keyType,
|
|
1057
|
+
keyIdentifier,
|
|
1058
|
+
algorithm: algorithm2,
|
|
1059
|
+
keyOperations,
|
|
1060
|
+
curveId,
|
|
1061
|
+
x,
|
|
1062
|
+
y: _CoseKey.resolveY(yValue, x, curveId),
|
|
1063
|
+
d,
|
|
1064
|
+
pub: null,
|
|
1065
|
+
priv: null,
|
|
1066
|
+
k: null,
|
|
1067
|
+
additional
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
static parseSymmetric(entries, keyType) {
|
|
1071
|
+
let keyIdentifier = null;
|
|
1072
|
+
let algorithm2 = null;
|
|
1073
|
+
let keyOperations = null;
|
|
1074
|
+
let k = null;
|
|
1075
|
+
const additional = [];
|
|
1076
|
+
for (const [key, item] of entries) {
|
|
1077
|
+
if (key.type !== "int") {
|
|
1078
|
+
additional.push([key, item]);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
switch (Number(key.value)) {
|
|
1082
|
+
case KeyLabel.keyType:
|
|
1083
|
+
break;
|
|
1084
|
+
case KeyLabel.keyIdentifier:
|
|
1085
|
+
keyIdentifier = bytesOf(item, "The key identifier of a COSE key");
|
|
1086
|
+
break;
|
|
1087
|
+
case KeyLabel.algorithm:
|
|
1088
|
+
algorithm2 = algorithmFromCbor(item);
|
|
1089
|
+
break;
|
|
1090
|
+
case KeyLabel.keyOperations:
|
|
1091
|
+
if (item.type !== "array")
|
|
1092
|
+
throw new CoseError("The key operations of a COSE key must be an array!");
|
|
1093
|
+
keyOperations = item;
|
|
1094
|
+
break;
|
|
1095
|
+
case KeyLabel.k:
|
|
1096
|
+
k = bytesOf(item, "The key value of a symmetric COSE key");
|
|
1097
|
+
break;
|
|
1098
|
+
default:
|
|
1099
|
+
additional.push([key, item]);
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (k === null)
|
|
1104
|
+
throw new CoseError("A COSE key of key type Symmetric must carry its key value [RFC 9053, Section 7.3]!");
|
|
1105
|
+
return new _CoseKey({
|
|
1106
|
+
keyType,
|
|
1107
|
+
keyIdentifier,
|
|
1108
|
+
algorithm: algorithm2,
|
|
1109
|
+
keyOperations,
|
|
1110
|
+
curveId: null,
|
|
1111
|
+
x: null,
|
|
1112
|
+
y: null,
|
|
1113
|
+
d: null,
|
|
1114
|
+
pub: null,
|
|
1115
|
+
priv: null,
|
|
1116
|
+
k,
|
|
1117
|
+
additional
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
static parseAkp(entries, keyType) {
|
|
1121
|
+
let keyIdentifier = null;
|
|
1122
|
+
let algorithm2 = null;
|
|
1123
|
+
let keyOperations = null;
|
|
1124
|
+
let pub = null;
|
|
1125
|
+
let priv = null;
|
|
1126
|
+
const additional = [];
|
|
1127
|
+
for (const [key, item] of entries) {
|
|
1128
|
+
if (key.type !== "int") {
|
|
1129
|
+
additional.push([key, item]);
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
switch (Number(key.value)) {
|
|
1133
|
+
case KeyLabel.keyType:
|
|
1134
|
+
break;
|
|
1135
|
+
case KeyLabel.keyIdentifier:
|
|
1136
|
+
keyIdentifier = bytesOf(item, "The key identifier of a COSE key");
|
|
1137
|
+
break;
|
|
1138
|
+
case KeyLabel.algorithm:
|
|
1139
|
+
algorithm2 = algorithmFromCbor(item);
|
|
1140
|
+
break;
|
|
1141
|
+
case KeyLabel.keyOperations:
|
|
1142
|
+
if (item.type !== "array")
|
|
1143
|
+
throw new CoseError("The key operations of a COSE key must be an array!");
|
|
1144
|
+
keyOperations = item;
|
|
1145
|
+
break;
|
|
1146
|
+
case KeyLabel.pub:
|
|
1147
|
+
pub = bytesOf(item, "The public key of an algorithm key pair");
|
|
1148
|
+
break;
|
|
1149
|
+
case KeyLabel.priv:
|
|
1150
|
+
priv = bytesOf(item, "The private key of an algorithm key pair");
|
|
1151
|
+
break;
|
|
1152
|
+
default:
|
|
1153
|
+
additional.push([key, item]);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return new _CoseKey({
|
|
1157
|
+
keyType,
|
|
1158
|
+
keyIdentifier,
|
|
1159
|
+
algorithm: algorithm2,
|
|
1160
|
+
keyOperations,
|
|
1161
|
+
curveId: null,
|
|
1162
|
+
x: null,
|
|
1163
|
+
y: null,
|
|
1164
|
+
d: null,
|
|
1165
|
+
pub,
|
|
1166
|
+
priv,
|
|
1167
|
+
k: null,
|
|
1168
|
+
additional
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* The y coordinate, which may travel as a byte string or as the parity bit
|
|
1173
|
+
* of a compressed point.
|
|
1174
|
+
*/
|
|
1175
|
+
static resolveY(value, x, curveId) {
|
|
1176
|
+
if (value === null)
|
|
1177
|
+
return null;
|
|
1178
|
+
if (value.type === "bytes")
|
|
1179
|
+
return value.value;
|
|
1180
|
+
if (value.type === "bool") {
|
|
1181
|
+
const curve2 = curveId === null ? null : curveById(curveId);
|
|
1182
|
+
if (curve2 === null)
|
|
1183
|
+
throw new CoseError("The y coordinate of a COSE key is a sign bit, which needs a known curve to be resolved!");
|
|
1184
|
+
if (x === null)
|
|
1185
|
+
throw new CoseError("The y coordinate of a COSE key is a sign bit, which needs the x coordinate to be resolved!");
|
|
1186
|
+
return decompressY(curve2, x, value.value);
|
|
1187
|
+
}
|
|
1188
|
+
throw new CoseError("The y coordinate of a COSE key must be a byte string or a boolean sign bit!");
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* A CBOR map holding this key.
|
|
1192
|
+
*
|
|
1193
|
+
* The labels are written in ascending encoded order — 1, 2, 3, 4, −1, −2,
|
|
1194
|
+
* −3, −4 — which happens to be exactly the deterministic order of
|
|
1195
|
+
* RFC 8949 Section 4.2.1, so a canonical re-encoding moves nothing.
|
|
1196
|
+
*
|
|
1197
|
+
* A key whose y arrived as a sign bit does *not* round-trip byte for byte:
|
|
1198
|
+
* y is always written as the byte string form, which every implementation
|
|
1199
|
+
* understands.
|
|
1200
|
+
*/
|
|
1201
|
+
toCbor() {
|
|
1202
|
+
const entries = [
|
|
1203
|
+
[cbor.int(KeyLabel.keyType), cbor.int(this.keyType)]
|
|
1204
|
+
];
|
|
1205
|
+
if (this.keyIdentifier !== null)
|
|
1206
|
+
entries.push([cbor.int(KeyLabel.keyIdentifier), cbor.bytes(this.keyIdentifier)]);
|
|
1207
|
+
if (this.algorithm !== null)
|
|
1208
|
+
entries.push([cbor.int(KeyLabel.algorithm), algorithmToCbor(this.algorithm)]);
|
|
1209
|
+
if (this.keyOperations !== null)
|
|
1210
|
+
entries.push([cbor.int(KeyLabel.keyOperations), this.keyOperations]);
|
|
1211
|
+
if (this.keyType === KEY_TYPE_AKP) {
|
|
1212
|
+
if (this.pub !== null)
|
|
1213
|
+
entries.push([cbor.int(KeyLabel.pub), cbor.bytes(this.pub)]);
|
|
1214
|
+
if (this.priv !== null)
|
|
1215
|
+
entries.push([cbor.int(KeyLabel.priv), cbor.bytes(this.priv)]);
|
|
1216
|
+
} else if (this.keyType === KEY_TYPE_SYMMETRIC) {
|
|
1217
|
+
if (this.k !== null)
|
|
1218
|
+
entries.push([cbor.int(KeyLabel.k), cbor.bytes(this.k)]);
|
|
1219
|
+
} else {
|
|
1220
|
+
if (this.curveId !== null)
|
|
1221
|
+
entries.push([cbor.int(KeyLabel.curve), cbor.int(this.curveId)]);
|
|
1222
|
+
if (this.x !== null)
|
|
1223
|
+
entries.push([cbor.int(KeyLabel.x), cbor.bytes(this.x)]);
|
|
1224
|
+
if (this.y !== null)
|
|
1225
|
+
entries.push([cbor.int(KeyLabel.y), cbor.bytes(this.y)]);
|
|
1226
|
+
if (this.d !== null)
|
|
1227
|
+
entries.push([cbor.int(KeyLabel.d), cbor.bytes(this.d)]);
|
|
1228
|
+
}
|
|
1229
|
+
entries.push(...this.additional);
|
|
1230
|
+
return cbor.map(entries);
|
|
1231
|
+
}
|
|
1232
|
+
/** The CBOR encoding of this key. */
|
|
1233
|
+
toBytes() {
|
|
1234
|
+
return encode(this.toCbor());
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* This key without its private half.
|
|
1238
|
+
*
|
|
1239
|
+
* A symmetric key has no such form, and saying so is the point rather than
|
|
1240
|
+
* a limitation: RFC 9053 states outright that the structure "does not have
|
|
1241
|
+
* a form that contains only public members". Returning the key unchanged
|
|
1242
|
+
* here — which is what stripping `d` and `priv` from it would do — would
|
|
1243
|
+
* hand a caller the shared secret under a name promising the opposite.
|
|
1244
|
+
*/
|
|
1245
|
+
publicKey() {
|
|
1246
|
+
if (this.keyType === KEY_TYPE_SYMMETRIC)
|
|
1247
|
+
throw new CoseError("A COSE key of key type Symmetric has no public half [RFC 9053, Section 7.3]!");
|
|
1248
|
+
return this.copy({ d: null, priv: null });
|
|
1249
|
+
}
|
|
1250
|
+
/** A copy of this key carrying the given algorithm. */
|
|
1251
|
+
withAlgorithm(algorithm2) {
|
|
1252
|
+
return this.copy({ algorithm: algorithm2 });
|
|
1253
|
+
}
|
|
1254
|
+
requireCurve() {
|
|
1255
|
+
if (this.keyType !== KEY_TYPE_EC2 && this.keyType !== KEY_TYPE_OKP)
|
|
1256
|
+
throw new CoseError(`A COSE key of key type ${String(this.keyType)} has no elliptic curve!`);
|
|
1257
|
+
const curve2 = this.curve;
|
|
1258
|
+
if (curve2 === null)
|
|
1259
|
+
throw new CoseError(this.curveId === null ? "This COSE key does not name an elliptic curve!" : `The elliptic curve ${String(this.curveId)} of this COSE key is not registered!`);
|
|
1260
|
+
return curve2;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* The public key, in whatever form its key type gives it: `04 ‖ x ‖ y` for
|
|
1264
|
+
* EC2, the whole of `x` for OKP, `pub` for AKP.
|
|
1265
|
+
*/
|
|
1266
|
+
publicKeyBytes() {
|
|
1267
|
+
if (this.keyType === KEY_TYPE_SYMMETRIC)
|
|
1268
|
+
throw new CoseError("A COSE key of key type Symmetric has no public key [RFC 9053, Section 7.3]!");
|
|
1269
|
+
if (this.keyType === KEY_TYPE_AKP) {
|
|
1270
|
+
if (this.pub === null)
|
|
1271
|
+
throw new CoseError("This COSE key carries no public key!");
|
|
1272
|
+
return this.pub;
|
|
1273
|
+
}
|
|
1274
|
+
const curve2 = this.requireCurve();
|
|
1275
|
+
const fieldSize = curve2.fieldSize ?? 0;
|
|
1276
|
+
if (this.x === null)
|
|
1277
|
+
throw new CoseError(`A COSE key on the curve '${curve2.name}' needs its public key!`);
|
|
1278
|
+
if (this.keyType === KEY_TYPE_OKP) {
|
|
1279
|
+
if (this.x.length !== fieldSize)
|
|
1280
|
+
throw new CoseError(`The public key of a COSE key on the curve '${curve2.name}' must be ${String(fieldSize)} bytes long, but was ${String(this.x.length)} bytes long!`);
|
|
1281
|
+
return this.x;
|
|
1282
|
+
}
|
|
1283
|
+
if (this.y === null)
|
|
1284
|
+
throw new CoseError(`A COSE key on the curve '${curve2.name}' needs both of its coordinates!`);
|
|
1285
|
+
if (this.x.length !== fieldSize || this.y.length !== fieldSize)
|
|
1286
|
+
throw new CoseError(`The coordinates of a COSE key on the curve '${curve2.name}' must be ${String(fieldSize)} bytes wide, including leading zeroes, but were ${String(this.x.length)} and ${String(this.y.length)} bytes wide!`);
|
|
1287
|
+
const uncompressed = new Uint8Array(1 + 2 * fieldSize);
|
|
1288
|
+
uncompressed[0] = 4;
|
|
1289
|
+
uncompressed.set(this.x, 1);
|
|
1290
|
+
uncompressed.set(this.y, 1 + fieldSize);
|
|
1291
|
+
if (!isOnCurve(curve2, uncompressed))
|
|
1292
|
+
throw new CoseError(`The public key of this COSE key does not lie on the curve '${curve2.name}'!`);
|
|
1293
|
+
return uncompressed;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* The secret this key holds: the scalar for EC2 and OKP, the seed for AKP,
|
|
1297
|
+
* the shared key itself for a symmetric one.
|
|
1298
|
+
*/
|
|
1299
|
+
privateKeyBytes() {
|
|
1300
|
+
if (this.keyType === KEY_TYPE_SYMMETRIC) {
|
|
1301
|
+
if (this.k === null)
|
|
1302
|
+
throw new CoseError("This COSE key carries no key value!");
|
|
1303
|
+
return this.k;
|
|
1304
|
+
}
|
|
1305
|
+
if (this.keyType === KEY_TYPE_AKP) {
|
|
1306
|
+
if (this.priv === null)
|
|
1307
|
+
throw new CoseError("This COSE key carries no private key material!");
|
|
1308
|
+
if (this.priv.length !== MLDSA_SEED_SIZE)
|
|
1309
|
+
throw new CoseError(`The private key of an algorithm key pair is the seed and must be ${String(MLDSA_SEED_SIZE)} bytes long [RFC 9964], but was ${String(this.priv.length)} bytes long!`);
|
|
1310
|
+
return this.priv;
|
|
1311
|
+
}
|
|
1312
|
+
const curve2 = this.requireCurve();
|
|
1313
|
+
const orderSize = curve2.orderSize ?? 0;
|
|
1314
|
+
if (this.d === null)
|
|
1315
|
+
throw new CoseError("This COSE key carries no private key material!");
|
|
1316
|
+
if (this.d.length !== orderSize)
|
|
1317
|
+
throw new CoseError(`The private key of a COSE key on the curve '${curve2.name}' must be ${String(orderSize)} bytes wide, including leading zeroes, but was ${String(this.d.length)} bytes wide!`);
|
|
1318
|
+
if (this.keyType === KEY_TYPE_EC2 && this.d.every((each) => each === 0))
|
|
1319
|
+
throw new CoseError(`The private key of this COSE key is not within the group order of the curve '${curve2.name}'!`);
|
|
1320
|
+
return this.d;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* The input of the COSE Key Thumbprint [RFC 9679, Section 3]: the required
|
|
1324
|
+
* parameters of this key, and nothing else, in deterministic encoding.
|
|
1325
|
+
*
|
|
1326
|
+
* Leaving out the optional parameters is what makes the thumbprint an
|
|
1327
|
+
* identity rather than a checksum — the public and the private half of one
|
|
1328
|
+
* key pair produce the same value, and adding a key identifier does not
|
|
1329
|
+
* change it.
|
|
1330
|
+
*
|
|
1331
|
+
* An AKP key is the exception that proves the rule: its algorithm *is*
|
|
1332
|
+
* required [RFC 9964], because an ML-DSA public key does not say which
|
|
1333
|
+
* parameter set produced it, and two keys of different strengths must not
|
|
1334
|
+
* be able to share an identity.
|
|
1335
|
+
*/
|
|
1336
|
+
thumbprintInput() {
|
|
1337
|
+
if (this.keyType === KEY_TYPE_EC2) {
|
|
1338
|
+
if (this.curveId === null || this.x === null || this.y === null)
|
|
1339
|
+
throw new CoseError("The thumbprint of a COSE key of key type EC2 needs its curve and both of its coordinates!");
|
|
1340
|
+
return cbor.encode(cbor.map([
|
|
1341
|
+
[cbor.int(KeyLabel.keyType), cbor.int(this.keyType)],
|
|
1342
|
+
[cbor.int(KeyLabel.curve), cbor.int(this.curveId)],
|
|
1343
|
+
[cbor.int(KeyLabel.x), cbor.bytes(this.x)],
|
|
1344
|
+
[cbor.int(KeyLabel.y), cbor.bytes(this.y)]
|
|
1345
|
+
]), DETERMINISTIC);
|
|
1346
|
+
}
|
|
1347
|
+
if (this.keyType === KEY_TYPE_OKP) {
|
|
1348
|
+
if (this.curveId === null || this.x === null)
|
|
1349
|
+
throw new CoseError("The thumbprint of a COSE key of key type OKP needs its curve and its public key!");
|
|
1350
|
+
return cbor.encode(cbor.map([
|
|
1351
|
+
[cbor.int(KeyLabel.keyType), cbor.int(this.keyType)],
|
|
1352
|
+
[cbor.int(KeyLabel.curve), cbor.int(this.curveId)],
|
|
1353
|
+
[cbor.int(KeyLabel.x), cbor.bytes(this.x)]
|
|
1354
|
+
]), DETERMINISTIC);
|
|
1355
|
+
}
|
|
1356
|
+
if (this.keyType === KEY_TYPE_AKP) {
|
|
1357
|
+
if (this.algorithm === null || this.pub === null)
|
|
1358
|
+
throw new CoseError("The thumbprint of a COSE key of key type AKP needs its algorithm and its public key!");
|
|
1359
|
+
return cbor.encode(cbor.map([
|
|
1360
|
+
[cbor.int(KeyLabel.keyType), cbor.int(this.keyType)],
|
|
1361
|
+
[cbor.int(KeyLabel.algorithm), algorithmToCbor(this.algorithm)],
|
|
1362
|
+
[cbor.int(KeyLabel.pub), cbor.bytes(this.pub)]
|
|
1363
|
+
]), DETERMINISTIC);
|
|
1364
|
+
}
|
|
1365
|
+
if (this.keyType === KEY_TYPE_SYMMETRIC) {
|
|
1366
|
+
if (this.k === null)
|
|
1367
|
+
throw new CoseError("The thumbprint of a COSE key of key type Symmetric needs its key value!");
|
|
1368
|
+
return cbor.encode(cbor.map([
|
|
1369
|
+
[cbor.int(KeyLabel.keyType), cbor.int(this.keyType)],
|
|
1370
|
+
[cbor.int(KeyLabel.k), cbor.bytes(this.k)]
|
|
1371
|
+
]), DETERMINISTIC);
|
|
1372
|
+
}
|
|
1373
|
+
throw new CoseError(`The thumbprint of a COSE key of key type ${String(this.keyType)} is not implemented!`);
|
|
1374
|
+
}
|
|
1375
|
+
/** The COSE Key Thumbprint [RFC 9679], untruncated. */
|
|
1376
|
+
thumbprint(hash = "sha256") {
|
|
1377
|
+
return digest(hash, this.thumbprintInput());
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* The leading bytes of the thumbprint, for use as a key identifier.
|
|
1381
|
+
*
|
|
1382
|
+
* Two properties make this worth preferring over a self-chosen prefix.
|
|
1383
|
+
* Everyone holding the public key can recompute it, so no registry is
|
|
1384
|
+
* needed beyond an agreement on its length. And because the thumbprint
|
|
1385
|
+
* covers the curve — or, for an algorithm key pair, the algorithm — a
|
|
1386
|
+
* signer who changes strength necessarily has a different key and
|
|
1387
|
+
* therefore a different identifier: a downgrade under an unchanged
|
|
1388
|
+
* identity is not expressible.
|
|
1389
|
+
*/
|
|
1390
|
+
thumbprintKeyIdentifier(length = 8, hash = "sha256") {
|
|
1391
|
+
const value = this.thumbprint(hash);
|
|
1392
|
+
if (length < 1 || length > value.length)
|
|
1393
|
+
throw new CoseError(`A thumbprint key identifier must be between 1 and ${String(value.length)} bytes long, but ${String(length)} bytes were asked for!`);
|
|
1394
|
+
return value.slice(0, length);
|
|
1395
|
+
}
|
|
1396
|
+
/** A copy of this key whose key identifier is its own thumbprint. */
|
|
1397
|
+
withThumbprintKeyIdentifier(length = 8, hash = "sha256") {
|
|
1398
|
+
return this.copy({ keyIdentifier: this.thumbprintKeyIdentifier(length, hash) });
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
function empty(keyType, parts) {
|
|
1402
|
+
return {
|
|
1403
|
+
keyType,
|
|
1404
|
+
keyIdentifier: parts.keyIdentifier ?? null,
|
|
1405
|
+
algorithm: parts.algorithm ?? null,
|
|
1406
|
+
keyOperations: null,
|
|
1407
|
+
curveId: null,
|
|
1408
|
+
x: null,
|
|
1409
|
+
y: null,
|
|
1410
|
+
d: null,
|
|
1411
|
+
pub: null,
|
|
1412
|
+
priv: null,
|
|
1413
|
+
k: null,
|
|
1414
|
+
additional: []
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
function bytesOf(value, what) {
|
|
1418
|
+
if (value.type !== "bytes")
|
|
1419
|
+
throw new CoseError(`${what} must be a byte string!`);
|
|
1420
|
+
return value.value;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/signature.ts
|
|
1424
|
+
var CoseSignature = class _CoseSignature {
|
|
1425
|
+
/** The serialized protected bucket, exactly as signed and as received. */
|
|
1426
|
+
protectedHeaderBytes;
|
|
1427
|
+
/** The protected header parameters, which this signature covers. */
|
|
1428
|
+
protectedHeader;
|
|
1429
|
+
/** The unprotected header parameters, which it does not. */
|
|
1430
|
+
unprotectedHeader;
|
|
1431
|
+
/** The signature: `r ‖ s`, never DER. */
|
|
1432
|
+
signature;
|
|
1433
|
+
constructor(protectedHeaderBytes, unprotectedHeader, signature) {
|
|
1434
|
+
this.protectedHeaderBytes = protectedHeaderBytes;
|
|
1435
|
+
this.protectedHeader = CoseHeaders.parseProtected(protectedHeaderBytes);
|
|
1436
|
+
this.unprotectedHeader = unprotectedHeader ?? CoseHeaders.empty;
|
|
1437
|
+
this.signature = signature;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* The signature algorithm, taken from the protected bucket and only
|
|
1441
|
+
* otherwise from the unprotected one.
|
|
1442
|
+
*/
|
|
1443
|
+
get algorithm() {
|
|
1444
|
+
return this.protectedHeader.algorithm ?? this.unprotectedHeader.algorithm;
|
|
1445
|
+
}
|
|
1446
|
+
/** The key identifier, protected bucket first. */
|
|
1447
|
+
get keyIdentifier() {
|
|
1448
|
+
return this.protectedHeader.keyIdentifier ?? this.unprotectedHeader.keyIdentifier;
|
|
1449
|
+
}
|
|
1450
|
+
/** Parse a three-element COSE_Signature. */
|
|
1451
|
+
static parse(value) {
|
|
1452
|
+
if (value.type !== "array")
|
|
1453
|
+
throw new CoseError(`A COSE_Signature must be a CBOR array, but was a CBOR ${value.type}!`);
|
|
1454
|
+
if (value.items.length !== 3)
|
|
1455
|
+
throw new CoseError(`A COSE_Signature must be a CBOR array of 3 elements, but had ${String(value.items.length)} element(s)!`);
|
|
1456
|
+
const [protectedBytes, unprotected, signature] = value.items;
|
|
1457
|
+
if (protectedBytes?.type !== "bytes")
|
|
1458
|
+
throw new CoseError("The protected header bucket of a COSE_Signature must be a byte string!");
|
|
1459
|
+
if (unprotected === void 0)
|
|
1460
|
+
throw new CoseError("A COSE_Signature must carry an unprotected header bucket!");
|
|
1461
|
+
if (signature?.type !== "bytes")
|
|
1462
|
+
throw new CoseError("The signature of a COSE_Signature must be a byte string!");
|
|
1463
|
+
return new _CoseSignature(
|
|
1464
|
+
protectedBytes.value,
|
|
1465
|
+
CoseHeaders.parse(unprotected),
|
|
1466
|
+
signature.value
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
/** A CBOR representation of this signature. */
|
|
1470
|
+
toCbor() {
|
|
1471
|
+
return cbor.array([
|
|
1472
|
+
cbor.bytes(this.protectedHeaderBytes),
|
|
1473
|
+
this.unprotectedHeader.toCbor(),
|
|
1474
|
+
cbor.bytes(this.signature)
|
|
1475
|
+
]);
|
|
1476
|
+
}
|
|
1477
|
+
/** The CBOR encoding of this signature. */
|
|
1478
|
+
toBytes() {
|
|
1479
|
+
return encode(this.toCbor());
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
// src/resolve.ts
|
|
1484
|
+
function resolveAlgorithm(protectedHeader, unprotectedHeader, expected, what) {
|
|
1485
|
+
const algorithm2 = expected ?? protectedHeader.algorithm;
|
|
1486
|
+
if (algorithm2 === null) {
|
|
1487
|
+
const stated2 = unprotectedHeader.algorithm;
|
|
1488
|
+
return {
|
|
1489
|
+
ok: false,
|
|
1490
|
+
reason: stated2 !== null ? `${what} states its algorithm '${stated2.name}' within the unprotected header bucket only, where it is not covered by the signature: pass the expected algorithm explicitly in order to accept it!` : `${what} does not state its signature algorithm: pass the expected algorithm explicitly!`
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
const stated = protectedHeader.algorithm ?? unprotectedHeader.algorithm;
|
|
1494
|
+
if (stated !== null && !sameAlgorithm(stated, algorithm2))
|
|
1495
|
+
return {
|
|
1496
|
+
ok: false,
|
|
1497
|
+
reason: `${what} was signed with the algorithm '${stated.name}', but the algorithm '${algorithm2.name}' was expected!`
|
|
1498
|
+
};
|
|
1499
|
+
return { ok: true, value: algorithm2 };
|
|
1500
|
+
}
|
|
1501
|
+
function resolvePayload(carried, detached, what) {
|
|
1502
|
+
if (carried !== null) {
|
|
1503
|
+
if (detached !== null)
|
|
1504
|
+
return { ok: false, reason: `${what} carries its payload, therefore no detached payload must be supplied!` };
|
|
1505
|
+
return { ok: true, value: carried };
|
|
1506
|
+
}
|
|
1507
|
+
if (detached === null)
|
|
1508
|
+
return { ok: false, reason: `The payload of ${what} is detached, therefore it must be supplied for the signature to be computed!` };
|
|
1509
|
+
return { ok: true, value: detached };
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/asn1.ts
|
|
1513
|
+
var Tag = {
|
|
1514
|
+
boolean: 1,
|
|
1515
|
+
integer: 2,
|
|
1516
|
+
bitString: 3,
|
|
1517
|
+
octetString: 4,
|
|
1518
|
+
null: 5,
|
|
1519
|
+
objectIdentifier: 6,
|
|
1520
|
+
utf8String: 12,
|
|
1521
|
+
printableString: 19,
|
|
1522
|
+
teletexString: 20,
|
|
1523
|
+
ia5String: 22,
|
|
1524
|
+
utcTime: 23,
|
|
1525
|
+
generalizedTime: 24,
|
|
1526
|
+
bmpString: 30,
|
|
1527
|
+
universalString: 28,
|
|
1528
|
+
sequence: 48,
|
|
1529
|
+
set: 49
|
|
1530
|
+
};
|
|
1531
|
+
var contextTag = (number) => 160 | number;
|
|
1532
|
+
var STRING_TAGS = [
|
|
1533
|
+
Tag.utf8String,
|
|
1534
|
+
Tag.printableString,
|
|
1535
|
+
Tag.teletexString,
|
|
1536
|
+
Tag.ia5String,
|
|
1537
|
+
Tag.bmpString,
|
|
1538
|
+
Tag.universalString
|
|
1539
|
+
];
|
|
1540
|
+
var DerReader = class {
|
|
1541
|
+
data;
|
|
1542
|
+
what;
|
|
1543
|
+
at;
|
|
1544
|
+
constructor(data, what) {
|
|
1545
|
+
this.data = data;
|
|
1546
|
+
this.what = what;
|
|
1547
|
+
this.at = 0;
|
|
1548
|
+
}
|
|
1549
|
+
/** Whether every record has been read. */
|
|
1550
|
+
get atEnd() {
|
|
1551
|
+
return this.at >= this.data.length;
|
|
1552
|
+
}
|
|
1553
|
+
/** The octets not yet read. */
|
|
1554
|
+
get remaining() {
|
|
1555
|
+
return this.data.subarray(this.at);
|
|
1556
|
+
}
|
|
1557
|
+
fail(reason) {
|
|
1558
|
+
throw new CoseError(`${this.what}: ${reason}`);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Read the next record, optionally insisting on a tag.
|
|
1562
|
+
*
|
|
1563
|
+
* The length rules are DER's: definite always, short form below 128,
|
|
1564
|
+
* long form minimally encoded and never longer than eight octets — which
|
|
1565
|
+
* is far more than a certificate can need and still shorter than the point
|
|
1566
|
+
* where a length stops fitting a JavaScript number.
|
|
1567
|
+
*/
|
|
1568
|
+
read(expectedTag) {
|
|
1569
|
+
const start = this.at;
|
|
1570
|
+
if (this.at + 2 > this.data.length)
|
|
1571
|
+
this.fail(`a DER record needs at least two octets, and only ${String(this.data.length - this.at)} remain`);
|
|
1572
|
+
const tag = this.data[this.at++];
|
|
1573
|
+
if ((tag & 31) === 31)
|
|
1574
|
+
this.fail("a DER identifier of more than one octet is not something a certificate contains");
|
|
1575
|
+
if (expectedTag !== void 0 && tag !== expectedTag)
|
|
1576
|
+
this.fail(`expected the DER tag 0x${expectedTag.toString(16).padStart(2, "0")}, but found 0x${tag.toString(16).padStart(2, "0")}`);
|
|
1577
|
+
const first = this.data[this.at++];
|
|
1578
|
+
let length;
|
|
1579
|
+
if (first === 128)
|
|
1580
|
+
this.fail("an indefinite length is BER, not DER, and a certificate is DER");
|
|
1581
|
+
else if (first === 255)
|
|
1582
|
+
this.fail("0xFF is not a valid DER length octet");
|
|
1583
|
+
else if (first < 128)
|
|
1584
|
+
length = first;
|
|
1585
|
+
else {
|
|
1586
|
+
const count = first & 127;
|
|
1587
|
+
if (count > 8)
|
|
1588
|
+
this.fail(`a length of ${String(count)} octets is longer than anything a certificate can hold`);
|
|
1589
|
+
if (this.at + count > this.data.length)
|
|
1590
|
+
this.fail("the long-form length runs past the end of the data");
|
|
1591
|
+
if (this.data[this.at] === 0)
|
|
1592
|
+
this.fail("a DER length must not be encoded with a leading zero octet");
|
|
1593
|
+
length = 0;
|
|
1594
|
+
for (let index = 0; index < count; index++)
|
|
1595
|
+
length = length * 256 + this.data[this.at++];
|
|
1596
|
+
if (length < 128)
|
|
1597
|
+
this.fail(`the length ${String(length)} must be encoded in the short form`);
|
|
1598
|
+
if (!Number.isSafeInteger(length))
|
|
1599
|
+
this.fail("the length is larger than this implementation can address");
|
|
1600
|
+
}
|
|
1601
|
+
if (this.at + length > this.data.length)
|
|
1602
|
+
this.fail(`a record claims ${String(length)} octets, and only ${String(this.data.length - this.at)} remain`);
|
|
1603
|
+
const content = this.data.subarray(this.at, this.at + length);
|
|
1604
|
+
this.at += length;
|
|
1605
|
+
return { tag, content, encoded: this.data.subarray(start, this.at) };
|
|
1606
|
+
}
|
|
1607
|
+
/** Read the next record when it carries the given tag, else read nothing. */
|
|
1608
|
+
readOptional(tag) {
|
|
1609
|
+
if (this.atEnd || this.data[this.at] !== tag)
|
|
1610
|
+
return null;
|
|
1611
|
+
return this.read(tag);
|
|
1612
|
+
}
|
|
1613
|
+
/** Whether a record with the given tag comes next. */
|
|
1614
|
+
peekIs(tag) {
|
|
1615
|
+
return !this.atEnd && this.data[this.at] === tag;
|
|
1616
|
+
}
|
|
1617
|
+
/** Insist that nothing follows, which is how trailing data is caught. */
|
|
1618
|
+
expectEnd() {
|
|
1619
|
+
if (!this.atEnd)
|
|
1620
|
+
this.fail(`${String(this.data.length - this.at)} octets follow the structure and should not`);
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
var contentsOf = (node, what) => new DerReader(node.content, what);
|
|
1624
|
+
function derInteger(node, what) {
|
|
1625
|
+
const bytes = node.content;
|
|
1626
|
+
if (bytes.length === 0)
|
|
1627
|
+
throw new CoseError(`${what}: a DER INTEGER must not be empty`);
|
|
1628
|
+
if (bytes.length > 1 && (bytes[0] === 0 && (bytes[1] & 128) === 0 || bytes[0] === 255 && (bytes[1] & 128) !== 0))
|
|
1629
|
+
throw new CoseError(`${what}: a DER INTEGER must be minimally encoded`);
|
|
1630
|
+
let value = 0n;
|
|
1631
|
+
for (const each of bytes)
|
|
1632
|
+
value = value << 8n | BigInt(each);
|
|
1633
|
+
return (bytes[0] & 128) === 0 ? value : value - (1n << BigInt(8 * bytes.length));
|
|
1634
|
+
}
|
|
1635
|
+
function derBitString(node, what) {
|
|
1636
|
+
const { unusedBits, bytes } = derBitStringRaw(node, what);
|
|
1637
|
+
if (unusedBits !== 0)
|
|
1638
|
+
throw new CoseError(`${what}: expected an octet-aligned BIT STRING, but ${String(unusedBits)} bits are unused`);
|
|
1639
|
+
return bytes;
|
|
1640
|
+
}
|
|
1641
|
+
function derBitStringRaw(node, what) {
|
|
1642
|
+
if (node.content.length === 0)
|
|
1643
|
+
throw new CoseError(`${what}: a DER BIT STRING needs at least its unused-bit count`);
|
|
1644
|
+
const unusedBits = node.content[0];
|
|
1645
|
+
if (unusedBits > 7)
|
|
1646
|
+
throw new CoseError(`${what}: a DER BIT STRING can not have ${String(unusedBits)} unused bits`);
|
|
1647
|
+
if (unusedBits !== 0 && node.content.length === 1)
|
|
1648
|
+
throw new CoseError(`${what}: an empty DER BIT STRING must declare zero unused bits`);
|
|
1649
|
+
return { unusedBits, bytes: node.content.subarray(1) };
|
|
1650
|
+
}
|
|
1651
|
+
function derBoolean(node, what) {
|
|
1652
|
+
if (node.content.length !== 1)
|
|
1653
|
+
throw new CoseError(`${what}: a DER BOOLEAN is one octet`);
|
|
1654
|
+
if (node.content[0] === 0)
|
|
1655
|
+
return false;
|
|
1656
|
+
if (node.content[0] === 255)
|
|
1657
|
+
return true;
|
|
1658
|
+
throw new CoseError(`${what}: a DER BOOLEAN is 0x00 or 0xFF, but was 0x${node.content[0].toString(16).padStart(2, "0")}`);
|
|
1659
|
+
}
|
|
1660
|
+
function derObjectIdentifier(node, what) {
|
|
1661
|
+
const bytes = node.content;
|
|
1662
|
+
if (bytes.length === 0)
|
|
1663
|
+
throw new CoseError(`${what}: a DER OBJECT IDENTIFIER must not be empty`);
|
|
1664
|
+
const arcs = [];
|
|
1665
|
+
let value = 0n;
|
|
1666
|
+
let started = false;
|
|
1667
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
1668
|
+
const octet = bytes[index];
|
|
1669
|
+
if (!started && octet === 128)
|
|
1670
|
+
throw new CoseError(`${what}: an arc of a DER OBJECT IDENTIFIER must be minimally encoded`);
|
|
1671
|
+
started = true;
|
|
1672
|
+
value = value << 7n | BigInt(octet & 127);
|
|
1673
|
+
if ((octet & 128) !== 0) {
|
|
1674
|
+
if (index === bytes.length - 1)
|
|
1675
|
+
throw new CoseError(`${what}: a DER OBJECT IDENTIFIER ends in the middle of an arc`);
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
if (arcs.length === 0) {
|
|
1679
|
+
const first = value < 40n ? 0n : value < 80n ? 1n : 2n;
|
|
1680
|
+
arcs.push(first.toString());
|
|
1681
|
+
arcs.push((value - first * 40n).toString());
|
|
1682
|
+
} else
|
|
1683
|
+
arcs.push(value.toString());
|
|
1684
|
+
value = 0n;
|
|
1685
|
+
started = false;
|
|
1686
|
+
}
|
|
1687
|
+
return arcs.join(".");
|
|
1688
|
+
}
|
|
1689
|
+
function derString(node, what) {
|
|
1690
|
+
if (!STRING_TAGS.includes(node.tag))
|
|
1691
|
+
throw new CoseError(`${what}: 0x${node.tag.toString(16).padStart(2, "0")} is not a string type an X.509 name may use`);
|
|
1692
|
+
if (node.tag === Tag.bmpString)
|
|
1693
|
+
return decodeWide(node.content, 2, what);
|
|
1694
|
+
if (node.tag === Tag.universalString)
|
|
1695
|
+
return decodeWide(node.content, 4, what);
|
|
1696
|
+
if (node.tag === Tag.teletexString)
|
|
1697
|
+
return Array.from(node.content, (each) => String.fromCharCode(each)).join("");
|
|
1698
|
+
try {
|
|
1699
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(node.content);
|
|
1700
|
+
} catch {
|
|
1701
|
+
throw new CoseError(`${what}: the content is not valid UTF-8`);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function decodeWide(bytes, width, what) {
|
|
1705
|
+
if (bytes.length % width !== 0)
|
|
1706
|
+
throw new CoseError(`${what}: a ${String(width * 8)}-bit string must be a whole number of characters`);
|
|
1707
|
+
let text = "";
|
|
1708
|
+
for (let index = 0; index < bytes.length; index += width) {
|
|
1709
|
+
let point = 0;
|
|
1710
|
+
for (let byte = 0; byte < width; byte++)
|
|
1711
|
+
point = point * 256 + bytes[index + byte];
|
|
1712
|
+
if (point > 1114111)
|
|
1713
|
+
throw new CoseError(`${what}: 0x${point.toString(16)} is not a Unicode code point`);
|
|
1714
|
+
text += String.fromCodePoint(point);
|
|
1715
|
+
}
|
|
1716
|
+
return text;
|
|
1717
|
+
}
|
|
1718
|
+
function derTime(node, what) {
|
|
1719
|
+
const text = Array.from(node.content, (each) => String.fromCharCode(each)).join("");
|
|
1720
|
+
if (node.tag === Tag.utcTime) {
|
|
1721
|
+
const match = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(text);
|
|
1722
|
+
if (match === null)
|
|
1723
|
+
throw new CoseError(`${what}: '${text}' is not a DER UTCTime of the form YYMMDDHHMMSSZ`);
|
|
1724
|
+
const year = Number(match[1]);
|
|
1725
|
+
return utc(
|
|
1726
|
+
year >= 50 ? 1900 + year : 2e3 + year,
|
|
1727
|
+
Number(match[2]),
|
|
1728
|
+
Number(match[3]),
|
|
1729
|
+
Number(match[4]),
|
|
1730
|
+
Number(match[5]),
|
|
1731
|
+
Number(match[6]),
|
|
1732
|
+
text,
|
|
1733
|
+
what
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
if (node.tag === Tag.generalizedTime) {
|
|
1737
|
+
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(text);
|
|
1738
|
+
if (match === null)
|
|
1739
|
+
throw new CoseError(`${what}: '${text}' is not a DER GeneralizedTime of the form YYYYMMDDHHMMSSZ`);
|
|
1740
|
+
return utc(
|
|
1741
|
+
Number(match[1]),
|
|
1742
|
+
Number(match[2]),
|
|
1743
|
+
Number(match[3]),
|
|
1744
|
+
Number(match[4]),
|
|
1745
|
+
Number(match[5]),
|
|
1746
|
+
Number(match[6]),
|
|
1747
|
+
text,
|
|
1748
|
+
what
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
throw new CoseError(`${what}: 0x${node.tag.toString(16).padStart(2, "0")} is not a time type`);
|
|
1752
|
+
}
|
|
1753
|
+
function utc(year, month, day, hour, minute, second, text, what) {
|
|
1754
|
+
const value = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
|
|
1755
|
+
if (value.getUTCFullYear() !== year || value.getUTCMonth() !== month - 1 || value.getUTCDate() !== day || value.getUTCHours() !== hour || value.getUTCMinutes() !== minute || value.getUTCSeconds() !== second)
|
|
1756
|
+
throw new CoseError(`${what}: '${text}' is not a point in time that exists`);
|
|
1757
|
+
return value;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/x509.ts
|
|
1761
|
+
var CURVE_OIDS = {
|
|
1762
|
+
"1.2.840.10045.3.1.7": CoseCurves.P256,
|
|
1763
|
+
// prime256v1
|
|
1764
|
+
"1.3.132.0.34": CoseCurves.P384,
|
|
1765
|
+
// secp384r1
|
|
1766
|
+
"1.3.132.0.35": CoseCurves.P521,
|
|
1767
|
+
// secp521r1
|
|
1768
|
+
"1.3.132.0.10": CoseCurves.secp256k1,
|
|
1769
|
+
"1.3.36.3.3.2.8.1.1.7": CoseCurves.brainpoolP256r1,
|
|
1770
|
+
// [RFC 5639, Appendix A]
|
|
1771
|
+
"1.3.36.3.3.2.8.1.1.9": CoseCurves.brainpoolP320r1,
|
|
1772
|
+
"1.3.36.3.3.2.8.1.1.11": CoseCurves.brainpoolP384r1,
|
|
1773
|
+
"1.3.36.3.3.2.8.1.1.13": CoseCurves.brainpoolP512r1
|
|
1774
|
+
};
|
|
1775
|
+
var OID_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
|
|
1776
|
+
var OID_ED25519 = "1.3.101.112";
|
|
1777
|
+
var OID_ED448 = "1.3.101.113";
|
|
1778
|
+
var OID_MLDSA44 = "2.16.840.1.101.3.4.3.17";
|
|
1779
|
+
var OID_MLDSA65 = "2.16.840.1.101.3.4.3.18";
|
|
1780
|
+
var OID_MLDSA87 = "2.16.840.1.101.3.4.3.19";
|
|
1781
|
+
var SIGNATURE_OIDS = {
|
|
1782
|
+
"1.2.840.10045.4.3.2": CoseAlgorithms.ES256,
|
|
1783
|
+
// ecdsa-with-SHA256
|
|
1784
|
+
"1.2.840.10045.4.3.3": CoseAlgorithms.ES384,
|
|
1785
|
+
// ecdsa-with-SHA384
|
|
1786
|
+
"1.2.840.10045.4.3.4": CoseAlgorithms.ES512,
|
|
1787
|
+
// ecdsa-with-SHA512
|
|
1788
|
+
[OID_ED25519]: CoseAlgorithms.Ed25519,
|
|
1789
|
+
[OID_ED448]: CoseAlgorithms.Ed448,
|
|
1790
|
+
[OID_MLDSA44]: CoseAlgorithms.MLDSA44,
|
|
1791
|
+
[OID_MLDSA65]: CoseAlgorithms.MLDSA65,
|
|
1792
|
+
[OID_MLDSA87]: CoseAlgorithms.MLDSA87
|
|
1793
|
+
};
|
|
1794
|
+
var NAME_OIDS = {
|
|
1795
|
+
"2.5.4.3": "CN",
|
|
1796
|
+
"2.5.4.4": "SN",
|
|
1797
|
+
"2.5.4.5": "SERIALNUMBER",
|
|
1798
|
+
"2.5.4.6": "C",
|
|
1799
|
+
"2.5.4.7": "L",
|
|
1800
|
+
"2.5.4.8": "ST",
|
|
1801
|
+
"2.5.4.9": "STREET",
|
|
1802
|
+
"2.5.4.10": "O",
|
|
1803
|
+
"2.5.4.11": "OU",
|
|
1804
|
+
"2.5.4.12": "T",
|
|
1805
|
+
"2.5.4.42": "GIVENNAME",
|
|
1806
|
+
"1.2.840.113549.1.9.1": "E",
|
|
1807
|
+
"0.9.2342.19200300.100.1.25": "DC",
|
|
1808
|
+
"0.9.2342.19200300.100.1.1": "UID"
|
|
1809
|
+
};
|
|
1810
|
+
var OID_BASIC_CONSTRAINTS = "2.5.29.19";
|
|
1811
|
+
var OID_KEY_USAGE = "2.5.29.15";
|
|
1812
|
+
var X509Name = class _X509Name {
|
|
1813
|
+
/** The attributes, outermost relative name first, as they were encoded. */
|
|
1814
|
+
attributes;
|
|
1815
|
+
/** The DER encoding, kept because a name is also compared by its bytes. */
|
|
1816
|
+
encoded;
|
|
1817
|
+
constructor(attributes, encoded) {
|
|
1818
|
+
this.attributes = attributes;
|
|
1819
|
+
this.encoded = encoded;
|
|
1820
|
+
}
|
|
1821
|
+
/** Parse an X.501 `Name`, which is a `SEQUENCE OF SET OF` attributes. */
|
|
1822
|
+
static parse(node, what) {
|
|
1823
|
+
const attributes = [];
|
|
1824
|
+
const relatives = contentsOf(node, `${what}: the relative distinguished names`);
|
|
1825
|
+
while (!relatives.atEnd) {
|
|
1826
|
+
const set = relatives.read(Tag.set);
|
|
1827
|
+
const members = contentsOf(set, `${what}: a relative distinguished name`);
|
|
1828
|
+
while (!members.atEnd) {
|
|
1829
|
+
const pair = contentsOf(members.read(Tag.sequence), `${what}: an attribute of the name`);
|
|
1830
|
+
const oid = derObjectIdentifier(pair.read(Tag.objectIdentifier), `${what}: an attribute type`);
|
|
1831
|
+
const value = pair.read();
|
|
1832
|
+
pair.expectEnd();
|
|
1833
|
+
attributes.push({ oid, value: derString(value, `${what}: the value of the attribute ${oid}`) });
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return new _X509Name(attributes, node.encoded);
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Whether this name and the given one name the same entity.
|
|
1840
|
+
*
|
|
1841
|
+
* The normalization is the practical part of RFC 4518: leading and
|
|
1842
|
+
* trailing whitespace removed, internal runs collapsed to one space, and
|
|
1843
|
+
* case folded. Not implemented, because no certificate in this project's
|
|
1844
|
+
* world uses them: the Unicode mapping and prohibition tables.
|
|
1845
|
+
*/
|
|
1846
|
+
equivalentTo(other) {
|
|
1847
|
+
if (bytesEqual2(this.encoded, other.encoded))
|
|
1848
|
+
return true;
|
|
1849
|
+
if (this.attributes.length !== other.attributes.length)
|
|
1850
|
+
return false;
|
|
1851
|
+
return this.attributes.every((each, index) => each.oid === other.attributes[index].oid && normalize(each.value) === normalize(other.attributes[index].value));
|
|
1852
|
+
}
|
|
1853
|
+
/** The name in the usual `CN=…,O=…` notation. */
|
|
1854
|
+
toString() {
|
|
1855
|
+
return this.attributes.map((each) => `${NAME_OIDS[each.oid] ?? each.oid}=${each.value}`).join(",");
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
var normalize = (value) => value.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
1859
|
+
var bytesEqual2 = (left, right) => left.length === right.length && left.every((each, index) => each === right[index]);
|
|
1860
|
+
var KeyUsage = {
|
|
1861
|
+
digitalSignature: 0,
|
|
1862
|
+
nonRepudiation: 1,
|
|
1863
|
+
keyEncipherment: 2,
|
|
1864
|
+
dataEncipherment: 3,
|
|
1865
|
+
keyAgreement: 4,
|
|
1866
|
+
keyCertSign: 5,
|
|
1867
|
+
cRLSign: 6,
|
|
1868
|
+
encipherOnly: 7,
|
|
1869
|
+
decipherOnly: 8
|
|
1870
|
+
};
|
|
1871
|
+
var X509Certificate = class _X509Certificate {
|
|
1872
|
+
/** The DER encoding, exactly as it arrived. */
|
|
1873
|
+
encoded;
|
|
1874
|
+
/** The `tbsCertificate`, which is the part the signature covers. */
|
|
1875
|
+
tbsBytes;
|
|
1876
|
+
/** The version, 1 to 3. */
|
|
1877
|
+
version;
|
|
1878
|
+
/** The serial number, which may be negative and is often very large. */
|
|
1879
|
+
serialNumber;
|
|
1880
|
+
/** Who issued this certificate. */
|
|
1881
|
+
issuer;
|
|
1882
|
+
/** Whom it was issued to. */
|
|
1883
|
+
subject;
|
|
1884
|
+
/** The start of the validity period, inclusive. */
|
|
1885
|
+
notBefore;
|
|
1886
|
+
/** The end of the validity period, inclusive. */
|
|
1887
|
+
notAfter;
|
|
1888
|
+
/** The certified public key. */
|
|
1889
|
+
publicKey;
|
|
1890
|
+
/** How this certificate was signed, as the COSE algorithm it corresponds to. */
|
|
1891
|
+
signatureAlgorithm;
|
|
1892
|
+
/** The signature, as it was encoded — DER for ECDSA, raw otherwise. */
|
|
1893
|
+
signatureValue;
|
|
1894
|
+
/** The `basicConstraints` extension, when present. */
|
|
1895
|
+
basicConstraints;
|
|
1896
|
+
/** The `keyUsage` extension as a bit array, when present. */
|
|
1897
|
+
keyUsage;
|
|
1898
|
+
/** The object identifiers of every extension marked critical. */
|
|
1899
|
+
criticalExtensions;
|
|
1900
|
+
constructor(fields) {
|
|
1901
|
+
this.encoded = fields.encoded;
|
|
1902
|
+
this.tbsBytes = fields.tbsBytes;
|
|
1903
|
+
this.version = fields.version;
|
|
1904
|
+
this.serialNumber = fields.serialNumber;
|
|
1905
|
+
this.issuer = fields.issuer;
|
|
1906
|
+
this.subject = fields.subject;
|
|
1907
|
+
this.notBefore = fields.notBefore;
|
|
1908
|
+
this.notAfter = fields.notAfter;
|
|
1909
|
+
this.publicKey = fields.publicKey;
|
|
1910
|
+
this.signatureAlgorithm = fields.signatureAlgorithm;
|
|
1911
|
+
this.signatureValue = fields.signatureValue;
|
|
1912
|
+
this.basicConstraints = fields.basicConstraints;
|
|
1913
|
+
this.keyUsage = fields.keyUsage;
|
|
1914
|
+
this.criticalExtensions = fields.criticalExtensions;
|
|
1915
|
+
}
|
|
1916
|
+
// ------------------------------------------------------------- parsing
|
|
1917
|
+
/**
|
|
1918
|
+
* Parse a DER encoded certificate.
|
|
1919
|
+
*
|
|
1920
|
+
* The `signatureAlgorithm` of the outer structure and the `signature`
|
|
1921
|
+
* field inside the `tbsCertificate` must agree, and it is checked: they
|
|
1922
|
+
* are the same statement written twice, and an implementation that reads
|
|
1923
|
+
* one while verifying with the other can be told two different things at
|
|
1924
|
+
* once.
|
|
1925
|
+
*/
|
|
1926
|
+
static parse(der) {
|
|
1927
|
+
const what = "This X.509 certificate";
|
|
1928
|
+
const outer = new DerReader(der, what);
|
|
1929
|
+
const certificate = outer.read(Tag.sequence);
|
|
1930
|
+
outer.expectEnd();
|
|
1931
|
+
const body = contentsOf(certificate, what);
|
|
1932
|
+
const tbs = body.read(Tag.sequence);
|
|
1933
|
+
const outerAlgorithmNode = body.read(Tag.sequence);
|
|
1934
|
+
const signatureNode = body.read(Tag.bitString);
|
|
1935
|
+
body.expectEnd();
|
|
1936
|
+
const outerAlgorithm = algorithmIdentifier(outerAlgorithmNode, `${what}: the signature algorithm`);
|
|
1937
|
+
const fields = contentsOf(tbs, `${what}: the certificate body`);
|
|
1938
|
+
const versionNode = fields.readOptional(contextTag(0));
|
|
1939
|
+
const version = versionNode === null ? 1 : Number(derInteger(
|
|
1940
|
+
contentsOf(versionNode, `${what}: the version`).read(Tag.integer),
|
|
1941
|
+
`${what}: the version`
|
|
1942
|
+
)) + 1;
|
|
1943
|
+
if (version < 1 || version > 3)
|
|
1944
|
+
throw new CoseError(`${what}: the version v${String(version)} is not one X.509 defines`);
|
|
1945
|
+
const serialNumber = derInteger(fields.read(Tag.integer), `${what}: the serial number`);
|
|
1946
|
+
const innerAlgorithm = algorithmIdentifier(fields.read(Tag.sequence), `${what}: the inner signature algorithm`);
|
|
1947
|
+
const issuer = X509Name.parse(fields.read(Tag.sequence), `${what}: the issuer`);
|
|
1948
|
+
const validity = contentsOf(fields.read(Tag.sequence), `${what}: the validity period`);
|
|
1949
|
+
const notBefore = derTime(validity.read(), `${what}: the start of the validity period`);
|
|
1950
|
+
const notAfter = derTime(validity.read(), `${what}: the end of the validity period`);
|
|
1951
|
+
validity.expectEnd();
|
|
1952
|
+
const subject = X509Name.parse(fields.read(Tag.sequence), `${what}: the subject`);
|
|
1953
|
+
const publicKey = subjectPublicKey(fields.read(Tag.sequence), what);
|
|
1954
|
+
if (outerAlgorithm.oid !== innerAlgorithm.oid)
|
|
1955
|
+
throw new CoseError(`${what}: says it was signed with '${outerAlgorithm.oid}' on the outside and with '${innerAlgorithm.oid}' within the body it signed`);
|
|
1956
|
+
const algorithm2 = SIGNATURE_OIDS[outerAlgorithm.oid];
|
|
1957
|
+
if (algorithm2 === void 0)
|
|
1958
|
+
throw new CoseError(`${what}: was signed with '${outerAlgorithm.oid}', which is not a signature algorithm this implementation knows`);
|
|
1959
|
+
fields.readOptional(129);
|
|
1960
|
+
fields.readOptional(130);
|
|
1961
|
+
const extensionsNode = fields.readOptional(contextTag(3));
|
|
1962
|
+
const extensions = extensionsNode === null ? { basicConstraints: null, keyUsage: null, critical: [] } : parseExtensions(extensionsNode, what);
|
|
1963
|
+
fields.expectEnd();
|
|
1964
|
+
if (extensionsNode !== null && version !== 3)
|
|
1965
|
+
throw new CoseError(`${what}: carries extensions, which only a v3 certificate may`);
|
|
1966
|
+
return new _X509Certificate({
|
|
1967
|
+
encoded: certificate.encoded,
|
|
1968
|
+
tbsBytes: tbs.encoded,
|
|
1969
|
+
version,
|
|
1970
|
+
serialNumber,
|
|
1971
|
+
issuer,
|
|
1972
|
+
subject,
|
|
1973
|
+
notBefore,
|
|
1974
|
+
notAfter,
|
|
1975
|
+
publicKey,
|
|
1976
|
+
signatureAlgorithm: algorithm2,
|
|
1977
|
+
signatureValue: derBitString(signatureNode, `${what}: the signature`),
|
|
1978
|
+
basicConstraints: extensions.basicConstraints,
|
|
1979
|
+
keyUsage: extensions.keyUsage,
|
|
1980
|
+
criticalExtensions: extensions.critical
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
// -------------------------------------------------------------- checks
|
|
1984
|
+
/** Whether this certificate is within its validity period at the given instant. */
|
|
1985
|
+
isValidAt(at) {
|
|
1986
|
+
if (at.getTime() < this.notBefore.getTime())
|
|
1987
|
+
return notVerified(`The certificate '${this.subject.toString()}' is not valid before ${iso(this.notBefore)}, and was checked at ${iso(at)}!`);
|
|
1988
|
+
if (at.getTime() > this.notAfter.getTime())
|
|
1989
|
+
return notVerified(`The certificate '${this.subject.toString()}' expired at ${iso(this.notAfter)}, and was checked at ${iso(at)}!`);
|
|
1990
|
+
return VERIFIED;
|
|
1991
|
+
}
|
|
1992
|
+
/** Whether the given key usage bit is set, an absent extension meaning unrestricted. */
|
|
1993
|
+
allows(usage) {
|
|
1994
|
+
if (this.keyUsage === null)
|
|
1995
|
+
return true;
|
|
1996
|
+
return this.keyUsage[usage] ?? false;
|
|
1997
|
+
}
|
|
1998
|
+
/** Whether the certified key may sign other certificates. */
|
|
1999
|
+
get isCertificateAuthority() {
|
|
2000
|
+
return this.basicConstraints?.certificateAuthority ?? false;
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Whether this certificate was signed by the given one.
|
|
2004
|
+
*
|
|
2005
|
+
* Both halves are checked and neither implies the other: the issuer field
|
|
2006
|
+
* has to name the candidate's subject, and the signature has to verify
|
|
2007
|
+
* with the candidate's key. A name that matches without a signature is a
|
|
2008
|
+
* claim, and a signature that verifies under a different name is somebody
|
|
2009
|
+
* else's certificate.
|
|
2010
|
+
*/
|
|
2011
|
+
verifySignedBy(issuer) {
|
|
2012
|
+
if (!this.issuer.equivalentTo(issuer.subject))
|
|
2013
|
+
return notVerified(`The certificate '${this.subject.toString()}' names the issuer '${this.issuer.toString()}', which is not the subject of '${issuer.subject.toString()}'!`);
|
|
2014
|
+
const key = issuer.publicKey;
|
|
2015
|
+
let signature;
|
|
2016
|
+
try {
|
|
2017
|
+
signature = this.signatureAlgorithm.family === "ecdsa" ? rawEcdsaSignature(this.signatureValue, key.curve, this.subject.toString()) : this.signatureValue;
|
|
2018
|
+
} catch (exception) {
|
|
2019
|
+
return notVerified(exception instanceof Error ? exception.message : String(exception));
|
|
2020
|
+
}
|
|
2021
|
+
let verified;
|
|
2022
|
+
try {
|
|
2023
|
+
verified = verifyWith(
|
|
2024
|
+
this.signatureAlgorithm,
|
|
2025
|
+
key.curve,
|
|
2026
|
+
this.tbsBytes,
|
|
2027
|
+
signature,
|
|
2028
|
+
key.publicKeyBytes()
|
|
2029
|
+
);
|
|
2030
|
+
} catch (exception) {
|
|
2031
|
+
return notVerified(`The certificate '${this.subject.toString()}' could not be checked against '${issuer.subject.toString()}': ${exception instanceof Error ? exception.message : String(exception)}`);
|
|
2032
|
+
}
|
|
2033
|
+
return verified ? VERIFIED : notVerified(`The certificate '${this.subject.toString()}' was not signed by '${issuer.subject.toString()}'!`);
|
|
2034
|
+
}
|
|
2035
|
+
/** The hash of this certificate's DER encoding, which is what `x5t` carries. */
|
|
2036
|
+
thumbprint(hash = "sha256") {
|
|
2037
|
+
return digest(hash, this.encoded);
|
|
2038
|
+
}
|
|
2039
|
+
/** Whether this is byte for byte the same certificate as the given one. */
|
|
2040
|
+
equals(other) {
|
|
2041
|
+
return bytesEqual2(this.encoded, other.encoded);
|
|
2042
|
+
}
|
|
2043
|
+
toString() {
|
|
2044
|
+
return this.subject.toString();
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
var iso = (value) => value.toISOString().replace(/\.\d{3}Z$/u, "Z");
|
|
2048
|
+
function algorithmIdentifier(node, what) {
|
|
2049
|
+
const fields = contentsOf(node, what);
|
|
2050
|
+
const oid = derObjectIdentifier(fields.read(Tag.objectIdentifier), what);
|
|
2051
|
+
const parameters = fields.atEnd ? null : fields.read();
|
|
2052
|
+
fields.expectEnd();
|
|
2053
|
+
return { oid, parameters };
|
|
2054
|
+
}
|
|
2055
|
+
function subjectPublicKey(node, what) {
|
|
2056
|
+
const fields = contentsOf(node, `${what}: the subject public key info`);
|
|
2057
|
+
const algorithm2 = algorithmIdentifier(fields.read(Tag.sequence), `${what}: the public key algorithm`);
|
|
2058
|
+
const bits = derBitString(fields.read(Tag.bitString), `${what}: the public key`);
|
|
2059
|
+
fields.expectEnd();
|
|
2060
|
+
if (algorithm2.oid === OID_EC_PUBLIC_KEY) {
|
|
2061
|
+
if (algorithm2.parameters === null)
|
|
2062
|
+
throw new CoseError(`${what}: holds an elliptic curve key that does not name its curve, which this implementation does not support`);
|
|
2063
|
+
if (algorithm2.parameters.tag !== Tag.objectIdentifier)
|
|
2064
|
+
throw new CoseError(`${what}: holds an elliptic curve key whose parameters are not a named curve, which this implementation does not support`);
|
|
2065
|
+
const oid = derObjectIdentifier(algorithm2.parameters, `${what}: the curve`);
|
|
2066
|
+
const curve2 = CURVE_OIDS[oid];
|
|
2067
|
+
if (curve2 === void 0)
|
|
2068
|
+
throw new CoseError(`${what}: holds a key on the curve '${oid}', which is not a curve this implementation knows`);
|
|
2069
|
+
return ecPublicKey(curve2, bits, what);
|
|
2070
|
+
}
|
|
2071
|
+
if (algorithm2.oid === OID_ED25519)
|
|
2072
|
+
return CoseKey.fromOkpPublicKey(
|
|
2073
|
+
CoseCurves.Ed25519,
|
|
2074
|
+
bits,
|
|
2075
|
+
{ algorithm: CoseAlgorithms.Ed25519 }
|
|
2076
|
+
);
|
|
2077
|
+
if (algorithm2.oid === OID_ED448)
|
|
2078
|
+
return CoseKey.fromOkpPublicKey(
|
|
2079
|
+
CoseCurves.Ed448,
|
|
2080
|
+
bits,
|
|
2081
|
+
{ algorithm: CoseAlgorithms.Ed448 }
|
|
2082
|
+
);
|
|
2083
|
+
const mldsa = algorithm2.oid === OID_MLDSA44 ? CoseAlgorithms.MLDSA44 : algorithm2.oid === OID_MLDSA65 ? CoseAlgorithms.MLDSA65 : algorithm2.oid === OID_MLDSA87 ? CoseAlgorithms.MLDSA87 : null;
|
|
2084
|
+
if (mldsa !== null)
|
|
2085
|
+
return CoseKey.fromAkpPublicKey(mldsa, bits);
|
|
2086
|
+
throw new CoseError(`${what}: holds a key of type '${algorithm2.oid}', which is not a key type this implementation knows`);
|
|
2087
|
+
}
|
|
2088
|
+
function ecPublicKey(curve2, point, what) {
|
|
2089
|
+
const width = curve2.fieldSize;
|
|
2090
|
+
if (width === null)
|
|
2091
|
+
throw new CoseError(`${what}: holds a key on the curve '${curve2.name}', which has no signature algorithm`);
|
|
2092
|
+
if (point.length === 0)
|
|
2093
|
+
throw new CoseError(`${what}: holds an empty elliptic curve point`);
|
|
2094
|
+
if (point[0] === 4) {
|
|
2095
|
+
if (point.length !== 1 + 2 * width)
|
|
2096
|
+
throw new CoseError(`${what}: holds an uncompressed point of ${String(point.length)} octets, where the curve '${curve2.name}' needs ${String(1 + 2 * width)}`);
|
|
2097
|
+
return CoseKey.fromCoordinates(
|
|
2098
|
+
curve2,
|
|
2099
|
+
point.subarray(1, 1 + width),
|
|
2100
|
+
point.subarray(1 + width)
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
if (point[0] === 2 || point[0] === 3) {
|
|
2104
|
+
if (point.length !== 1 + width)
|
|
2105
|
+
throw new CoseError(`${what}: holds a compressed point of ${String(point.length)} octets, where the curve '${curve2.name}' needs ${String(1 + width)}`);
|
|
2106
|
+
const x = point.subarray(1);
|
|
2107
|
+
return CoseKey.fromCoordinates(curve2, x, decompressY(curve2, x, point[0] === 3));
|
|
2108
|
+
}
|
|
2109
|
+
throw new CoseError(`${what}: holds an elliptic curve point whose first octet is 0x${point[0].toString(16).padStart(2, "0")}, which is neither 0x04, 0x02 nor 0x03`);
|
|
2110
|
+
}
|
|
2111
|
+
function rawEcdsaSignature(der, curve2, subject) {
|
|
2112
|
+
const width = curve2?.orderSize ?? null;
|
|
2113
|
+
if (width === null)
|
|
2114
|
+
throw new CoseError(`The signature of the certificate '${subject}' can not be read without knowing the issuer's curve!`);
|
|
2115
|
+
const what = `The ECDSA signature of the certificate '${subject}'`;
|
|
2116
|
+
const outer = new DerReader(der, what);
|
|
2117
|
+
const sequence = contentsOf(outer.read(Tag.sequence), what);
|
|
2118
|
+
outer.expectEnd();
|
|
2119
|
+
const r = derInteger(sequence.read(Tag.integer), `${what}: r`);
|
|
2120
|
+
const s = derInteger(sequence.read(Tag.integer), `${what}: s`);
|
|
2121
|
+
sequence.expectEnd();
|
|
2122
|
+
if (r <= 0n || s <= 0n)
|
|
2123
|
+
throw new CoseError(`${what}: both halves must be positive`);
|
|
2124
|
+
const raw = new Uint8Array(2 * width);
|
|
2125
|
+
writeBigInt(r, raw, 0, width, what, "r");
|
|
2126
|
+
writeBigInt(s, raw, width, width, what, "s");
|
|
2127
|
+
return raw;
|
|
2128
|
+
}
|
|
2129
|
+
function writeBigInt(value, into, at, width, what, half) {
|
|
2130
|
+
let hex = value.toString(16);
|
|
2131
|
+
if (hex.length % 2 !== 0)
|
|
2132
|
+
hex = `0${hex}`;
|
|
2133
|
+
if (hex.length / 2 > width)
|
|
2134
|
+
throw new CoseError(`${what}: ${half} is ${String(hex.length / 2)} octets, which is wider than the group order`);
|
|
2135
|
+
const offset = at + width - hex.length / 2;
|
|
2136
|
+
for (let index = 0; index < hex.length / 2; index++)
|
|
2137
|
+
into[offset + index] = Number.parseInt(hex.slice(2 * index, 2 * index + 2), 16);
|
|
2138
|
+
}
|
|
2139
|
+
function parseExtensions(node, what) {
|
|
2140
|
+
const list = contentsOf(
|
|
2141
|
+
contentsOf(node, `${what}: the extensions`).read(Tag.sequence),
|
|
2142
|
+
`${what}: the extensions`
|
|
2143
|
+
);
|
|
2144
|
+
const critical = [];
|
|
2145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2146
|
+
let basicConstraints = null;
|
|
2147
|
+
let keyUsage = null;
|
|
2148
|
+
while (!list.atEnd) {
|
|
2149
|
+
const extension = contentsOf(list.read(Tag.sequence), `${what}: an extension`);
|
|
2150
|
+
const oid = derObjectIdentifier(extension.read(Tag.objectIdentifier), `${what}: an extension identifier`);
|
|
2151
|
+
if (seen.has(oid))
|
|
2152
|
+
throw new CoseError(`${what}: carries the extension '${oid}' more than once, which RFC 5280 forbids`);
|
|
2153
|
+
seen.add(oid);
|
|
2154
|
+
const criticalNode = extension.peekIs(Tag.boolean) ? extension.read(Tag.boolean) : null;
|
|
2155
|
+
if (criticalNode !== null && derBoolean(criticalNode, `${what}: the criticality of '${oid}'`))
|
|
2156
|
+
critical.push(oid);
|
|
2157
|
+
const value = extension.read(Tag.octetString);
|
|
2158
|
+
extension.expectEnd();
|
|
2159
|
+
if (oid === OID_BASIC_CONSTRAINTS)
|
|
2160
|
+
basicConstraints = parseBasicConstraints(value, what);
|
|
2161
|
+
else if (oid === OID_KEY_USAGE)
|
|
2162
|
+
keyUsage = parseKeyUsage(value, what);
|
|
2163
|
+
}
|
|
2164
|
+
return { basicConstraints, keyUsage, critical };
|
|
2165
|
+
}
|
|
2166
|
+
function parseBasicConstraints(value, what) {
|
|
2167
|
+
const where = `${what}: the basicConstraints extension`;
|
|
2168
|
+
const outer = new DerReader(value.content, where);
|
|
2169
|
+
const sequence = contentsOf(outer.read(Tag.sequence), where);
|
|
2170
|
+
outer.expectEnd();
|
|
2171
|
+
const caNode = sequence.peekIs(Tag.boolean) ? sequence.read(Tag.boolean) : null;
|
|
2172
|
+
const pathNode = sequence.peekIs(Tag.integer) ? sequence.read(Tag.integer) : null;
|
|
2173
|
+
sequence.expectEnd();
|
|
2174
|
+
return {
|
|
2175
|
+
certificateAuthority: caNode === null ? false : derBoolean(caNode, where),
|
|
2176
|
+
pathLength: pathNode === null ? null : Number(derInteger(pathNode, where))
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
function parseKeyUsage(value, what) {
|
|
2180
|
+
const where = `${what}: the keyUsage extension`;
|
|
2181
|
+
const outer = new DerReader(value.content, where);
|
|
2182
|
+
const { unusedBits, bytes } = derBitStringRaw(outer.read(Tag.bitString), where);
|
|
2183
|
+
outer.expectEnd();
|
|
2184
|
+
const bits = [];
|
|
2185
|
+
for (let index = 0; index < bytes.length * 8 - unusedBits; index++)
|
|
2186
|
+
bits.push((bytes[index >> 3] & 128 >> (index & 7)) !== 0);
|
|
2187
|
+
return bits;
|
|
2188
|
+
}
|
|
2189
|
+
var signatureAlgorithmByOid = (oid) => SIGNATURE_OIDS[oid] ?? null;
|
|
2190
|
+
var curveByOid = (oid) => CURVE_OIDS[oid] ?? null;
|
|
2191
|
+
function oidOfCurve(curve2) {
|
|
2192
|
+
for (const [oid, each] of Object.entries(CURVE_OIDS)) {
|
|
2193
|
+
if (each.id === curve2.id)
|
|
2194
|
+
return oid;
|
|
2195
|
+
}
|
|
2196
|
+
return null;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/x5chain.ts
|
|
2200
|
+
var CoseCertificateChain = class _CoseCertificateChain {
|
|
2201
|
+
/** The certificates, end-entity first. */
|
|
2202
|
+
certificates;
|
|
2203
|
+
constructor(certificates) {
|
|
2204
|
+
if (certificates.length === 0)
|
|
2205
|
+
throw new CoseError("A certificate chain must hold at least one certificate!");
|
|
2206
|
+
this.certificates = [...certificates];
|
|
2207
|
+
}
|
|
2208
|
+
/** The certificate holding the key that is expected to have signed. */
|
|
2209
|
+
get endEntity() {
|
|
2210
|
+
return this.certificates[0];
|
|
2211
|
+
}
|
|
2212
|
+
/** The public key of the end-entity certificate. */
|
|
2213
|
+
publicKey() {
|
|
2214
|
+
return this.endEntity.publicKey;
|
|
2215
|
+
}
|
|
2216
|
+
// ------------------------------------------------------------- parsing
|
|
2217
|
+
/** Read a `COSE_X509` from its CBOR form. */
|
|
2218
|
+
static fromCbor(value) {
|
|
2219
|
+
const encoded = [];
|
|
2220
|
+
if (value.type === "bytes")
|
|
2221
|
+
encoded.push(value.value);
|
|
2222
|
+
else if (value.type === "array") {
|
|
2223
|
+
if (value.items.length < 2)
|
|
2224
|
+
throw new CoseError("A COSE_X509 of fewer than two certificates must be a bare byte string, not an array!");
|
|
2225
|
+
for (const item of value.items) {
|
|
2226
|
+
if (item.type !== "bytes")
|
|
2227
|
+
throw new CoseError("Every certificate of a COSE_X509 must be a byte string!");
|
|
2228
|
+
encoded.push(item.value);
|
|
2229
|
+
}
|
|
2230
|
+
} else
|
|
2231
|
+
throw new CoseError(`A COSE_X509 must be a byte string or an array of them, but was a CBOR ${value.type}!`);
|
|
2232
|
+
return new _CoseCertificateChain(encoded.map((each, index) => {
|
|
2233
|
+
try {
|
|
2234
|
+
return X509Certificate.parse(each);
|
|
2235
|
+
} catch (exception) {
|
|
2236
|
+
throw new CoseError(
|
|
2237
|
+
`The certificate at position ${String(index)} of the COSE_X509 could not be read: ${exception instanceof Error ? exception.message : String(exception)}`,
|
|
2238
|
+
{ cause: exception }
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
}));
|
|
2242
|
+
}
|
|
2243
|
+
/** The CBOR form: a bare byte string for one certificate, an array for several. */
|
|
2244
|
+
toCbor() {
|
|
2245
|
+
return this.certificates.length === 1 ? cbor.bytes(this.certificates[0].encoded) : cbor.array(this.certificates.map((each) => cbor.bytes(each.encoded)));
|
|
2246
|
+
}
|
|
2247
|
+
// ---------------------------------------------------------- validation
|
|
2248
|
+
/**
|
|
2249
|
+
* Walk this chain to one of the given trust anchors.
|
|
2250
|
+
*
|
|
2251
|
+
* Every certificate within its validity period, every certificate signed
|
|
2252
|
+
* by the next one, every issuing certificate actually allowed to issue,
|
|
2253
|
+
* the last one either an anchor or issued by one, and the end-entity
|
|
2254
|
+
* certificate allowed to sign.
|
|
2255
|
+
*
|
|
2256
|
+
* This answers "does this chain lead somewhere I trust". It does *not*
|
|
2257
|
+
* answer whether the key of the end-entity certificate is the key that
|
|
2258
|
+
* signed the message — without that binding a perfectly valid chain says
|
|
2259
|
+
* nothing about the message it travelled with.
|
|
2260
|
+
*
|
|
2261
|
+
* Not checked, and the same list as Styx so that the two agree about what
|
|
2262
|
+
* a pass means: revocation, name constraints, certificate policies, and
|
|
2263
|
+
* path length beyond the CA flag.
|
|
2264
|
+
*/
|
|
2265
|
+
validate(trustAnchors, options = {}) {
|
|
2266
|
+
const at = options.at ?? /* @__PURE__ */ new Date();
|
|
2267
|
+
if (trustAnchors.length === 0)
|
|
2268
|
+
return notVerified("A certificate chain can not be validated without at least one trust anchor!");
|
|
2269
|
+
for (const certificate of this.certificates) {
|
|
2270
|
+
const valid = certificate.isValidAt(at);
|
|
2271
|
+
if (!valid.verified)
|
|
2272
|
+
return valid;
|
|
2273
|
+
}
|
|
2274
|
+
for (let index = 0; index < this.certificates.length - 1; index++) {
|
|
2275
|
+
const issuer = this.certificates[index + 1];
|
|
2276
|
+
const issued = this.certificates[index].verifySignedBy(issuer);
|
|
2277
|
+
if (!issued.verified)
|
|
2278
|
+
return issued;
|
|
2279
|
+
const allowed = mayIssue(issuer);
|
|
2280
|
+
if (!allowed.verified)
|
|
2281
|
+
return allowed;
|
|
2282
|
+
}
|
|
2283
|
+
const last = this.certificates[this.certificates.length - 1];
|
|
2284
|
+
if (!trustAnchors.some((anchor) => anchor.equals(last))) {
|
|
2285
|
+
const issuer = trustAnchors.find((anchor) => last.issuer.equivalentTo(anchor.subject));
|
|
2286
|
+
if (issuer === void 0)
|
|
2287
|
+
return notVerified(`The certificate chain ends at '${last.subject.toString()}', which is neither a trust anchor nor issued by one!`);
|
|
2288
|
+
for (const check of [issuer.isValidAt(at), mayIssue(issuer), last.verifySignedBy(issuer)]) {
|
|
2289
|
+
if (!check.verified)
|
|
2290
|
+
return check;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
if (!this.endEntity.allows(KeyUsage.digitalSignature))
|
|
2294
|
+
return notVerified(`The end-entity certificate '${this.endEntity.subject.toString()}' is not allowed to create digital signatures!`);
|
|
2295
|
+
return VERIFIED;
|
|
2296
|
+
}
|
|
2297
|
+
toString() {
|
|
2298
|
+
return this.certificates.map((each) => each.subject.toString()).join(" <- ");
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
function mayIssue(certificate) {
|
|
2302
|
+
if (!certificate.isCertificateAuthority)
|
|
2303
|
+
return notVerified(`The certificate '${certificate.subject.toString()}' issued another one although it is not a certification authority!`);
|
|
2304
|
+
if (!certificate.allows(KeyUsage.keyCertSign))
|
|
2305
|
+
return notVerified(`The certificate '${certificate.subject.toString()}' issued another one although it is not allowed to sign certificates!`);
|
|
2306
|
+
return VERIFIED;
|
|
2307
|
+
}
|
|
2308
|
+
var CoseCertificateHash = class _CoseCertificateHash {
|
|
2309
|
+
/** The hash algorithm the thumbprint was computed with. */
|
|
2310
|
+
algorithm;
|
|
2311
|
+
/** The hash of the certificate's DER encoding. */
|
|
2312
|
+
value;
|
|
2313
|
+
constructor(algorithm2, value) {
|
|
2314
|
+
this.algorithm = algorithm2;
|
|
2315
|
+
this.value = value;
|
|
2316
|
+
}
|
|
2317
|
+
/** Compute the thumbprint of a certificate, with SHA-256 unless told otherwise. */
|
|
2318
|
+
static from(certificate, algorithm2 = CoseAlgorithms.SHA256) {
|
|
2319
|
+
if (algorithm2.hash === null)
|
|
2320
|
+
throw new CoseError(`The COSE algorithm '${algorithm2.name}' is not a hash algorithm!`);
|
|
2321
|
+
return new _CoseCertificateHash(algorithm2, certificate.thumbprint(algorithm2.hash));
|
|
2322
|
+
}
|
|
2323
|
+
/** Read a `COSE_CertHash` from its CBOR form. */
|
|
2324
|
+
static fromCbor(value) {
|
|
2325
|
+
if (value.type !== "array")
|
|
2326
|
+
throw new CoseError(`A COSE_CertHash must be a CBOR array, but was a CBOR ${value.type}!`);
|
|
2327
|
+
if (value.items.length !== 2)
|
|
2328
|
+
throw new CoseError(`A COSE_CertHash must hold exactly two elements, but held ${String(value.items.length)}!`);
|
|
2329
|
+
const [algorithmValue, hashValue] = value.items;
|
|
2330
|
+
if (hashValue.type !== "bytes")
|
|
2331
|
+
throw new CoseError("The hash value of a COSE_CertHash must be a byte string!");
|
|
2332
|
+
if (algorithmValue.type !== "int")
|
|
2333
|
+
throw new CoseError("The hash algorithm of a COSE_CertHash must be an integer, as no name is registered for one!");
|
|
2334
|
+
const algorithm2 = algorithmById(Number(algorithmValue.value));
|
|
2335
|
+
if (algorithm2 === null)
|
|
2336
|
+
throw new CoseError(`The COSE algorithm ${String(algorithmValue.value)} of the COSE_CertHash is not registered!`);
|
|
2337
|
+
if (algorithm2.hash === null)
|
|
2338
|
+
throw new CoseError(`The COSE algorithm '${algorithm2.name}' of the COSE_CertHash is not a hash algorithm!`);
|
|
2339
|
+
return new _CoseCertificateHash(algorithm2, hashValue.value);
|
|
2340
|
+
}
|
|
2341
|
+
/** The CBOR form. */
|
|
2342
|
+
toCbor() {
|
|
2343
|
+
return cbor.array([cbor.int(BigInt(this.algorithm.id)), cbor.bytes(this.value)]);
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2346
|
+
* Whether this thumbprint names the given certificate.
|
|
2347
|
+
*
|
|
2348
|
+
* A thumbprint that names a *different* certificate than the one that
|
|
2349
|
+
* travelled is not a detail to shrug at: the message states which
|
|
2350
|
+
* certificate it means, and the chain offers another. One of the two is
|
|
2351
|
+
* wrong and there is no way to tell which.
|
|
2352
|
+
*/
|
|
2353
|
+
matches(certificate) {
|
|
2354
|
+
if (this.algorithm.hash === null)
|
|
2355
|
+
return notVerified(`The COSE algorithm '${this.algorithm.name}' of the certificate thumbprint is not a hash algorithm!`);
|
|
2356
|
+
const computed = certificate.thumbprint(this.algorithm.hash);
|
|
2357
|
+
if (computed.length !== this.value.length || !computed.every((each, index) => each === this.value[index]))
|
|
2358
|
+
return notVerified(`The certificate thumbprint of this message does not name the certificate '${certificate.subject.toString()}' that travelled with it!`);
|
|
2359
|
+
return VERIFIED;
|
|
2360
|
+
}
|
|
2361
|
+
};
|
|
2362
|
+
|
|
2363
|
+
// src/sign1.ts
|
|
2364
|
+
var COSE_SIGN1_TAG = 18;
|
|
2365
|
+
var SIGNATURE_CONTEXT = "Signature1";
|
|
2366
|
+
var COUNTERSIGNATURE_CONTEXT = "CounterSignatureV2";
|
|
2367
|
+
var CoseSign1 = class _CoseSign1 {
|
|
2368
|
+
/**
|
|
2369
|
+
* The serialized protected bucket, exactly as signed and as received: a
|
|
2370
|
+
* zero-length byte string when there are no protected header parameters.
|
|
2371
|
+
*/
|
|
2372
|
+
protectedHeaderBytes;
|
|
2373
|
+
/** The protected header parameters, which the signature covers. */
|
|
2374
|
+
protectedHeader;
|
|
2375
|
+
/**
|
|
2376
|
+
* The unprotected header parameters, which the signature does not cover
|
|
2377
|
+
* and which therefore must not be trusted after a successful verification.
|
|
2378
|
+
*/
|
|
2379
|
+
unprotectedHeader;
|
|
2380
|
+
/** The signed payload, or null when it is detached. */
|
|
2381
|
+
payload;
|
|
2382
|
+
/** The signature: `r ‖ s`, each half zero-padded, never DER. */
|
|
2383
|
+
signature;
|
|
2384
|
+
/**
|
|
2385
|
+
* Whether this message is wrapped within CBOR tag 18. The tag is not
|
|
2386
|
+
* covered by the signature, but it is preserved so that a parsed message
|
|
2387
|
+
* re-encodes to the very same bytes.
|
|
2388
|
+
*/
|
|
2389
|
+
isTagged;
|
|
2390
|
+
constructor(protectedHeaderBytes, unprotectedHeader, payload, signature, isTagged = true) {
|
|
2391
|
+
this.protectedHeaderBytes = protectedHeaderBytes;
|
|
2392
|
+
this.protectedHeader = CoseHeaders.parseProtected(protectedHeaderBytes);
|
|
2393
|
+
this.unprotectedHeader = unprotectedHeader ?? CoseHeaders.empty;
|
|
2394
|
+
this.payload = payload;
|
|
2395
|
+
this.signature = signature;
|
|
2396
|
+
this.isTagged = isTagged;
|
|
2397
|
+
}
|
|
2398
|
+
/** Whether the payload is detached. */
|
|
2399
|
+
get isDetached() {
|
|
2400
|
+
return this.payload === null;
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* The signature algorithm, taken from the protected bucket and only
|
|
2404
|
+
* otherwise from the unprotected one.
|
|
2405
|
+
*/
|
|
2406
|
+
get algorithm() {
|
|
2407
|
+
return this.protectedHeader.algorithm ?? this.unprotectedHeader.algorithm;
|
|
2408
|
+
}
|
|
2409
|
+
/** The key identifier, protected bucket first. */
|
|
2410
|
+
get keyIdentifier() {
|
|
2411
|
+
return this.protectedHeader.keyIdentifier ?? this.unprotectedHeader.keyIdentifier;
|
|
2412
|
+
}
|
|
2413
|
+
// ---------------------------------------------------------------- signing
|
|
2414
|
+
/**
|
|
2415
|
+
* The encoded Sig_structure of a COSE_Sign1 message
|
|
2416
|
+
* [RFC 9052, Section 4.4], which is the byte string an ECDSA signer
|
|
2417
|
+
* actually signs:
|
|
2418
|
+
*
|
|
2419
|
+
* <code>
|
|
2420
|
+
* Sig_structure = [
|
|
2421
|
+
* context : "Signature1",
|
|
2422
|
+
* protected : empty_or_serialized_map,
|
|
2423
|
+
* external_aad : bstr,
|
|
2424
|
+
* payload : bstr
|
|
2425
|
+
* ]
|
|
2426
|
+
* </code>
|
|
2427
|
+
*
|
|
2428
|
+
* This is public on purpose. Whenever the signing key does not live in
|
|
2429
|
+
* this process — a meter, a smart card, a hardware security module — this
|
|
2430
|
+
* is the input to hand over.
|
|
2431
|
+
*/
|
|
2432
|
+
static toBeSigned(protectedHeaderBytes, payload, externalAad = null) {
|
|
2433
|
+
return encode(cbor.array([
|
|
2434
|
+
cbor.text(SIGNATURE_CONTEXT),
|
|
2435
|
+
cbor.bytes(protectedHeaderBytes),
|
|
2436
|
+
cbor.bytes(externalAad ?? NO_BYTES),
|
|
2437
|
+
cbor.bytes(payload)
|
|
2438
|
+
]));
|
|
2439
|
+
}
|
|
2440
|
+
/** The encoded Sig_structure of this message. */
|
|
2441
|
+
toBeSigned(options = {}) {
|
|
2442
|
+
const payload = resolvePayload(
|
|
2443
|
+
this.payload,
|
|
2444
|
+
options.detachedPayload ?? null,
|
|
2445
|
+
"this COSE_Sign1 message"
|
|
2446
|
+
);
|
|
2447
|
+
if (!payload.ok)
|
|
2448
|
+
throw new CoseError(payload.reason);
|
|
2449
|
+
return _CoseSign1.toBeSigned(
|
|
2450
|
+
this.protectedHeaderBytes,
|
|
2451
|
+
payload.value,
|
|
2452
|
+
options.externalAad ?? null
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Sign a payload, placing the algorithm within the protected bucket and
|
|
2457
|
+
* the key identifier within the unprotected one — the layout of the
|
|
2458
|
+
* examples of RFC 9052 and of virtually every deployed COSE message.
|
|
2459
|
+
*/
|
|
2460
|
+
static sign(payload, key, options = {}) {
|
|
2461
|
+
const algorithm2 = key.algorithm;
|
|
2462
|
+
if (algorithm2 === null)
|
|
2463
|
+
throw new CoseError("The COSE key does not name the signature algorithm to use!");
|
|
2464
|
+
const keyIdentifier = options.keyIdentifier === void 0 ? key.keyIdentifier : options.keyIdentifier;
|
|
2465
|
+
return _CoseSign1.signWithHeaders(
|
|
2466
|
+
payload,
|
|
2467
|
+
key,
|
|
2468
|
+
CoseHeaders.create(algorithm2),
|
|
2469
|
+
keyIdentifier !== null ? CoseHeaders.create(null, keyIdentifier) : CoseHeaders.empty,
|
|
2470
|
+
options
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Sign a payload with the algorithm taken from the application context
|
|
2475
|
+
* rather than from the message: the protected bucket stays empty and only
|
|
2476
|
+
* the key identifier travels.
|
|
2477
|
+
*
|
|
2478
|
+
* This is the leanest signed COSE message there is, and for a protocol
|
|
2479
|
+
* that already agrees on its algorithm it is also the safest arrangement:
|
|
2480
|
+
* an algorithm that is not in the message cannot be tampered with on the
|
|
2481
|
+
* way, whereas one within the unprotected bucket can. The price is that
|
|
2482
|
+
* agility becomes a property of the profile rather than of the message.
|
|
2483
|
+
*/
|
|
2484
|
+
static signWithApplicationAlgorithm(payload, key, options = {}) {
|
|
2485
|
+
const algorithm2 = key.algorithm;
|
|
2486
|
+
if (algorithm2 === null)
|
|
2487
|
+
throw new CoseError("The COSE key does not name the signature algorithm to use!");
|
|
2488
|
+
const keyIdentifier = options.keyIdentifier === void 0 ? key.keyIdentifier : options.keyIdentifier;
|
|
2489
|
+
return _CoseSign1.signWithHeaders(
|
|
2490
|
+
payload,
|
|
2491
|
+
key,
|
|
2492
|
+
CoseHeaders.empty,
|
|
2493
|
+
keyIdentifier !== null ? CoseHeaders.create(null, keyIdentifier) : CoseHeaders.empty,
|
|
2494
|
+
options,
|
|
2495
|
+
algorithm2
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Sign a payload with full control over both header buckets.
|
|
2500
|
+
*
|
|
2501
|
+
* The signature algorithm is taken from the protected bucket, because an
|
|
2502
|
+
* algorithm that is not covered by the signature could be changed by
|
|
2503
|
+
* anyone on the way.
|
|
2504
|
+
*/
|
|
2505
|
+
static signWithHeaders(payload, key, protectedHeader, unprotectedHeader = null, options = {}, applicationAlgorithm = null) {
|
|
2506
|
+
const stated = protectedHeader.algorithm;
|
|
2507
|
+
if (applicationAlgorithm !== null && stated !== null && stated.id !== applicationAlgorithm.id)
|
|
2508
|
+
throw new CoseError(`The protected header bucket names the algorithm '${stated.name}', but the application context names '${applicationAlgorithm.name}'!`);
|
|
2509
|
+
const algorithm2 = stated ?? applicationAlgorithm;
|
|
2510
|
+
if (algorithm2 === null)
|
|
2511
|
+
throw new CoseError("Neither the protected header bucket nor the application context names the signature algorithm!");
|
|
2512
|
+
const protectedBytes = protectedHeader.toProtectedBytes();
|
|
2513
|
+
const signed = options.canonicalizePayload === false ? payload : canonicalizePayload(payload);
|
|
2514
|
+
if (options.detachPayload === true && !bytesEqual(signed, payload))
|
|
2515
|
+
throw new CoseError("The payload of this COSE_Sign1 message is detached, so canonicalizing it here would sign bytes that nobody holds: canonicalize the payload yourself (canonicalizePayload), sign and transmit those, or pass canonicalizePayload: false to sign the payload exactly as it is!");
|
|
2516
|
+
const signature = signWith(
|
|
2517
|
+
algorithm2,
|
|
2518
|
+
key.curve,
|
|
2519
|
+
_CoseSign1.toBeSigned(protectedBytes, signed, options.externalAad ?? null),
|
|
2520
|
+
key.privateKeyBytes()
|
|
2521
|
+
);
|
|
2522
|
+
return new _CoseSign1(
|
|
2523
|
+
protectedBytes,
|
|
2524
|
+
unprotectedHeader,
|
|
2525
|
+
options.detachPayload === true ? null : signed,
|
|
2526
|
+
signature,
|
|
2527
|
+
options.tagged ?? true
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
// ----------------------------------------------------------- verification
|
|
2531
|
+
/**
|
|
2532
|
+
* Verify the signature of this message.
|
|
2533
|
+
*
|
|
2534
|
+
* A failed verification is not an exception — it is the expected outcome
|
|
2535
|
+
* of checking untrusted data — so the reason travels in the result.
|
|
2536
|
+
*/
|
|
2537
|
+
verify(key, options = {}) {
|
|
2538
|
+
const critical = verifyCriticalHeaderParameters(
|
|
2539
|
+
this.protectedHeader,
|
|
2540
|
+
this.unprotectedHeader,
|
|
2541
|
+
options.alsoUnderstood ?? []
|
|
2542
|
+
);
|
|
2543
|
+
if (!critical.verified)
|
|
2544
|
+
return critical;
|
|
2545
|
+
const algorithm2 = resolveAlgorithm(
|
|
2546
|
+
this.protectedHeader,
|
|
2547
|
+
this.unprotectedHeader,
|
|
2548
|
+
options.expectedAlgorithm ?? key.algorithm,
|
|
2549
|
+
"This COSE_Sign1 message"
|
|
2550
|
+
);
|
|
2551
|
+
if (!algorithm2.ok)
|
|
2552
|
+
return notVerified(algorithm2.reason);
|
|
2553
|
+
const payload = resolvePayload(
|
|
2554
|
+
this.payload,
|
|
2555
|
+
options.detachedPayload ?? null,
|
|
2556
|
+
"this COSE_Sign1 message"
|
|
2557
|
+
);
|
|
2558
|
+
if (!payload.ok)
|
|
2559
|
+
return notVerified(payload.reason);
|
|
2560
|
+
return verifySignature(
|
|
2561
|
+
algorithm2.value,
|
|
2562
|
+
key,
|
|
2563
|
+
_CoseSign1.toBeSigned(
|
|
2564
|
+
this.protectedHeaderBytes,
|
|
2565
|
+
payload.value,
|
|
2566
|
+
options.externalAad ?? null
|
|
2567
|
+
),
|
|
2568
|
+
this.signature
|
|
2569
|
+
);
|
|
2570
|
+
}
|
|
2571
|
+
// -------------------------------------------------- X.509 [RFC 9360]
|
|
2572
|
+
/**
|
|
2573
|
+
* The certificate chain of this message, from either header bucket.
|
|
2574
|
+
*
|
|
2575
|
+
* A chain that travels in a message is untrusted input, and reading one
|
|
2576
|
+
* says nothing at all about it — {@link verifyWithCertificateChain} has to
|
|
2577
|
+
* walk it to an anchor first. Throws when the parameter is present but is
|
|
2578
|
+
* not a `COSE_X509`, rather than pretending there is none.
|
|
2579
|
+
*/
|
|
2580
|
+
get certificateChain() {
|
|
2581
|
+
const value = this.protectedHeader.get(label(HeaderLabel.x5Chain)) ?? this.unprotectedHeader.get(label(HeaderLabel.x5Chain));
|
|
2582
|
+
if (value === null)
|
|
2583
|
+
return null;
|
|
2584
|
+
return CoseCertificateChain.fromCbor(value);
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* The certificate thumbprint of this message, from either header bucket.
|
|
2588
|
+
*
|
|
2589
|
+
* Throws when the parameter is present but is not a `COSE_CertHash`.
|
|
2590
|
+
*/
|
|
2591
|
+
get certificateThumbprint() {
|
|
2592
|
+
const value = this.protectedHeader.get(label(HeaderLabel.x5T)) ?? this.unprotectedHeader.get(label(HeaderLabel.x5T));
|
|
2593
|
+
if (value === null)
|
|
2594
|
+
return null;
|
|
2595
|
+
return CoseCertificateHash.fromCbor(value);
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Verify this message against a certificate chain rather than against a
|
|
2599
|
+
* public key somebody handed over: the chain travels within the message,
|
|
2600
|
+
* is walked to one of the given trust anchors, and the key of its
|
|
2601
|
+
* end-entity certificate is then the key the signature has to verify with.
|
|
2602
|
+
*
|
|
2603
|
+
* That last step is what turns a chain into an answer. A chain that
|
|
2604
|
+
* validates beautifully says nothing whatsoever about the message it
|
|
2605
|
+
* arrived with unless the key it ends in is the key that signed, so the
|
|
2606
|
+
* two are never checked apart from one another here.
|
|
2607
|
+
*
|
|
2608
|
+
* The X.509 header parameters become understood *by this call* rather than
|
|
2609
|
+
* by the library, which is why a `crit` demanding `x5chain` is accepted
|
|
2610
|
+
* here and refused by {@link verify}: whoever verifies with a bare public
|
|
2611
|
+
* key has not looked at any certificate and must not claim otherwise.
|
|
2612
|
+
*/
|
|
2613
|
+
verifyWithCertificateChain(trustAnchors, options = {}) {
|
|
2614
|
+
const understood = [
|
|
2615
|
+
...options.alsoUnderstood ?? [],
|
|
2616
|
+
label(HeaderLabel.x5Chain),
|
|
2617
|
+
label(HeaderLabel.x5T)
|
|
2618
|
+
];
|
|
2619
|
+
const critical = verifyCriticalHeaderParameters(
|
|
2620
|
+
this.protectedHeader,
|
|
2621
|
+
this.unprotectedHeader,
|
|
2622
|
+
understood
|
|
2623
|
+
);
|
|
2624
|
+
if (!critical.verified)
|
|
2625
|
+
return critical;
|
|
2626
|
+
let chain;
|
|
2627
|
+
let thumbprint;
|
|
2628
|
+
try {
|
|
2629
|
+
chain = this.certificateChain;
|
|
2630
|
+
thumbprint = this.certificateThumbprint;
|
|
2631
|
+
} catch (exception) {
|
|
2632
|
+
return notVerified(exception instanceof Error ? exception.message : String(exception));
|
|
2633
|
+
}
|
|
2634
|
+
if (chain === null)
|
|
2635
|
+
return notVerified("This COSE_Sign1 message carries no certificate chain!");
|
|
2636
|
+
if (thumbprint !== null) {
|
|
2637
|
+
const names = thumbprint.matches(chain.endEntity);
|
|
2638
|
+
if (!names.verified)
|
|
2639
|
+
return names;
|
|
2640
|
+
}
|
|
2641
|
+
const validated = chain.validate(trustAnchors, { at: options.at });
|
|
2642
|
+
if (!validated.verified)
|
|
2643
|
+
return validated;
|
|
2644
|
+
const signed = this.verify(
|
|
2645
|
+
chain.publicKey(),
|
|
2646
|
+
{ ...options, alsoUnderstood: understood }
|
|
2647
|
+
);
|
|
2648
|
+
if (!signed.verified)
|
|
2649
|
+
return signed;
|
|
2650
|
+
return { verified: true, signer: chain.endEntity };
|
|
2651
|
+
}
|
|
2652
|
+
// ------------------------------------------------------- countersignatures
|
|
2653
|
+
/**
|
|
2654
|
+
* The countersignatures of this message [RFC 9338], which live in the
|
|
2655
|
+
* unprotected bucket under label 11.
|
|
2656
|
+
*
|
|
2657
|
+
* A message carrying one holds it bare; a message carrying several holds
|
|
2658
|
+
* an array of them. The two shapes are told apart by the kind of the first
|
|
2659
|
+
* element, because a bare countersignature starts with its protected
|
|
2660
|
+
* bucket, a byte string, whereas an array of them starts with an array.
|
|
2661
|
+
*/
|
|
2662
|
+
get countersignatures() {
|
|
2663
|
+
const value = this.unprotectedHeader.get(label(HeaderLabel.counterSignatureV2));
|
|
2664
|
+
if (value === null)
|
|
2665
|
+
return [];
|
|
2666
|
+
if (value.type !== "array")
|
|
2667
|
+
throw new CoseError("The countersignature header parameter must be a COSE_Countersignature or an array of them!");
|
|
2668
|
+
if (value.items.length > 0 && value.items[0].type === "array")
|
|
2669
|
+
return value.items.map((each) => CoseSignature.parse(each));
|
|
2670
|
+
return [CoseSignature.parse(value)];
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* The encoded Countersign_structure [RFC 9338, Section 3.3].
|
|
2674
|
+
*
|
|
2675
|
+
* Six elements rather than four, and the difference is not cosmetic. It
|
|
2676
|
+
* names the body's protected bucket *and* the countersigner's, and its
|
|
2677
|
+
* last element carries the signature being countersigned — which is what
|
|
2678
|
+
* makes this "I vouch for that signature" rather than "I signed the same
|
|
2679
|
+
* thing". The countersignature of RFC 8152 covered the payload but not the
|
|
2680
|
+
* signature, so it never actually attested to having seen it.
|
|
2681
|
+
*
|
|
2682
|
+
* `other_fields` is an array holding the one signature, not a bare byte
|
|
2683
|
+
* string.
|
|
2684
|
+
*/
|
|
2685
|
+
static toBeCountersigned(bodyProtectedHeaderBytes, countersignatureProtectedHeaderBytes, payload, signature, externalAad = null) {
|
|
2686
|
+
return encode(cbor.array([
|
|
2687
|
+
cbor.text(COUNTERSIGNATURE_CONTEXT),
|
|
2688
|
+
cbor.bytes(bodyProtectedHeaderBytes),
|
|
2689
|
+
cbor.bytes(countersignatureProtectedHeaderBytes),
|
|
2690
|
+
cbor.bytes(externalAad ?? NO_BYTES),
|
|
2691
|
+
cbor.bytes(payload),
|
|
2692
|
+
cbor.array([cbor.bytes(signature)])
|
|
2693
|
+
]));
|
|
2694
|
+
}
|
|
2695
|
+
/** The encoded Countersign_structure of one countersignature of this message. */
|
|
2696
|
+
toBeCountersigned(countersignature, options = {}) {
|
|
2697
|
+
const payload = resolvePayload(
|
|
2698
|
+
this.payload,
|
|
2699
|
+
options.detachedPayload ?? null,
|
|
2700
|
+
"this COSE_Sign1 message"
|
|
2701
|
+
);
|
|
2702
|
+
if (!payload.ok)
|
|
2703
|
+
throw new CoseError(payload.reason);
|
|
2704
|
+
return _CoseSign1.toBeCountersigned(
|
|
2705
|
+
this.protectedHeaderBytes,
|
|
2706
|
+
countersignature.protectedHeaderBytes,
|
|
2707
|
+
payload.value,
|
|
2708
|
+
this.signature,
|
|
2709
|
+
options.externalAad ?? null
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Return a copy of this message with one more countersignature.
|
|
2714
|
+
*
|
|
2715
|
+
* Countersignatures live in the unprotected bucket, which no signature
|
|
2716
|
+
* covers, so this leaves the body signature, the protected bucket and the
|
|
2717
|
+
* payload byte-identical: the message stays valid for everybody, including
|
|
2718
|
+
* readers that ignore label 11 entirely.
|
|
2719
|
+
*/
|
|
2720
|
+
addCountersignature(key, options = {}) {
|
|
2721
|
+
const algorithm2 = key.algorithm;
|
|
2722
|
+
if (algorithm2 === null)
|
|
2723
|
+
throw new CoseError("The COSE key does not name the signature algorithm to use!");
|
|
2724
|
+
const keyIdentifier = options.keyIdentifier === void 0 ? key.keyIdentifier : options.keyIdentifier;
|
|
2725
|
+
const protectedBytes = CoseHeaders.create(algorithm2).toProtectedBytes();
|
|
2726
|
+
const payload = resolvePayload(
|
|
2727
|
+
this.payload,
|
|
2728
|
+
options.detachedPayload ?? null,
|
|
2729
|
+
"this COSE_Sign1 message"
|
|
2730
|
+
);
|
|
2731
|
+
if (!payload.ok)
|
|
2732
|
+
throw new CoseError(payload.reason);
|
|
2733
|
+
const signature = signWith(
|
|
2734
|
+
algorithm2,
|
|
2735
|
+
key.curve,
|
|
2736
|
+
_CoseSign1.toBeCountersigned(
|
|
2737
|
+
this.protectedHeaderBytes,
|
|
2738
|
+
protectedBytes,
|
|
2739
|
+
payload.value,
|
|
2740
|
+
this.signature,
|
|
2741
|
+
options.externalAad ?? null
|
|
2742
|
+
),
|
|
2743
|
+
key.privateKeyBytes()
|
|
2744
|
+
);
|
|
2745
|
+
const added = new CoseSignature(
|
|
2746
|
+
protectedBytes,
|
|
2747
|
+
keyIdentifier !== null ? CoseHeaders.create(null, keyIdentifier) : CoseHeaders.empty,
|
|
2748
|
+
signature
|
|
2749
|
+
);
|
|
2750
|
+
const all = [...this.countersignatures, added];
|
|
2751
|
+
return new _CoseSign1(
|
|
2752
|
+
this.protectedHeaderBytes,
|
|
2753
|
+
this.unprotectedHeader.set(
|
|
2754
|
+
label(HeaderLabel.counterSignatureV2),
|
|
2755
|
+
all.length === 1 ? all[0].toCbor() : cbor.array(all.map((each) => each.toCbor()))
|
|
2756
|
+
),
|
|
2757
|
+
this.payload,
|
|
2758
|
+
this.signature,
|
|
2759
|
+
this.isTagged
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Verify one countersignature of this message.
|
|
2764
|
+
*
|
|
2765
|
+
* This says nothing about whether the body signature itself is valid —
|
|
2766
|
+
* that is a separate question with a separate key, and answering both at
|
|
2767
|
+
* once would hide which of the two failed.
|
|
2768
|
+
*/
|
|
2769
|
+
verifyCountersignature(countersignature, key, options = {}) {
|
|
2770
|
+
const critical = verifyCriticalHeaderParameters(
|
|
2771
|
+
countersignature.protectedHeader,
|
|
2772
|
+
countersignature.unprotectedHeader,
|
|
2773
|
+
options.alsoUnderstood ?? []
|
|
2774
|
+
);
|
|
2775
|
+
if (!critical.verified)
|
|
2776
|
+
return critical;
|
|
2777
|
+
const algorithm2 = resolveAlgorithm(
|
|
2778
|
+
countersignature.protectedHeader,
|
|
2779
|
+
countersignature.unprotectedHeader,
|
|
2780
|
+
options.expectedAlgorithm ?? key.algorithm,
|
|
2781
|
+
"This countersignature"
|
|
2782
|
+
);
|
|
2783
|
+
if (!algorithm2.ok)
|
|
2784
|
+
return notVerified(algorithm2.reason);
|
|
2785
|
+
const payload = resolvePayload(
|
|
2786
|
+
this.payload,
|
|
2787
|
+
options.detachedPayload ?? null,
|
|
2788
|
+
"this COSE_Sign1 message"
|
|
2789
|
+
);
|
|
2790
|
+
if (!payload.ok)
|
|
2791
|
+
return notVerified(payload.reason);
|
|
2792
|
+
return verifySignature(
|
|
2793
|
+
algorithm2.value,
|
|
2794
|
+
key,
|
|
2795
|
+
_CoseSign1.toBeCountersigned(
|
|
2796
|
+
this.protectedHeaderBytes,
|
|
2797
|
+
countersignature.protectedHeaderBytes,
|
|
2798
|
+
payload.value,
|
|
2799
|
+
this.signature,
|
|
2800
|
+
options.externalAad ?? null
|
|
2801
|
+
),
|
|
2802
|
+
countersignature.signature
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
// ------------------------------------------------------ reading & writing
|
|
2806
|
+
/**
|
|
2807
|
+
* Parse a COSE_Sign1 message.
|
|
2808
|
+
*
|
|
2809
|
+
* Both the tagged and the untagged form are accepted; which one it was is
|
|
2810
|
+
* remembered, as the CBOR tag is not covered by the signature but is part
|
|
2811
|
+
* of the bytes on the wire.
|
|
2812
|
+
*/
|
|
2813
|
+
static parse(input) {
|
|
2814
|
+
const value = input instanceof Uint8Array ? decode(input) : input;
|
|
2815
|
+
let isTagged = false;
|
|
2816
|
+
let message = value;
|
|
2817
|
+
if (message.type === "tag") {
|
|
2818
|
+
if (message.tag !== BigInt(COSE_SIGN1_TAG))
|
|
2819
|
+
throw new CoseError(`A COSE_Sign1 message must be tagged with CBOR tag ${String(COSE_SIGN1_TAG)}, but was tagged with CBOR tag ${String(message.tag)}!`);
|
|
2820
|
+
isTagged = true;
|
|
2821
|
+
message = message.value;
|
|
2822
|
+
}
|
|
2823
|
+
if (message.type !== "array")
|
|
2824
|
+
throw new CoseError(`A COSE_Sign1 message must be a CBOR array, but was a CBOR ${message.type}!`);
|
|
2825
|
+
if (message.items.length !== 4)
|
|
2826
|
+
throw new CoseError(`A COSE_Sign1 message must be a CBOR array of 4 elements, but had ${String(message.items.length)} element(s)!`);
|
|
2827
|
+
const [protectedBytes, unprotected, payload, signature] = message.items;
|
|
2828
|
+
if (protectedBytes?.type !== "bytes")
|
|
2829
|
+
throw new CoseError("The protected header bucket of a COSE_Sign1 message must be a byte string!");
|
|
2830
|
+
if (unprotected === void 0)
|
|
2831
|
+
throw new CoseError("A COSE_Sign1 message must carry an unprotected header bucket!");
|
|
2832
|
+
if (payload === void 0 || payload.type !== "null" && payload.type !== "bytes")
|
|
2833
|
+
throw new CoseError("The payload of a COSE_Sign1 message must be a byte string, or null when it is detached!");
|
|
2834
|
+
if (signature?.type !== "bytes")
|
|
2835
|
+
throw new CoseError("The signature of a COSE_Sign1 message must be a byte string!");
|
|
2836
|
+
return new _CoseSign1(
|
|
2837
|
+
protectedBytes.value,
|
|
2838
|
+
CoseHeaders.parse(unprotected),
|
|
2839
|
+
payload.type === "bytes" ? payload.value : null,
|
|
2840
|
+
signature.value,
|
|
2841
|
+
isTagged
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
/** A CBOR representation of this message. */
|
|
2845
|
+
toCbor() {
|
|
2846
|
+
const message = cbor.array([
|
|
2847
|
+
cbor.bytes(this.protectedHeaderBytes),
|
|
2848
|
+
this.unprotectedHeader.toCbor(),
|
|
2849
|
+
this.payload !== null ? cbor.bytes(this.payload) : cbor.nullValue,
|
|
2850
|
+
cbor.bytes(this.signature)
|
|
2851
|
+
]);
|
|
2852
|
+
return this.isTagged ? cbor.tag(COSE_SIGN1_TAG, message) : message;
|
|
2853
|
+
}
|
|
2854
|
+
/** The CBOR encoding of this message. */
|
|
2855
|
+
toBytes() {
|
|
2856
|
+
return encode(this.toCbor());
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* A copy of this message without its payload, e.g. because the payload
|
|
2860
|
+
* travels elsewhere.
|
|
2861
|
+
*
|
|
2862
|
+
* The signature stays valid: it never covered the message, only the
|
|
2863
|
+
* Sig_structure.
|
|
2864
|
+
*/
|
|
2865
|
+
detach() {
|
|
2866
|
+
return new _CoseSign1(
|
|
2867
|
+
this.protectedHeaderBytes,
|
|
2868
|
+
this.unprotectedHeader,
|
|
2869
|
+
null,
|
|
2870
|
+
this.signature,
|
|
2871
|
+
this.isTagged
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
/** A copy of this message carrying the given payload again. */
|
|
2875
|
+
attach(payload) {
|
|
2876
|
+
if (this.payload !== null && !bytesEqual(this.payload, payload))
|
|
2877
|
+
throw new CoseError("This COSE_Sign1 message already carries a different payload!");
|
|
2878
|
+
return new _CoseSign1(
|
|
2879
|
+
this.protectedHeaderBytes,
|
|
2880
|
+
this.unprotectedHeader,
|
|
2881
|
+
payload,
|
|
2882
|
+
this.signature,
|
|
2883
|
+
this.isTagged
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
function verifySignature(algorithm2, key, toBeSigned, signature) {
|
|
2888
|
+
let publicKey;
|
|
2889
|
+
try {
|
|
2890
|
+
publicKey = key.publicKeyBytes();
|
|
2891
|
+
} catch (cause) {
|
|
2892
|
+
return notVerified(cause instanceof Error ? cause.message : String(cause));
|
|
2893
|
+
}
|
|
2894
|
+
try {
|
|
2895
|
+
return verifyWith(algorithm2, key.curve, toBeSigned, signature, publicKey) ? VERIFIED : notVerified("The signature is not valid for this payload and this key!");
|
|
2896
|
+
} catch (cause) {
|
|
2897
|
+
return notVerified(cause instanceof Error ? cause.message : String(cause));
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
// src/sign.ts
|
|
2902
|
+
var COSE_SIGN_TAG = 98;
|
|
2903
|
+
var SIGNATURE_CONTEXT2 = "Signature";
|
|
2904
|
+
var CoseSign = class _CoseSign {
|
|
2905
|
+
/** The serialized protected bucket of the body, exactly as signed. */
|
|
2906
|
+
protectedHeaderBytes;
|
|
2907
|
+
/** The protected header parameters of the body, which every signature covers. */
|
|
2908
|
+
protectedHeader;
|
|
2909
|
+
/** The unprotected header parameters of the body, which none of them covers. */
|
|
2910
|
+
unprotectedHeader;
|
|
2911
|
+
/** The signed payload, or null when it is detached. */
|
|
2912
|
+
payload;
|
|
2913
|
+
/** The signatures, in the order they travel. */
|
|
2914
|
+
signatures;
|
|
2915
|
+
/** Whether this message is wrapped within CBOR tag 98. */
|
|
2916
|
+
isTagged;
|
|
2917
|
+
constructor(protectedHeaderBytes, unprotectedHeader, payload, signatures, isTagged = true) {
|
|
2918
|
+
if (signatures.length === 0)
|
|
2919
|
+
throw new CoseError("A COSE_Sign message must carry at least one signature!");
|
|
2920
|
+
this.protectedHeaderBytes = protectedHeaderBytes;
|
|
2921
|
+
this.protectedHeader = CoseHeaders.parseProtected(protectedHeaderBytes);
|
|
2922
|
+
this.unprotectedHeader = unprotectedHeader ?? CoseHeaders.empty;
|
|
2923
|
+
this.payload = payload;
|
|
2924
|
+
this.signatures = [...signatures];
|
|
2925
|
+
this.isTagged = isTagged;
|
|
2926
|
+
}
|
|
2927
|
+
/** Whether the payload is detached. */
|
|
2928
|
+
get isDetached() {
|
|
2929
|
+
return this.payload === null;
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* The encoded Sig_structure of one signature of a COSE_Sign message
|
|
2933
|
+
* [RFC 9052, Section 4.4]:
|
|
2934
|
+
*
|
|
2935
|
+
* <code>
|
|
2936
|
+
* Sig_structure = [
|
|
2937
|
+
* context : "Signature",
|
|
2938
|
+
* body_protected : empty_or_serialized_map,
|
|
2939
|
+
* sign_protected : empty_or_serialized_map,
|
|
2940
|
+
* external_aad : bstr,
|
|
2941
|
+
* payload : bstr
|
|
2942
|
+
* ]
|
|
2943
|
+
* </code>
|
|
2944
|
+
*/
|
|
2945
|
+
static toBeSigned(bodyProtectedHeaderBytes, signatureProtectedHeaderBytes, payload, externalAad = null) {
|
|
2946
|
+
return encode(cbor.array([
|
|
2947
|
+
cbor.text(SIGNATURE_CONTEXT2),
|
|
2948
|
+
cbor.bytes(bodyProtectedHeaderBytes),
|
|
2949
|
+
cbor.bytes(signatureProtectedHeaderBytes),
|
|
2950
|
+
cbor.bytes(externalAad ?? NO_BYTES),
|
|
2951
|
+
cbor.bytes(payload)
|
|
2952
|
+
]));
|
|
2953
|
+
}
|
|
2954
|
+
/** The encoded Sig_structure of one signature of this message. */
|
|
2955
|
+
toBeSigned(signature, options = {}) {
|
|
2956
|
+
const payload = resolvePayload(
|
|
2957
|
+
this.payload,
|
|
2958
|
+
options.detachedPayload ?? null,
|
|
2959
|
+
"this COSE_Sign message"
|
|
2960
|
+
);
|
|
2961
|
+
if (!payload.ok)
|
|
2962
|
+
throw new CoseError(payload.reason);
|
|
2963
|
+
return _CoseSign.toBeSigned(
|
|
2964
|
+
this.protectedHeaderBytes,
|
|
2965
|
+
signature.protectedHeaderBytes,
|
|
2966
|
+
payload.value,
|
|
2967
|
+
options.externalAad ?? null
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2971
|
+
* Sign a payload, producing a message with one signature.
|
|
2972
|
+
*
|
|
2973
|
+
* The body's protected bucket stays empty: what a signer has to say about
|
|
2974
|
+
* its own signature — its algorithm, its key identifier — belongs to that
|
|
2975
|
+
* signature rather than to the body every other signer shares.
|
|
2976
|
+
*/
|
|
2977
|
+
static sign(payload, key, options = {}) {
|
|
2978
|
+
const signed = options.canonicalizePayload === false ? payload : canonicalizePayload(payload);
|
|
2979
|
+
if (options.detachPayload === true && !bytesEqual(signed, payload))
|
|
2980
|
+
throw new CoseError("The payload of this COSE_Sign message is detached, so canonicalizing it here would sign bytes that nobody holds: canonicalize the payload yourself (canonicalizePayload), sign and transmit those, or pass canonicalizePayload: false to sign the payload exactly as it is!");
|
|
2981
|
+
return new _CoseSign(
|
|
2982
|
+
NO_BYTES,
|
|
2983
|
+
CoseHeaders.empty,
|
|
2984
|
+
options.detachPayload === true ? null : signed,
|
|
2985
|
+
[signatureOver(NO_BYTES, signed, key, options)],
|
|
2986
|
+
options.tagged ?? true
|
|
2987
|
+
);
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Return a copy of this message with one more signature.
|
|
2991
|
+
*
|
|
2992
|
+
* The existing signatures are untouched and stay valid, because no
|
|
2993
|
+
* signature ever covers another.
|
|
2994
|
+
*/
|
|
2995
|
+
addSignature(key, options = {}) {
|
|
2996
|
+
const payload = resolvePayload(
|
|
2997
|
+
this.payload,
|
|
2998
|
+
options.detachedPayload ?? null,
|
|
2999
|
+
"this COSE_Sign message"
|
|
3000
|
+
);
|
|
3001
|
+
if (!payload.ok)
|
|
3002
|
+
throw new CoseError(payload.reason);
|
|
3003
|
+
return new _CoseSign(
|
|
3004
|
+
this.protectedHeaderBytes,
|
|
3005
|
+
this.unprotectedHeader,
|
|
3006
|
+
this.payload,
|
|
3007
|
+
[
|
|
3008
|
+
...this.signatures,
|
|
3009
|
+
signatureOver(this.protectedHeaderBytes, payload.value, key, options)
|
|
3010
|
+
],
|
|
3011
|
+
this.isTagged
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
/** Verify one signature of this message. */
|
|
3015
|
+
verify(signature, key, options = {}) {
|
|
3016
|
+
if (!this.signatures.includes(signature))
|
|
3017
|
+
return notVerified("The given signature is not one of the signatures of this COSE_Sign message!");
|
|
3018
|
+
const bodyCritical = verifyCriticalHeaderParameters(
|
|
3019
|
+
this.protectedHeader,
|
|
3020
|
+
this.unprotectedHeader,
|
|
3021
|
+
options.alsoUnderstood ?? []
|
|
3022
|
+
);
|
|
3023
|
+
if (!bodyCritical.verified)
|
|
3024
|
+
return bodyCritical;
|
|
3025
|
+
const signatureCritical = verifyCriticalHeaderParameters(
|
|
3026
|
+
signature.protectedHeader,
|
|
3027
|
+
signature.unprotectedHeader,
|
|
3028
|
+
options.alsoUnderstood ?? []
|
|
3029
|
+
);
|
|
3030
|
+
if (!signatureCritical.verified)
|
|
3031
|
+
return signatureCritical;
|
|
3032
|
+
const algorithm2 = resolveAlgorithm(
|
|
3033
|
+
signature.protectedHeader,
|
|
3034
|
+
signature.unprotectedHeader,
|
|
3035
|
+
options.expectedAlgorithm ?? key.algorithm,
|
|
3036
|
+
"This signature of a COSE_Sign message"
|
|
3037
|
+
);
|
|
3038
|
+
if (!algorithm2.ok)
|
|
3039
|
+
return notVerified(algorithm2.reason);
|
|
3040
|
+
const payload = resolvePayload(
|
|
3041
|
+
this.payload,
|
|
3042
|
+
options.detachedPayload ?? null,
|
|
3043
|
+
"this COSE_Sign message"
|
|
3044
|
+
);
|
|
3045
|
+
if (!payload.ok)
|
|
3046
|
+
return notVerified(payload.reason);
|
|
3047
|
+
return verifySignature(
|
|
3048
|
+
algorithm2.value,
|
|
3049
|
+
key,
|
|
3050
|
+
_CoseSign.toBeSigned(
|
|
3051
|
+
this.protectedHeaderBytes,
|
|
3052
|
+
signature.protectedHeaderBytes,
|
|
3053
|
+
payload.value,
|
|
3054
|
+
options.externalAad ?? null
|
|
3055
|
+
),
|
|
3056
|
+
signature.signature
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* The first signature this key verifies, or null.
|
|
3061
|
+
*
|
|
3062
|
+
* Signatures are tried in wire order. A signature that states its
|
|
3063
|
+
* algorithm in the unprotected bucket only still needs an expected
|
|
3064
|
+
* algorithm to be accepted, here as everywhere else — which the key
|
|
3065
|
+
* supplies when it carries one, and otherwise nobody does.
|
|
3066
|
+
*/
|
|
3067
|
+
verifyAny(key, options = {}) {
|
|
3068
|
+
for (const signature of this.signatures) {
|
|
3069
|
+
if (this.verify(signature, key, options).verified)
|
|
3070
|
+
return signature;
|
|
3071
|
+
}
|
|
3072
|
+
return null;
|
|
3073
|
+
}
|
|
3074
|
+
/** Parse a COSE_Sign message, tagged or untagged. */
|
|
3075
|
+
static parse(input) {
|
|
3076
|
+
const value = input instanceof Uint8Array ? decode(input) : input;
|
|
3077
|
+
let isTagged = false;
|
|
3078
|
+
let message = value;
|
|
3079
|
+
if (message.type === "tag") {
|
|
3080
|
+
if (message.tag !== BigInt(COSE_SIGN_TAG))
|
|
3081
|
+
throw new CoseError(`A COSE_Sign message must be tagged with CBOR tag ${String(COSE_SIGN_TAG)}, but was tagged with CBOR tag ${String(message.tag)}!`);
|
|
3082
|
+
isTagged = true;
|
|
3083
|
+
message = message.value;
|
|
3084
|
+
}
|
|
3085
|
+
if (message.type !== "array")
|
|
3086
|
+
throw new CoseError(`A COSE_Sign message must be a CBOR array, but was a CBOR ${message.type}!`);
|
|
3087
|
+
if (message.items.length !== 4)
|
|
3088
|
+
throw new CoseError(`A COSE_Sign message must be a CBOR array of 4 elements, but had ${String(message.items.length)} element(s)!`);
|
|
3089
|
+
const [protectedBytes, unprotected, payload, signatures] = message.items;
|
|
3090
|
+
if (protectedBytes?.type !== "bytes")
|
|
3091
|
+
throw new CoseError("The protected header bucket of a COSE_Sign message must be a byte string!");
|
|
3092
|
+
if (unprotected === void 0)
|
|
3093
|
+
throw new CoseError("A COSE_Sign message must carry an unprotected header bucket!");
|
|
3094
|
+
if (payload === void 0 || payload.type !== "null" && payload.type !== "bytes")
|
|
3095
|
+
throw new CoseError("The payload of a COSE_Sign message must be a byte string, or null when it is detached!");
|
|
3096
|
+
if (signatures?.type !== "array")
|
|
3097
|
+
throw new CoseError("The signatures of a COSE_Sign message must be a CBOR array!");
|
|
3098
|
+
return new _CoseSign(
|
|
3099
|
+
protectedBytes.value,
|
|
3100
|
+
CoseHeaders.parse(unprotected),
|
|
3101
|
+
payload.type === "bytes" ? payload.value : null,
|
|
3102
|
+
signatures.items.map((each) => CoseSignature.parse(each)),
|
|
3103
|
+
isTagged
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
/** A CBOR representation of this message. */
|
|
3107
|
+
toCbor() {
|
|
3108
|
+
const message = cbor.array([
|
|
3109
|
+
cbor.bytes(this.protectedHeaderBytes),
|
|
3110
|
+
this.unprotectedHeader.toCbor(),
|
|
3111
|
+
this.payload !== null ? cbor.bytes(this.payload) : cbor.nullValue,
|
|
3112
|
+
cbor.array(this.signatures.map((each) => each.toCbor()))
|
|
3113
|
+
]);
|
|
3114
|
+
return this.isTagged ? cbor.tag(COSE_SIGN_TAG, message) : message;
|
|
3115
|
+
}
|
|
3116
|
+
/** The CBOR encoding of this message. */
|
|
3117
|
+
toBytes() {
|
|
3118
|
+
return encode(this.toCbor());
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
function signatureOver(bodyProtectedHeaderBytes, payload, key, options) {
|
|
3122
|
+
const algorithm2 = key.algorithm;
|
|
3123
|
+
if (algorithm2 === null)
|
|
3124
|
+
throw new CoseError("The COSE key does not name the signature algorithm to use!");
|
|
3125
|
+
const keyIdentifier = options.keyIdentifier === void 0 ? key.keyIdentifier : options.keyIdentifier;
|
|
3126
|
+
const protectedBytes = CoseHeaders.create(algorithm2).toProtectedBytes();
|
|
3127
|
+
const signature = signWith(
|
|
3128
|
+
algorithm2,
|
|
3129
|
+
key.curve,
|
|
3130
|
+
CoseSign.toBeSigned(
|
|
3131
|
+
bodyProtectedHeaderBytes,
|
|
3132
|
+
protectedBytes,
|
|
3133
|
+
payload,
|
|
3134
|
+
options.externalAad ?? null
|
|
3135
|
+
),
|
|
3136
|
+
key.privateKeyBytes()
|
|
3137
|
+
);
|
|
3138
|
+
return new CoseSignature(
|
|
3139
|
+
protectedBytes,
|
|
3140
|
+
keyIdentifier !== null ? CoseHeaders.create(null, keyIdentifier) : CoseHeaders.empty,
|
|
3141
|
+
signature
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
// src/mac0.ts
|
|
3146
|
+
var COSE_MAC0_TAG = 17;
|
|
3147
|
+
var MAC0_CONTEXT = "MAC0";
|
|
3148
|
+
var CoseMac0 = class _CoseMac0 {
|
|
3149
|
+
/**
|
|
3150
|
+
* The serialized protected bucket, exactly as authenticated and as
|
|
3151
|
+
* received: a zero-length byte string when there are no protected header
|
|
3152
|
+
* parameters.
|
|
3153
|
+
*/
|
|
3154
|
+
protectedHeaderBytes;
|
|
3155
|
+
/** The protected header parameters, which the authentication tag covers. */
|
|
3156
|
+
protectedHeader;
|
|
3157
|
+
/**
|
|
3158
|
+
* The unprotected header parameters, which the tag does not cover and
|
|
3159
|
+
* which therefore must not be trusted after a successful verification.
|
|
3160
|
+
*/
|
|
3161
|
+
unprotectedHeader;
|
|
3162
|
+
/** The authenticated payload, or null when it is detached. */
|
|
3163
|
+
payload;
|
|
3164
|
+
/** The authentication tag — not the CBOR tag. */
|
|
3165
|
+
tag;
|
|
3166
|
+
/**
|
|
3167
|
+
* Whether this message is wrapped within CBOR tag 17. The tag is not
|
|
3168
|
+
* covered by the authentication tag, but it is preserved so that a parsed
|
|
3169
|
+
* message re-encodes to the very same bytes.
|
|
3170
|
+
*/
|
|
3171
|
+
isTagged;
|
|
3172
|
+
constructor(protectedHeaderBytes, unprotectedHeader, payload, tag, isTagged = true) {
|
|
3173
|
+
this.protectedHeaderBytes = protectedHeaderBytes;
|
|
3174
|
+
this.protectedHeader = CoseHeaders.parseProtected(protectedHeaderBytes);
|
|
3175
|
+
this.unprotectedHeader = unprotectedHeader ?? CoseHeaders.empty;
|
|
3176
|
+
this.payload = payload;
|
|
3177
|
+
this.tag = tag;
|
|
3178
|
+
this.isTagged = isTagged;
|
|
3179
|
+
}
|
|
3180
|
+
/** Whether the payload is detached. */
|
|
3181
|
+
get isDetached() {
|
|
3182
|
+
return this.payload === null;
|
|
3183
|
+
}
|
|
3184
|
+
/**
|
|
3185
|
+
* The MAC algorithm, taken from the protected bucket and only otherwise
|
|
3186
|
+
* from the unprotected one.
|
|
3187
|
+
*/
|
|
3188
|
+
get algorithm() {
|
|
3189
|
+
return this.protectedHeader.algorithm ?? this.unprotectedHeader.algorithm;
|
|
3190
|
+
}
|
|
3191
|
+
/** The key identifier, protected bucket first. */
|
|
3192
|
+
get keyIdentifier() {
|
|
3193
|
+
return this.protectedHeader.keyIdentifier ?? this.unprotectedHeader.keyIdentifier;
|
|
3194
|
+
}
|
|
3195
|
+
// ----------------------------------------------------- the MAC_structure
|
|
3196
|
+
/**
|
|
3197
|
+
* The encoded MAC_structure [RFC 9052, Section 6.3], which is the byte
|
|
3198
|
+
* string the MAC is actually computed over:
|
|
3199
|
+
*
|
|
3200
|
+
* <code>
|
|
3201
|
+
* MAC_structure = [
|
|
3202
|
+
* context : "MAC0",
|
|
3203
|
+
* protected : empty_or_serialized_map,
|
|
3204
|
+
* external_aad : bstr,
|
|
3205
|
+
* payload : bstr
|
|
3206
|
+
* ]
|
|
3207
|
+
* </code>
|
|
3208
|
+
*
|
|
3209
|
+
* The full payload goes in here regardless of how it travels, so a
|
|
3210
|
+
* detached message and an attached one carry the very same tag.
|
|
3211
|
+
*/
|
|
3212
|
+
static toBeMaced(protectedHeaderBytes, payload, externalAad = null) {
|
|
3213
|
+
return encode(cbor.array([
|
|
3214
|
+
cbor.text(MAC0_CONTEXT),
|
|
3215
|
+
cbor.bytes(protectedHeaderBytes),
|
|
3216
|
+
cbor.bytes(externalAad ?? NO_BYTES),
|
|
3217
|
+
cbor.bytes(payload)
|
|
3218
|
+
]));
|
|
3219
|
+
}
|
|
3220
|
+
/** The encoded MAC_structure of this message. */
|
|
3221
|
+
toBeMaced(options = {}) {
|
|
3222
|
+
const payload = resolvePayload(
|
|
3223
|
+
this.payload,
|
|
3224
|
+
options.detachedPayload ?? null,
|
|
3225
|
+
"this COSE_Mac0 message"
|
|
3226
|
+
);
|
|
3227
|
+
if (!payload.ok)
|
|
3228
|
+
throw new CoseError(payload.reason);
|
|
3229
|
+
return _CoseMac0.toBeMaced(
|
|
3230
|
+
this.protectedHeaderBytes,
|
|
3231
|
+
payload.value,
|
|
3232
|
+
options.externalAad ?? null
|
|
3233
|
+
);
|
|
3234
|
+
}
|
|
3235
|
+
// --------------------------------------------------------------- creating
|
|
3236
|
+
/**
|
|
3237
|
+
* Authenticate a payload, placing the algorithm within the protected
|
|
3238
|
+
* bucket and the key identifier within the unprotected one.
|
|
3239
|
+
*/
|
|
3240
|
+
static create(payload, key, options = {}) {
|
|
3241
|
+
const algorithm2 = requireMacAlgorithm(key);
|
|
3242
|
+
const keyIdentifier = options.keyIdentifier === void 0 ? key.keyIdentifier : options.keyIdentifier;
|
|
3243
|
+
const protectedHeader = new CoseHeaders([
|
|
3244
|
+
[label(HeaderLabel.algorithm), cbor.int(algorithm2.id)]
|
|
3245
|
+
]);
|
|
3246
|
+
return _CoseMac0.createWithHeaders(
|
|
3247
|
+
payload,
|
|
3248
|
+
key,
|
|
3249
|
+
protectedHeader,
|
|
3250
|
+
keyIdentifier === null ? null : new CoseHeaders([[
|
|
3251
|
+
label(HeaderLabel.keyIdentifier),
|
|
3252
|
+
cbor.bytes(keyIdentifier)
|
|
3253
|
+
]]),
|
|
3254
|
+
options
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
/** Authenticate a payload with header buckets the caller composed. */
|
|
3258
|
+
static createWithHeaders(payload, key, protectedHeader, unprotectedHeader = null, options = {}) {
|
|
3259
|
+
const stated = protectedHeader.algorithm;
|
|
3260
|
+
const algorithm2 = stated ?? requireMacAlgorithm(key);
|
|
3261
|
+
requireSymmetric(key, algorithm2);
|
|
3262
|
+
const protectedBytes = protectedHeader.toProtectedBytes();
|
|
3263
|
+
const authenticated = options.canonicalizePayload === false ? payload : canonicalizePayload(payload);
|
|
3264
|
+
if (options.detachPayload === true && !bytesEqual(authenticated, payload))
|
|
3265
|
+
throw new CoseError("The payload of this COSE_Mac0 message is detached, so canonicalizing it here would authenticate bytes that nobody holds: canonicalize the payload yourself (canonicalizePayload), authenticate and transmit those, or pass canonicalizePayload: false to authenticate the payload exactly as it is!");
|
|
3266
|
+
const tag = macWith(
|
|
3267
|
+
algorithm2,
|
|
3268
|
+
_CoseMac0.toBeMaced(protectedBytes, authenticated, options.externalAad ?? null),
|
|
3269
|
+
key.privateKeyBytes()
|
|
3270
|
+
);
|
|
3271
|
+
return new _CoseMac0(
|
|
3272
|
+
protectedBytes,
|
|
3273
|
+
unprotectedHeader,
|
|
3274
|
+
options.detachPayload === true ? null : authenticated,
|
|
3275
|
+
tag,
|
|
3276
|
+
options.tagged ?? true
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
// ----------------------------------------------------------- verification
|
|
3280
|
+
/**
|
|
3281
|
+
* Verify the authentication tag of this message.
|
|
3282
|
+
*
|
|
3283
|
+
* A failed verification is not an exception — it is the expected outcome
|
|
3284
|
+
* of checking untrusted data — so the reason travels in the result. The
|
|
3285
|
+
* comparison itself is constant time, which matters here in a way it does
|
|
3286
|
+
* not for a signature: an early-returning compare would tell whoever is
|
|
3287
|
+
* guessing a tag how many of their leading bytes were right.
|
|
3288
|
+
*/
|
|
3289
|
+
verify(key, options = {}) {
|
|
3290
|
+
const critical = verifyCriticalHeaderParameters(
|
|
3291
|
+
this.protectedHeader,
|
|
3292
|
+
this.unprotectedHeader,
|
|
3293
|
+
options.alsoUnderstood ?? []
|
|
3294
|
+
);
|
|
3295
|
+
if (!critical.verified)
|
|
3296
|
+
return critical;
|
|
3297
|
+
const algorithm2 = resolveAlgorithm(
|
|
3298
|
+
this.protectedHeader,
|
|
3299
|
+
this.unprotectedHeader,
|
|
3300
|
+
options.expectedAlgorithm ?? key.algorithm,
|
|
3301
|
+
"This COSE_Mac0 message"
|
|
3302
|
+
);
|
|
3303
|
+
if (!algorithm2.ok)
|
|
3304
|
+
return notVerified(algorithm2.reason);
|
|
3305
|
+
const payload = resolvePayload(
|
|
3306
|
+
this.payload,
|
|
3307
|
+
options.detachedPayload ?? null,
|
|
3308
|
+
"this COSE_Mac0 message"
|
|
3309
|
+
);
|
|
3310
|
+
if (!payload.ok)
|
|
3311
|
+
return notVerified(payload.reason);
|
|
3312
|
+
if (algorithm2.value.family !== "hmac")
|
|
3313
|
+
return notVerified(`The COSE algorithm '${algorithm2.value.name}' is not a message authentication algorithm: a COSE_Mac0 message can not be authenticated with a signature algorithm!`);
|
|
3314
|
+
if (key.keyType !== KEY_TYPE_SYMMETRIC)
|
|
3315
|
+
return notVerified(`A COSE_Mac0 message needs a key of key type Symmetric [RFC 9053, Section 3.1], but a key of key type ${String(key.keyType)} was given!`);
|
|
3316
|
+
let verified;
|
|
3317
|
+
try {
|
|
3318
|
+
verified = verifyMacWith(
|
|
3319
|
+
algorithm2.value,
|
|
3320
|
+
_CoseMac0.toBeMaced(
|
|
3321
|
+
this.protectedHeaderBytes,
|
|
3322
|
+
payload.value,
|
|
3323
|
+
options.externalAad ?? null
|
|
3324
|
+
),
|
|
3325
|
+
this.tag,
|
|
3326
|
+
key.privateKeyBytes()
|
|
3327
|
+
);
|
|
3328
|
+
} catch (exception) {
|
|
3329
|
+
return notVerified(exception instanceof Error ? exception.message : String(exception));
|
|
3330
|
+
}
|
|
3331
|
+
return verified ? VERIFIED : notVerified("The authentication tag is not the right one for this payload and this key!");
|
|
3332
|
+
}
|
|
3333
|
+
// ------------------------------------------------------- serialization
|
|
3334
|
+
/** Read a COSE_Mac0 message, tagged or not. */
|
|
3335
|
+
static parse(input) {
|
|
3336
|
+
const value = input instanceof Uint8Array ? decode(input) : input;
|
|
3337
|
+
let isTagged = false;
|
|
3338
|
+
let message = value;
|
|
3339
|
+
if (message.type === "tag") {
|
|
3340
|
+
if (message.tag !== BigInt(COSE_MAC0_TAG))
|
|
3341
|
+
throw new CoseError(`A COSE_Mac0 message must be tagged with CBOR tag ${String(COSE_MAC0_TAG)}, but was tagged with CBOR tag ${String(message.tag)}!`);
|
|
3342
|
+
isTagged = true;
|
|
3343
|
+
message = message.value;
|
|
3344
|
+
}
|
|
3345
|
+
if (message.type !== "array")
|
|
3346
|
+
throw new CoseError(`A COSE_Mac0 message must be a CBOR array, but was a CBOR ${message.type}!`);
|
|
3347
|
+
if (message.items.length !== 4)
|
|
3348
|
+
throw new CoseError(`A COSE_Mac0 message must be a CBOR array of 4 elements, but had ${String(message.items.length)} element(s)!`);
|
|
3349
|
+
const [protectedBytes, unprotected, payload, tag] = message.items;
|
|
3350
|
+
if (protectedBytes?.type !== "bytes")
|
|
3351
|
+
throw new CoseError("The protected header bucket of a COSE_Mac0 message must be a byte string!");
|
|
3352
|
+
if (unprotected === void 0)
|
|
3353
|
+
throw new CoseError("A COSE_Mac0 message must carry an unprotected header bucket!");
|
|
3354
|
+
if (payload === void 0 || payload.type !== "null" && payload.type !== "bytes")
|
|
3355
|
+
throw new CoseError("The payload of a COSE_Mac0 message must be a byte string, or null when it is detached!");
|
|
3356
|
+
if (tag?.type !== "bytes")
|
|
3357
|
+
throw new CoseError("The authentication tag of a COSE_Mac0 message must be a byte string!");
|
|
3358
|
+
return new _CoseMac0(
|
|
3359
|
+
protectedBytes.value,
|
|
3360
|
+
CoseHeaders.parse(unprotected),
|
|
3361
|
+
payload.type === "bytes" ? payload.value : null,
|
|
3362
|
+
tag.value,
|
|
3363
|
+
isTagged
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
3366
|
+
/** The CBOR form of this message. */
|
|
3367
|
+
toCbor() {
|
|
3368
|
+
const message = cbor.array([
|
|
3369
|
+
cbor.bytes(this.protectedHeaderBytes),
|
|
3370
|
+
this.unprotectedHeader.toCbor(),
|
|
3371
|
+
this.payload !== null ? cbor.bytes(this.payload) : cbor.nullValue,
|
|
3372
|
+
cbor.bytes(this.tag)
|
|
3373
|
+
]);
|
|
3374
|
+
return this.isTagged ? cbor.tag(COSE_MAC0_TAG, message) : message;
|
|
3375
|
+
}
|
|
3376
|
+
/** The CBOR encoding of this message. */
|
|
3377
|
+
toBytes() {
|
|
3378
|
+
return encode(this.toCbor());
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3381
|
+
* A copy of this message without its payload.
|
|
3382
|
+
*
|
|
3383
|
+
* The tag stays valid: it never covered the message, only the
|
|
3384
|
+
* MAC_structure, and the MAC_structure always holds the full payload.
|
|
3385
|
+
*/
|
|
3386
|
+
detach() {
|
|
3387
|
+
return new _CoseMac0(
|
|
3388
|
+
this.protectedHeaderBytes,
|
|
3389
|
+
this.unprotectedHeader,
|
|
3390
|
+
null,
|
|
3391
|
+
this.tag,
|
|
3392
|
+
this.isTagged
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
function requireMacAlgorithm(key) {
|
|
3397
|
+
if (key.algorithm === null)
|
|
3398
|
+
throw new CoseError("A COSE_Mac0 message needs a MAC algorithm: either on the key or within the protected header bucket!");
|
|
3399
|
+
if (key.algorithm.family !== "hmac")
|
|
3400
|
+
throw new CoseError(`The COSE algorithm '${key.algorithm.name}' is not a message authentication algorithm!`);
|
|
3401
|
+
return key.algorithm;
|
|
3402
|
+
}
|
|
3403
|
+
function requireSymmetric(key, algorithm2) {
|
|
3404
|
+
if (algorithm2.family !== "hmac")
|
|
3405
|
+
throw new CoseError(`The COSE algorithm '${algorithm2.name}' is not a message authentication algorithm!`);
|
|
3406
|
+
if (key.keyType !== KEY_TYPE_SYMMETRIC)
|
|
3407
|
+
throw new CoseError(`A COSE_Mac0 message needs a key of key type Symmetric [RFC 9053, Section 3.1], but a key of key type ${String(key.keyType)} was given!`);
|
|
3408
|
+
if (key.algorithm !== null && key.algorithm.id !== algorithm2.id)
|
|
3409
|
+
throw new CoseError(`This message is to be authenticated with '${algorithm2.name}', but the key names '${key.algorithm.name}' [RFC 9053, Section 3.1]!`);
|
|
3410
|
+
}
|
|
3411
|
+
var GCM_NONCE_SIZE = 12;
|
|
3412
|
+
var GCM_TAG_SIZE = 16;
|
|
3413
|
+
function checkKeySize(keySize, what) {
|
|
3414
|
+
if (keySize !== 16 && keySize !== 24 && keySize !== 32)
|
|
3415
|
+
throw new CoseError(`${what} needs a key of 16, 24 or 32 bytes, but a ${String(keySize)}-byte key was given!`);
|
|
3416
|
+
}
|
|
3417
|
+
function aesGcmEncrypt(key, nonce, plaintext, additionalData) {
|
|
3418
|
+
if (nonce.length !== GCM_NONCE_SIZE)
|
|
3419
|
+
throw new CoseError(`AES-GCM within COSE uses a ${String(GCM_NONCE_SIZE)}-byte nonce [RFC 9053, Section 4.1], but a ${String(nonce.length)}-byte one was given!`);
|
|
3420
|
+
checkKeySize(key.length, "AES-GCM");
|
|
3421
|
+
return gcm(key, nonce, additionalData).encrypt(plaintext);
|
|
3422
|
+
}
|
|
3423
|
+
function aesGcmDecrypt(key, nonce, ciphertext, additionalData) {
|
|
3424
|
+
if (nonce.length !== GCM_NONCE_SIZE)
|
|
3425
|
+
throw new CoseError(`AES-GCM within COSE uses a ${String(GCM_NONCE_SIZE)}-byte nonce [RFC 9053, Section 4.1], but a ${String(nonce.length)}-byte one was given!`);
|
|
3426
|
+
if (ciphertext.length < GCM_TAG_SIZE)
|
|
3427
|
+
return null;
|
|
3428
|
+
checkKeySize(key.length, "AES-GCM");
|
|
3429
|
+
try {
|
|
3430
|
+
return gcm(key, nonce, additionalData).decrypt(ciphertext);
|
|
3431
|
+
} catch {
|
|
3432
|
+
return null;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
function aesKeyWrap(keyEncryptionKey, contentKey) {
|
|
3436
|
+
if (contentKey.length % 8 !== 0 || contentKey.length < 16)
|
|
3437
|
+
throw new CoseError(`AES key wrap needs a key of at least 16 bytes and a multiple of 8 [RFC 3394], but a ${String(contentKey.length)}-byte key was given!`);
|
|
3438
|
+
checkKeySize(keyEncryptionKey.length, "AES key wrap");
|
|
3439
|
+
return aeskw(keyEncryptionKey).encrypt(contentKey);
|
|
3440
|
+
}
|
|
3441
|
+
function aesKeyUnwrap(keyEncryptionKey, wrapped) {
|
|
3442
|
+
if (wrapped.length % 8 !== 0 || wrapped.length < 24)
|
|
3443
|
+
return null;
|
|
3444
|
+
try {
|
|
3445
|
+
return aeskw(keyEncryptionKey).decrypt(wrapped);
|
|
3446
|
+
} catch {
|
|
3447
|
+
return null;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// src/recipient.ts
|
|
3452
|
+
var CoseRecipient = class _CoseRecipient {
|
|
3453
|
+
/** The serialized protected bucket — empty for both implemented routes. */
|
|
3454
|
+
protectedHeaderBytes;
|
|
3455
|
+
/** The protected header parameters. */
|
|
3456
|
+
protectedHeader;
|
|
3457
|
+
/** The unprotected header parameters, which is where `alg` and `kid` live here. */
|
|
3458
|
+
unprotectedHeader;
|
|
3459
|
+
/** The wrapped content key, or a zero-length string for `direct`. */
|
|
3460
|
+
ciphertext;
|
|
3461
|
+
/** Recipient structures nested below this one. */
|
|
3462
|
+
recipients;
|
|
3463
|
+
constructor(protectedHeaderBytes, unprotectedHeader, ciphertext, recipients = []) {
|
|
3464
|
+
this.protectedHeaderBytes = protectedHeaderBytes;
|
|
3465
|
+
this.protectedHeader = CoseHeaders.parseProtected(protectedHeaderBytes);
|
|
3466
|
+
this.unprotectedHeader = unprotectedHeader ?? CoseHeaders.empty;
|
|
3467
|
+
this.ciphertext = ciphertext;
|
|
3468
|
+
this.recipients = [...recipients];
|
|
3469
|
+
}
|
|
3470
|
+
/** The recipient algorithm, protected bucket first. */
|
|
3471
|
+
get algorithm() {
|
|
3472
|
+
return this.protectedHeader.algorithm ?? this.unprotectedHeader.algorithm;
|
|
3473
|
+
}
|
|
3474
|
+
/** The key identifier, protected bucket first. */
|
|
3475
|
+
get keyIdentifier() {
|
|
3476
|
+
return this.protectedHeader.keyIdentifier ?? this.unprotectedHeader.keyIdentifier;
|
|
3477
|
+
}
|
|
3478
|
+
// ---------------------------------------------------------- constructing
|
|
3479
|
+
/**
|
|
3480
|
+
* A `direct` recipient: the key it names *is* the content key.
|
|
3481
|
+
*
|
|
3482
|
+
* RFC 9053 §6.1.1 requires the protected bucket to be zero length, and
|
|
3483
|
+
* nothing is carried in the ciphertext either — so this structure conveys
|
|
3484
|
+
* a key identifier and an algorithm and no key material at all.
|
|
3485
|
+
*/
|
|
3486
|
+
static direct(key, options = {}) {
|
|
3487
|
+
requireSymmetric2(key, "A direct recipient");
|
|
3488
|
+
return new _CoseRecipient(
|
|
3489
|
+
NO_BYTES,
|
|
3490
|
+
headersFor(
|
|
3491
|
+
CoseAlgorithms.direct,
|
|
3492
|
+
options.keyIdentifier === void 0 ? key.keyIdentifier : options.keyIdentifier
|
|
3493
|
+
),
|
|
3494
|
+
NO_BYTES,
|
|
3495
|
+
options.recipients ?? []
|
|
3496
|
+
);
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* A key-wrap recipient: the content key, encrypted under the given
|
|
3500
|
+
* key-encryption key.
|
|
3501
|
+
*
|
|
3502
|
+
* The algorithm follows from the width of the *key-encryption* key rather
|
|
3503
|
+
* than from the content key, which is the direction people get wrong:
|
|
3504
|
+
* `A256KW` wraps a 128-bit content key perfectly well.
|
|
3505
|
+
*/
|
|
3506
|
+
static keyWrap(contentKey, keyEncryptionKey, options = {}) {
|
|
3507
|
+
requireSymmetric2(keyEncryptionKey, "A key-wrap recipient");
|
|
3508
|
+
const kek = keyEncryptionKey.privateKeyBytes();
|
|
3509
|
+
const algorithm2 = keyWrapAlgorithmFor(kek.length);
|
|
3510
|
+
if (keyEncryptionKey.algorithm !== null && keyEncryptionKey.algorithm.id !== algorithm2.id && keyEncryptionKey.algorithm.family === "keywrap")
|
|
3511
|
+
throw new CoseError(`The key-encryption key names '${keyEncryptionKey.algorithm.name}', but its ${String(kek.length)} bytes are '${algorithm2.name}'!`);
|
|
3512
|
+
return new _CoseRecipient(
|
|
3513
|
+
NO_BYTES,
|
|
3514
|
+
headersFor(
|
|
3515
|
+
algorithm2,
|
|
3516
|
+
options.keyIdentifier === void 0 ? keyEncryptionKey.keyIdentifier : options.keyIdentifier
|
|
3517
|
+
),
|
|
3518
|
+
aesKeyWrap(kek, contentKey),
|
|
3519
|
+
options.recipients ?? []
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
// ------------------------------------------------------------ recovering
|
|
3523
|
+
/**
|
|
3524
|
+
* The content key this recipient carries, or null when the given key is
|
|
3525
|
+
* not the one it was built for.
|
|
3526
|
+
*
|
|
3527
|
+
* Null rather than an exception, and rather than a reason: a party holding
|
|
3528
|
+
* several keys tries them in turn, and "not this one" is the ordinary
|
|
3529
|
+
* answer rather than an error. For key wrap it is also the *only* honest
|
|
3530
|
+
* answer — RFC 3394's integrity check is what distinguishes a wrong key
|
|
3531
|
+
* from a right one, and saying more about which would say something about
|
|
3532
|
+
* the key.
|
|
3533
|
+
*/
|
|
3534
|
+
contentKey(key) {
|
|
3535
|
+
const algorithm2 = this.algorithm;
|
|
3536
|
+
if (algorithm2 === null)
|
|
3537
|
+
return null;
|
|
3538
|
+
if (key.keyType !== KEY_TYPE_SYMMETRIC)
|
|
3539
|
+
return null;
|
|
3540
|
+
if (algorithm2.family === "direct") {
|
|
3541
|
+
if (this.protectedHeaderBytes.length !== 0 || this.ciphertext.length !== 0)
|
|
3542
|
+
return null;
|
|
3543
|
+
return key.privateKeyBytes();
|
|
3544
|
+
}
|
|
3545
|
+
if (algorithm2.family === "keywrap")
|
|
3546
|
+
return aesKeyUnwrap(key.privateKeyBytes(), this.ciphertext);
|
|
3547
|
+
return null;
|
|
3548
|
+
}
|
|
3549
|
+
// --------------------------------------------------------- serialization
|
|
3550
|
+
/** Read a COSE_recipient. */
|
|
3551
|
+
static parse(value) {
|
|
3552
|
+
if (value.type !== "array")
|
|
3553
|
+
throw new CoseError(`A COSE_recipient must be a CBOR array, but was a CBOR ${value.type}!`);
|
|
3554
|
+
if (value.items.length !== 3 && value.items.length !== 4)
|
|
3555
|
+
throw new CoseError(`A COSE_recipient must be a CBOR array of 3 or 4 elements, but had ${String(value.items.length)} element(s)!`);
|
|
3556
|
+
const [protectedBytes, unprotected, ciphertext, nested] = value.items;
|
|
3557
|
+
if (protectedBytes?.type !== "bytes")
|
|
3558
|
+
throw new CoseError("The protected header bucket of a COSE_recipient must be a byte string!");
|
|
3559
|
+
if (unprotected === void 0)
|
|
3560
|
+
throw new CoseError("A COSE_recipient must carry an unprotected header bucket!");
|
|
3561
|
+
if (ciphertext === void 0 || ciphertext.type !== "bytes" && ciphertext.type !== "null")
|
|
3562
|
+
throw new CoseError("The ciphertext of a COSE_recipient must be a byte string, or null!");
|
|
3563
|
+
let recipients = [];
|
|
3564
|
+
if (nested !== void 0) {
|
|
3565
|
+
if (nested.type !== "array" || nested.items.length === 0)
|
|
3566
|
+
throw new CoseError("The nested recipients of a COSE_recipient must be a non-empty CBOR array!");
|
|
3567
|
+
recipients = nested.items.map((each) => _CoseRecipient.parse(each));
|
|
3568
|
+
}
|
|
3569
|
+
return new _CoseRecipient(
|
|
3570
|
+
protectedBytes.value,
|
|
3571
|
+
CoseHeaders.parse(unprotected),
|
|
3572
|
+
ciphertext.type === "bytes" ? ciphertext.value : NO_BYTES,
|
|
3573
|
+
recipients
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
/** The CBOR form of this recipient. */
|
|
3577
|
+
toCbor() {
|
|
3578
|
+
const items = [
|
|
3579
|
+
cbor.bytes(this.protectedHeaderBytes),
|
|
3580
|
+
this.unprotectedHeader.toCbor(),
|
|
3581
|
+
cbor.bytes(this.ciphertext)
|
|
3582
|
+
];
|
|
3583
|
+
if (this.recipients.length > 0)
|
|
3584
|
+
items.push(cbor.array(this.recipients.map((each) => each.toCbor())));
|
|
3585
|
+
return cbor.array(items);
|
|
3586
|
+
}
|
|
3587
|
+
toString() {
|
|
3588
|
+
return `${this.algorithm?.name ?? "unknown"} recipient, ${String(this.ciphertext.length)} bytes`;
|
|
3589
|
+
}
|
|
3590
|
+
};
|
|
3591
|
+
function keyWrapAlgorithmFor(keySize) {
|
|
3592
|
+
switch (keySize) {
|
|
3593
|
+
case 16:
|
|
3594
|
+
return CoseAlgorithms.A128KW;
|
|
3595
|
+
case 24:
|
|
3596
|
+
return CoseAlgorithms.A192KW;
|
|
3597
|
+
case 32:
|
|
3598
|
+
return CoseAlgorithms.A256KW;
|
|
3599
|
+
}
|
|
3600
|
+
throw new CoseError(`AES key wrap needs a key-encryption key of 16, 24 or 32 bytes, but a ${String(keySize)}-byte key was given!`);
|
|
3601
|
+
}
|
|
3602
|
+
function requireSymmetric2(key, what) {
|
|
3603
|
+
if (key.keyType !== KEY_TYPE_SYMMETRIC)
|
|
3604
|
+
throw new CoseError(`${what} needs a COSE key of key type Symmetric, but a key of key type ${String(key.keyType)} was given!`);
|
|
3605
|
+
}
|
|
3606
|
+
function headersFor(algorithm2, keyIdentifier) {
|
|
3607
|
+
const parameters = [
|
|
3608
|
+
[label(HeaderLabel.algorithm), algorithmToCbor(algorithm2)]
|
|
3609
|
+
];
|
|
3610
|
+
if (keyIdentifier !== null)
|
|
3611
|
+
parameters.push([label(HeaderLabel.keyIdentifier), cbor.bytes(keyIdentifier)]);
|
|
3612
|
+
return new CoseHeaders(parameters);
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
// src/mac.ts
|
|
3616
|
+
var COSE_MAC_TAG = 97;
|
|
3617
|
+
var MAC_CONTEXT = "MAC";
|
|
3618
|
+
var CoseMac = class _CoseMac {
|
|
3619
|
+
/** The serialized protected bucket, exactly as authenticated and received. */
|
|
3620
|
+
protectedHeaderBytes;
|
|
3621
|
+
/** The protected header parameters, which the authentication tag covers. */
|
|
3622
|
+
protectedHeader;
|
|
3623
|
+
/** The unprotected header parameters, which it does not. */
|
|
3624
|
+
unprotectedHeader;
|
|
3625
|
+
/** The authenticated payload, or null when it is detached. */
|
|
3626
|
+
payload;
|
|
3627
|
+
/** The authentication tag — not the CBOR tag. */
|
|
3628
|
+
tag;
|
|
3629
|
+
/** How the content key reaches each party. */
|
|
3630
|
+
recipients;
|
|
3631
|
+
/** Whether this message is wrapped within CBOR tag 97. */
|
|
3632
|
+
isTagged;
|
|
3633
|
+
constructor(protectedHeaderBytes, unprotectedHeader, payload, tag, recipients, isTagged = true) {
|
|
3634
|
+
if (recipients.length === 0)
|
|
3635
|
+
throw new CoseError("A COSE_Mac message must carry at least one recipient!");
|
|
3636
|
+
this.protectedHeaderBytes = protectedHeaderBytes;
|
|
3637
|
+
this.protectedHeader = CoseHeaders.parseProtected(protectedHeaderBytes);
|
|
3638
|
+
this.unprotectedHeader = unprotectedHeader ?? CoseHeaders.empty;
|
|
3639
|
+
this.payload = payload;
|
|
3640
|
+
this.tag = tag;
|
|
3641
|
+
this.recipients = [...recipients];
|
|
3642
|
+
this.isTagged = isTagged;
|
|
3643
|
+
}
|
|
3644
|
+
/** Whether the payload is detached. */
|
|
3645
|
+
get isDetached() {
|
|
3646
|
+
return this.payload === null;
|
|
3647
|
+
}
|
|
3648
|
+
/** The MAC algorithm, protected bucket first. */
|
|
3649
|
+
get algorithm() {
|
|
3650
|
+
return this.protectedHeader.algorithm ?? this.unprotectedHeader.algorithm;
|
|
3651
|
+
}
|
|
3652
|
+
/**
|
|
3653
|
+
* The encoded MAC_structure [RFC 9052, Section 6.3] with the `"MAC"`
|
|
3654
|
+
* context — the one difference from a COSE_Mac0's.
|
|
3655
|
+
*/
|
|
3656
|
+
static toBeMaced(protectedHeaderBytes, payload, externalAad = null) {
|
|
3657
|
+
return encode(cbor.array([
|
|
3658
|
+
cbor.text(MAC_CONTEXT),
|
|
3659
|
+
cbor.bytes(protectedHeaderBytes),
|
|
3660
|
+
cbor.bytes(externalAad ?? NO_BYTES),
|
|
3661
|
+
cbor.bytes(payload)
|
|
3662
|
+
]));
|
|
3663
|
+
}
|
|
3664
|
+
/** The encoded MAC_structure of this message. */
|
|
3665
|
+
toBeMaced(options = {}) {
|
|
3666
|
+
const payload = resolvePayload(
|
|
3667
|
+
this.payload,
|
|
3668
|
+
options.detachedPayload ?? null,
|
|
3669
|
+
"this COSE_Mac message"
|
|
3670
|
+
);
|
|
3671
|
+
if (!payload.ok)
|
|
3672
|
+
throw new CoseError(payload.reason);
|
|
3673
|
+
return _CoseMac.toBeMaced(
|
|
3674
|
+
this.protectedHeaderBytes,
|
|
3675
|
+
payload.value,
|
|
3676
|
+
options.externalAad ?? null
|
|
3677
|
+
);
|
|
3678
|
+
}
|
|
3679
|
+
/**
|
|
3680
|
+
* Authenticate a payload under a content key, and deliver that key to each
|
|
3681
|
+
* recipient.
|
|
3682
|
+
*
|
|
3683
|
+
* The content key is the caller's rather than generated here, for the same
|
|
3684
|
+
* reason as everywhere else in this package: a generated one would make the
|
|
3685
|
+
* message unreproducible, and whoever has a key management scheme has it
|
|
3686
|
+
* for a reason.
|
|
3687
|
+
*/
|
|
3688
|
+
static create(payload, contentKey, recipients, options = {}) {
|
|
3689
|
+
const algorithm2 = contentKey.algorithm ?? (() => {
|
|
3690
|
+
throw new CoseError("A COSE_Mac message needs a MAC algorithm on its content key!");
|
|
3691
|
+
})();
|
|
3692
|
+
if (algorithm2.family !== "hmac")
|
|
3693
|
+
throw new CoseError(`The COSE algorithm '${algorithm2.name}' is not a message authentication algorithm!`);
|
|
3694
|
+
if (contentKey.keyType !== KEY_TYPE_SYMMETRIC)
|
|
3695
|
+
throw new CoseError(`A COSE_Mac message needs a content key of key type Symmetric, but a key of key type ${String(contentKey.keyType)} was given!`);
|
|
3696
|
+
const protectedHeader = new CoseHeaders([
|
|
3697
|
+
[label(HeaderLabel.algorithm), cbor.int(algorithm2.id)]
|
|
3698
|
+
]);
|
|
3699
|
+
const protectedBytes = protectedHeader.toProtectedBytes();
|
|
3700
|
+
const authenticated = options.canonicalizePayload === false ? payload : canonicalizePayload(payload);
|
|
3701
|
+
if (options.detachPayload === true && !bytesEqual(authenticated, payload))
|
|
3702
|
+
throw new CoseError("The payload of this COSE_Mac message is detached, so canonicalizing it here would authenticate bytes that nobody holds: canonicalize the payload yourself (canonicalizePayload), authenticate and transmit those, or pass canonicalizePayload: false to authenticate the payload exactly as it is!");
|
|
3703
|
+
const tag = macWith(
|
|
3704
|
+
algorithm2,
|
|
3705
|
+
_CoseMac.toBeMaced(protectedBytes, authenticated, options.externalAad ?? null),
|
|
3706
|
+
contentKey.privateKeyBytes()
|
|
3707
|
+
);
|
|
3708
|
+
return new _CoseMac(
|
|
3709
|
+
protectedBytes,
|
|
3710
|
+
null,
|
|
3711
|
+
options.detachPayload === true ? null : authenticated,
|
|
3712
|
+
tag,
|
|
3713
|
+
recipients,
|
|
3714
|
+
options.tagged ?? true
|
|
3715
|
+
);
|
|
3716
|
+
}
|
|
3717
|
+
/**
|
|
3718
|
+
* Verify this message with a key one of its recipients was built for.
|
|
3719
|
+
*
|
|
3720
|
+
* Every recipient is tried, because a party holding one key does not
|
|
3721
|
+
* generally know which entry in the list is theirs.
|
|
3722
|
+
*/
|
|
3723
|
+
verify(key, options = {}) {
|
|
3724
|
+
const critical = verifyCriticalHeaderParameters(
|
|
3725
|
+
this.protectedHeader,
|
|
3726
|
+
this.unprotectedHeader,
|
|
3727
|
+
options.alsoUnderstood ?? []
|
|
3728
|
+
);
|
|
3729
|
+
if (!critical.verified)
|
|
3730
|
+
return critical;
|
|
3731
|
+
const algorithm2 = resolveAlgorithm(
|
|
3732
|
+
this.protectedHeader,
|
|
3733
|
+
this.unprotectedHeader,
|
|
3734
|
+
options.expectedAlgorithm ?? null,
|
|
3735
|
+
"This COSE_Mac message"
|
|
3736
|
+
);
|
|
3737
|
+
if (!algorithm2.ok)
|
|
3738
|
+
return notVerified(algorithm2.reason);
|
|
3739
|
+
if (algorithm2.value.family !== "hmac")
|
|
3740
|
+
return notVerified(`The COSE algorithm '${algorithm2.value.name}' is not a message authentication algorithm: a COSE_Mac message can not be authenticated with a signature algorithm!`);
|
|
3741
|
+
const payload = resolvePayload(
|
|
3742
|
+
this.payload,
|
|
3743
|
+
options.detachedPayload ?? null,
|
|
3744
|
+
"this COSE_Mac message"
|
|
3745
|
+
);
|
|
3746
|
+
if (!payload.ok)
|
|
3747
|
+
return notVerified(payload.reason);
|
|
3748
|
+
if (key.keyType !== KEY_TYPE_SYMMETRIC)
|
|
3749
|
+
return notVerified(`A COSE_Mac message needs a key of key type Symmetric, but a key of key type ${String(key.keyType)} was given!`);
|
|
3750
|
+
const toBeMaced = _CoseMac.toBeMaced(
|
|
3751
|
+
this.protectedHeaderBytes,
|
|
3752
|
+
payload.value,
|
|
3753
|
+
options.externalAad ?? null
|
|
3754
|
+
);
|
|
3755
|
+
let tried = 0;
|
|
3756
|
+
for (const recipient of this.recipients) {
|
|
3757
|
+
const contentKey = recipient.contentKey(key);
|
|
3758
|
+
if (contentKey === null)
|
|
3759
|
+
continue;
|
|
3760
|
+
tried++;
|
|
3761
|
+
try {
|
|
3762
|
+
if (verifyMacWith(algorithm2.value, toBeMaced, this.tag, contentKey))
|
|
3763
|
+
return VERIFIED;
|
|
3764
|
+
} catch {
|
|
3765
|
+
continue;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
return notVerified(tried === 0 ? "None of the recipients of this COSE_Mac message yielded a content key for the given key!" : "A recipient yielded a content key, and the authentication tag is not the right one under it!");
|
|
3769
|
+
}
|
|
3770
|
+
// --------------------------------------------------------- serialization
|
|
3771
|
+
/** Read a COSE_Mac message, tagged or not. */
|
|
3772
|
+
static parse(input) {
|
|
3773
|
+
const value = input instanceof Uint8Array ? decode(input) : input;
|
|
3774
|
+
let isTagged = false;
|
|
3775
|
+
let message = value;
|
|
3776
|
+
if (message.type === "tag") {
|
|
3777
|
+
if (message.tag !== BigInt(COSE_MAC_TAG))
|
|
3778
|
+
throw new CoseError(`A COSE_Mac message must be tagged with CBOR tag ${String(COSE_MAC_TAG)}, but was tagged with CBOR tag ${String(message.tag)}!`);
|
|
3779
|
+
isTagged = true;
|
|
3780
|
+
message = message.value;
|
|
3781
|
+
}
|
|
3782
|
+
if (message.type !== "array")
|
|
3783
|
+
throw new CoseError(`A COSE_Mac message must be a CBOR array, but was a CBOR ${message.type}!`);
|
|
3784
|
+
if (message.items.length !== 5)
|
|
3785
|
+
throw new CoseError(`A COSE_Mac message must be a CBOR array of 5 elements, but had ${String(message.items.length)} element(s)!`);
|
|
3786
|
+
const [protectedBytes, unprotected, payload, tag, recipients] = message.items;
|
|
3787
|
+
if (protectedBytes?.type !== "bytes")
|
|
3788
|
+
throw new CoseError("The protected header bucket of a COSE_Mac message must be a byte string!");
|
|
3789
|
+
if (unprotected === void 0)
|
|
3790
|
+
throw new CoseError("A COSE_Mac message must carry an unprotected header bucket!");
|
|
3791
|
+
if (payload === void 0 || payload.type !== "null" && payload.type !== "bytes")
|
|
3792
|
+
throw new CoseError("The payload of a COSE_Mac message must be a byte string, or null when it is detached!");
|
|
3793
|
+
if (tag?.type !== "bytes")
|
|
3794
|
+
throw new CoseError("The authentication tag of a COSE_Mac message must be a byte string!");
|
|
3795
|
+
if (recipients?.type !== "array" || recipients.items.length === 0)
|
|
3796
|
+
throw new CoseError("A COSE_Mac message must carry a non-empty array of recipients!");
|
|
3797
|
+
return new _CoseMac(
|
|
3798
|
+
protectedBytes.value,
|
|
3799
|
+
CoseHeaders.parse(unprotected),
|
|
3800
|
+
payload.type === "bytes" ? payload.value : null,
|
|
3801
|
+
tag.value,
|
|
3802
|
+
recipients.items.map((each) => CoseRecipient.parse(each)),
|
|
3803
|
+
isTagged
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3806
|
+
/** The CBOR form of this message. */
|
|
3807
|
+
toCbor() {
|
|
3808
|
+
const message = cbor.array([
|
|
3809
|
+
cbor.bytes(this.protectedHeaderBytes),
|
|
3810
|
+
this.unprotectedHeader.toCbor(),
|
|
3811
|
+
this.payload !== null ? cbor.bytes(this.payload) : cbor.nullValue,
|
|
3812
|
+
cbor.bytes(this.tag),
|
|
3813
|
+
cbor.array(this.recipients.map((each) => each.toCbor()))
|
|
3814
|
+
]);
|
|
3815
|
+
return this.isTagged ? cbor.tag(COSE_MAC_TAG, message) : message;
|
|
3816
|
+
}
|
|
3817
|
+
/** The CBOR encoding of this message. */
|
|
3818
|
+
toBytes() {
|
|
3819
|
+
return encode(this.toCbor());
|
|
3820
|
+
}
|
|
3821
|
+
toString() {
|
|
3822
|
+
return `COSE_Mac${this.algorithm !== null ? ` ${this.algorithm.name}` : ""}, ${String(this.recipients.length)} recipient(s)`;
|
|
3823
|
+
}
|
|
3824
|
+
};
|
|
3825
|
+
|
|
3826
|
+
// src/encrypt.ts
|
|
3827
|
+
var COSE_ENCRYPT0_TAG = 16;
|
|
3828
|
+
var COSE_ENCRYPT_TAG = 96;
|
|
3829
|
+
var ENCRYPT0_CONTEXT = "Encrypt0";
|
|
3830
|
+
var ENCRYPT_CONTEXT = "Encrypt";
|
|
3831
|
+
function encStructure(context, protectedHeaderBytes, externalAad = null) {
|
|
3832
|
+
return encode(cbor.array([
|
|
3833
|
+
cbor.text(context),
|
|
3834
|
+
cbor.bytes(protectedHeaderBytes),
|
|
3835
|
+
cbor.bytes(externalAad ?? NO_BYTES)
|
|
3836
|
+
]));
|
|
3837
|
+
}
|
|
3838
|
+
var CoseEncryptedBase = class {
|
|
3839
|
+
/** The serialized protected bucket, exactly as authenticated and received. */
|
|
3840
|
+
protectedHeaderBytes;
|
|
3841
|
+
/** The protected header parameters, which the AEAD tag covers. */
|
|
3842
|
+
protectedHeader;
|
|
3843
|
+
/** The unprotected header parameters, which it does not. */
|
|
3844
|
+
unprotectedHeader;
|
|
3845
|
+
/** The ciphertext with the AEAD tag appended, or null when detached. */
|
|
3846
|
+
ciphertext;
|
|
3847
|
+
/** Whether this message is wrapped within its CBOR tag. */
|
|
3848
|
+
isTagged;
|
|
3849
|
+
constructor(protectedHeaderBytes, unprotectedHeader, ciphertext, isTagged) {
|
|
3850
|
+
this.protectedHeaderBytes = protectedHeaderBytes;
|
|
3851
|
+
this.protectedHeader = CoseHeaders.parseProtected(protectedHeaderBytes);
|
|
3852
|
+
this.unprotectedHeader = unprotectedHeader ?? CoseHeaders.empty;
|
|
3853
|
+
this.ciphertext = ciphertext;
|
|
3854
|
+
this.isTagged = isTagged;
|
|
3855
|
+
}
|
|
3856
|
+
/** The content encryption algorithm, protected bucket first. */
|
|
3857
|
+
get algorithm() {
|
|
3858
|
+
return this.protectedHeader.algorithm ?? this.unprotectedHeader.algorithm;
|
|
3859
|
+
}
|
|
3860
|
+
/** The key identifier, protected bucket first. */
|
|
3861
|
+
get keyIdentifier() {
|
|
3862
|
+
return this.protectedHeader.keyIdentifier ?? this.unprotectedHeader.keyIdentifier;
|
|
3863
|
+
}
|
|
3864
|
+
/** The nonce, from either bucket. It travels in the clear and must never repeat. */
|
|
3865
|
+
get iv() {
|
|
3866
|
+
const value = this.protectedHeader.get(label(HeaderLabel.iv)) ?? this.unprotectedHeader.get(label(HeaderLabel.iv));
|
|
3867
|
+
if (value === null)
|
|
3868
|
+
return null;
|
|
3869
|
+
if (value.type !== "bytes")
|
|
3870
|
+
throw new CoseError("The initialization vector of a COSE message must be a byte string!");
|
|
3871
|
+
return value.value;
|
|
3872
|
+
}
|
|
3873
|
+
/** Whether the ciphertext is detached. */
|
|
3874
|
+
get isDetached() {
|
|
3875
|
+
return this.ciphertext === null;
|
|
3876
|
+
}
|
|
3877
|
+
/**
|
|
3878
|
+
* Decrypt with a content encryption key that has already been established.
|
|
3879
|
+
*
|
|
3880
|
+
* The shared half of both structures: `COSE_Encrypt0` uses it with the key
|
|
3881
|
+
* the caller passed, `COSE_Encrypt` with the one a recipient yielded.
|
|
3882
|
+
*/
|
|
3883
|
+
decryptWith(contentKey, context, options) {
|
|
3884
|
+
const critical = verifyCriticalHeaderParameters(
|
|
3885
|
+
this.protectedHeader,
|
|
3886
|
+
this.unprotectedHeader,
|
|
3887
|
+
options.alsoUnderstood ?? []
|
|
3888
|
+
);
|
|
3889
|
+
if (!critical.verified)
|
|
3890
|
+
return notDecrypted(critical.reason);
|
|
3891
|
+
const algorithm2 = options.expectedAlgorithm ?? this.protectedHeader.algorithm ?? this.unprotectedHeader.algorithm;
|
|
3892
|
+
if (algorithm2 === null)
|
|
3893
|
+
return notDecrypted("This COSE message does not state its content encryption algorithm: pass the expected algorithm explicitly!");
|
|
3894
|
+
const stated = this.algorithm;
|
|
3895
|
+
if (stated !== null && stated.id !== algorithm2.id)
|
|
3896
|
+
return notDecrypted(`This COSE message was encrypted with the algorithm '${stated.name}', but the algorithm '${algorithm2.name}' was expected!`);
|
|
3897
|
+
if (algorithm2.family !== "aesgcm")
|
|
3898
|
+
return notDecrypted(`The COSE algorithm '${algorithm2.name}' is not a content encryption algorithm this implementation supports!`);
|
|
3899
|
+
if (algorithm2.keySize !== null && contentKey.length !== algorithm2.keySize)
|
|
3900
|
+
return notDecrypted(`The algorithm '${algorithm2.name}' needs a ${String(algorithm2.keySize)}-byte key, but the content key is ${String(contentKey.length)} bytes long!`);
|
|
3901
|
+
let nonce;
|
|
3902
|
+
try {
|
|
3903
|
+
nonce = this.iv;
|
|
3904
|
+
} catch (exception) {
|
|
3905
|
+
return notDecrypted(exception instanceof Error ? exception.message : String(exception));
|
|
3906
|
+
}
|
|
3907
|
+
if (nonce === null)
|
|
3908
|
+
return notDecrypted("This COSE message carries no initialization vector!");
|
|
3909
|
+
if (nonce.length !== GCM_NONCE_SIZE)
|
|
3910
|
+
return notDecrypted(`AES-GCM within COSE uses a ${String(GCM_NONCE_SIZE)}-byte nonce, but this message carries a ${String(nonce.length)}-byte one!`);
|
|
3911
|
+
const carried = this.ciphertext;
|
|
3912
|
+
const given = options.detachedCiphertext ?? null;
|
|
3913
|
+
if (carried !== null && given !== null)
|
|
3914
|
+
return notDecrypted("This COSE message carries its ciphertext, and a detached ciphertext was supplied as well!");
|
|
3915
|
+
const body = carried ?? given;
|
|
3916
|
+
if (body === null)
|
|
3917
|
+
return notDecrypted("This COSE message carries a detached ciphertext, which has to be supplied in order to decrypt it!");
|
|
3918
|
+
const plaintext = aesGcmDecrypt(
|
|
3919
|
+
contentKey,
|
|
3920
|
+
nonce,
|
|
3921
|
+
body,
|
|
3922
|
+
encStructure(
|
|
3923
|
+
context,
|
|
3924
|
+
this.protectedHeaderBytes,
|
|
3925
|
+
options.externalAad ?? null
|
|
3926
|
+
)
|
|
3927
|
+
);
|
|
3928
|
+
return plaintext === null ? notDecrypted("The ciphertext does not authenticate under this key: it was altered, or the key, the nonce or the additional data is not the right one!") : { decrypted: true, plaintext };
|
|
3929
|
+
}
|
|
3930
|
+
};
|
|
3931
|
+
var CoseEncrypt0 = class _CoseEncrypt0 extends CoseEncryptedBase {
|
|
3932
|
+
constructor(protectedHeaderBytes, unprotectedHeader, ciphertext, isTagged = true) {
|
|
3933
|
+
super(protectedHeaderBytes, unprotectedHeader, ciphertext, isTagged);
|
|
3934
|
+
}
|
|
3935
|
+
/** The encoded Enc_structure of this message. */
|
|
3936
|
+
toBeEncrypted(externalAad = null) {
|
|
3937
|
+
return encStructure(ENCRYPT0_CONTEXT, this.protectedHeaderBytes, externalAad);
|
|
3938
|
+
}
|
|
3939
|
+
/** Encrypt a payload, with the algorithm in the protected bucket. */
|
|
3940
|
+
static encrypt(plaintext, key, options) {
|
|
3941
|
+
const algorithm2 = requireAesGcm(key);
|
|
3942
|
+
const keyIdentifier = options.keyIdentifier === void 0 ? key.keyIdentifier : options.keyIdentifier;
|
|
3943
|
+
const unprotected = [[label(HeaderLabel.iv), cbor.bytes(options.iv)]];
|
|
3944
|
+
if (keyIdentifier !== null)
|
|
3945
|
+
unprotected.push([label(HeaderLabel.keyIdentifier), cbor.bytes(keyIdentifier)]);
|
|
3946
|
+
return _CoseEncrypt0.encryptWithHeaders(
|
|
3947
|
+
plaintext,
|
|
3948
|
+
key,
|
|
3949
|
+
new CoseHeaders([[label(HeaderLabel.algorithm), algorithmToCbor(algorithm2)]]),
|
|
3950
|
+
new CoseHeaders(unprotected),
|
|
3951
|
+
options
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
/** Encrypt a payload with header buckets the caller composed. */
|
|
3955
|
+
static encryptWithHeaders(plaintext, key, protectedHeader, unprotectedHeader, options) {
|
|
3956
|
+
const algorithm2 = protectedHeader.algorithm ?? requireAesGcm(key);
|
|
3957
|
+
requireUsable(key, algorithm2);
|
|
3958
|
+
const protectedBytes = protectedHeader.toProtectedBytes();
|
|
3959
|
+
const ciphertext = aesGcmEncrypt(
|
|
3960
|
+
key.privateKeyBytes(),
|
|
3961
|
+
options.iv,
|
|
3962
|
+
plaintext,
|
|
3963
|
+
encStructure(
|
|
3964
|
+
ENCRYPT0_CONTEXT,
|
|
3965
|
+
protectedBytes,
|
|
3966
|
+
options.externalAad ?? null
|
|
3967
|
+
)
|
|
3968
|
+
);
|
|
3969
|
+
return new _CoseEncrypt0(
|
|
3970
|
+
protectedBytes,
|
|
3971
|
+
unprotectedHeader,
|
|
3972
|
+
options.detachPayload === true ? null : ciphertext,
|
|
3973
|
+
options.tagged ?? true
|
|
3974
|
+
);
|
|
3975
|
+
}
|
|
3976
|
+
/** Decrypt this message with the key it was encrypted for. */
|
|
3977
|
+
decrypt(key, options = {}) {
|
|
3978
|
+
if (key.keyType !== KEY_TYPE_SYMMETRIC)
|
|
3979
|
+
return notDecrypted(`A COSE_Encrypt0 message needs a key of key type Symmetric, but a key of key type ${String(key.keyType)} was given!`);
|
|
3980
|
+
return this.decryptWith(key.privateKeyBytes(), ENCRYPT0_CONTEXT, options);
|
|
3981
|
+
}
|
|
3982
|
+
/** Read a COSE_Encrypt0 message, tagged or not. */
|
|
3983
|
+
static parse(input) {
|
|
3984
|
+
const { isTagged, items } = untag(input, COSE_ENCRYPT0_TAG, 3, "COSE_Encrypt0");
|
|
3985
|
+
const [protectedBytes, unprotected, ciphertext] = items;
|
|
3986
|
+
return new _CoseEncrypt0(
|
|
3987
|
+
bytesOfProtected(protectedBytes, "COSE_Encrypt0"),
|
|
3988
|
+
CoseHeaders.parse(unprotected),
|
|
3989
|
+
ciphertextOf(ciphertext, "COSE_Encrypt0"),
|
|
3990
|
+
isTagged
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
/** The CBOR form of this message. */
|
|
3994
|
+
toCbor() {
|
|
3995
|
+
const message = cbor.array([
|
|
3996
|
+
cbor.bytes(this.protectedHeaderBytes),
|
|
3997
|
+
this.unprotectedHeader.toCbor(),
|
|
3998
|
+
this.ciphertext !== null ? cbor.bytes(this.ciphertext) : cbor.nullValue
|
|
3999
|
+
]);
|
|
4000
|
+
return this.isTagged ? cbor.tag(COSE_ENCRYPT0_TAG, message) : message;
|
|
4001
|
+
}
|
|
4002
|
+
/** The CBOR encoding of this message. */
|
|
4003
|
+
toBytes() {
|
|
4004
|
+
return encode(this.toCbor());
|
|
4005
|
+
}
|
|
4006
|
+
toString() {
|
|
4007
|
+
return `COSE_Encrypt0${this.algorithm !== null ? ` ${this.algorithm.name}` : ""}, ${String(this.ciphertext?.length ?? 0)} bytes`;
|
|
4008
|
+
}
|
|
4009
|
+
};
|
|
4010
|
+
var CoseEncrypt = class _CoseEncrypt extends CoseEncryptedBase {
|
|
4011
|
+
/** How the content key reaches each party. */
|
|
4012
|
+
recipients;
|
|
4013
|
+
constructor(protectedHeaderBytes, unprotectedHeader, ciphertext, recipients, isTagged = true) {
|
|
4014
|
+
super(protectedHeaderBytes, unprotectedHeader, ciphertext, isTagged);
|
|
4015
|
+
if (recipients.length === 0)
|
|
4016
|
+
throw new CoseError("A COSE_Encrypt message must carry at least one recipient!");
|
|
4017
|
+
this.recipients = [...recipients];
|
|
4018
|
+
}
|
|
4019
|
+
/** The encoded Enc_structure of this message. */
|
|
4020
|
+
toBeEncrypted(externalAad = null) {
|
|
4021
|
+
return encStructure(ENCRYPT_CONTEXT, this.protectedHeaderBytes, externalAad);
|
|
4022
|
+
}
|
|
4023
|
+
/**
|
|
4024
|
+
* Encrypt a payload under a content key, and deliver that key to each
|
|
4025
|
+
* recipient.
|
|
4026
|
+
*
|
|
4027
|
+
* The content key is the caller's, not this method's, for the same reason
|
|
4028
|
+
* the nonce is: generating one here would make the message
|
|
4029
|
+
* unreproducible, and a caller who has a key management scheme has one for
|
|
4030
|
+
* a reason.
|
|
4031
|
+
*/
|
|
4032
|
+
static encrypt(plaintext, contentKey, recipients, options) {
|
|
4033
|
+
const algorithm2 = requireAesGcm(contentKey);
|
|
4034
|
+
requireUsable(contentKey, algorithm2);
|
|
4035
|
+
const protectedHeader = new CoseHeaders([
|
|
4036
|
+
[label(HeaderLabel.algorithm), algorithmToCbor(algorithm2)]
|
|
4037
|
+
]);
|
|
4038
|
+
const protectedBytes = protectedHeader.toProtectedBytes();
|
|
4039
|
+
const ciphertext = aesGcmEncrypt(
|
|
4040
|
+
contentKey.privateKeyBytes(),
|
|
4041
|
+
options.iv,
|
|
4042
|
+
plaintext,
|
|
4043
|
+
encStructure(
|
|
4044
|
+
ENCRYPT_CONTEXT,
|
|
4045
|
+
protectedBytes,
|
|
4046
|
+
options.externalAad ?? null
|
|
4047
|
+
)
|
|
4048
|
+
);
|
|
4049
|
+
return new _CoseEncrypt(
|
|
4050
|
+
protectedBytes,
|
|
4051
|
+
new CoseHeaders([[label(HeaderLabel.iv), cbor.bytes(options.iv)]]),
|
|
4052
|
+
options.detachPayload === true ? null : ciphertext,
|
|
4053
|
+
recipients,
|
|
4054
|
+
options.tagged ?? true
|
|
4055
|
+
);
|
|
4056
|
+
}
|
|
4057
|
+
/**
|
|
4058
|
+
* Decrypt this message with a key one of its recipients was built for.
|
|
4059
|
+
*
|
|
4060
|
+
* Every recipient is tried, because a party holding one key does not
|
|
4061
|
+
* generally know which entry in the list is theirs. A recipient that does
|
|
4062
|
+
* not yield a key is not an error — it is somebody else's.
|
|
4063
|
+
*/
|
|
4064
|
+
decrypt(key, options = {}) {
|
|
4065
|
+
if (key.keyType !== KEY_TYPE_SYMMETRIC)
|
|
4066
|
+
return notDecrypted(`A COSE_Encrypt message needs a key of key type Symmetric, but a key of key type ${String(key.keyType)} was given!`);
|
|
4067
|
+
let tried = 0;
|
|
4068
|
+
for (const recipient of this.recipients) {
|
|
4069
|
+
const contentKey = recipient.contentKey(key);
|
|
4070
|
+
if (contentKey === null)
|
|
4071
|
+
continue;
|
|
4072
|
+
tried++;
|
|
4073
|
+
const result = this.decryptWith(contentKey, ENCRYPT_CONTEXT, options);
|
|
4074
|
+
if (result.decrypted)
|
|
4075
|
+
return result;
|
|
4076
|
+
}
|
|
4077
|
+
return notDecrypted(tried === 0 ? "None of the recipients of this COSE_Encrypt message yielded a content key for the given key!" : "A recipient yielded a content key, but the ciphertext did not authenticate under it!");
|
|
4078
|
+
}
|
|
4079
|
+
/** Read a COSE_Encrypt message, tagged or not. */
|
|
4080
|
+
static parse(input) {
|
|
4081
|
+
const { isTagged, items } = untag(input, COSE_ENCRYPT_TAG, 4, "COSE_Encrypt");
|
|
4082
|
+
const [protectedBytes, unprotected, ciphertext, recipients] = items;
|
|
4083
|
+
if (recipients?.type !== "array" || recipients.items.length === 0)
|
|
4084
|
+
throw new CoseError("A COSE_Encrypt message must carry a non-empty array of recipients!");
|
|
4085
|
+
return new _CoseEncrypt(
|
|
4086
|
+
bytesOfProtected(protectedBytes, "COSE_Encrypt"),
|
|
4087
|
+
CoseHeaders.parse(unprotected),
|
|
4088
|
+
ciphertextOf(ciphertext, "COSE_Encrypt"),
|
|
4089
|
+
recipients.items.map((each) => CoseRecipient.parse(each)),
|
|
4090
|
+
isTagged
|
|
4091
|
+
);
|
|
4092
|
+
}
|
|
4093
|
+
/** The CBOR form of this message. */
|
|
4094
|
+
toCbor() {
|
|
4095
|
+
const message = cbor.array([
|
|
4096
|
+
cbor.bytes(this.protectedHeaderBytes),
|
|
4097
|
+
this.unprotectedHeader.toCbor(),
|
|
4098
|
+
this.ciphertext !== null ? cbor.bytes(this.ciphertext) : cbor.nullValue,
|
|
4099
|
+
cbor.array(this.recipients.map((each) => each.toCbor()))
|
|
4100
|
+
]);
|
|
4101
|
+
return this.isTagged ? cbor.tag(COSE_ENCRYPT_TAG, message) : message;
|
|
4102
|
+
}
|
|
4103
|
+
/** The CBOR encoding of this message. */
|
|
4104
|
+
toBytes() {
|
|
4105
|
+
return encode(this.toCbor());
|
|
4106
|
+
}
|
|
4107
|
+
toString() {
|
|
4108
|
+
return `COSE_Encrypt${this.algorithm !== null ? ` ${this.algorithm.name}` : ""}, ${String(this.recipients.length)} recipient(s)`;
|
|
4109
|
+
}
|
|
4110
|
+
};
|
|
4111
|
+
function untag(input, tag, elements, what) {
|
|
4112
|
+
const value = input instanceof Uint8Array ? decode(input) : input;
|
|
4113
|
+
let isTagged = false;
|
|
4114
|
+
let message = value;
|
|
4115
|
+
if (message.type === "tag") {
|
|
4116
|
+
if (message.tag !== BigInt(tag))
|
|
4117
|
+
throw new CoseError(`A ${what} message must be tagged with CBOR tag ${String(tag)}, but was tagged with CBOR tag ${String(message.tag)}!`);
|
|
4118
|
+
isTagged = true;
|
|
4119
|
+
message = message.value;
|
|
4120
|
+
}
|
|
4121
|
+
if (message.type !== "array")
|
|
4122
|
+
throw new CoseError(`A ${what} message must be a CBOR array, but was a CBOR ${message.type}!`);
|
|
4123
|
+
if (message.items.length !== elements)
|
|
4124
|
+
throw new CoseError(`A ${what} message must be a CBOR array of ${String(elements)} elements, but had ${String(message.items.length)} element(s)!`);
|
|
4125
|
+
return { isTagged, items: message.items };
|
|
4126
|
+
}
|
|
4127
|
+
function bytesOfProtected(value, what) {
|
|
4128
|
+
if (value?.type !== "bytes")
|
|
4129
|
+
throw new CoseError(`The protected header bucket of a ${what} message must be a byte string!`);
|
|
4130
|
+
return value.value;
|
|
4131
|
+
}
|
|
4132
|
+
function ciphertextOf(value, what) {
|
|
4133
|
+
if (value.type === "null")
|
|
4134
|
+
return null;
|
|
4135
|
+
if (value.type !== "bytes")
|
|
4136
|
+
throw new CoseError(`The ciphertext of a ${what} message must be a byte string, or null when it is detached!`);
|
|
4137
|
+
return value.value;
|
|
4138
|
+
}
|
|
4139
|
+
function requireAesGcm(key) {
|
|
4140
|
+
if (key.algorithm === null)
|
|
4141
|
+
throw new CoseError("An encrypted COSE message needs a content encryption algorithm: either on the key or within the protected header bucket!");
|
|
4142
|
+
if (key.algorithm.family !== "aesgcm")
|
|
4143
|
+
throw new CoseError(`The COSE algorithm '${key.algorithm.name}' is not a content encryption algorithm this implementation supports!`);
|
|
4144
|
+
return key.algorithm;
|
|
4145
|
+
}
|
|
4146
|
+
function requireUsable(key, algorithm2) {
|
|
4147
|
+
if (algorithm2.family !== "aesgcm")
|
|
4148
|
+
throw new CoseError(`The COSE algorithm '${algorithm2.name}' is not a content encryption algorithm this implementation supports!`);
|
|
4149
|
+
if (key.keyType !== KEY_TYPE_SYMMETRIC)
|
|
4150
|
+
throw new CoseError(`An encrypted COSE message needs a key of key type Symmetric [RFC 9053, Section 4.1], but a key of key type ${String(key.keyType)} was given!`);
|
|
4151
|
+
if (key.algorithm !== null && key.algorithm.id !== algorithm2.id)
|
|
4152
|
+
throw new CoseError(`This message is to be encrypted with '${algorithm2.name}', but the key names '${key.algorithm.name}' [RFC 9053, Section 4.1]!`);
|
|
4153
|
+
const width = key.privateKeyBytes().length;
|
|
4154
|
+
if (algorithm2.keySize !== null && width !== algorithm2.keySize)
|
|
4155
|
+
throw new CoseError(`The algorithm '${algorithm2.name}' needs a ${String(algorithm2.keySize)}-byte key, but a ${String(width)}-byte key was given!`);
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
export { ALL_ALGORITHMS, ALL_CURVES, COSE_ENCRYPT0_TAG, COSE_ENCRYPT_TAG, COSE_MAC0_TAG, COSE_MAC_TAG, COSE_SIGN1_TAG, COSE_SIGN_TAG, COUNTERSIGNATURE_CONTEXT, CoseAlgorithms, CoseCertificateChain, CoseCertificateHash, CoseCurves, CoseEncrypt, CoseEncrypt0, CoseError, CoseHeaders, CoseKey, CoseMac, CoseMac0, CoseRecipient, CoseSign, CoseSign1, CoseSignature, DETERMINISTIC, DerReader, ENCRYPT0_CONTEXT, ENCRYPT_CONTEXT, GCM_NONCE_SIZE, GCM_TAG_SIZE, HeaderLabel, KEY_TYPE_AKP, KEY_TYPE_EC2, KEY_TYPE_OKP, KEY_TYPE_SYMMETRIC, KeyLabel, KeyUsage, MAC0_CONTEXT, MAC_CONTEXT, MLDSA_PARAMETER_SETS, MLDSA_SEED_SIZE, MLDSA_SIZES, NO_BYTES, PRESERVE, SIGNATURE_CONTEXT, Tag, VERIFIED, X509Certificate, X509Name, aesGcmDecrypt, aesGcmEncrypt, aesKeyUnwrap, aesKeyWrap, algorithmById, algorithmByName, algorithmFromCbor, algorithmToCbor, bytesEqual, canonicalizePayload, contentsOf, contextTag, curveById, curveByName, curveByOid, decompressY, derBitString, derBitStringRaw, derBoolean, derInteger, derObjectIdentifier, derString, derTime, digest, eddsaPublicKeyFor, eddsaSign, eddsaSignatureSize, eddsaVerify, encStructure, headerLabelName, hmac, isCanonicalPayload, isEddsaCurve, isImplemented, isMldsaParameterSet, isOnCurve, isUnderstood, keyWrapAlgorithmFor, label, macTag, macWith, mldsaPublicKeyFor, mldsaSign, mldsaVerify, notDecrypted, notVerified, oidOfCurve, publicKeyFor, resolveCurve, sameAlgorithm, sameLabel, signWith, signatureAlgorithmByOid, tagsEqual, verifyCriticalHeaderParameters, verifyMacWith, verifyWith };
|
|
4159
|
+
//# sourceMappingURL=index.js.map
|