@dwk/http-signatures 0.1.0-beta.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/LICENSE +15 -0
- package/README.md +141 -0
- package/dist/algorithms.d.ts +31 -0
- package/dist/algorithms.d.ts.map +1 -0
- package/dist/algorithms.js +106 -0
- package/dist/algorithms.js.map +1 -0
- package/dist/base64.d.ts +8 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/base64.js +23 -0
- package/dist/base64.js.map +1 -0
- package/dist/cavage.d.ts +38 -0
- package/dist/cavage.d.ts.map +1 -0
- package/dist/cavage.js +249 -0
- package/dist/cavage.js.map +1 -0
- package/dist/components.d.ts +26 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +65 -0
- package/dist/components.js.map +1 -0
- package/dist/digest.d.ts +45 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +128 -0
- package/dist/digest.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/rfc9421.d.ts +49 -0
- package/dist/rfc9421.d.ts.map +1 -0
- package/dist/rfc9421.js +216 -0
- package/dist/rfc9421.js.map +1 -0
- package/dist/sf.d.ts +49 -0
- package/dist/sf.d.ts.map +1 -0
- package/dist/sf.js +234 -0
- package/dist/sf.js.map +1 -0
- package/dist/sign.d.ts +46 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +39 -0
- package/dist/sign.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +37 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +60 -0
- package/dist/verify.js.map +1 -0
- package/package.json +44 -0
- package/src/algorithms.ts +168 -0
- package/src/base64.ts +25 -0
- package/src/cavage.ts +292 -0
- package/src/components.ts +79 -0
- package/src/digest.ts +156 -0
- package/src/index.ts +45 -0
- package/src/rfc9421.ts +280 -0
- package/src/sf.ts +246 -0
- package/src/sign.ts +79 -0
- package/src/types.ts +94 -0
- package/src/verify.ts +102 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Covered-component value derivation (RFC 9421 §2.1–2.2), shared by the
|
|
3
|
+
* signature-base builder and the verifier.
|
|
4
|
+
*
|
|
5
|
+
* Supports the derived components needed for request signing — `@method`,
|
|
6
|
+
* `@target-uri`, `@authority`, `@scheme`, `@request-target`, `@path`,
|
|
7
|
+
* `@query` — and any HTTP header field (matched case-insensitively, with
|
|
8
|
+
* multiple field lines folded into one `", "`-joined value per §2.1).
|
|
9
|
+
* Component parameters (`;sf`, `;key`, `;bs`, `@query-param`, …) are not
|
|
10
|
+
* supported and are reported by the caller as malformed.
|
|
11
|
+
*/
|
|
12
|
+
import type { HttpMessage } from "./types";
|
|
13
|
+
export interface DerivationContext {
|
|
14
|
+
message: HttpMessage;
|
|
15
|
+
url: URL;
|
|
16
|
+
headers: Map<string, string>;
|
|
17
|
+
}
|
|
18
|
+
/** Parse the message URL once and capture a normalized header view. */
|
|
19
|
+
export declare function derivationContext(message: HttpMessage): DerivationContext | null;
|
|
20
|
+
/**
|
|
21
|
+
* Derive the value of a single covered component. Returns `null` when the
|
|
22
|
+
* component is unsupported (a `@`-derived name we do not implement) or absent
|
|
23
|
+
* (a header the message does not carry).
|
|
24
|
+
*/
|
|
25
|
+
export declare function deriveComponentValue(ctx: DerivationContext, name: string): string | null;
|
|
26
|
+
//# sourceMappingURL=components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAiB3C,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,WAAW,CAAC;IACrB,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,GACnB,iBAAiB,GAAG,IAAI,CAQ1B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,iBAAiB,EACtB,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,IAAI,CAsBf"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Covered-component value derivation (RFC 9421 §2.1–2.2), shared by the
|
|
3
|
+
* signature-base builder and the verifier.
|
|
4
|
+
*
|
|
5
|
+
* Supports the derived components needed for request signing — `@method`,
|
|
6
|
+
* `@target-uri`, `@authority`, `@scheme`, `@request-target`, `@path`,
|
|
7
|
+
* `@query` — and any HTTP header field (matched case-insensitively, with
|
|
8
|
+
* multiple field lines folded into one `", "`-joined value per §2.1).
|
|
9
|
+
* Component parameters (`;sf`, `;key`, `;bs`, `@query-param`, …) are not
|
|
10
|
+
* supported and are reported by the caller as malformed.
|
|
11
|
+
*/
|
|
12
|
+
/** Build a case-insensitive view of the message's header values, OWS-trimmed. */
|
|
13
|
+
function headerMap(headers) {
|
|
14
|
+
const map = new Map();
|
|
15
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
16
|
+
// RFC 9421 §2.1: strip leading/trailing OWS from each field value and join
|
|
17
|
+
// multiple values with ", ". Internal whitespace is preserved verbatim —
|
|
18
|
+
// collapsing it would change the signed value and break verification.
|
|
19
|
+
const joined = Array.isArray(value)
|
|
20
|
+
? value.map((v) => v.trim()).join(", ")
|
|
21
|
+
: value.trim();
|
|
22
|
+
map.set(name.toLowerCase(), joined);
|
|
23
|
+
}
|
|
24
|
+
return map;
|
|
25
|
+
}
|
|
26
|
+
/** Parse the message URL once and capture a normalized header view. */
|
|
27
|
+
export function derivationContext(message) {
|
|
28
|
+
let url;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(message.url);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return { message, url, headers: headerMap(message.headers) };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Derive the value of a single covered component. Returns `null` when the
|
|
39
|
+
* component is unsupported (a `@`-derived name we do not implement) or absent
|
|
40
|
+
* (a header the message does not carry).
|
|
41
|
+
*/
|
|
42
|
+
export function deriveComponentValue(ctx, name) {
|
|
43
|
+
if (name.startsWith("@")) {
|
|
44
|
+
switch (name) {
|
|
45
|
+
case "@method":
|
|
46
|
+
return ctx.message.method.toUpperCase();
|
|
47
|
+
case "@target-uri":
|
|
48
|
+
return ctx.url.href;
|
|
49
|
+
case "@authority":
|
|
50
|
+
return ctx.url.host.toLowerCase();
|
|
51
|
+
case "@scheme":
|
|
52
|
+
return ctx.url.protocol.replace(/:$/, "").toLowerCase();
|
|
53
|
+
case "@request-target":
|
|
54
|
+
return `${ctx.url.pathname}${ctx.url.search}`;
|
|
55
|
+
case "@path":
|
|
56
|
+
return ctx.url.pathname;
|
|
57
|
+
case "@query":
|
|
58
|
+
return ctx.url.search === "" ? "?" : ctx.url.search;
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return ctx.headers.get(name.toLowerCase()) ?? null;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=components.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.js","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,iFAAiF;AACjF,SAAS,SAAS,CAAC,OAA+B;IAChD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,2EAA2E;QAC3E,yEAAyE;QACzE,sEAAsE;QACtE,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACjC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YACvC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACjB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAQD,uEAAuE;AACvE,MAAM,UAAU,iBAAiB,CAC/B,OAAoB;IAEpB,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAsB,EACtB,IAAY;IAEZ,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,SAAS;gBACZ,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAC1C,KAAK,aAAa;gBAChB,OAAO,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YACtB,KAAK,YAAY;gBACf,OAAO,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACpC,KAAK,SAAS;gBACZ,OAAO,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;YAC1D,KAAK,iBAAiB;gBACpB,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;YAChD,KAAK,OAAO;gBACV,OAAO,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC1B,KAAK,QAAQ;gBACX,OAAO,GAAG,CAAC,GAAG,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;YACtD;gBACE,OAAO,IAAI,CAAC;QAChB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC;AACrD,CAAC"}
|
package/dist/digest.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body integrity headers.
|
|
3
|
+
*
|
|
4
|
+
* - `Content-Digest` (RFC 9530) — a structured-field dictionary whose values
|
|
5
|
+
* are byte sequences, e.g. `sha-256=:<base64>:`.
|
|
6
|
+
* - `Digest` (the legacy RFC 3230 field much of the fediverse still sends),
|
|
7
|
+
* e.g. `SHA-256=<base64>`.
|
|
8
|
+
*
|
|
9
|
+
* Including a digest in the covered component set is how body tampering is
|
|
10
|
+
* caught: the signature covers the digest header value, and recomputing the
|
|
11
|
+
* digest over the received body proves that value still describes the body.
|
|
12
|
+
*/
|
|
13
|
+
/** Hash algorithms supported for digests. */
|
|
14
|
+
export type DigestAlgorithm = "sha-256" | "sha-512";
|
|
15
|
+
/**
|
|
16
|
+
* Compute the RFC 9530 `Content-Digest` field value for `body`, e.g.
|
|
17
|
+
* `sha-256=:<base64>:`. The result is meant to be set as the `content-digest`
|
|
18
|
+
* header and listed among the covered components before signing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createContentDigest(body: Uint8Array | string, alg?: DigestAlgorithm): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Compute the legacy RFC 3230 `Digest` field value for `body`, e.g.
|
|
23
|
+
* `SHA-256=<base64>` — the spelling fediverse peers expect on the `Digest`
|
|
24
|
+
* header alongside a `draft-cavage` signature.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createDigest(body: Uint8Array | string, alg?: DigestAlgorithm): Promise<string>;
|
|
27
|
+
/** A reason a digest header failed to verify, or `null` when it matched. */
|
|
28
|
+
export type DigestRejection = "digest_missing" | "digest_unsupported" | "digest_mismatch";
|
|
29
|
+
/**
|
|
30
|
+
* Verify a received `Content-Digest` field value against `body`. The field is
|
|
31
|
+
* an RFC 8941 Dictionary of Byte Sequences (RFC 9530 §2), so it is parsed with
|
|
32
|
+
* the strict structured-fields parser rather than split by hand: a malformed
|
|
33
|
+
* value — including a lone uppercase algorithm key, which is not a valid sf-key
|
|
34
|
+
* — fails closed as `digest_mismatch`. Per RFC 9530 §3, entries whose algorithm
|
|
35
|
+
* the verifier does not support are ignored; every supported entry must match,
|
|
36
|
+
* and at least one supported entry must be present (otherwise the value is
|
|
37
|
+
* rejected as `digest_unsupported`).
|
|
38
|
+
*/
|
|
39
|
+
export declare function verifyContentDigest(headerValue: string | null | undefined, body: Uint8Array | string): Promise<DigestRejection | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Verify a received legacy `Digest` field value (`SHA-256=<base64>`, no colon
|
|
42
|
+
* wrapping) against `body`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function verifyDigest(headerValue: string | null | undefined, body: Uint8Array | string): Promise<DigestRejection | null>;
|
|
45
|
+
//# sourceMappingURL=digest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,6CAA6C;AAC7C,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,CAAC;AA0BpD;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,UAAU,GAAG,MAAM,EACzB,GAAG,GAAE,eAA2B,GAC/B,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,UAAU,GAAG,MAAM,EACzB,GAAG,GAAE,eAA2B,GAC/B,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,4EAA4E;AAC5E,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,oBAAoB,GACpB,iBAAiB,CAAC;AAEtB;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,IAAI,EAAE,UAAU,GAAG,MAAM,GACxB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAmBjC;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,IAAI,EAAE,UAAU,GAAG,MAAM,GACxB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAejC"}
|
package/dist/digest.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body integrity headers.
|
|
3
|
+
*
|
|
4
|
+
* - `Content-Digest` (RFC 9530) — a structured-field dictionary whose values
|
|
5
|
+
* are byte sequences, e.g. `sha-256=:<base64>:`.
|
|
6
|
+
* - `Digest` (the legacy RFC 3230 field much of the fediverse still sends),
|
|
7
|
+
* e.g. `SHA-256=<base64>`.
|
|
8
|
+
*
|
|
9
|
+
* Including a digest in the covered component set is how body tampering is
|
|
10
|
+
* caught: the signature covers the digest header value, and recomputing the
|
|
11
|
+
* digest over the received body proves that value still describes the body.
|
|
12
|
+
*/
|
|
13
|
+
import { base64ToBytes, bytesToBase64 } from "./base64";
|
|
14
|
+
import { parseByteSequenceDictionary, SfParseError } from "./sf";
|
|
15
|
+
const SUBTLE_HASH = {
|
|
16
|
+
"sha-256": "SHA-256",
|
|
17
|
+
"sha-512": "SHA-512",
|
|
18
|
+
};
|
|
19
|
+
function toBytes(body) {
|
|
20
|
+
return typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
21
|
+
}
|
|
22
|
+
async function hashBytes(body, alg) {
|
|
23
|
+
const digest = await crypto.subtle.digest(SUBTLE_HASH[alg], toBytes(body));
|
|
24
|
+
return new Uint8Array(digest);
|
|
25
|
+
}
|
|
26
|
+
async function hash(body, alg) {
|
|
27
|
+
return bytesToBase64(await hashBytes(body, alg));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compute the RFC 9530 `Content-Digest` field value for `body`, e.g.
|
|
31
|
+
* `sha-256=:<base64>:`. The result is meant to be set as the `content-digest`
|
|
32
|
+
* header and listed among the covered components before signing.
|
|
33
|
+
*/
|
|
34
|
+
export async function createContentDigest(body, alg = "sha-256") {
|
|
35
|
+
return `${alg}=:${await hash(body, alg)}:`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compute the legacy RFC 3230 `Digest` field value for `body`, e.g.
|
|
39
|
+
* `SHA-256=<base64>` — the spelling fediverse peers expect on the `Digest`
|
|
40
|
+
* header alongside a `draft-cavage` signature.
|
|
41
|
+
*/
|
|
42
|
+
export async function createDigest(body, alg = "sha-256") {
|
|
43
|
+
return `${alg.toUpperCase()}=${await hash(body, alg)}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Verify a received `Content-Digest` field value against `body`. The field is
|
|
47
|
+
* an RFC 8941 Dictionary of Byte Sequences (RFC 9530 §2), so it is parsed with
|
|
48
|
+
* the strict structured-fields parser rather than split by hand: a malformed
|
|
49
|
+
* value — including a lone uppercase algorithm key, which is not a valid sf-key
|
|
50
|
+
* — fails closed as `digest_mismatch`. Per RFC 9530 §3, entries whose algorithm
|
|
51
|
+
* the verifier does not support are ignored; every supported entry must match,
|
|
52
|
+
* and at least one supported entry must be present (otherwise the value is
|
|
53
|
+
* rejected as `digest_unsupported`).
|
|
54
|
+
*/
|
|
55
|
+
export async function verifyContentDigest(headerValue, body) {
|
|
56
|
+
if (!headerValue)
|
|
57
|
+
return "digest_missing";
|
|
58
|
+
let dict;
|
|
59
|
+
try {
|
|
60
|
+
dict = parseByteSequenceDictionary(headerValue);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err instanceof SfParseError)
|
|
64
|
+
return "digest_mismatch";
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
let sawSupported = false;
|
|
68
|
+
for (const [key, received] of dict) {
|
|
69
|
+
if (!(key in SUBTLE_HASH))
|
|
70
|
+
continue;
|
|
71
|
+
const alg = key;
|
|
72
|
+
if (!constantTimeEqualBytes(received, await hashBytes(body, alg))) {
|
|
73
|
+
return "digest_mismatch";
|
|
74
|
+
}
|
|
75
|
+
sawSupported = true;
|
|
76
|
+
}
|
|
77
|
+
return sawSupported ? null : "digest_unsupported";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Verify a received legacy `Digest` field value (`SHA-256=<base64>`, no colon
|
|
81
|
+
* wrapping) against `body`.
|
|
82
|
+
*/
|
|
83
|
+
export async function verifyDigest(headerValue, body) {
|
|
84
|
+
if (!headerValue)
|
|
85
|
+
return "digest_missing";
|
|
86
|
+
let sawSupported = false;
|
|
87
|
+
for (const entry of headerValue.split(",")) {
|
|
88
|
+
const eq = entry.indexOf("=");
|
|
89
|
+
if (eq < 0)
|
|
90
|
+
continue;
|
|
91
|
+
const alg = entry.slice(0, eq).trim().toLowerCase();
|
|
92
|
+
if (!(alg in SUBTLE_HASH))
|
|
93
|
+
continue;
|
|
94
|
+
const value = entry.slice(eq + 1).trim();
|
|
95
|
+
if (!constantTimeEqualBase64(value, await hash(body, alg))) {
|
|
96
|
+
return "digest_mismatch";
|
|
97
|
+
}
|
|
98
|
+
sawSupported = true;
|
|
99
|
+
}
|
|
100
|
+
return sawSupported ? null : "digest_unsupported";
|
|
101
|
+
}
|
|
102
|
+
/** Compare two byte arrays in length-constant time. */
|
|
103
|
+
function constantTimeEqualBytes(x, y) {
|
|
104
|
+
if (x.length !== y.length)
|
|
105
|
+
return false;
|
|
106
|
+
let diff = 0;
|
|
107
|
+
for (let i = 0; i < x.length; i++) {
|
|
108
|
+
diff |= x[i] ^ y[i];
|
|
109
|
+
}
|
|
110
|
+
return diff === 0;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Compare two base64 strings by their decoded bytes in length-constant time.
|
|
114
|
+
* A malformed encoding compares unequal rather than throwing.
|
|
115
|
+
*/
|
|
116
|
+
function constantTimeEqualBase64(a, b) {
|
|
117
|
+
let x;
|
|
118
|
+
let y;
|
|
119
|
+
try {
|
|
120
|
+
x = base64ToBytes(a);
|
|
121
|
+
y = base64ToBytes(b);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return constantTimeEqualBytes(x, y);
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=digest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.js","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/http-signatures` — HTTP Message Signatures (RFC 9421), with the legacy
|
|
3
|
+
* `draft-cavage-http-signatures` profile for fediverse interop.
|
|
4
|
+
*
|
|
5
|
+
* A pure, runtime-agnostic library: a request reduced to plain data (method,
|
|
6
|
+
* URL, headers) plus a resolved {@link CryptoKey} goes in, a signing result or a
|
|
7
|
+
* {@link VerifyResult} comes out. It performs no I/O beyond Web Crypto, holds no
|
|
8
|
+
* state, and needs no Cloudflare bindings, so it unit-tests without a Workers
|
|
9
|
+
* runtime.
|
|
10
|
+
*
|
|
11
|
+
* It is **protocol-agnostic** — it knows nothing about ActivityPub, IndieWeb, or
|
|
12
|
+
* Solid. Key resolution (fetching an actor's public key, caching it) is the
|
|
13
|
+
* caller's responsibility, supplied as a {@link KeyResolver}. Mirroring the
|
|
14
|
+
* `@dwk/dpop` hardening posture, only asymmetric algorithms from an explicit
|
|
15
|
+
* allow-list are accepted — never `none` or HMAC — and a resolved key is
|
|
16
|
+
* validated against the claimed algorithm (RSA modulus floor, EC curve) before
|
|
17
|
+
* any signature is checked.
|
|
18
|
+
*
|
|
19
|
+
* @see spec/packages/http-signatures.md
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
export { signMessage, type SignParams } from "./sign";
|
|
23
|
+
export { verifyMessage, type VerifyParams } from "./verify";
|
|
24
|
+
export type { KeyResolver } from "./rfc9421";
|
|
25
|
+
export { createContentDigest, createDigest, verifyContentDigest, verifyDigest, type DigestAlgorithm, type DigestRejection, } from "./digest";
|
|
26
|
+
export { isSupportedAlgorithm } from "./algorithms";
|
|
27
|
+
export type { HttpMessage, SignatureAlgorithm, SignatureProfile, SignatureFailureReason, VerifyResult, } from "./types";
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAE5D,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,YAAY,EACV,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,GACb,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/http-signatures` — HTTP Message Signatures (RFC 9421), with the legacy
|
|
3
|
+
* `draft-cavage-http-signatures` profile for fediverse interop.
|
|
4
|
+
*
|
|
5
|
+
* A pure, runtime-agnostic library: a request reduced to plain data (method,
|
|
6
|
+
* URL, headers) plus a resolved {@link CryptoKey} goes in, a signing result or a
|
|
7
|
+
* {@link VerifyResult} comes out. It performs no I/O beyond Web Crypto, holds no
|
|
8
|
+
* state, and needs no Cloudflare bindings, so it unit-tests without a Workers
|
|
9
|
+
* runtime.
|
|
10
|
+
*
|
|
11
|
+
* It is **protocol-agnostic** — it knows nothing about ActivityPub, IndieWeb, or
|
|
12
|
+
* Solid. Key resolution (fetching an actor's public key, caching it) is the
|
|
13
|
+
* caller's responsibility, supplied as a {@link KeyResolver}. Mirroring the
|
|
14
|
+
* `@dwk/dpop` hardening posture, only asymmetric algorithms from an explicit
|
|
15
|
+
* allow-list are accepted — never `none` or HMAC — and a resolved key is
|
|
16
|
+
* validated against the claimed algorithm (RSA modulus floor, EC curve) before
|
|
17
|
+
* any signature is checked.
|
|
18
|
+
*
|
|
19
|
+
* @see spec/packages/http-signatures.md
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
export { signMessage } from "./sign";
|
|
23
|
+
export { verifyMessage } from "./verify";
|
|
24
|
+
export { createContentDigest, createDigest, verifyContentDigest, verifyDigest, } from "./digest";
|
|
25
|
+
export { isSupportedAlgorithm } from "./algorithms";
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,WAAW,EAAmB,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,aAAa,EAAqB,MAAM,UAAU,CAAC;AAI5D,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,mBAAmB,EACnB,YAAY,GAGb,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RFC 9421 wire profile: the `Signature-Input` / `Signature`
|
|
3
|
+
* structured-field pair, the signature base of §2.5, and the `@signature-params`
|
|
4
|
+
* line.
|
|
5
|
+
*/
|
|
6
|
+
import type { HttpMessage, SignatureAlgorithm, VerifyResult } from "./types";
|
|
7
|
+
/** Resolved signing inputs for the RFC 9421 profile. */
|
|
8
|
+
export interface Rfc9421SignParams {
|
|
9
|
+
key: CryptoKey;
|
|
10
|
+
keyId: string;
|
|
11
|
+
alg: SignatureAlgorithm;
|
|
12
|
+
components: string[];
|
|
13
|
+
created: number;
|
|
14
|
+
expires?: number;
|
|
15
|
+
nonce?: string;
|
|
16
|
+
tag?: string;
|
|
17
|
+
label: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sign `message` under the RFC 9421 profile. Returns the `Signature-Input` and
|
|
21
|
+
* `Signature` headers to merge into the request. Throws if a covered component
|
|
22
|
+
* is absent from the message or the URL cannot be parsed — both signer bugs.
|
|
23
|
+
*/
|
|
24
|
+
export declare function signRfc9421(message: HttpMessage, params: Rfc9421SignParams): Promise<Record<string, string>>;
|
|
25
|
+
/** Resolves a public key (and optionally the algorithm it dictates) for verification. */
|
|
26
|
+
export type KeyResolver = (params: {
|
|
27
|
+
keyId: string | null;
|
|
28
|
+
alg: SignatureAlgorithm | null;
|
|
29
|
+
}) => Promise<CryptoKey | null> | (CryptoKey | null);
|
|
30
|
+
/** Parameters for verifying an RFC 9421 signature. */
|
|
31
|
+
export interface Rfc9421VerifyParams {
|
|
32
|
+
resolveKey: KeyResolver;
|
|
33
|
+
/** Which labelled signature to verify; required when the message carries more than one. */
|
|
34
|
+
label?: string;
|
|
35
|
+
/** Component identifiers that MUST be covered (e.g. `@method`, `date`). */
|
|
36
|
+
requiredComponents?: string[];
|
|
37
|
+
/** Require a `created` parameter to be present. */
|
|
38
|
+
requireCreated?: boolean;
|
|
39
|
+
/** Current time, epoch seconds. Defaults to `Date.now()`. */
|
|
40
|
+
now?: number;
|
|
41
|
+
/** Allowed clock skew, in seconds, for `created`/`expires`. Defaults to 300. */
|
|
42
|
+
toleranceSeconds?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Verify an RFC 9421 signature on `message`. Never throws — every failure is a
|
|
46
|
+
* `{ valid: false, reason }` with a stable {@link SignatureFailureReason}.
|
|
47
|
+
*/
|
|
48
|
+
export declare function verifyRfc9421(message: HttpMessage, params: Rfc9421VerifyParams): Promise<VerifyResult>;
|
|
49
|
+
//# sourceMappingURL=rfc9421.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rfc9421.d.ts","sourceRoot":"","sources":["../src/rfc9421.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiBH,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAElB,YAAY,EACb,MAAM,SAAS,CAAC;AAEjB,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,SAAS,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,kBAAkB,CAAC;IACxB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAoBD;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAkCjC;AAED,yFAAyF;AACzF,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE;IACjC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChC,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;AAErD,sDAAsD;AACtD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,WAAW,CAAC;IACxB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,mDAAmD;IACnD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAwBD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA8HvB"}
|
package/dist/rfc9421.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RFC 9421 wire profile: the `Signature-Input` / `Signature`
|
|
3
|
+
* structured-field pair, the signature base of §2.5, and the `@signature-params`
|
|
4
|
+
* line.
|
|
5
|
+
*/
|
|
6
|
+
import { deriveAlgFromKey, isSupportedAlgorithm, signBytes, validateKey, verifyBytes, } from "./algorithms";
|
|
7
|
+
import { bytesToBase64, utf8 } from "./base64";
|
|
8
|
+
import { deriveComponentValue, derivationContext } from "./components";
|
|
9
|
+
import { parseSignatureHeader, parseSignatureInput, serializeString, } from "./sf";
|
|
10
|
+
/** Build the `@signature-params` value (inner list plus its parameters). */
|
|
11
|
+
function signatureParamsValue(params) {
|
|
12
|
+
const items = params.components.map(serializeString).join(" ");
|
|
13
|
+
let out = `(${items});created=${params.created}`;
|
|
14
|
+
if (params.expires !== undefined)
|
|
15
|
+
out += `;expires=${params.expires}`;
|
|
16
|
+
out += `;keyid=${serializeString(params.keyId)}`;
|
|
17
|
+
out += `;alg=${serializeString(params.alg)}`;
|
|
18
|
+
if (params.nonce !== undefined)
|
|
19
|
+
out += `;nonce=${serializeString(params.nonce)}`;
|
|
20
|
+
if (params.tag !== undefined)
|
|
21
|
+
out += `;tag=${serializeString(params.tag)}`;
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** Build the signature base over `lines` and the `@signature-params` value. */
|
|
25
|
+
function signatureBase(lines, sigParams) {
|
|
26
|
+
return [...lines, `"@signature-params": ${sigParams}`].join("\n");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Sign `message` under the RFC 9421 profile. Returns the `Signature-Input` and
|
|
30
|
+
* `Signature` headers to merge into the request. Throws if a covered component
|
|
31
|
+
* is absent from the message or the URL cannot be parsed — both signer bugs.
|
|
32
|
+
*/
|
|
33
|
+
export async function signRfc9421(message, params) {
|
|
34
|
+
const ctx = derivationContext(message);
|
|
35
|
+
if (ctx === null)
|
|
36
|
+
throw new Error("http-signatures: message.url is not a valid URL");
|
|
37
|
+
const sigParams = signatureParamsValue(params);
|
|
38
|
+
const lines = [];
|
|
39
|
+
// RFC 9421 §2: each component identifier MUST occur only once in the covered
|
|
40
|
+
// component list. A repeated identifier is a signer bug — reject it loudly.
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
for (const name of params.components) {
|
|
43
|
+
const id = name.toLowerCase();
|
|
44
|
+
if (seen.has(id)) {
|
|
45
|
+
throw new Error(`http-signatures: duplicate covered component "${name}" (RFC 9421 §2)`);
|
|
46
|
+
}
|
|
47
|
+
seen.add(id);
|
|
48
|
+
const value = deriveComponentValue(ctx, name);
|
|
49
|
+
if (value === null) {
|
|
50
|
+
throw new Error(`http-signatures: covered component "${name}" is missing from the message`);
|
|
51
|
+
}
|
|
52
|
+
lines.push(`${serializeString(name)}: ${value}`);
|
|
53
|
+
}
|
|
54
|
+
const signature = await signBytes(params.key, params.alg, utf8(signatureBase(lines, sigParams)));
|
|
55
|
+
return {
|
|
56
|
+
"Signature-Input": `${params.label}=${sigParams}`,
|
|
57
|
+
Signature: `${params.label}=:${bytesToBase64(new Uint8Array(signature))}:`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function paramMap(params) {
|
|
61
|
+
const map = new Map();
|
|
62
|
+
for (const [k, v] of params)
|
|
63
|
+
map.set(k, v);
|
|
64
|
+
return map;
|
|
65
|
+
}
|
|
66
|
+
function getHeader(message, name) {
|
|
67
|
+
const lower = name.toLowerCase();
|
|
68
|
+
for (const [k, v] of Object.entries(message.headers)) {
|
|
69
|
+
if (k.toLowerCase() === lower)
|
|
70
|
+
return Array.isArray(v) ? v.join(", ") : v;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function fail(reason) {
|
|
75
|
+
return { valid: false, profile: "rfc9421", reason };
|
|
76
|
+
}
|
|
77
|
+
const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
78
|
+
/**
|
|
79
|
+
* Verify an RFC 9421 signature on `message`. Never throws — every failure is a
|
|
80
|
+
* `{ valid: false, reason }` with a stable {@link SignatureFailureReason}.
|
|
81
|
+
*/
|
|
82
|
+
export async function verifyRfc9421(message, params) {
|
|
83
|
+
const inputHeader = getHeader(message, "signature-input");
|
|
84
|
+
const sigHeader = getHeader(message, "signature");
|
|
85
|
+
if (inputHeader === null || sigHeader === null)
|
|
86
|
+
return fail("signature_missing");
|
|
87
|
+
let inputs;
|
|
88
|
+
let signatures;
|
|
89
|
+
try {
|
|
90
|
+
inputs = parseSignatureInput(inputHeader);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return fail("signature_input_malformed");
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
signatures = parseSignatureHeader(sigHeader);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return fail("signature_malformed");
|
|
100
|
+
}
|
|
101
|
+
// Choose the label to verify.
|
|
102
|
+
let label = params.label;
|
|
103
|
+
if (label === undefined) {
|
|
104
|
+
if (inputs.size === 0)
|
|
105
|
+
return fail("label_missing");
|
|
106
|
+
if (inputs.size > 1)
|
|
107
|
+
return fail("label_ambiguous");
|
|
108
|
+
label = [...inputs.keys()][0];
|
|
109
|
+
}
|
|
110
|
+
const input = inputs.get(label);
|
|
111
|
+
if (input === undefined)
|
|
112
|
+
return fail("label_missing");
|
|
113
|
+
const signature = signatures.get(label);
|
|
114
|
+
if (signature === undefined)
|
|
115
|
+
return fail("signature_missing");
|
|
116
|
+
const sigParams = paramMap(input.params);
|
|
117
|
+
// RFC 9421 §2.3: `alg` is OPTIONAL. When present it must be allow-listed;
|
|
118
|
+
// when absent the algorithm is derived from the resolved key below.
|
|
119
|
+
const algRaw = sigParams.get("alg");
|
|
120
|
+
let alg = null;
|
|
121
|
+
if (algRaw !== undefined) {
|
|
122
|
+
if (typeof algRaw !== "string" || !isSupportedAlgorithm(algRaw)) {
|
|
123
|
+
return fail("alg_unsupported");
|
|
124
|
+
}
|
|
125
|
+
alg = algRaw;
|
|
126
|
+
}
|
|
127
|
+
const keyIdRaw = sigParams.get("keyid");
|
|
128
|
+
const keyId = typeof keyIdRaw === "string" ? keyIdRaw : null;
|
|
129
|
+
if (keyId === null)
|
|
130
|
+
return fail("keyid_missing");
|
|
131
|
+
// created / expires window.
|
|
132
|
+
const now = params.now ?? Math.floor(Date.now() / 1000);
|
|
133
|
+
const tolerance = params.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
134
|
+
let created;
|
|
135
|
+
if (sigParams.has("created")) {
|
|
136
|
+
const c = sigParams.get("created");
|
|
137
|
+
if (typeof c !== "number" || !Number.isInteger(c))
|
|
138
|
+
return fail("created_invalid");
|
|
139
|
+
created = c;
|
|
140
|
+
if (c > now + tolerance)
|
|
141
|
+
return fail("signature_future");
|
|
142
|
+
}
|
|
143
|
+
else if (params.requireCreated) {
|
|
144
|
+
return fail("created_required");
|
|
145
|
+
}
|
|
146
|
+
let expires;
|
|
147
|
+
if (sigParams.has("expires")) {
|
|
148
|
+
const e = sigParams.get("expires");
|
|
149
|
+
if (typeof e !== "number" || !Number.isInteger(e))
|
|
150
|
+
return fail("expires_invalid");
|
|
151
|
+
expires = e;
|
|
152
|
+
if (now > e + tolerance)
|
|
153
|
+
return fail("signature_expired");
|
|
154
|
+
}
|
|
155
|
+
// RFC 9421 §2.3: `expires` MUST NOT precede `created`.
|
|
156
|
+
if (created !== undefined && expires !== undefined && expires < created) {
|
|
157
|
+
return fail("expires_invalid");
|
|
158
|
+
}
|
|
159
|
+
const coveredComponents = input.items.map((item) => String(item.value));
|
|
160
|
+
// Required-component policy.
|
|
161
|
+
if (params.requiredComponents) {
|
|
162
|
+
const covered = new Set(coveredComponents.map((c) => c.toLowerCase()));
|
|
163
|
+
for (const required of params.requiredComponents) {
|
|
164
|
+
if (!covered.has(required.toLowerCase()))
|
|
165
|
+
return fail("required_component_missing");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Rebuild the signature base from the named components.
|
|
169
|
+
const ctx = derivationContext(message);
|
|
170
|
+
if (ctx === null)
|
|
171
|
+
return fail("covered_component_missing");
|
|
172
|
+
const lines = [];
|
|
173
|
+
// RFC 9421 §2 / §2.5: each component identifier (name plus its serialized
|
|
174
|
+
// parameters) MUST occur only once; a repeated identifier is malformed.
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
for (const item of input.items) {
|
|
177
|
+
if (item.params.length > 0 || typeof item.value !== "string") {
|
|
178
|
+
return fail("components_malformed");
|
|
179
|
+
}
|
|
180
|
+
const id = item.value.toLowerCase();
|
|
181
|
+
if (seen.has(id))
|
|
182
|
+
return fail("components_malformed");
|
|
183
|
+
seen.add(id);
|
|
184
|
+
const value = deriveComponentValue(ctx, item.value);
|
|
185
|
+
if (value === null)
|
|
186
|
+
return fail("covered_component_missing");
|
|
187
|
+
lines.push(`${item.raw}: ${value}`);
|
|
188
|
+
}
|
|
189
|
+
const base = [...lines, `"@signature-params": ${input.raw}`].join("\n");
|
|
190
|
+
// Resolve and validate the key, then verify. When `alg` was omitted, derive
|
|
191
|
+
// it from the resolved key's type (RFC 9421 §2.3).
|
|
192
|
+
const key = await params.resolveKey({ keyId, alg });
|
|
193
|
+
if (key === null)
|
|
194
|
+
return fail("key_unresolved");
|
|
195
|
+
if (alg === null) {
|
|
196
|
+
alg = deriveAlgFromKey(key);
|
|
197
|
+
if (alg === null)
|
|
198
|
+
return fail("alg_unsupported");
|
|
199
|
+
}
|
|
200
|
+
const rejection = validateKey(key, alg);
|
|
201
|
+
if (rejection !== null)
|
|
202
|
+
return fail(rejection);
|
|
203
|
+
const ok = await verifyBytes(key, alg, signature, utf8(base));
|
|
204
|
+
if (!ok)
|
|
205
|
+
return fail("signature_invalid");
|
|
206
|
+
return {
|
|
207
|
+
valid: true,
|
|
208
|
+
profile: "rfc9421",
|
|
209
|
+
label,
|
|
210
|
+
keyId,
|
|
211
|
+
coveredComponents,
|
|
212
|
+
created,
|
|
213
|
+
expires,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=rfc9421.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rfc9421.js","sourceRoot":"","sources":["../src/rfc9421.ts"],"names":[],"mappings":"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"}
|