@bradford-tech/supabase-integrity-attest 0.4.1 → 0.7.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/esm/mod.d.ts CHANGED
@@ -15,10 +15,14 @@
15
15
  * ```ts
16
16
  * import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest";
17
17
  *
18
+ * // clientDataHash = SHA-256(challenge) — most client SDKs hash internally
19
+ * const clientDataHash = new Uint8Array(
20
+ * await crypto.subtle.digest("SHA-256", new TextEncoder().encode(challenge)),
21
+ * );
18
22
  * const { publicKeyPem, receipt, signCount } = await verifyAttestation(
19
23
  * { appId: "TEAMID.com.example.app" },
20
24
  * keyId,
21
- * challenge,
25
+ * clientDataHash,
22
26
  * attestation,
23
27
  * );
24
28
  * // Store publicKeyPem and signCount for future assertion verification
package/esm/mod.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyFG;AAEH,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6FG;AAEH,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC"}
package/esm/mod.js CHANGED
@@ -15,10 +15,14 @@
15
15
  * ```ts
16
16
  * import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest";
17
17
  *
18
+ * // clientDataHash = SHA-256(challenge) — most client SDKs hash internally
19
+ * const clientDataHash = new Uint8Array(
20
+ * await crypto.subtle.digest("SHA-256", new TextEncoder().encode(challenge)),
21
+ * );
18
22
  * const { publicKeyPem, receipt, signCount } = await verifyAttestation(
19
23
  * { appId: "TEAMID.com.example.app" },
20
24
  * keyId,
21
- * challenge,
25
+ * clientDataHash,
22
26
  * attestation,
23
27
  * );
24
28
  * // Store publicKeyPem and signCount for future assertion verification
@@ -50,7 +50,15 @@ export declare function decodeAttestationCbor(data: Uint8Array): AttestationCbor
50
50
  * CBOR decode, certificate chain validation, nonce check, key extraction,
51
51
  * AAGUID check, and credential ID verification.
52
52
  *
