@forestrie/receipt-verify 0.11.0 → 1.0.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/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/resolve-delegated-verify-key.d.ts +6 -0
- package/dist/resolve-delegated-verify-key.d.ts.map +1 -1
- package/dist/resolve-delegated-verify-key.js +10 -3
- package/dist/session-key-endorsement.d.ts +161 -0
- package/dist/session-key-endorsement.d.ts.map +1 -0
- package/dist/session-key-endorsement.js +299 -0
- package/dist/verify-endorsed-leaf.d.ts +102 -0
- package/dist/verify-endorsed-leaf.d.ts.map +1 -0
- package/dist/verify-endorsed-leaf.js +189 -0
- package/package.json +2 -2
- package/src/index.ts +44 -0
- package/src/resolve-delegated-verify-key.ts +12 -3
- package/src/session-key-endorsement.ts +445 -0
- package/src/verify-endorsed-leaf.ts +287 -0
package/dist/index.d.ts
CHANGED
|
@@ -24,10 +24,25 @@ export { verifyReceiptOffline } from "./verify-grant-receipt-offline.js";
|
|
|
24
24
|
*/
|
|
25
25
|
export { verifyGrantReceiptOfflineWithKeys, verifyReceiptOfflineWithKeys, } from "./verify-grant-receipt-offline.js";
|
|
26
26
|
export type { VerifyGrantReceiptOfflineWithKeysInput, VerifyReceiptOfflineWithKeysInput, } from "./verify-grant-receipt-offline.js";
|
|
27
|
-
export { checkDelegationConstraints, idtimestampToUnixSeconds, resolveDelegatedVerifyKey, } from "./resolve-delegated-verify-key.js";
|
|
27
|
+
export { checkDelegationConstraints, idtimestampToUnixMs, idtimestampToUnixSeconds, resolveDelegatedVerifyKey, } from "./resolve-delegated-verify-key.js";
|
|
28
28
|
export type { DelegatedResolution, DelegationConstraints, } from "./resolve-delegated-verify-key.js";
|
|
29
29
|
/** Import a raw 64-byte x||y P-256 public key as an ES256 verify key. */
|
|
30
30
|
export { importEs256PublicKeyFromGrantDataXy64 } from "./decode-trust-root-cbor.js";
|
|
31
|
+
/**
|
|
32
|
+
* Passkey session-key endorsement v2 (ADR-0064 as amended by ADR-0065):
|
|
33
|
+
* build, assemble, and verify the artifact chaining a passkey log root to the
|
|
34
|
+
* plain-ES256 per-turn session key for a validity window. The endorsement
|
|
35
|
+
* rides inside every endorsed leaf at unprotected label -65801
|
|
36
|
+
* (`COSE_LABEL_SESSION_KEY_ENDORSEMENT`, @forestrie/encoding).
|
|
37
|
+
*/
|
|
38
|
+
export { assembleSessionKeyEndorsement, buildSessionKeyEndorsementTbs, checkEndorsementWindow, DEFAULT_ENDORSEMENT_WINDOW_MS, NOT_AFTER_PAYLOAD_KEY, NOT_BEFORE_PAYLOAD_KEY, SESSION_KEY_ENDORSEMENT_CONTENT_TYPE, SESSION_KEY_ENDORSEMENT_V1_CONTENT_TYPE, SESSION_KEY_PAYLOAD_KEY, verifySessionKeyEndorsement, } from "./session-key-endorsement.js";
|
|
39
|
+
export type { EndorsementWindow, EndorsementWindowCheck, SessionKeyEndorsementFailureReason, SessionKeyEndorsementTbs, SessionKeyEndorsementVerifyResult, VerifySessionKeyEndorsementOptions, } from "./session-key-endorsement.js";
|
|
40
|
+
/**
|
|
41
|
+
* The single offline rung for a passkey-rooted log (ADR-0065 §5): root →
|
|
42
|
+
* endorsement (inside the leaf) → session key → leaf → receipt + window.
|
|
43
|
+
*/
|
|
44
|
+
export { endorsementAdmissionReason, extractLeafEndorsement, verifyEndorsedLeaf, } from "./verify-endorsed-leaf.js";
|
|
45
|
+
export type { EndorsementAdmissionReason, VerifyEndorsedLeafInput, VerifyEndorsedLeafOptions, VerifyEndorsedLeafResult, VerifyEndorsedLeafStage, } from "./verify-endorsed-leaf.js";
|
|
31
46
|
export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
|
|
32
47
|
export { decodeGrantPayload, decodeGrantResponse } from "./grant-codec.js";
|
|
33
48
|
/** Deterministic receipt construction (plan-2607-12 Phase 2, FOR-353). */
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,oFAAoF;AACpF,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,YAAY,EAAE,8BAA8B,EAAE,MAAM,mCAAmC,CAAC;AACxF,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC,+EAA+E;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EACV,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE;;;;GAIG;AACH,OAAO,EACL,iCAAiC,EACjC,4BAA4B,GAC7B,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,sCAAsC,EACtC,iCAAiC,GAClC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,mCAAmC,CAAC;AAC3C,yEAAyE;AACzE,OAAO,EAAE,qCAAqC,EAAE,MAAM,6BAA6B,CAAC;AACpF,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,0EAA0E;AAC1E,OAAO,EACL,cAAc,EACd,iDAAiD,GAClD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,6EAA6E;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,oEAAoE;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,8EAA8E;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,kBAAkB,EAClB,0BAA0B,EAC1B,4BAA4B,EAC5B,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,CAAC;AAC/B;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD;;;;;;GAMG;AACH,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,2CAA2C,EAC3C,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -16,9 +16,22 @@ export { verifyReceiptOffline } from "./verify-grant-receipt-offline.js";
|
|
|
16
16
|
* See the trust-ladder notes on verifyReceiptOfflineWithKeys.
|
|
17
17
|
*/
|
|
18
18
|
export { verifyGrantReceiptOfflineWithKeys, verifyReceiptOfflineWithKeys, } from "./verify-grant-receipt-offline.js";
|
|
19
|
-
export { checkDelegationConstraints, idtimestampToUnixSeconds, resolveDelegatedVerifyKey, } from "./resolve-delegated-verify-key.js";
|
|
19
|
+
export { checkDelegationConstraints, idtimestampToUnixMs, idtimestampToUnixSeconds, resolveDelegatedVerifyKey, } from "./resolve-delegated-verify-key.js";
|
|
20
20
|
/** Import a raw 64-byte x||y P-256 public key as an ES256 verify key. */
|
|
21
21
|
export { importEs256PublicKeyFromGrantDataXy64 } from "./decode-trust-root-cbor.js";
|
|
22
|
+
/**
|
|
23
|
+
* Passkey session-key endorsement v2 (ADR-0064 as amended by ADR-0065):
|
|
24
|
+
* build, assemble, and verify the artifact chaining a passkey log root to the
|
|
25
|
+
* plain-ES256 per-turn session key for a validity window. The endorsement
|
|
26
|
+
* rides inside every endorsed leaf at unprotected label -65801
|
|
27
|
+
* (`COSE_LABEL_SESSION_KEY_ENDORSEMENT`, @forestrie/encoding).
|
|
28
|
+
*/
|
|
29
|
+
export { assembleSessionKeyEndorsement, buildSessionKeyEndorsementTbs, checkEndorsementWindow, DEFAULT_ENDORSEMENT_WINDOW_MS, NOT_AFTER_PAYLOAD_KEY, NOT_BEFORE_PAYLOAD_KEY, SESSION_KEY_ENDORSEMENT_CONTENT_TYPE, SESSION_KEY_ENDORSEMENT_V1_CONTENT_TYPE, SESSION_KEY_PAYLOAD_KEY, verifySessionKeyEndorsement, } from "./session-key-endorsement.js";
|
|
30
|
+
/**
|
|
31
|
+
* The single offline rung for a passkey-rooted log (ADR-0065 §5): root →
|
|
32
|
+
* endorsement (inside the leaf) → session key → leaf → receipt + window.
|
|
33
|
+
*/
|
|
34
|
+
export { endorsementAdmissionReason, extractLeafEndorsement, verifyEndorsedLeaf, } from "./verify-endorsed-leaf.js";
|
|
22
35
|
export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
|
|
23
36
|
export { decodeGrantPayload, decodeGrantResponse } from "./grant-codec.js";
|
|
24
37
|
/** Deterministic receipt construction (plan-2607-12 Phase 2, FOR-353). */
|
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
* earlier "does not yet enforce the window" gap.
|
|
36
36
|
*/
|
|
37
37
|
export declare function idtimestampToUnixSeconds(idtimestamp: bigint): number;
|
|
38
|
+
/**
|
|
39
|
+
* Snowflake idtimestamp → Unix milliseconds: the time component an
|
|
40
|
+
* ADR-0065 §3 endorsement window is expressed in
|
|
41
|
+
* (`ms = (id >> TimeShift) + EpochMS(epoch)`).
|
|
42
|
+
*/
|
|
43
|
+
export declare function idtimestampToUnixMs(idtimestamp: bigint): number;
|
|
38
44
|
/**
|
|
39
45
|
* Coverage + validity window a root-signed delegation cert imposes on its
|
|
40
46
|
* delegated key. `mmrStart`/`mmrEnd` are inclusive MMR-index bounds;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-delegated-verify-key.d.ts","sourceRoot":"","sources":["../src/resolve-delegated-verify-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAqCH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"resolve-delegated-verify-key.d.ts","sourceRoot":"","sources":["../src/resolve-delegated-verify-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAqCH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,EAAE,SAAS,CAAC;IACxB,WAAW,EAAE,qBAAqB,GAAG,IAAI,CAAC;CAC3C,GACD;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAsGvB;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,qBAAqB,EAClC,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,MAAM,GACtB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAS9C;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,mBAAmB,CAAC,CAgC9B"}
|
|
@@ -60,9 +60,16 @@ const COMMITMENT_EPOCH = 1n;
|
|
|
60
60
|
const SNOWFLAKE_TIME_SHIFT = 24n;
|
|
61
61
|
const SNOWFLAKE_EPOCH_SPAN_MS = (1n << 40n) - 1n;
|
|
62
62
|
export function idtimestampToUnixSeconds(idtimestamp) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
return Math.floor(idtimestampToUnixMs(idtimestamp) / 1000);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Snowflake idtimestamp → Unix milliseconds: the time component an
|
|
67
|
+
* ADR-0065 §3 endorsement window is expressed in
|
|
68
|
+
* (`ms = (id >> TimeShift) + EpochMS(epoch)`).
|
|
69
|
+
*/
|
|
70
|
+
export function idtimestampToUnixMs(idtimestamp) {
|
|
71
|
+
return Number(COMMITMENT_EPOCH * SNOWFLAKE_EPOCH_SPAN_MS +
|
|
72
|
+
(idtimestamp >> SNOWFLAKE_TIME_SHIFT));
|
|
66
73
|
}
|
|
67
74
|
function extractDelegationCertBytes(unprotected) {
|
|
68
75
|
const umap = coseUnprotectedToMap(unprotected);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey session-key endorsement, payload v2 (devdocs ADR-0064 as amended by
|
|
3
|
+
* ADR-0065 §3; plan-2608-14 1.2).
|
|
4
|
+
*
|
|
5
|
+
* A passkey log root signs ceremonies only; the per-turn user-envelope
|
|
6
|
+
* signer is a separate session key, endorsed by the root via a COSE Sign1 in
|
|
7
|
+
* the ADR-0063 WebAuthn envelope. This module owns both sides of that
|
|
8
|
+
* artifact's byte-exact shape:
|
|
9
|
+
*
|
|
10
|
+
* - build/assemble: the client constructs the TBS, hashes its Sig_structure
|
|
11
|
+
* into the WebAuthn challenge, and attaches the assertion + signature;
|
|
12
|
+
* - verify: given the passkey root as trust anchor, validate the artifact
|
|
13
|
+
* (typed, challenge-bound, fail-closed) and yield the session public key
|
|
14
|
+
* and its validity window.
|
|
15
|
+
*
|
|
16
|
+
* The endorsement rides INSIDE every endorsed leaf (unprotected label
|
|
17
|
+
* `COSE_LABEL_SESSION_KEY_ENDORSEMENT`, -65801) and the chain
|
|
18
|
+
* root → endorsement → session key → leaf → receipt is walked by
|
|
19
|
+
* {@link verifyEndorsedLeaf} (the single offline rung, ADR-0065 §5) and by
|
|
20
|
+
* canopy SCRAPI admission (§4).
|
|
21
|
+
*
|
|
22
|
+
* Domain separation: the protected content type below names payload v2 and
|
|
23
|
+
* the payload is EXACTLY `{"sessionKey": bstr .size 64, "notBefore": uint,
|
|
24
|
+
* "notAfter": uint}` (unix milliseconds, the idtimestamp time domain).
|
|
25
|
+
* Anything else — including a v1 (window-less) artifact — is a verification
|
|
26
|
+
* failure, never a fallback (ADR-0065 §3: v1 is rejected, not grandfathered).
|
|
27
|
+
*
|
|
28
|
+
* The window itself is NOT checked here: the reference time differs per
|
|
29
|
+
* verifier (canopy's clock at admission; the receipted idtimestamp offline).
|
|
30
|
+
* Callers apply {@link checkEndorsementWindow} with the time they hold.
|
|
31
|
+
*/
|
|
32
|
+
import { type ParsedVerifyKey } from "@forestrie/encoding";
|
|
33
|
+
/**
|
|
34
|
+
* Protected-header content type (label 3) naming the v2 endorsement artifact.
|
|
35
|
+
* Signed via Sig_structure, so both the artifact's type AND its payload
|
|
36
|
+
* version are non-malleable.
|
|
37
|
+
*/
|
|
38
|
+
export declare const SESSION_KEY_ENDORSEMENT_CONTENT_TYPE = "application/vnd.forestrie.session-key-endorsement.v2+cbor";
|
|
39
|
+
/**
|
|
40
|
+
* The retired ADR-0064 v1 content type (payload without a window). Exported
|
|
41
|
+
* only so verifiers and tests can name what is being REJECTED; no code path
|
|
42
|
+
* accepts it.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SESSION_KEY_ENDORSEMENT_V1_CONTENT_TYPE = "application/vnd.forestrie.session-key-endorsement+cbor";
|
|
45
|
+
/** Payload map key holding the endorsed session public key (x‖y, 64 bytes). */
|
|
46
|
+
export declare const SESSION_KEY_PAYLOAD_KEY = "sessionKey";
|
|
47
|
+
/** Payload map key: window start, unix milliseconds, inclusive. */
|
|
48
|
+
export declare const NOT_BEFORE_PAYLOAD_KEY = "notBefore";
|
|
49
|
+
/** Payload map key: window end, unix milliseconds, inclusive. */
|
|
50
|
+
export declare const NOT_AFTER_PAYLOAD_KEY = "notAfter";
|
|
51
|
+
/** Default client window length: 7 days (ADR-0065 §3). */
|
|
52
|
+
export declare const DEFAULT_ENDORSEMENT_WINDOW_MS: number;
|
|
53
|
+
/** Validity window of an endorsement, unix milliseconds, both ends inclusive. */
|
|
54
|
+
export interface EndorsementWindow {
|
|
55
|
+
notBefore: number;
|
|
56
|
+
notAfter: number;
|
|
57
|
+
}
|
|
58
|
+
/** To-be-signed endorsement halves plus the challenge preimage. */
|
|
59
|
+
export interface SessionKeyEndorsementTbs {
|
|
60
|
+
/** Protected header map bytes `{1: -65800, 3: cty(v2), 4: root x}`. */
|
|
61
|
+
protectedBstr: Uint8Array;
|
|
62
|
+
/** Payload bytes: deterministic CBOR `{sessionKey, notBefore, notAfter}`. */
|
|
63
|
+
payloadBstr: Uint8Array;
|
|
64
|
+
/**
|
|
65
|
+
* `Sig_structure` over the halves. The WebAuthn challenge MUST be
|
|
66
|
+
* `base64url(sha256(sigStructureBytes))` (ADR-0063 §3).
|
|
67
|
+
*/
|
|
68
|
+
sigStructureBytes: Uint8Array;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the v2 endorsement TBS. The caller hashes `sigStructureBytes` into
|
|
72
|
+
* the `navigator.credentials.get` challenge, then assembles with the assertion.
|
|
73
|
+
*
|
|
74
|
+
* @param input.rootPublicKeyX - Passkey root x coordinate (kid, 32 bytes)
|
|
75
|
+
* @param input.sessionPublicKeyXY - Endorsed session public key x‖y (64 bytes)
|
|
76
|
+
* @param input.notBefore - Window start, unix ms inclusive
|
|
77
|
+
* @param input.notAfter - Window end, unix ms inclusive; must exceed notBefore
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildSessionKeyEndorsementTbs(input: {
|
|
80
|
+
rootPublicKeyX: Uint8Array;
|
|
81
|
+
sessionPublicKeyXY: Uint8Array;
|
|
82
|
+
notBefore: number;
|
|
83
|
+
notAfter: number;
|
|
84
|
+
}): SessionKeyEndorsementTbs;
|
|
85
|
+
/**
|
|
86
|
+
* Assemble the final endorsement COSE Sign1: the assertion rides in the
|
|
87
|
+
* unprotected header at label -65800 (ADR-0063 §2), the signature is the
|
|
88
|
+
* assertion's, converted to low-s P1363 r‖s by the caller (see
|
|
89
|
+
* delegation-cose `derSignatureToP1363` / low-s normalisation).
|
|
90
|
+
*/
|
|
91
|
+
export declare function assembleSessionKeyEndorsement(input: {
|
|
92
|
+
tbs: SessionKeyEndorsementTbs;
|
|
93
|
+
authenticatorData: Uint8Array;
|
|
94
|
+
clientDataJSON: Uint8Array;
|
|
95
|
+
/** Low-s P1363 r‖s (64 bytes). */
|
|
96
|
+
signature: Uint8Array;
|
|
97
|
+
}): Uint8Array;
|
|
98
|
+
/** Verification failure reasons; the first check that broke. */
|
|
99
|
+
export type SessionKeyEndorsementFailureReason = "endorsement_malformed" | "wrong_alg" | "wrong_content_type" | "kid_invalid" | "kid_mismatch" | "payload_invalid" | "window_invalid" | "uv_required" | "signature_invalid" | "session_key_import_failed";
|
|
100
|
+
/** Verification outcome; failures name the first check that broke. */
|
|
101
|
+
export type SessionKeyEndorsementVerifyResult = ({
|
|
102
|
+
ok: true;
|
|
103
|
+
/** Endorsed session public key, raw x‖y (64 bytes). */
|
|
104
|
+
sessionPublicKeyXY: Uint8Array;
|
|
105
|
+
/** The same key imported for ES256 verify of per-turn leaves. */
|
|
106
|
+
sessionKey: CryptoKey;
|
|
107
|
+
} & EndorsementWindow) | {
|
|
108
|
+
ok: false;
|
|
109
|
+
reason: SessionKeyEndorsementFailureReason;
|
|
110
|
+
};
|
|
111
|
+
/** Options for {@link verifySessionKeyEndorsement}. */
|
|
112
|
+
export interface VerifySessionKeyEndorsementOptions {
|
|
113
|
+
/**
|
|
114
|
+
* Require the assertion's UV flag. At admission this is the grant's
|
|
115
|
+
* `GF_REQUIRES_USER_VERIFICATION` (ADR-0065 §4); at DO onboarding, where
|
|
116
|
+
* no grant exists yet, deployment config (ADR-0064 §3). User presence is
|
|
117
|
+
* always required regardless.
|
|
118
|
+
*/
|
|
119
|
+
requireUserVerification?: boolean;
|
|
120
|
+
/** Emit JSON warning lines on failure paths (no secrets). */
|
|
121
|
+
logFailures?: boolean;
|
|
122
|
+
/** Included in JSON log lines. */
|
|
123
|
+
logPrefix?: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Verify a v2 session-key endorsement under the passkey root and yield the
|
|
127
|
+
* endorsed session key plus its window. All the ADR-0063 envelope checks
|
|
128
|
+
* (challenge binding to this artifact's `Sig_structure`, UP flag, ceremony
|
|
129
|
+
* type, low-s, both fail-closed directions) run inside the shared `-65800`
|
|
130
|
+
* verify branch; this function adds the ADR-0064/0065 typing rules on top
|
|
131
|
+
* and reports a missing-UV rejection under its own reason so admission can
|
|
132
|
+
* name it (`endorsement_uv_required`, ADR-0065 §4).
|
|
133
|
+
*
|
|
134
|
+
* @param endorsementCbor - The endorsement COSE Sign1 bytes
|
|
135
|
+
* @param rootKeys - Passkey root trust anchor(s), tried in order. Raw
|
|
136
|
+
* coordinate anchors additionally pin `kid == root x`.
|
|
137
|
+
* @param opts - UV requirement and failure logging
|
|
138
|
+
*/
|
|
139
|
+
export declare function verifySessionKeyEndorsement(endorsementCbor: Uint8Array, rootKeys: ParsedVerifyKey | ParsedVerifyKey[], opts?: VerifySessionKeyEndorsementOptions): Promise<SessionKeyEndorsementVerifyResult>;
|
|
140
|
+
/** Outcome of {@link checkEndorsementWindow}. */
|
|
141
|
+
export type EndorsementWindowCheck = {
|
|
142
|
+
ok: true;
|
|
143
|
+
} | {
|
|
144
|
+
ok: false;
|
|
145
|
+
reason: "endorsement_not_yet_valid" | "endorsement_expired";
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Is `atMs` inside the endorsement's window? `notBefore ≤ atMs ≤ notAfter`,
|
|
149
|
+
* both inclusive (ADR-0065 §3). `skewMs` (default 0) tolerates a `notBefore`
|
|
150
|
+
* slightly in the future of a wall-clock verifier; offline verifiers
|
|
151
|
+
* comparing against the receipted idtimestamp should leave it at 0 — that
|
|
152
|
+
* comparison is exact and authoritative.
|
|
153
|
+
*
|
|
154
|
+
* Deliberately a pure function of the two numbers so callers can (and must)
|
|
155
|
+
* run it on every request, structurally outside any verified-endorsement
|
|
156
|
+
* cache (ADR-0065 §4).
|
|
157
|
+
*/
|
|
158
|
+
export declare function checkEndorsementWindow(window: EndorsementWindow, atMs: number, opts?: {
|
|
159
|
+
skewMs?: number;
|
|
160
|
+
}): EndorsementWindowCheck;
|
|
161
|
+
//# sourceMappingURL=session-key-endorsement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-key-endorsement.d.ts","sourceRoot":"","sources":["../src/session-key-endorsement.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey session-key endorsement, payload v2 (devdocs ADR-0064 as amended by
|
|
3
|
+
* ADR-0065 §3; plan-2608-14 1.2).
|
|
4
|
+
*
|
|
5
|
+
* A passkey log root signs ceremonies only; the per-turn user-envelope
|
|
6
|
+
* signer is a separate session key, endorsed by the root via a COSE Sign1 in
|
|
7
|
+
* the ADR-0063 WebAuthn envelope. This module owns both sides of that
|
|
8
|
+
* artifact's byte-exact shape:
|
|
9
|
+
*
|
|
10
|
+
* - build/assemble: the client constructs the TBS, hashes its Sig_structure
|
|
11
|
+
* into the WebAuthn challenge, and attaches the assertion + signature;
|
|
12
|
+
* - verify: given the passkey root as trust anchor, validate the artifact
|
|
13
|
+
* (typed, challenge-bound, fail-closed) and yield the session public key
|
|
14
|
+
* and its validity window.
|
|
15
|
+
*
|
|
16
|
+
* The endorsement rides INSIDE every endorsed leaf (unprotected label
|
|
17
|
+
* `COSE_LABEL_SESSION_KEY_ENDORSEMENT`, -65801) and the chain
|
|
18
|
+
* root → endorsement → session key → leaf → receipt is walked by
|
|
19
|
+
* {@link verifyEndorsedLeaf} (the single offline rung, ADR-0065 §5) and by
|
|
20
|
+
* canopy SCRAPI admission (§4).
|
|
21
|
+
*
|
|
22
|
+
* Domain separation: the protected content type below names payload v2 and
|
|
23
|
+
* the payload is EXACTLY `{"sessionKey": bstr .size 64, "notBefore": uint,
|
|
24
|
+
* "notAfter": uint}` (unix milliseconds, the idtimestamp time domain).
|
|
25
|
+
* Anything else — including a v1 (window-less) artifact — is a verification
|
|
26
|
+
* failure, never a fallback (ADR-0065 §3: v1 is rejected, not grandfathered).
|
|
27
|
+
*
|
|
28
|
+
* The window itself is NOT checked here: the reference time differs per
|
|
29
|
+
* verifier (canopy's clock at admission; the receipted idtimestamp offline).
|
|
30
|
+
* Callers apply {@link checkEndorsementWindow} with the time they hold.
|
|
31
|
+
*/
|
|
32
|
+
import { COSE_ALG_ES256_WEBAUTHN, coseUnprotectedToMap, decodeCborDeterministic, decodeCoseSign1, encodeCborDeterministic, encodeCoseSign1Raw, encodeSigStructure, verifyCoseSign1WithParsedKey, WEBAUTHN_ENVELOPE_LABEL, } from "@forestrie/encoding";
|
|
33
|
+
import { importEs256PublicKeyFromGrantDataXy64 } from "./decode-trust-root-cbor.js";
|
|
34
|
+
/**
|
|
35
|
+
* Protected-header content type (label 3) naming the v2 endorsement artifact.
|
|
36
|
+
* Signed via Sig_structure, so both the artifact's type AND its payload
|
|
37
|
+
* version are non-malleable.
|
|
38
|
+
*/
|
|
39
|
+
export const SESSION_KEY_ENDORSEMENT_CONTENT_TYPE = "application/vnd.forestrie.session-key-endorsement.v2+cbor";
|
|
40
|
+
/**
|
|
41
|
+
* The retired ADR-0064 v1 content type (payload without a window). Exported
|
|
42
|
+
* only so verifiers and tests can name what is being REJECTED; no code path
|
|
43
|
+
* accepts it.
|
|
44
|
+
*/
|
|
45
|
+
export const SESSION_KEY_ENDORSEMENT_V1_CONTENT_TYPE = "application/vnd.forestrie.session-key-endorsement+cbor";
|
|
46
|
+
/** Payload map key holding the endorsed session public key (x‖y, 64 bytes). */
|
|
47
|
+
export const SESSION_KEY_PAYLOAD_KEY = "sessionKey";
|
|
48
|
+
/** Payload map key: window start, unix milliseconds, inclusive. */
|
|
49
|
+
export const NOT_BEFORE_PAYLOAD_KEY = "notBefore";
|
|
50
|
+
/** Payload map key: window end, unix milliseconds, inclusive. */
|
|
51
|
+
export const NOT_AFTER_PAYLOAD_KEY = "notAfter";
|
|
52
|
+
/** Default client window length: 7 days (ADR-0065 §3). */
|
|
53
|
+
export const DEFAULT_ENDORSEMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
54
|
+
/** WebAuthn authenticatorData UV flag bit (WebAuthn L2 §6.1). */
|
|
55
|
+
const AUTH_FLAG_UV = 0x04;
|
|
56
|
+
function isUnixMs(v) {
|
|
57
|
+
return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Structural window check shared by the builder and the verifier: both ends
|
|
61
|
+
* are unsigned safe integers and the window is non-empty (ADR-0065 §3
|
|
62
|
+
* "malformed windows are rejected": `notAfter ≤ notBefore`).
|
|
63
|
+
*/
|
|
64
|
+
function windowWellFormed(w) {
|
|
65
|
+
return (isUnixMs(w.notBefore) && isUnixMs(w.notAfter) && w.notAfter > w.notBefore);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the v2 endorsement TBS. The caller hashes `sigStructureBytes` into
|
|
69
|
+
* the `navigator.credentials.get` challenge, then assembles with the assertion.
|
|
70
|
+
*
|
|
71
|
+
* @param input.rootPublicKeyX - Passkey root x coordinate (kid, 32 bytes)
|
|
72
|
+
* @param input.sessionPublicKeyXY - Endorsed session public key x‖y (64 bytes)
|
|
73
|
+
* @param input.notBefore - Window start, unix ms inclusive
|
|
74
|
+
* @param input.notAfter - Window end, unix ms inclusive; must exceed notBefore
|
|
75
|
+
*/
|
|
76
|
+
export function buildSessionKeyEndorsementTbs(input) {
|
|
77
|
+
if (input.rootPublicKeyX.length !== 32) {
|
|
78
|
+
throw new Error("endorsement kid must be the 32-byte root x coordinate");
|
|
79
|
+
}
|
|
80
|
+
if (input.sessionPublicKeyXY.length !== 64) {
|
|
81
|
+
throw new Error("endorsed session key must be 64 bytes (x||y)");
|
|
82
|
+
}
|
|
83
|
+
if (!windowWellFormed(input)) {
|
|
84
|
+
throw new Error("endorsement window must be unsigned safe-integer unix ms with notAfter > notBefore");
|
|
85
|
+
}
|
|
86
|
+
const protectedBstr = encodeCborDeterministic(new Map([
|
|
87
|
+
[1, COSE_ALG_ES256_WEBAUTHN],
|
|
88
|
+
[3, SESSION_KEY_ENDORSEMENT_CONTENT_TYPE],
|
|
89
|
+
[4, input.rootPublicKeyX],
|
|
90
|
+
]));
|
|
91
|
+
// Deterministic CBOR sorts the keys (RFC 8949 §4.2.1); insertion order here
|
|
92
|
+
// is irrelevant to the bytes.
|
|
93
|
+
const payloadBstr = encodeCborDeterministic(new Map([
|
|
94
|
+
[SESSION_KEY_PAYLOAD_KEY, input.sessionPublicKeyXY],
|
|
95
|
+
[NOT_BEFORE_PAYLOAD_KEY, input.notBefore],
|
|
96
|
+
[NOT_AFTER_PAYLOAD_KEY, input.notAfter],
|
|
97
|
+
]));
|
|
98
|
+
const sigStructureBytes = encodeSigStructure(protectedBstr, new Uint8Array(0), payloadBstr);
|
|
99
|
+
return { protectedBstr, payloadBstr, sigStructureBytes };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Assemble the final endorsement COSE Sign1: the assertion rides in the
|
|
103
|
+
* unprotected header at label -65800 (ADR-0063 §2), the signature is the
|
|
104
|
+
* assertion's, converted to low-s P1363 r‖s by the caller (see
|
|
105
|
+
* delegation-cose `derSignatureToP1363` / low-s normalisation).
|
|
106
|
+
*/
|
|
107
|
+
export function assembleSessionKeyEndorsement(input) {
|
|
108
|
+
if (input.signature.length !== 64) {
|
|
109
|
+
throw new Error("endorsement signature must be 64-byte P1363 r||s");
|
|
110
|
+
}
|
|
111
|
+
const unprotected = new Map([
|
|
112
|
+
[WEBAUTHN_ENVELOPE_LABEL, [input.authenticatorData, input.clientDataJSON]],
|
|
113
|
+
]);
|
|
114
|
+
return encodeCoseSign1Raw(input.tbs.protectedBstr, unprotected, input.tbs.payloadBstr, input.signature);
|
|
115
|
+
}
|
|
116
|
+
function protectedHeaderEntries(protectedBstr) {
|
|
117
|
+
let decoded;
|
|
118
|
+
try {
|
|
119
|
+
decoded = decodeCborDeterministic(protectedBstr);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (decoded instanceof Map) {
|
|
125
|
+
return { alg: decoded.get(1), cty: decoded.get(3), kid: decoded.get(4) };
|
|
126
|
+
}
|
|
127
|
+
if (typeof decoded === "object" && decoded !== null) {
|
|
128
|
+
const obj = decoded;
|
|
129
|
+
return {
|
|
130
|
+
alg: obj[1] ?? obj["1"],
|
|
131
|
+
cty: obj[3] ?? obj["3"],
|
|
132
|
+
kid: obj[4] ?? obj["4"],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function toNumberIfInt(v) {
|
|
138
|
+
// Deterministic decode may surface large ints as bigint; the window is
|
|
139
|
+
// unix ms (< 2^53) so a safe bigint is folded back to a number.
|
|
140
|
+
if (typeof v === "bigint" &&
|
|
141
|
+
v >= 0n &&
|
|
142
|
+
v <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
143
|
+
return Number(v);
|
|
144
|
+
}
|
|
145
|
+
return v;
|
|
146
|
+
}
|
|
147
|
+
function decodePayloadV2(payloadBstr) {
|
|
148
|
+
let decoded;
|
|
149
|
+
try {
|
|
150
|
+
decoded = decodeCborDeterministic(payloadBstr);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return { kind: "payload_invalid" };
|
|
154
|
+
}
|
|
155
|
+
// Exactly three entries, exactly the three labels, exactly these shapes
|
|
156
|
+
// (ADR-0065 §3) — anything else is a failure, never a partial read.
|
|
157
|
+
let entries;
|
|
158
|
+
if (decoded instanceof Map) {
|
|
159
|
+
entries = [...decoded.entries()];
|
|
160
|
+
}
|
|
161
|
+
else if (typeof decoded === "object" && decoded !== null) {
|
|
162
|
+
entries = Object.entries(decoded);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
return { kind: "payload_invalid" };
|
|
166
|
+
}
|
|
167
|
+
if (entries.length !== 3)
|
|
168
|
+
return { kind: "payload_invalid" };
|
|
169
|
+
const byKey = new Map(entries);
|
|
170
|
+
const sessionPublicKeyXY = byKey.get(SESSION_KEY_PAYLOAD_KEY);
|
|
171
|
+
if (!(sessionPublicKeyXY instanceof Uint8Array) ||
|
|
172
|
+
sessionPublicKeyXY.length !== 64) {
|
|
173
|
+
return { kind: "payload_invalid" };
|
|
174
|
+
}
|
|
175
|
+
const notBefore = toNumberIfInt(byKey.get(NOT_BEFORE_PAYLOAD_KEY));
|
|
176
|
+
const notAfter = toNumberIfInt(byKey.get(NOT_AFTER_PAYLOAD_KEY));
|
|
177
|
+
if (!isUnixMs(notBefore) || !isUnixMs(notAfter)) {
|
|
178
|
+
return { kind: "payload_invalid" };
|
|
179
|
+
}
|
|
180
|
+
const window = { notBefore, notAfter };
|
|
181
|
+
if (!windowWellFormed(window))
|
|
182
|
+
return { kind: "window_invalid" };
|
|
183
|
+
return { kind: "ok", sessionPublicKeyXY, window };
|
|
184
|
+
}
|
|
185
|
+
/** Read the assertion's flags byte from the -65800 envelope, if well-shaped. */
|
|
186
|
+
function envelopeFlags(unprotected) {
|
|
187
|
+
const envelope = coseUnprotectedToMap(unprotected).get(WEBAUTHN_ENVELOPE_LABEL);
|
|
188
|
+
if (!Array.isArray(envelope) || envelope.length !== 2)
|
|
189
|
+
return null;
|
|
190
|
+
const authenticatorData = envelope[0];
|
|
191
|
+
if (!(authenticatorData instanceof Uint8Array) ||
|
|
192
|
+
authenticatorData.length < 37) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return authenticatorData[32];
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Verify a v2 session-key endorsement under the passkey root and yield the
|
|
199
|
+
* endorsed session key plus its window. All the ADR-0063 envelope checks
|
|
200
|
+
* (challenge binding to this artifact's `Sig_structure`, UP flag, ceremony
|
|
201
|
+
* type, low-s, both fail-closed directions) run inside the shared `-65800`
|
|
202
|
+
* verify branch; this function adds the ADR-0064/0065 typing rules on top
|
|
203
|
+
* and reports a missing-UV rejection under its own reason so admission can
|
|
204
|
+
* name it (`endorsement_uv_required`, ADR-0065 §4).
|
|
205
|
+
*
|
|
206
|
+
* @param endorsementCbor - The endorsement COSE Sign1 bytes
|
|
207
|
+
* @param rootKeys - Passkey root trust anchor(s), tried in order. Raw
|
|
208
|
+
* coordinate anchors additionally pin `kid == root x`.
|
|
209
|
+
* @param opts - UV requirement and failure logging
|
|
210
|
+
*/
|
|
211
|
+
export async function verifySessionKeyEndorsement(endorsementCbor, rootKeys, opts) {
|
|
212
|
+
const anchors = Array.isArray(rootKeys) ? rootKeys : [rootKeys];
|
|
213
|
+
const decoded = decodeCoseSign1(endorsementCbor);
|
|
214
|
+
if (!decoded)
|
|
215
|
+
return { ok: false, reason: "endorsement_malformed" };
|
|
216
|
+
const header = protectedHeaderEntries(decoded.protectedBstr);
|
|
217
|
+
if (!header)
|
|
218
|
+
return { ok: false, reason: "endorsement_malformed" };
|
|
219
|
+
const alg = typeof header.alg === "bigint" ? Number(header.alg) : header.alg;
|
|
220
|
+
if (alg !== COSE_ALG_ES256_WEBAUTHN) {
|
|
221
|
+
return { ok: false, reason: "wrong_alg" };
|
|
222
|
+
}
|
|
223
|
+
if (header.cty !== SESSION_KEY_ENDORSEMENT_CONTENT_TYPE) {
|
|
224
|
+
return { ok: false, reason: "wrong_content_type" };
|
|
225
|
+
}
|
|
226
|
+
if (!(header.kid instanceof Uint8Array) || header.kid.length !== 32) {
|
|
227
|
+
return { ok: false, reason: "kid_invalid" };
|
|
228
|
+
}
|
|
229
|
+
const payload = decodePayloadV2(decoded.payloadBstr);
|
|
230
|
+
if (payload.kind !== "ok")
|
|
231
|
+
return { ok: false, reason: payload.kind };
|
|
232
|
+
if (opts?.requireUserVerification) {
|
|
233
|
+
const flags = envelopeFlags(decoded.unprotected);
|
|
234
|
+
// A malformed envelope is left to the shared branch to reject; only a
|
|
235
|
+
// well-shaped assertion that plainly lacks UV gets the distinct reason.
|
|
236
|
+
if (flags !== null && (flags & AUTH_FLAG_UV) === 0) {
|
|
237
|
+
return { ok: false, reason: "uv_required" };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
let kidMismatch = false;
|
|
241
|
+
for (const anchor of anchors) {
|
|
242
|
+
// When the anchor exposes raw coordinates, the signed kid must name it —
|
|
243
|
+
// a valid signature under a root whose x is not the kid is evidence of
|
|
244
|
+
// artifact confusion, not authorization (ADR-0064 §2 kid = root x).
|
|
245
|
+
if (!(anchor instanceof CryptoKey)) {
|
|
246
|
+
const x = anchor.x;
|
|
247
|
+
if (x.length !== header.kid.length ||
|
|
248
|
+
!x.every((b, i) => b === header.kid[i])) {
|
|
249
|
+
kidMismatch = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const sigOk = await verifyCoseSign1WithParsedKey(endorsementCbor, anchor, {
|
|
254
|
+
requireUserVerification: opts?.requireUserVerification,
|
|
255
|
+
logFailures: opts?.logFailures,
|
|
256
|
+
logPrefix: opts?.logPrefix ?? "session-key-endorsement",
|
|
257
|
+
});
|
|
258
|
+
if (!sigOk)
|
|
259
|
+
continue;
|
|
260
|
+
let sessionKey;
|
|
261
|
+
try {
|
|
262
|
+
sessionKey = await importEs256PublicKeyFromGrantDataXy64(payload.sessionPublicKeyXY);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return { ok: false, reason: "session_key_import_failed" };
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
ok: true,
|
|
269
|
+
sessionPublicKeyXY: payload.sessionPublicKeyXY,
|
|
270
|
+
sessionKey,
|
|
271
|
+
...payload.window,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
reason: kidMismatch ? "kid_mismatch" : "signature_invalid",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Is `atMs` inside the endorsement's window? `notBefore ≤ atMs ≤ notAfter`,
|
|
281
|
+
* both inclusive (ADR-0065 §3). `skewMs` (default 0) tolerates a `notBefore`
|
|
282
|
+
* slightly in the future of a wall-clock verifier; offline verifiers
|
|
283
|
+
* comparing against the receipted idtimestamp should leave it at 0 — that
|
|
284
|
+
* comparison is exact and authoritative.
|
|
285
|
+
*
|
|
286
|
+
* Deliberately a pure function of the two numbers so callers can (and must)
|
|
287
|
+
* run it on every request, structurally outside any verified-endorsement
|
|
288
|
+
* cache (ADR-0065 §4).
|
|
289
|
+
*/
|
|
290
|
+
export function checkEndorsementWindow(window, atMs, opts) {
|
|
291
|
+
const skew = opts?.skewMs ?? 0;
|
|
292
|
+
if (atMs + skew < window.notBefore) {
|
|
293
|
+
return { ok: false, reason: "endorsement_not_yet_valid" };
|
|
294
|
+
}
|
|
295
|
+
if (atMs > window.notAfter) {
|
|
296
|
+
return { ok: false, reason: "endorsement_expired" };
|
|
297
|
+
}
|
|
298
|
+
return { ok: true };
|
|
299
|
+
}
|