@forestrie/receipt-verify 0.12.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 CHANGED
@@ -24,17 +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
31
  /**
32
- * Passkey session-key endorsement (ADR-0064): build, assemble, and verify
33
- * the one-gesture artifact chaining a passkey log root to the plain-ES256
34
- * per-turn session key — the offline rung root → endorsement → leaves.
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).
35
37
  */
36
- export { assembleSessionKeyEndorsement, buildSessionKeyEndorsementTbs, SESSION_KEY_ENDORSEMENT_CONTENT_TYPE, SESSION_KEY_PAYLOAD_KEY, verifySessionKeyEndorsement, } from "./session-key-endorsement.js";
37
- export type { SessionKeyEndorsementTbs, SessionKeyEndorsementVerifyResult, VerifySessionKeyEndorsementOptions, } from "./session-key-endorsement.js";
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";
38
46
  export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
39
47
  export { decodeGrantPayload, decodeGrantResponse } from "./grant-codec.js";
40
48
  /** Deterministic receipt construction (plan-2607-12 Phase 2, FOR-353). */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -16,15 +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
22
  /**
23
- * Passkey session-key endorsement (ADR-0064): build, assemble, and verify
24
- * the one-gesture artifact chaining a passkey log root to the plain-ES256
25
- * per-turn session key — the offline rung root → endorsement → leaves.
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).
26
28
  */
27
- export { assembleSessionKeyEndorsement, buildSessionKeyEndorsementTbs, SESSION_KEY_ENDORSEMENT_CONTENT_TYPE, SESSION_KEY_PAYLOAD_KEY, verifySessionKeyEndorsement, } from "./session-key-endorsement.js";
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";
28
35
  export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
29
36
  export { decodeGrantPayload, decodeGrantResponse } from "./grant-codec.js";
30
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,CAKpE;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"}
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
- const ms = COMMITMENT_EPOCH * SNOWFLAKE_EPOCH_SPAN_MS +
64
- (idtimestamp >> SNOWFLAKE_TIME_SHIFT);
65
- return Number(ms / 1000n);
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);
@@ -1,37 +1,65 @@
1
1
  /**
2
- * Passkey session-key endorsement (devdocs ADR-0064, plan-2608-13 Q5).
2
+ * Passkey session-key endorsement, payload v2 (devdocs ADR-0064 as amended by
3
+ * ADR-0065 §3; plan-2608-14 1.2).
3
4
  *
4
5
  * A passkey log root signs ceremonies only; the per-turn user-envelope
5
- * signer is a separate session key, endorsed ONCE by the root via a COSE
6
- * Sign1 in the ADR-0063 WebAuthn envelope. This module owns both sides of
7
- * that artifact's byte-exact shape:
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:
8
9
  *
9
10
  * - build/assemble: the client constructs the TBS, hashes its Sig_structure
10
11
  * into the WebAuthn challenge, and attaches the assertion + signature;
11
12
  * - verify: given the passkey root as trust anchor, validate the artifact
12
- * (typed, challenge-bound, fail-closed) and yield the session public key.
13
+ * (typed, challenge-bound, fail-closed) and yield the session public key
14
+ * and its validity window.
13
15
  *
14
- * The offline trust chain is then:
15
- * grant (root = passkey x‖y) → endorsement → session key → plain-ES256
16
- * per-turn leaves — zero per-leaf format change (ADR-0064 §4).
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).
17
21
  *
18
- * Domain separation (ADR-0064 §2): the protected header carries the content
19
- * type below, and the payload is EXACTLY `{"sessionKey": bstr .size 64}`.
20
- * Anything else is a verification failure, never a fallback.
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.
21
31
  */
22
32
  import { type ParsedVerifyKey } from "@forestrie/encoding";
23
33
  /**
24
- * Protected-header content type (label 3) naming the endorsement artifact.
25
- * Signed via Sig_structure, so the artifact's type is non-malleable.
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.
26
43
  */
27
- export declare const SESSION_KEY_ENDORSEMENT_CONTENT_TYPE = "application/vnd.forestrie.session-key-endorsement+cbor";
44
+ export declare const SESSION_KEY_ENDORSEMENT_V1_CONTENT_TYPE = "application/vnd.forestrie.session-key-endorsement+cbor";
28
45
  /** Payload map key holding the endorsed session public key (x‖y, 64 bytes). */
