@blamejs/pki 0.4.15 → 0.5.1
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 +51 -1
- package/MIGRATING.md +2 -2
- package/README.md +142 -137
- package/index.js +4 -0
- package/lib/acme.js +73 -1
- package/lib/asn1-der.js +2 -0
- package/lib/attrcert-sign.js +4 -0
- package/lib/cbor-det.js +32 -16
- package/lib/cmc-build.js +880 -0
- package/lib/cmc-verify.js +657 -0
- package/lib/cmp-build.js +8 -7
- package/lib/cmp-verify.js +11 -1
- package/lib/cms-sign.js +170 -8
- package/lib/cms-verify.js +80 -14
- package/lib/crl-sign.js +22 -0
- package/lib/crmf-sign.js +5 -2
- package/lib/csr-sign.js +3 -0
- package/lib/ct.js +72 -0
- package/lib/est.js +828 -32
- package/lib/framework-error.js +13 -0
- package/lib/guard-bytes.js +37 -1
- package/lib/guard-range.js +23 -1
- package/lib/http-transport.js +9 -3
- package/lib/inspect.js +28 -5
- package/lib/jose.js +64 -6
- package/lib/lint.js +4 -0
- package/lib/merkle.js +5 -5
- package/lib/ocsp.js +139 -11
- package/lib/oid.js +69 -1
- package/lib/path-validate.js +27 -4
- package/lib/pkcs12-build.js +12 -0
- package/lib/schema-all.js +19 -1
- package/lib/schema-attrcert.js +27 -0
- package/lib/schema-c509.js +6 -0
- package/lib/schema-cmc.js +791 -0
- package/lib/schema-cmp.js +25 -0
- package/lib/schema-cms.js +17 -1
- package/lib/schema-crl.js +23 -1
- package/lib/schema-crmf.js +13 -0
- package/lib/schema-csr.js +11 -0
- package/lib/schema-csrattrs.js +6 -0
- package/lib/schema-engine.js +6 -2
- package/lib/schema-ocsp.js +41 -0
- package/lib/schema-pkcs12.js +16 -0
- package/lib/schema-pkcs8.js +8 -0
- package/lib/schema-smime.js +4 -4
- package/lib/schema-tsp.js +32 -1
- package/lib/schema-x509.js +14 -1
- package/lib/shbs.js +12 -4
- package/lib/sigstore.js +62 -6
- package/lib/smime.js +28 -7
- package/lib/tls-cert-compress.js +15 -3
- package/lib/trust.js +27 -4
- package/lib/tsp-sign.js +41 -6
- package/lib/vendor/README.md +19 -19
- package/lib/webauthn.js +895 -26
- package/lib/webcrypto.js +35 -2
- package/lib/x509-sign.js +3 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/framework-error.js
CHANGED
|
@@ -47,6 +47,7 @@ var CODE_SHAPE = /^[a-z0-9-]+\/[a-z0-9-]+$/;
|
|
|
47
47
|
* site instead of shipping prose into a code-switching consumer.
|
|
48
48
|
*
|
|
49
49
|
* @example
|
|
50
|
+
* var bytes = Buffer.from([0x30, 0x80]); // indefinite length -- not valid DER
|
|
50
51
|
* try { pki.asn1.decode(bytes); }
|
|
51
52
|
* catch (e) {
|
|
52
53
|
* if (e instanceof pki.errors.PkiError) console.error(e.code);
|
|
@@ -88,6 +89,7 @@ class PkiError extends Error {
|
|
|
88
89
|
* withCause: boolean, // default: false -- constructor becomes (code, message, cause)
|
|
89
90
|
*
|
|
90
91
|
* @example
|
|
92
|
+
* // throws: my/bad-input -- raising the new error type IS what this shows
|
|
91
93
|
* var MyError = pki.errors.defineClass("MyError");
|
|
92
94
|
* throw new MyError("my/bad-input", "explanation");
|
|
93
95
|
*/
|
|
@@ -273,6 +275,16 @@ var CsrattrsError = defineClass("CsrattrsError", { withCause: true });
|
|
|
273
275
|
// `.cause`.
|
|
274
276
|
var EstError = defineClass("EstError", { withCause: true });
|
|
275
277
|
|
|
278
|
+
// CmcError -- an RFC 5272 / 6402 Certificate Management over CMS message-layer
|
|
279
|
+
// fault: a PKIData / PKIResponse whose structure, body-part identity, control
|
|
280
|
+
// placement or status encoding is malformed, a control this side must act on
|
|
281
|
+
// that cannot be decoded, or an exchange binding (transactionId / recipientNonce)
|
|
282
|
+
// that does not match the request it answers. Distinct from EstError because the
|
|
283
|
+
// CMC layer is the MESSAGE and EST is the TRANSPORT that carried it: an operator
|
|
284
|
+
// needs to know whether the CA's answer was unreadable or the HTTP hop failed.
|
|
285
|
+
// Carries the underlying cms/* or asn1/* leaf fault as `.cause`.
|
|
286
|
+
var CmcError = defineClass("CmcError", { withCause: true });
|
|
287
|
+
|
|
276
288
|
// TransportError -- a fault from the shared node:https transport (pki.transport):
|
|
277
289
|
// a non-https / unparseable request URL, a missing trust anchor, a TLS handshake /
|
|
278
290
|
// server-authentication failure, a negotiated protocol below the floor, a response
|
|
@@ -380,6 +392,7 @@ module.exports = {
|
|
|
380
392
|
SmimeError: SmimeError,
|
|
381
393
|
CsrattrsError: CsrattrsError,
|
|
382
394
|
EstError: EstError,
|
|
395
|
+
CmcError: CmcError,
|
|
383
396
|
TransportError: TransportError,
|
|
384
397
|
JoseError: JoseError,
|
|
385
398
|
AcmeError: AcmeError,
|
package/lib/guard-bytes.js
CHANGED
|
@@ -57,4 +57,40 @@ function source(input, ErrorClass, code, label) {
|
|
|
57
57
|
throw new ErrorClass(code, label + ": expected a BufferSource (ArrayBuffer / TypedArray / Buffer)");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// snapshot(input, ErrorClass, code, label) -> private Buffer copy | throws ErrorClass
|
|
61
|
+
//
|
|
62
|
+
// The parse-then-verify time-of-check/time-of-use defence. A verification entry
|
|
63
|
+
// point that PARSES its input synchronously and then VERIFIES a signature over
|
|
64
|
+
// the same bytes in a later promise turn is reading the caller's memory twice
|
|
65
|
+
// with an await in between. Every byte range the parse surfaced -- the signed
|
|
66
|
+
// content, the signer set, the values the verdict is built from -- is a VIEW into
|
|
67
|
+
// that memory, so anything that rewrites the buffer in the gap makes the verdict
|
|
68
|
+
// describe bytes other than the ones the signature was checked against
|
|
69
|
+
// (CWE-367 TOCTOU reaching a CWE-347 wrong-verdict). The window is real without
|
|
70
|
+
// an attacker in the process: a caller recycling a pooled read buffer across
|
|
71
|
+
// concurrent verifies hits it by accident.
|
|
72
|
+
//
|
|
73
|
+
// So take one private copy at the boundary and read EVERYTHING from it. `view`
|
|
74
|
+
// re-views and is the right guard where the input is consumed in one synchronous
|
|
75
|
+
// pass; this is its sibling for the boundary that spans an await.
|
|
76
|
+
// @enforced-by behavioral -- a copy has no rename-proof code shape to detect (any
|
|
77
|
+
// `Buffer.from(x)` is one, and most are legitimate). The guard is the RED vector
|
|
78
|
+
// that mutates the caller's buffer between parse and verify and asserts the
|
|
79
|
+
// verdict still describes the bytes that were verified.
|
|
80
|
+
function snapshot(input, ErrorClass, code, label) {
|
|
81
|
+
return Buffer.from(view(input, ErrorClass, code, label));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// snapshotSource(input, ErrorClass, code, label) -> private Buffer copy | throws
|
|
85
|
+
// The same parse-then-verify defence as `snapshot`, over the FULL W3C BufferSource
|
|
86
|
+
// (raw ArrayBuffer / DataView / any typed-array view) rather than only the
|
|
87
|
+
// Buffer / Uint8Array contract. A parser that accepts a BufferSource must snapshot
|
|
88
|
+
// the same set: leaving an ArrayBuffer or a DataView aliased reopens the window for
|
|
89
|
+
// exactly the inputs that took the wider path in.
|
|
90
|
+
// @enforced-by behavioral -- a copy has no rename-proof shape to detect; the guard
|
|
91
|
+
// is the RED vector that mutates the caller's backing buffer across the await.
|
|
92
|
+
function snapshotSource(input, ErrorClass, code, label) {
|
|
93
|
+
return Buffer.from(source(input, ErrorClass, code, label));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { view: view, source: source, snapshot: snapshot, snapshotSource: snapshotSource };
|
package/lib/guard-range.js
CHANGED
|
@@ -99,4 +99,26 @@ function uint64(value, E, code, label) {
|
|
|
99
99
|
return v;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
// authoredInteger(value, E, code, label) -> BigInt. The AUTHORING counterpart to
|
|
103
|
+
// int(): a value a CALLER supplies for an ASN.1 INTEGER with no upper bound (a CMC
|
|
104
|
+
// Transaction Identifier, a CRMF certReqId), normalized to the BigInt the builders
|
|
105
|
+
// take. Accepts a bigint, or a Number that is a SAFE integer -- isSafeInteger, not
|
|
106
|
+
// isInteger, because a Number above 2^53 has already lost precision by the time it
|
|
107
|
+
// arrives, so a large identifier MUST come as a bigint rather than be silently
|
|
108
|
+
// rounded to a neighbour. Everything else is a config-time reject.
|
|
109
|
+
//
|
|
110
|
+
// Distinct from int() on purpose: int() BOUNDS a value the wire decoded and narrows
|
|
111
|
+
// it to Number; this one is unbounded and returns BigInt, because the field has no
|
|
112
|
+
// ceiling to check against and the encoder wants the wide type.
|
|
113
|
+
// @enforced-by guard-shape-reinlined
|
|
114
|
+
// @guard-shape Number\.isSafeInteger\(\s*[A-Za-z_$][\w$]*\s*\)\s*\)\s*return BigInt
|
|
115
|
+
function authoredInteger(value, E, code, label) {
|
|
116
|
+
if (typeof value === "bigint") return value;
|
|
117
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
|
|
118
|
+
throw E(code, label + " must be an integer (a safe-integer number, or a bigint for a large value)");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
int: int, uint31: uint31, positiveInt31: positiveInt31, uint64: uint64,
|
|
123
|
+
authoredInteger: authoredInteger,
|
|
124
|
+
};
|
package/lib/http-transport.js
CHANGED
|
@@ -13,11 +13,17 @@
|
|
|
13
13
|
* drive -- `pki.est` now, `pki.acme` and `pki.cmp` next. This is the ONLY module in
|
|
14
14
|
* the toolkit that opens a socket; every protocol layer stays transport-agnostic and
|
|
15
15
|
* composes it (or an injected substitute) through one contract:
|
|
16
|
-
* `transport(request) -> Promise<{ status, headers, body }>`. The
|
|
16
|
+
* `transport(request) -> Promise<{ status, headers, body, tls }>`. The first three are
|
|
17
17
|
* exactly what a message layer's classifier consumes, so no protocol semantics leak
|
|
18
18
|
* into the socket layer -- the transport owns socket lifecycle, the TLS trust policy,
|
|
19
19
|
* the streaming size cap, and the timeout budget; the caller owns HTTP status,
|
|
20
|
-
* content-type, redirect, and authentication decisions.
|
|
20
|
+
* content-type, redirect, and authentication decisions. `tls` reports the negotiated
|
|
21
|
+
* channel -- `{ protocol, cipher, peerCertificate }` -- which is the one fact a caller
|
|
22
|
+
* cannot recover from the response bytes. An INJECTED substitute should return it too:
|
|
23
|
+
* `pki.est.serverkeygen` asserts the negotiated cipher can protect the private key it
|
|
24
|
+
* is about to accept, and a transport that reports no cipher is trusted rather than
|
|
25
|
+
* refused (so a loopback test channel works), which means omitting the field silently
|
|
26
|
+
* skips that assertion.
|
|
21
27
|
*
|
|
22
28
|
* `pki.transport.https(defaults?)` binds TLS + budget defaults and returns a
|
|
23
29
|
* transport. Trust is EXPLICIT and fail-closed: a request is refused unless it
|
|
@@ -33,7 +39,7 @@
|
|
|
33
39
|
* @card
|
|
34
40
|
* The shared fail-closed node:https transport (est / acme / cmp): explicit trust
|
|
35
41
|
* anchors, rejectUnauthorized always on, a TLS floor, a streaming response-size cap,
|
|
36
|
-
* and a timeout -- behind one `transport(request) -> {status, headers, body}` seam.
|
|
42
|
+
* and a timeout -- behind one `transport(request) -> {status, headers, body, tls}` seam.
|
|
37
43
|
*/
|
|
38
44
|
|
|
39
45
|
var nodeHttps = require("node:https");
|
package/lib/inspect.js
CHANGED
|
@@ -607,7 +607,7 @@ function _parse(input) {
|
|
|
607
607
|
* @primitive pki.inspect.certificate
|
|
608
608
|
* @signature pki.inspect.certificate(input) -> string
|
|
609
609
|
* @since 0.2.4
|
|
610
|
-
* @status
|
|
610
|
+
* @status stable
|
|
611
611
|
* @spec RFC 5280
|
|
612
612
|
* @related pki.schema.x509.parse
|
|
613
613
|
*
|
|
@@ -618,6 +618,10 @@ function _parse(input) {
|
|
|
618
618
|
* hex dump rather than failing the whole report. Pure -- no OpenSSL dependency.
|
|
619
619
|
*
|
|
620
620
|
* @example
|
|
621
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
622
|
+
* var der = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
623
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
624
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
621
625
|
* var cert = pki.schema.x509.parse(der);
|
|
622
626
|
* pki.inspect.certificate(cert).split("\n")[0]; // "Certificate:"
|
|
623
627
|
*/
|
|
@@ -753,7 +757,7 @@ function _crlExtension(ext, pad) {
|
|
|
753
757
|
* @primitive pki.inspect.crl
|
|
754
758
|
* @signature pki.inspect.crl(input) -> string
|
|
755
759
|
* @since 0.3.8
|
|
756
|
-
* @status
|
|
760
|
+
* @status stable
|
|
757
761
|
* @spec RFC 5280
|
|
758
762
|
* @related pki.schema.crl.parse, pki.inspect.certificate
|
|
759
763
|
*
|
|
@@ -765,6 +769,13 @@ function _crlExtension(ext, pad) {
|
|
|
765
769
|
* extension renders as hex rather than failing the report.
|
|
766
770
|
*
|
|
767
771
|
* @example
|
|
772
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
773
|
+
* var key = await pki.key.export(pair.privateKey);
|
|
774
|
+
* var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
775
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
776
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["cRLSign"] } }, { key: key });
|
|
777
|
+
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"), crlNumber: 1n, revoked: [] },
|
|
778
|
+
* { cert: caCert, key: key });
|
|
768
779
|
* pki.inspect.crl(crlDer).split("\n")[0]; // "Certificate Revocation List (CRL):"
|
|
769
780
|
*/
|
|
770
781
|
function crlReport(input) {
|
|
@@ -818,7 +829,7 @@ function _attribute(attr, pad) {
|
|
|
818
829
|
* @primitive pki.inspect.csr
|
|
819
830
|
* @signature pki.inspect.csr(input) -> string
|
|
820
831
|
* @since 0.3.8
|
|
821
|
-
* @status
|
|
832
|
+
* @status stable
|
|
822
833
|
* @spec RFC 2986
|
|
823
834
|
* @related pki.schema.csr.parse, pki.inspect.certificate
|
|
824
835
|
*
|
|
@@ -829,6 +840,9 @@ function _attribute(attr, pad) {
|
|
|
829
840
|
* input `inspect/bad-input`. Best-effort like `certificate`.
|
|
830
841
|
*
|
|
831
842
|
* @example
|
|
843
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
844
|
+
* var csrDer = await pki.csr.sign({ subject: "req.example", subjectPublicKey: await pki.key.export(pair.publicKey) },
|
|
845
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
832
846
|
* pki.inspect.csr(csrDer).split("\n")[0]; // "Certificate Request:"
|
|
833
847
|
*/
|
|
834
848
|
function csrReport(input) {
|
|
@@ -927,7 +941,7 @@ function _signerInfo(si, pad) {
|
|
|
927
941
|
* @primitive pki.inspect.cms
|
|
928
942
|
* @signature pki.inspect.cms(input) -> string
|
|
929
943
|
* @since 0.3.8
|
|
930
|
-
* @status
|
|
944
|
+
* @status stable
|
|
931
945
|
* @spec RFC 5652
|
|
932
946
|
* @related pki.schema.cms.parse, pki.inspect.certificate
|
|
933
947
|
*
|
|
@@ -939,6 +953,11 @@ function _signerInfo(si, pad) {
|
|
|
939
953
|
* `pki.schema.cms.parse` result; a non-CMS throws `inspect/bad-cms`. Best-effort.
|
|
940
954
|
*
|
|
941
955
|
* @example
|
|
956
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
957
|
+
* var key = await pki.key.export(pair.privateKey);
|
|
958
|
+
* var cert = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
959
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
|
|
960
|
+
* var cmsDer = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key });
|
|
942
961
|
* pki.inspect.cms(cmsDer).split("\n")[0]; // "CMS ContentInfo:"
|
|
943
962
|
*/
|
|
944
963
|
// A ContentInfo whose content type pki.schema.cms.parse does not dispatch (id-data,
|
|
@@ -1003,7 +1022,7 @@ var _INSPECT_BY_FORMAT = { x509: certificate, crl: crlReport, csr: csrReport, cm
|
|
|
1003
1022
|
* @primitive pki.inspect.any
|
|
1004
1023
|
* @signature pki.inspect.any(input) -> string
|
|
1005
1024
|
* @since 0.3.8
|
|
1006
|
-
* @status
|
|
1025
|
+
* @status stable
|
|
1007
1026
|
* @spec RFC 5280
|
|
1008
1027
|
* @related pki.schema.detectFormat, pki.inspect.certificate
|
|
1009
1028
|
*
|
|
@@ -1015,6 +1034,10 @@ var _INSPECT_BY_FORMAT = { x509: certificate, crl: crlReport, csr: csrReport, cm
|
|
|
1015
1034
|
* `inspect/bad-input`.
|
|
1016
1035
|
*
|
|
1017
1036
|
* @example
|
|
1037
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
1038
|
+
* var der = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
1039
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
1040
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
1018
1041
|
* pki.inspect.any(der); // routes to the right report by detected format
|
|
1019
1042
|
*/
|
|
1020
1043
|
function any(input) {
|
package/lib/jose.js
CHANGED
|
@@ -327,7 +327,7 @@ function assertPublicJwk(jwk) {
|
|
|
327
327
|
|
|
328
328
|
/**
|
|
329
329
|
* @primitive pki.jose.verify
|
|
330
|
-
* @signature pki.jose.verify(jws, opts) -> Promise<{ header, payload }>
|
|
330
|
+
* @signature pki.jose.verify(jws, opts) -> Promise<{ header, payload, keySource }>
|
|
331
331
|
* @since 0.1.25
|
|
332
332
|
* @status stable
|
|
333
333
|
* @spec RFC 7515, RFC 7518, RFC 8555
|
|
@@ -337,16 +337,31 @@ function assertPublicJwk(jwk) {
|
|
|
337
337
|
* `"acme-outer"`). Structural rules fail closed BEFORE any crypto: the
|
|
338
338
|
* `signatures`/`header` members and a detached payload are rejected, the
|
|
339
339
|
* protected header is validated against the profile (alg registry, nonce, url,
|
|
340
|
-
* exactly-one-of jwk/kid, crit), the signature byte length is pinned per alg
|
|
341
|
-
*
|
|
342
|
-
* `opts.key`
|
|
343
|
-
* `
|
|
340
|
+
* exactly-one-of jwk/kid, crit), and the signature byte length is pinned per alg.
|
|
341
|
+
*
|
|
342
|
+
* `opts.key` names the key the message must be signed under, and it governs: where
|
|
343
|
+
* the profile also permits an embedded header `jwk`, the two must be the SAME key
|
|
344
|
+
* or the message is refused with `jose/key-mismatch`. They are compared as RFC 7638
|
|
345
|
+
* thumbprints, so member order and members outside the key itself cannot make equal
|
|
346
|
+
* keys look different. Without `opts.key` the embedded `jwk` is used where the
|
|
347
|
+
* profile permits one -- which verifies that the message is internally consistent,
|
|
348
|
+
* not that any particular signer produced it. `keySource` reports which of the two
|
|
349
|
+
* answered, so a signature checked against a caller-named key is distinguishable
|
|
350
|
+
* from one checked against the key the message brought with it.
|
|
351
|
+
*
|
|
352
|
+
* Returns `{ header, payload, keySource }` (payload a raw `Buffer`); a failed
|
|
353
|
+
* signature throws `jose/verify-failed`.
|
|
344
354
|
*
|
|
345
355
|
* @opts
|
|
346
356
|
* profile: string // "acme-outer" | "eab-inner" | "keychange-inner"
|
|
347
357
|
* key: object // a public JWK, required unless the profile embeds jwk
|
|
348
358
|
*
|
|
349
359
|
* @example
|
|
360
|
+
* var ec = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
361
|
+
* var accountJwk = await pki.webcrypto.subtle.exportKey("jwk", ec.publicKey);
|
|
362
|
+
* var jws = await pki.jose.sign({ protected: { alg: "ES256", jwk: accountJwk,
|
|
363
|
+
* nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" },
|
|
364
|
+
* payload: Buffer.from("{}"), key: ec.privateKey });
|
|
350
365
|
* var v = await pki.jose.verify(jws, { profile: "acme-outer", key: accountJwk });
|
|
351
366
|
* v.header.alg; // -> "ES256"
|
|
352
367
|
*/
|
|
@@ -360,8 +375,38 @@ async function verify(jws, opts) {
|
|
|
360
375
|
var header = parseJson(b64uDecode(jws.protected));
|
|
361
376
|
var profileName = opts.profile || "acme-outer";
|
|
362
377
|
var checked = _checkHeader(header, profileName);
|
|
378
|
+
// A caller supplying opts.key is NAMING the key this message must be signed under. A profile
|
|
379
|
+
// that also permits an embedded jwk lets the message carry one, and preferring that would let
|
|
380
|
+
// the sender choose which key verifies it -- exactly the question opts.key was asked to settle.
|
|
381
|
+
// So when both are present they must be the SAME key, compared as RFC 7638 thumbprints: that
|
|
382
|
+
// canonicalizes member order and ignores members outside the key itself, so two spellings of
|
|
383
|
+
// one key agree and two different keys cannot.
|
|
384
|
+
// A SUPPLIED key that cannot be used is refused, never quietly treated as absent. Falling back
|
|
385
|
+
// to the embedded jwk there would drop the caller's intent to pin a signer at the moment it
|
|
386
|
+
// matters most -- a `key` that came back null from a lookup would verify against whatever the
|
|
387
|
+
// message carried, and the verdict would report the embedded key as though none was named.
|
|
388
|
+
// `undefined` alone means "not supplied", so spreading an options object stays safe.
|
|
389
|
+
if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null || Array.isArray(opts.key))) {
|
|
390
|
+
throw E("jose/bad-key", "opts.key was supplied but is not a JWK object, so the key this message must be signed under cannot be established");
|
|
391
|
+
}
|
|
363
392
|
var jwk = header.jwk || opts.key;
|
|
364
393
|
if (!jwk) throw E("jose/bad-key", "a verification key is required (opts.key) when the profile does not embed a jwk");
|
|
394
|
+
var keySource = "embedded-jwk";
|
|
395
|
+
if (opts.key) {
|
|
396
|
+
keySource = "opts.key";
|
|
397
|
+
if (header.jwk) {
|
|
398
|
+
var embeddedTp, suppliedTp;
|
|
399
|
+
try { embeddedTp = await thumbprint(header.jwk); suppliedTp = await thumbprint(opts.key); }
|
|
400
|
+
catch (e) { throw E("jose/bad-key", "the embedded jwk and opts.key could not be compared as RFC 7638 thumbprints", e); }
|
|
401
|
+
if (embeddedTp !== suppliedTp) {
|
|
402
|
+
throw E("jose/key-mismatch",
|
|
403
|
+
"the JWS embeds a jwk that is not the key supplied as opts.key, so the message names a " +
|
|
404
|
+
"different signer than the caller expects (embedded thumbprint " + embeddedTp +
|
|
405
|
+
", supplied " + suppliedTp + ")");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
jwk = opts.key;
|
|
409
|
+
}
|
|
365
410
|
_assertKeyType(checked.algRow, jwk);
|
|
366
411
|
var sig = b64uDecode(jws.signature);
|
|
367
412
|
var want = _expectedSigBytes(checked.algRow, jwk);
|
|
@@ -373,7 +418,10 @@ async function verify(jws, opts) {
|
|
|
373
418
|
catch (e) { throw E("jose/bad-key", "the JWK could not be imported for verification", e); }
|
|
374
419
|
var ok = await webcrypto.subtle.verify(_cryptoAlg(checked.algRow, jwk), key, sig, signingInput);
|
|
375
420
|
if (!ok) throw E("jose/verify-failed", "the JWS signature did not verify");
|
|
376
|
-
|
|
421
|
+
// WHICH key answered. A caller auditing the decision needs "checked against the key I named"
|
|
422
|
+
// to be distinguishable from "checked against the one the message brought with it"; the two
|
|
423
|
+
// are different claims and only the first says anything about who the signer is.
|
|
424
|
+
return { header: header, payload: b64uDecode(jws.payload), keySource: keySource };
|
|
377
425
|
}
|
|
378
426
|
|
|
379
427
|
// ---- flattened-JWS sign (RFC 7515 sec. 5.1) ------------------------------
|
|
@@ -405,6 +453,10 @@ async function verify(jws, opts) {
|
|
|
405
453
|
* jwk: object // the public JWK, when the header embeds jwk
|
|
406
454
|
*
|
|
407
455
|
* @example
|
|
456
|
+
* var ec = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
457
|
+
* var priv = ec.privateKey;
|
|
458
|
+
* var hdr = { alg: "ES256", jwk: await pki.webcrypto.subtle.exportKey("jwk", ec.publicKey),
|
|
459
|
+
* nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" };
|
|
408
460
|
* var jws = await pki.jose.sign({ protected: hdr, payload: Buffer.from("{}"), key: priv });
|
|
409
461
|
*/
|
|
410
462
|
async function sign(opts) {
|
|
@@ -491,6 +543,12 @@ var THUMBPRINT_MEMBERS = {
|
|
|
491
543
|
* always yields the same thumbprint (the ACME key-authorization anchor).
|
|
492
544
|
*
|
|
493
545
|
* @example
|
|
546
|
+
* // the RFC 7638 sec. 3.1 worked example, so the thumbprint below is the spec's own
|
|
547
|
+
* var accountJwk = { kty: "RSA", e: "AQAB", n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78L" +
|
|
548
|
+
* "hWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXA" +
|
|
549
|
+
* "rwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajr" +
|
|
550
|
+
* "n1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-" +
|
|
551
|
+
* "kEgU8awapJzKnqDKgw" };
|
|
494
552
|
* await pki.jose.thumbprint(accountJwk); // -> "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
|
|
495
553
|
*/
|
|
496
554
|
async function thumbprint(jwk) {
|
package/lib/lint.js
CHANGED
|
@@ -715,6 +715,10 @@ function _applyThreshold(report, severity) {
|
|
|
715
715
|
* @opts severity Suppress findings below this floor (default `"notice"`). `counts` and
|
|
716
716
|
* `worst` always reflect the complete, unfiltered result.
|
|
717
717
|
* @example
|
|
718
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
719
|
+
* var pemString = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
720
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
721
|
+
* { key: await pki.key.export(pair.privateKey) }, { pem: true });
|
|
718
722
|
* var report = pki.lint.certificate(pemString);
|
|
719
723
|
* report.worst; // "notice" | "error" | ...
|
|
720
724
|
* report.findings.map(function (f) { return f.id; });
|
package/lib/merkle.js
CHANGED
|
@@ -102,7 +102,7 @@ function _ctEq(a, b) {
|
|
|
102
102
|
* @primitive pki.merkle.leafHash
|
|
103
103
|
* @signature pki.merkle.leafHash(entry) -> Buffer
|
|
104
104
|
* @since 0.1.28
|
|
105
|
-
* @status
|
|
105
|
+
* @status stable
|
|
106
106
|
* @spec RFC 6962, RFC 9162
|
|
107
107
|
* @related pki.merkle.nodeHash, pki.merkle.verifyInclusion
|
|
108
108
|
*
|
|
@@ -122,7 +122,7 @@ function leafHash(entry) {
|
|
|
122
122
|
* @primitive pki.merkle.nodeHash
|
|
123
123
|
* @signature pki.merkle.nodeHash(left, right) -> Buffer
|
|
124
124
|
* @since 0.1.28
|
|
125
|
-
* @status
|
|
125
|
+
* @status stable
|
|
126
126
|
* @spec RFC 6962, RFC 9162
|
|
127
127
|
* @related pki.merkle.leafHash
|
|
128
128
|
*
|
|
@@ -146,7 +146,7 @@ function nodeHash(left, right) {
|
|
|
146
146
|
* @primitive pki.merkle.emptyRootHash
|
|
147
147
|
* @signature pki.merkle.emptyRootHash() -> Buffer
|
|
148
148
|
* @since 0.1.28
|
|
149
|
-
* @status
|
|
149
|
+
* @status stable
|
|
150
150
|
* @spec RFC 6962, RFC 9162
|
|
151
151
|
* @related pki.merkle.verifyConsistency
|
|
152
152
|
*
|
|
@@ -164,7 +164,7 @@ function emptyRootHash() {
|
|
|
164
164
|
* @primitive pki.merkle.verifyInclusion
|
|
165
165
|
* @signature pki.merkle.verifyInclusion(opts) -> boolean
|
|
166
166
|
* @since 0.1.28
|
|
167
|
-
* @status
|
|
167
|
+
* @status stable
|
|
168
168
|
* @spec RFC 6962, RFC 9162
|
|
169
169
|
* @related pki.merkle.leafHash, pki.merkle.verifyConsistency
|
|
170
170
|
*
|
|
@@ -223,7 +223,7 @@ function verifyInclusion(opts) {
|
|
|
223
223
|
* @primitive pki.merkle.verifyConsistency
|
|
224
224
|
* @signature pki.merkle.verifyConsistency(opts) -> boolean
|
|
225
225
|
* @since 0.1.28
|
|
226
|
-
* @status
|
|
226
|
+
* @status stable
|
|
227
227
|
* @spec RFC 6962, RFC 9162
|
|
228
228
|
* @related pki.merkle.verifyInclusion, pki.merkle.emptyRootHash
|
|
229
229
|
*
|