53
+ * **Important:** The `clientDataHash` parameter corresponds to Apple's
54
+ * `clientDataHash` argument on `DCAppAttestService.attestKey(_:clientDataHash:)`.
55
+ * Most client SDKs (Expo's `attestKeyAsync`, native wrappers) hash the
56
+ * caller's challenge with SHA-256 before passing to Apple. If you are using
57
+ * the {@linkcode withAttestation} middleware, this hashing is handled
58
+ * automatically. If calling `verifyAttestation` directly, you must pass
59
+ * `SHA-256(challenge)` — not the raw challenge — as `clientDataHash`.
60
+ *
53
61
  * @throws {AttestationError} If any verification step fails.
54
62
  */
55
- export declare function verifyAttestation(appInfo: AppInfo, keyId: string, challenge: Uint8Array | string, attestation: Uint8Array | string, options?: VerifyAttestationOptions): Promise<AttestationResult>;
63
+ export declare function verifyAttestation(appInfo: AppInfo, keyId: string, clientDataHash: Uint8Array | string, attestation: Uint8Array | string, options?: VerifyAttestationOptions): Promise<AttestationResult>;
56
64
  //# sourceMappingURL=attestation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../src/src/attestation.ts"],"names":[],"mappings":"AAaA,yCAAyC;AACzC,MAAM,WAAW,OAAO;IACtB,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,OAAO,EAAE,UAAU,CAAC;IACpB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iDAAiD;AACjD,MAAM,WAAW,wBAAwB;IACvC,uFAAuF;IACvF,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QACP,GAAG,EAAE,UAAU,EAAE,CAAC;QAClB,OAAO,EAAE,UAAU,CAAC;KACrB,CAAC;IACF,QAAQ,EAAE,UAAU,CAAC;CACtB;AAsGD,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,eAAe,CAyEvE;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,UAAU,GAAG,MAAM,EAC9B,WAAW,EAAE,UAAU,GAAG,MAAM,EAChC,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,iBAAiB,CAAC,CAkJ5B"}
1
+ {"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../src/src/attestation.ts"],"names":[],"mappings":"AAaA,yCAAyC;AACzC,MAAM,WAAW,OAAO;IACtB,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,OAAO,EAAE,UAAU,CAAC;IACpB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iDAAiD;AACjD,MAAM,WAAW,wBAAwB;IACvC,uFAAuF;IACvF,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QACP,GAAG,EAAE,UAAU,EAAE,CAAC;QAClB,OAAO,EAAE,UAAU,CAAC;KACrB,CAAC;IACF,QAAQ,EAAE,UAAU,CAAC;CACtB;AAsGD,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,eAAe,CAyEvE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,cAAc,EAAE,UAAU,GAAG,MAAM,EACnC,WAAW,EAAE,UAAU,GAAG,MAAM,EAChC,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,iBAAiB,CAAC,CAkJ5B"}
@@ -163,9 +163,17 @@ export function decodeAttestationCbor(data) {
163
163
  * CBOR decode, certificate chain validation, nonce check, key extraction,
164
164
  * AAGUID check, and credential ID verification.
165
165
  *
166
+ * **Important:** The `clientDataHash` parameter corresponds to Apple's
167
+ * `clientDataHash` argument on `DCAppAttestService.attestKey(_:clientDataHash:)`.
168
+ * Most client SDKs (Expo's `attestKeyAsync`, native wrappers) hash the
169
+ * caller's challenge with SHA-256 before passing to Apple. If you are using
170
+ * the {@linkcode withAttestation} middleware, this hashing is handled
171
+ * automatically. If calling `verifyAttestation` directly, you must pass
172
+ * `SHA-256(challenge)` — not the raw challenge — as `clientDataHash`.
173
+ *
166
174
  * @throws {AttestationError} If any verification step fails.
167
175
  */
168
- export async function verifyAttestation(appInfo, keyId, challenge, attestation, options) {
176
+ export async function verifyAttestation(appInfo, keyId, clientDataHash, attestation, options) {
169
177
  // Decode attestation bytes from base64 if string
170
178
  let attestationBytes;
171
179
  if (typeof attestation === "string") {
@@ -199,11 +207,11 @@ export async function verifyAttestation(appInfo, keyId, challenge, attestation,
199
207
  }
200
208
  // Step 4: Verify certificate chain
201
209
  await verifyCertificateChain(x5c, options?.checkDate);
202
- // Step 5-6: Compute nonce = SHA-256(authData || challenge)
203
- // Apple's nonce verification uses the raw challenge bytes concatenated with authData,
204
- // NOT a hash of the challenge.
205
- const challengeBytes = toBytes(challenge);
206
- const nonceInput = concat(authData, challengeBytes);
210
+ // Step 5-6: Compute nonce = SHA-256(authData || clientDataHash)
211
+ // The clientDataHash is typically SHA-256(challenge) see the JSDoc above.
212
+ // The withAttestation middleware handles this hashing automatically.
213
+ const clientDataHashBytes = toBytes(clientDataHash);
214
+ const nonceInput = concat(authData, clientDataHashBytes);
207
215
  const computedNonce = new Uint8Array(await crypto.subtle.digest("SHA-256", nonceInput));
208
216
  // Step 7: Extract nonce from leaf cert
209
217
  const certNonce = extractNonceFromCert(x5c[0]);
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/src/utils.ts"],"names":[],"mappings":"AAGA,iDAAiD;AACjD,wBAAgB,MAAM,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAU1D;AAED,mDAAmD;AACnD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAOvE;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAGxE;AAED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAG9D;AAED,wFAAwF;AACxF,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAaxE;AAED,qDAAqD;AACrD,wBAAsB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpE"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/src/utils.ts"],"names":[],"mappings":"AAGA,iDAAiD;AACjD,wBAAgB,MAAM,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAU1D;AAED,mDAAmD;AACnD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAMvE;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAGxE;AAED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAG9D;AAED,wFAAwF;AACxF,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAaxE;AAED,qDAAqD;AACrD,wBAAsB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpE"}
package/esm/src/utils.js CHANGED
@@ -15,10 +15,8 @@ export function concat(...arrays) {
15
15
  }
16
16
  /** Constant-time comparison of two byte arrays. */
17
17
  export function constantTimeEqual(a, b) {
18
- if (a.length !== b.length)
19
- return false;
20
- let diff = 0;
21
- for (let i = 0; i < a.length; i++) {
18
+ let diff = a.length ^ b.length;
19
+ for (let i = 0; i < a.length && i < b.length; i++) {
22
20
  diff |= a[i] ^ b[i];
23
21
  }
24
22
  return diff === 0;
@@ -47,6 +45,6 @@ export async function importPemPublicKey(pem) {
47
45
  /** Export a CryptoKey to PEM-encoded SPKI format. */
48
46
  export async function exportKeyToPem(key) {
49
47
  const spki = await crypto.subtle.exportKey("spki", key);
50
- const base64 = encodeBase64(spki);
48
+ const base64 = encodeBase64(spki).match(/.{1,64}/g).join("\n");
51
49
  return `-----BEGIN PUBLIC KEY-----\n${base64}\n-----END PUBLIC KEY-----`;
52
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"with-assertion.d.ts","sourceRoot":"","sources":["../../src/src/with-assertion.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAsB,MAAM,aAAa,CAAC;AAEjE,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AACjE,0DAA0D;AAC1D,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG;IACtB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,mFAAmF;AACnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,UAAU,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;CACxB,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,MAAM,oBAAoB,GAAG;IACjC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wFAAwF;IACxF,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC9D;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IACtC,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAmCF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,gBAAgB,KACtB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA+FrC"}
1
+ {"version":3,"file":"with-assertion.d.ts","sourceRoot":"","sources":["../../src/src/with-assertion.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAsB,MAAM,aAAa,CAAC;AAEjE,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AACjE,0DAA0D;AAC1D,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG;IACtB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,mFAAmF;AACnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,UAAU,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;CACxB,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,MAAM,oBAAoB,GAAG;IACjC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wFAAwF;IACxF,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC9D;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IACtC,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAmCF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,gBAAgB,KACtB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiGrC"}
@@ -87,11 +87,9 @@ export function withAssertion(options, handler) {
87
87
  newSignCount = result.signCount;
88
88
  }
89
89
  catch (err) {
90
- const error = err instanceof AssertionError
91
- ? err
92
- : new AssertionError(AssertionErrorCode.INTERNAL_ERROR, String(err), {
93
- cause: err,
94
- });
90
+ const error = err instanceof AssertionError ? err : new AssertionError(AssertionErrorCode.INTERNAL_ERROR, "Internal error", {
91
+ cause: err,
92
+ });
95
93
  return options.onError?.(error, req) ?? defaultErrorResponse(error);
96
94
  }
97
95
  // Step 5: handler — outside try/catch, errors bubble up
@@ -29,7 +29,18 @@ export type AttestationContext = {
29
29
  /** Custom function to extract attestation data from an incoming request. */
30
30
  export type ExtractAttestationFn = (req: Request) => Promise<{
31
31
  deviceId: string;
32
+ /** Raw challenge bytes for the `consumeChallenge` DB lookup. */
32
33
  challenge: Uint8Array;
34
+ /**
35
+ * The challenge in the exact form the client SDK received it, before
36
+ * any server-side decoding. This is what the client SDK hashed to
37
+ * produce `clientDataHash` — Expo's `attestKeyAsync` and native
38
+ * `DCAppAttestService` wrappers convert this string to UTF-8 bytes
39
+ * and SHA-256 hash them before passing to Apple. The middleware must
40
+ * hash this same string to produce the matching `clientDataHash` for
41
+ * `verifyAttestation`.
42
+ */
43
+ challengeAsSent: string;
33
44
  attestation: Uint8Array;
34
45
  }>;
35
46
  /** Configuration for the {@linkcode withAttestation} middleware. */
@@ -1 +1 @@
1
- {"version":3,"file":"with-attestation.d.ts","sourceRoot":"","sources":["../../src/src/with-attestation.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAErE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,OAAO,EAAE,UAAU,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,kBAAkB,CAAC;CAC7B,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,UAAU,CAAC;IACtB,WAAW,EAAE,UAAU,CAAC;CACzB,CAAC,CAAC;AAEH,oEAAoE;AACpE,MAAM,MAAM,sBAAsB,GAAG;IACnC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,gBAAgB,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D;;;;OAIG;IACH,cAAc,EAAE,CAAC,GAAG,EAAE;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,UAAU,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,gBAAgB,EACvB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAwEF;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,kBAAkB,KACxB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAkGrC"}
1
+ {"version":3,"file":"with-attestation.d.ts","sourceRoot":"","sources":["../../src/src/with-attestation.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAErE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,OAAO,EAAE,UAAU,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,kBAAkB,CAAC;CAC7B,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,SAAS,EAAE,UAAU,CAAC;IACtB;;;;;;;;OAQG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,UAAU,CAAC;CACzB,CAAC,CAAC;AAEH,oEAAoE;AACpE,MAAM,MAAM,sBAAsB,GAAG;IACnC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,gBAAgB,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D;;;;OAIG;IACH,cAAc,EAAE,CAAC,GAAG,EAAE;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,UAAU,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,gBAAgB,EACvB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AA8EF;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,kBAAkB,KACxB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2HrC"}
@@ -36,7 +36,12 @@ async function defaultExtractAttestation(req) {
36
36
  catch {
37
37
  throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "attestation is not valid base64");
38
38
  }
39
- return { deviceId: typed.keyId, challenge, attestation };
39
+ return {
40
+ deviceId: typed.keyId,
41
+ challenge,
42
+ challengeAsSent: typed.challenge,
43
+ attestation,
44
+ };
40
45
  }
41
46
  function defaultErrorResponse(error) {
42
47
  const status = error.code === AttestationErrorCode.INTERNAL_ERROR
@@ -79,6 +84,11 @@ export function withAttestation(options, handler) {
79
84
  const extracted = await extract(req);
80
85
  timings.extractMs = performance.now() - extractStart;
81
86
  deviceId = extracted.deviceId;
87
+ // Consume the challenge BEFORE verification to prevent a TOCTOU race:
88
+ // two concurrent requests with the same challenge could both pass
89
+ // verifyAttestation before either consumes. The trade-off is that a
90
+ // verification failure (malformed attestation, cert-chain error) burns
91
+ // the challenge, requiring the client to request a new one.
82
92
  const consumeStart = performance.now();
83
93
  let challengeOk;
84
94
  try {
@@ -95,8 +105,22 @@ export function withAttestation(options, handler) {
95
105
  if (!challengeOk) {
96
106
  throw new AttestationError(AttestationErrorCode.CHALLENGE_INVALID, "Challenge is missing, expired, or already consumed");
97
107
  }
108
+ // Hash the challenge AS THE CLIENT SAW IT — the exact string passed
109
+ // to attestKeyAsync / DCAppAttestService.attestKey. Client SDKs
110
+ // convert this string to UTF-8 bytes and SHA-256 hash them before
111
+ // passing to Apple as clientDataHash. The attestation certificate's
112
+ // nonce is SHA-256(authData || clientDataHash), so we must hash the
113
+ // same string to produce the matching clientDataHash.
114
+ //
115
+ // This is different from the assertion path, which has no encoding
116
+ // layer: the string passed to generateAssertionAsync IS the raw
117
+ // body, and both sides hash identical bytes by definition.
118
+ // Attestation has a transport encoding layer (base64 in a JSON
119
+ // body), so the middleware must hash BEFORE decoding to match what
120
+ // the client SDK hashed.
121
+ const clientDataHash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(extracted.challengeAsSent)));
98
122
  const verifyStart = performance.now();
99
- const result = await verifyAttestation(appInfo, deviceId, extracted.challenge, extracted.attestation);
123
+ const result = await verifyAttestation(appInfo, deviceId, clientDataHash, extracted.attestation);
100
124
  timings.verifyMs = performance.now() - verifyStart;
101
125
  publicKeyPem = result.publicKeyPem;
102
126
  receipt = result.receipt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bradford-tech/supabase-integrity-attest",
3
- "version": "0.4.1",
3
+ "version": "0.7.0",
4
4
  "description": "Verify Apple App Attest attestations and assertions using WebCrypto.",
5
5
  "homepage": "https://integrity-attest.bradford.tech",
6
6
  "repository": {