29
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
+ }
30
58
  /** To-be-signed endorsement halves plus the challenge preimage. */
31
59
  export interface SessionKeyEndorsementTbs {
32
- /** Protected header map bytes `{1: -65800, 3: cty, 4: root x}`. */
60
+ /** Protected header map bytes `{1: -65800, 3: cty(v2), 4: root x}`. */
33
61
  protectedBstr: Uint8Array;
34
- /** Payload bytes: deterministic CBOR `{"sessionKey": x‖y}`. */
62
+ /** Payload bytes: deterministic CBOR `{sessionKey, notBefore, notAfter}`. */
35
63
  payloadBstr: Uint8Array;
36
64
  /**
37
65
  * `Sig_structure` over the halves. The WebAuthn challenge MUST be
@@ -40,15 +68,19 @@ export interface SessionKeyEndorsementTbs {
40
68
  sigStructureBytes: Uint8Array;
41
69
  }
42
70
  /**
43
- * Build the endorsement TBS. The caller hashes `sigStructureBytes` into the
44
- * `navigator.credentials.get` challenge, then assembles with the assertion.
71
+ * Build the v2 endorsement TBS. The caller hashes `sigStructureBytes` into
72
+ * the `navigator.credentials.get` challenge, then assembles with the assertion.
45
73
  *
46
74
  * @param input.rootPublicKeyX - Passkey root x coordinate (kid, 32 bytes)
47
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
48
78
  */
49
79
  export declare function buildSessionKeyEndorsementTbs(input: {
50
80
  rootPublicKeyX: Uint8Array;
51
81
  sessionPublicKeyXY: Uint8Array;
82
+ notBefore: number;
83
+ notAfter: number;
52
84
  }): SessionKeyEndorsementTbs;
53
85
  /**
54
86
  * Assemble the final endorsement COSE Sign1: the assertion rides in the
@@ -63,23 +95,26 @@ export declare function assembleSessionKeyEndorsement(input: {
63
95
  /** Low-s P1363 r‖s (64 bytes). */
64
96
  signature: Uint8Array;
65
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";
66
100
  /** Verification outcome; failures name the first check that broke. */
67
- export type SessionKeyEndorsementVerifyResult = {
101
+ export type SessionKeyEndorsementVerifyResult = ({
68
102
  ok: true;
69
103
  /** Endorsed session public key, raw x‖y (64 bytes). */
70
104
  sessionPublicKeyXY: Uint8Array;
71
105
  /** The same key imported for ES256 verify of per-turn leaves. */
72
106
  sessionKey: CryptoKey;
73
- } | {
107
+ } & EndorsementWindow) | {
74
108
  ok: false;
75
- reason: "endorsement_malformed" | "wrong_alg" | "wrong_content_type" | "kid_invalid" | "kid_mismatch" | "payload_invalid" | "signature_invalid" | "session_key_import_failed";
109
+ reason: SessionKeyEndorsementFailureReason;
76
110
  };
77
111
  /** Options for {@link verifySessionKeyEndorsement}. */
78
112
  export interface VerifySessionKeyEndorsementOptions {
79
113
  /**
80
- * Require the assertion's UV flag. Onboarding UV is DEPLOYMENT config,
81
- * not grant policy — the endorsement precedes the grant (ADR-0064 §3).
82
- * User presence is always required regardless.
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.
83
118
  */
84
119
  requireUserVerification?: boolean;
85
120
  /** Emit JSON warning lines on failure paths (no secrets). */
@@ -88,16 +123,39 @@ export interface VerifySessionKeyEndorsementOptions {
88
123
  logPrefix?: string;
89
124
  }
90
125
  /**
91
- * Verify a session-key endorsement under the passkey root and yield the
92
- * endorsed session key. All the ADR-0063 envelope checks (challenge binding
93
- * to this artifact's `Sig_structure`, UP flag, ceremony type, low-s, both
94
- * fail-closed directions) run inside the shared `-65800` verify branch;
95
- * this function adds the ADR-0064 typing rules on top.
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).
96
133
  *
97
134
  * @param endorsementCbor - The endorsement COSE Sign1 bytes
98
135
  * @param rootKeys - Passkey root trust anchor(s), tried in order. Raw
99
136
  * coordinate anchors additionally pin `kid == root x`.
100
- * @param opts - UV requirement (deployment config) and failure logging
137
+ * @param opts - UV requirement and failure logging
101
138
  */
102
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;
103
161
  //# sourceMappingURL=session-key-endorsement.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-key-endorsement.d.ts","sourceRoot":"","sources":["../src/session-key-endorsement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EASL,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAG7B;;;GAGG;AACH,eAAO,MAAM,oCAAoC,2DACS,CAAC;AAE3D,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,eAAe,CAAC;AAEpD,mEAAmE;AACnE,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,aAAa,EAAE,UAAU,CAAC;IAC1B,+DAA+D;IAC/D,WAAW,EAAE,UAAU,CAAC;IACxB;;;OAGG;IACH,iBAAiB,EAAE,UAAU,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE;IACnD,cAAc,EAAE,UAAU,CAAC;IAC3B,kBAAkB,EAAE,UAAU,CAAC;CAChC,GAAG,wBAAwB,CAyB3B;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE;IACnD,GAAG,EAAE,wBAAwB,CAAC;IAC9B,iBAAiB,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,UAAU,CAAC;IAC3B,kCAAkC;IAClC,SAAS,EAAE,UAAU,CAAC;CACvB,GAAG,UAAU,CAab;AAED,sEAAsE;AACtE,MAAM,MAAM,iCAAiC,GACzC;IACE,EAAE,EAAE,IAAI,CAAC;IACT,uDAAuD;IACvD,kBAAkB,EAAE,UAAU,CAAC;IAC/B,iEAAiE;IACjE,UAAU,EAAE,SAAS,CAAC;CACvB,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EACF,uBAAuB,GACvB,WAAW,GACX,oBAAoB,GACpB,aAAa,GACb,cAAc,GACd,iBAAiB,GACjB,mBAAmB,GACnB,2BAA2B,CAAC;CACjC,CAAC;AAEN,uDAAuD;AACvD,MAAM,WAAW,kCAAkC;IACjD;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAiDD;;;;;;;;;;;GAWG;AACH,wBAAsB,2BAA2B,CAC/C,eAAe,EAAE,UAAU,EAC3B,QAAQ,EAAE,eAAe,GAAG,eAAe,EAAE,EAC7C,IAAI,CAAC,EAAE,kCAAkC,GACxC,OAAO,CAAC,iCAAiC,CAAC,CA2D5C"}
1
+ {"version":3,"file":"session-key-endorsement.d.ts","sourceRoot":"","sources":["../src/session-key-endorsement.ts"],"names":[],"mappings":"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"}
@@ -1,39 +1,77 @@
1
1
  /**
2
- * Passkey session-key endorsement (devdocs ADR-0064, plan-2608-13 Q5).
2
+ * Passkey session-key endorsement, payload v2 (devdocs ADR-0064 as amended by
3
+ * ADR-0065 §3; plan-2608-14 1.2).
3
4
  *
4
5
  * A passkey log root signs ceremonies only; the per-turn user-envelope
5
- * signer is a separate session key, endorsed ONCE by the root via a COSE
6
- * Sign1 in the ADR-0063 WebAuthn envelope. This module owns both sides of
7
- * that artifact's byte-exact shape:
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:
8
9
  *
9
10
  * - build/assemble: the client constructs the TBS, hashes its Sig_structure
10
11
  * into the WebAuthn challenge, and attaches the assertion + signature;
11
12
  * - verify: given the passkey root as trust anchor, validate the artifact
12
- * (typed, challenge-bound, fail-closed) and yield the session public key.
13
+ * (typed, challenge-bound, fail-closed) and yield the session public key
14
+ * and its validity window.
13
15
  *
14
- * The offline trust chain is then:
15
- * grant (root = passkey x‖y) → endorsement → session key → plain-ES256
16
- * per-turn leaves — zero per-leaf format change (ADR-0064 §4).
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).
17
21
  *
18
- * Domain separation (ADR-0064 §2): the protected header carries the content
19
- * type below, and the payload is EXACTLY `{"sessionKey": bstr .size 64}`.
20
- * Anything else is a verification failure, never a fallback.
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.
21
31
  */
22
- import { COSE_ALG_ES256_WEBAUTHN, decodeCborDeterministic, decodeCoseSign1, encodeCborDeterministic, encodeCoseSign1Raw, encodeSigStructure, verifyCoseSign1WithParsedKey, WEBAUTHN_ENVELOPE_LABEL, } from "@forestrie/encoding";
32
+ import { COSE_ALG_ES256_WEBAUTHN, coseUnprotectedToMap, decodeCborDeterministic, decodeCoseSign1, encodeCborDeterministic, encodeCoseSign1Raw, encodeSigStructure, verifyCoseSign1WithParsedKey, WEBAUTHN_ENVELOPE_LABEL, } from "@forestrie/encoding";
23
33
  import { importEs256PublicKeyFromGrantDataXy64 } from "./decode-trust-root-cbor.js";
24
34
  /**
25
- * Protected-header content type (label 3) naming the endorsement artifact.
26
- * Signed via Sig_structure, so the artifact's type is non-malleable.
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.
27
44
  */
28
- export const SESSION_KEY_ENDORSEMENT_CONTENT_TYPE = "application/vnd.forestrie.session-key-endorsement+cbor";
45
+ export const SESSION_KEY_ENDORSEMENT_V1_CONTENT_TYPE = "application/vnd.forestrie.session-key-endorsement+cbor";
29
46
  /** Payload map key holding the endorsed session public key (x‖y, 64 bytes). */
30
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
+ }
31
67
  /**
32
- * Build the endorsement TBS. The caller hashes `sigStructureBytes` into the
33
- * `navigator.credentials.get` challenge, then assembles with the assertion.
68
+ * Build the v2 endorsement TBS. The caller hashes `sigStructureBytes` into
69
+ * the `navigator.credentials.get` challenge, then assembles with the assertion.
34
70
  *
35
71
  * @param input.rootPublicKeyX - Passkey root x coordinate (kid, 32 bytes)
36
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
37
75
  */
38
76
  export function buildSessionKeyEndorsementTbs(input) {
39
77
  if (input.rootPublicKeyX.length !== 32) {
@@ -42,13 +80,20 @@ export function buildSessionKeyEndorsementTbs(input) {
42
80
  if (input.sessionPublicKeyXY.length !== 64) {
43
81
  throw new Error("endorsed session key must be 64 bytes (x||y)");
44
82
  }
83
+ if (!windowWellFormed(input)) {
84
+ throw new Error("endorsement window must be unsigned safe-integer unix ms with notAfter > notBefore");
85
+ }
45
86
  const protectedBstr = encodeCborDeterministic(new Map([
46
87
  [1, COSE_ALG_ES256_WEBAUTHN],
47
88
  [3, SESSION_KEY_ENDORSEMENT_CONTENT_TYPE],
48
89
  [4, input.rootPublicKeyX],
49
90
  ]));
91
+ // Deterministic CBOR sorts the keys (RFC 8949 §4.2.1); insertion order here
92
+ // is irrelevant to the bytes.
50
93
  const payloadBstr = encodeCborDeterministic(new Map([
51
94
  [SESSION_KEY_PAYLOAD_KEY, input.sessionPublicKeyXY],
95
+ [NOT_BEFORE_PAYLOAD_KEY, input.notBefore],
96
+ [NOT_AFTER_PAYLOAD_KEY, input.notAfter],
52
97
  ]));
53
98
  const sigStructureBytes = encodeSigStructure(protectedBstr, new Uint8Array(0), payloadBstr);
54
99
  return { protectedBstr, payloadBstr, sigStructureBytes };
@@ -89,16 +134,26 @@ function protectedHeaderEntries(protectedBstr) {
89
134
  }
90
135
  return null;
91
136
  }
92
- function decodePayloadSessionKeyXy(payloadBstr) {
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) {
93
148
  let decoded;
94
149
  try {
95
150
  decoded = decodeCborDeterministic(payloadBstr);
96
151
  }
97
152
  catch {
98
- return null;
153
+ return { kind: "payload_invalid" };
99
154
  }
100
- // Exactly one entry, exactly the sessionKey label, exactly 64 bytes
101
- // (ADR-0064 §2) — anything else is a failure, never a partial read.
155
+ // Exactly three entries, exactly the three labels, exactly these shapes
156
+ // (ADR-0065 §3) — anything else is a failure, never a partial read.
102
157
  let entries;
103
158
  if (decoded instanceof Map) {
104
159
  entries = [...decoded.entries()];
@@ -107,28 +162,51 @@ function decodePayloadSessionKeyXy(payloadBstr) {
107
162
  entries = Object.entries(decoded);
108
163
  }
109
164
  else {
110
- return null;
165
+ return { kind: "payload_invalid" };
111
166
  }
112
- if (entries.length !== 1)
113
- return null;
114
- const [key, value] = entries[0];
115
- if (key !== SESSION_KEY_PAYLOAD_KEY)
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)
116
189
  return null;
117
- if (!(value instanceof Uint8Array) || value.length !== 64)
190
+ const authenticatorData = envelope[0];
191
+ if (!(authenticatorData instanceof Uint8Array) ||
192
+ authenticatorData.length < 37) {
118
193
  return null;
119
- return value;
194
+ }
195
+ return authenticatorData[32];
120
196
  }
121
197
  /**
122
- * Verify a session-key endorsement under the passkey root and yield the
123
- * endorsed session key. All the ADR-0063 envelope checks (challenge binding
124
- * to this artifact's `Sig_structure`, UP flag, ceremony type, low-s, both
125
- * fail-closed directions) run inside the shared `-65800` verify branch;
126
- * this function adds the ADR-0064 typing rules on top.
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).
127
205
  *
128
206
  * @param endorsementCbor - The endorsement COSE Sign1 bytes
129
207
  * @param rootKeys - Passkey root trust anchor(s), tried in order. Raw
130
208
  * coordinate anchors additionally pin `kid == root x`.
131
- * @param opts - UV requirement (deployment config) and failure logging
209
+ * @param opts - UV requirement and failure logging
132
210
  */
133
211
  export async function verifySessionKeyEndorsement(endorsementCbor, rootKeys, opts) {
134
212
  const anchors = Array.isArray(rootKeys) ? rootKeys : [rootKeys];
@@ -148,9 +226,17 @@ export async function verifySessionKeyEndorsement(endorsementCbor, rootKeys, opt
148
226
  if (!(header.kid instanceof Uint8Array) || header.kid.length !== 32) {
149
227
  return { ok: false, reason: "kid_invalid" };
150
228
  }
151
- const sessionPublicKeyXY = decodePayloadSessionKeyXy(decoded.payloadBstr);
152
- if (!sessionPublicKeyXY)
153
- return { ok: false, reason: "payload_invalid" };
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
+ }
154
240
  let kidMismatch = false;
155
241
  for (const anchor of anchors) {
156
242
  // When the anchor exposes raw coordinates, the signed kid must name it —
@@ -173,16 +259,41 @@ export async function verifySessionKeyEndorsement(endorsementCbor, rootKeys, opt
173
259
  continue;
174
260
  let sessionKey;
175
261
  try {
176
- sessionKey =
177
- await importEs256PublicKeyFromGrantDataXy64(sessionPublicKeyXY);
262
+ sessionKey = await importEs256PublicKeyFromGrantDataXy64(payload.sessionPublicKeyXY);
178
263
  }
179
264
  catch {
180
265
  return { ok: false, reason: "session_key_import_failed" };
181
266
  }
182
- return { ok: true, sessionPublicKeyXY, sessionKey };
267
+ return {
268
+ ok: true,
269
+ sessionPublicKeyXY: payload.sessionPublicKeyXY,
270
+ sessionKey,
271
+ ...payload.window,
272
+ };
183
273
  }
184
274
  return {
185
275
  ok: false,
186
276
  reason: kidMismatch ? "kid_mismatch" : "signature_invalid",
187
277
  };
188
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
+ }