@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 +5 -1
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +5 -1
- package/esm/src/attestation.d.ts +9 -1
- package/esm/src/attestation.d.ts.map +1 -1
- package/esm/src/attestation.js +14 -6
- package/esm/src/utils.d.ts.map +1 -1
- package/esm/src/utils.js +3 -5
- package/esm/src/with-assertion.d.ts.map +1 -1
- package/esm/src/with-assertion.js +3 -5
- package/esm/src/with-attestation.d.ts +11 -0
- package/esm/src/with-attestation.d.ts.map +1 -1
- package/esm/src/with-attestation.js +26 -2
- package/package.json +1 -1
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
25
|
+
* clientDataHash,
|
|
22
26
|
* attestation,
|
|
23
27
|
* );
|
|
24
28
|
* // Store publicKeyPem and signCount for future assertion verification
|
package/esm/src/attestation.d.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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"}
|
package/esm/src/attestation.js
CHANGED
|
@@ -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,
|
|
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 ||
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
const
|
|
206
|
-
const nonceInput = concat(authData,
|
|
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]);
|
package/esm/src/utils.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
19
|
-
|
|
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,
|
|
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
|
-
|
|
92
|
-
|
|
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;
|
|
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 {
|
|
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,
|
|
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