@bradford-tech/supabase-integrity-attest 0.3.0 → 0.3.2
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/README.md +98 -2
- package/esm/_dnt.test_polyfills.d.ts.map +1 -0
- package/esm/assertion.d.ts +21 -0
- package/esm/assertion.d.ts.map +1 -1
- package/esm/assertion.js +21 -1
- package/esm/attestation.d.ts +17 -0
- package/esm/attestation.d.ts.map +1 -1
- package/esm/attestation.js +17 -1
- package/esm/mod.d.ts +19 -0
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +19 -1
- package/esm/src/assertion.d.ts +14 -0
- package/esm/src/assertion.d.ts.map +1 -1
- package/esm/src/assertion.js +9 -0
- package/esm/src/attestation.d.ts +20 -1
- package/esm/src/attestation.d.ts.map +1 -1
- package/esm/src/attestation.js +15 -4
- package/esm/src/errors.d.ts +30 -3
- package/esm/src/errors.d.ts.map +1 -1
- package/esm/src/errors.js +26 -3
- package/esm/src/with-assertion.d.ts +51 -0
- package/esm/src/with-assertion.d.ts.map +1 -0
- package/esm/src/with-assertion.js +81 -0
- package/esm/tests/assertion-entry.test.d.ts.map +1 -1
- package/esm/tests/assertion.test.d.ts.map +1 -1
- package/esm/tests/attestation-entry.test.d.ts.map +1 -1
- package/esm/tests/attestation.test.d.ts.map +1 -1
- package/esm/tests/authdata.test.d.ts.map +1 -1
- package/esm/tests/certificate.test.d.ts.map +1 -1
- package/esm/tests/cose.test.d.ts.map +1 -1
- package/esm/tests/der.test.d.ts.map +1 -1
- package/esm/tests/errors.test.d.ts.map +1 -1
- package/esm/tests/utils.test.d.ts.map +1 -1
- package/esm/tests/with-assertion.test.d.ts.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,22 @@ assertions using the WebCrypto API. Built for Deno and Supabase Edge Functions.
|
|
|
10
10
|
deno add jsr:@bradford-tech/supabase-integrity-attest
|
|
11
11
|
|
|
12
12
|
# npm
|
|
13
|
-
|
|
13
|
+
npm install @bradford-tech/supabase-integrity-attest
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Subpath imports
|
|
17
|
+
|
|
18
|
+
If you only need assertion verification, import from the lighter entry point:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// Full library (attestation + assertion)
|
|
22
|
+
import { verifyAttestation, verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
23
|
+
|
|
24
|
+
// Assertion only — skips asn1js and @noble/curves
|
|
25
|
+
import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest/assertion";
|
|
26
|
+
|
|
27
|
+
// Attestation only
|
|
28
|
+
import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest/attestation";
|
|
14
29
|
```
|
|
15
30
|
|
|
16
31
|
## Usage
|
|
@@ -30,7 +45,88 @@ const result = await verifyAttestation(
|
|
|
30
45
|
// Store result.publicKeyPem and signCount (0) for this device
|
|
31
46
|
```
|
|
32
47
|
|
|
33
|
-
###
|
|
48
|
+
### Protecting edge functions with `withAssertion`
|
|
49
|
+
|
|
50
|
+
`withAssertion` wraps a request handler so that assertion verification,
|
|
51
|
+
device lookup, and sign count updates happen before your business logic runs.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { createClient } from "jsr:@supabase/supabase-js@2";
|
|
55
|
+
import { withAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
56
|
+
|
|
57
|
+
const supabase = createClient(
|
|
58
|
+
Deno.env.get("SUPABASE_URL")!,
|
|
59
|
+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
Deno.serve(withAssertion({
|
|
63
|
+
appId: Deno.env.get("APP_ATTEST_APP_ID")!,
|
|
64
|
+
developmentEnv: Deno.env.get("APP_ATTEST_ENV") === "development",
|
|
65
|
+
getDeviceKey: async (deviceId) => {
|
|
66
|
+
const { data } = await supabase
|
|
67
|
+
.from("device_attestations")
|
|
68
|
+
.select("public_key_pem, sign_count")
|
|
69
|
+
.eq("key_id", deviceId)
|
|
70
|
+
.single();
|
|
71
|
+
return data
|
|
72
|
+
? { publicKeyPem: data.public_key_pem, signCount: data.sign_count }
|
|
73
|
+
: null;
|
|
74
|
+
},
|
|
75
|
+
updateSignCount: async (deviceId, newSignCount) => {
|
|
76
|
+
await supabase
|
|
77
|
+
.from("device_attestations")
|
|
78
|
+
.update({ sign_count: newSignCount })
|
|
79
|
+
.eq("key_id", deviceId);
|
|
80
|
+
},
|
|
81
|
+
}, async (_req, { rawBody }) => {
|
|
82
|
+
const { text, voice } = JSON.parse(new TextDecoder().decode(rawBody));
|
|
83
|
+
// business logic
|
|
84
|
+
return new Response(JSON.stringify({ audio: "..." }), {
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
});
|
|
87
|
+
}));
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The client sends the assertion and device ID in headers. The request body
|
|
91
|
+
is the client data that was signed:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
POST /functions/v1/your-endpoint
|
|
95
|
+
Headers:
|
|
96
|
+
Content-Type: application/json
|
|
97
|
+
X-App-Attest-Assertion: <base64-encoded assertion>
|
|
98
|
+
X-App-Attest-Device-Id: <base64-encoded keyId>
|
|
99
|
+
Body:
|
|
100
|
+
{"text": "Hello world", "voice": "en-US"}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Once you have multiple protected functions, extract the shared options into
|
|
104
|
+
a helper:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// supabase/functions/_shared/attest.ts
|
|
108
|
+
import type { WithAssertionOptions } from "@bradford-tech/supabase-integrity-attest";
|
|
109
|
+
|
|
110
|
+
export const attestOptions: WithAssertionOptions = {
|
|
111
|
+
appId: Deno.env.get("APP_ATTEST_APP_ID")!,
|
|
112
|
+
// ... getDeviceKey, updateSignCount as above
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// supabase/functions/text-to-speech/index.ts
|
|
118
|
+
import { withAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
119
|
+
import { attestOptions } from "../_shared/attest.ts";
|
|
120
|
+
|
|
121
|
+
Deno.serve(withAssertion(attestOptions, async (_req, { rawBody }) => {
|
|
122
|
+
const { text, voice } = JSON.parse(new TextDecoder().decode(rawBody));
|
|
123
|
+
return new Response(JSON.stringify({ audio: "..." }));
|
|
124
|
+
}));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Assertion (low-level)
|
|
128
|
+
|
|
129
|
+
For full control over the verification flow, use `verifyAssertion` directly:
|
|
34
130
|
|
|
35
131
|
```typescript
|
|
36
132
|
import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dnt.test_polyfills.d.ts","sourceRoot":"","sources":["../src/_dnt.test_polyfills.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,KAAK;QACb,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CACF;AAED,OAAO,EAAE,CAAC"}
|
package/esm/assertion.d.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight assertion-only entry point. Avoids pulling in `asn1js` and
|
|
3
|
+
* `@noble/curves`, keeping the bundle minimal for assertion-only use cases.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest/assertion";
|
|
7
|
+
*
|
|
8
|
+
* const { signCount } = await verifyAssertion(
|
|
9
|
+
* { appId: "TEAMID.com.example.app" },
|
|
10
|
+
* assertion,
|
|
11
|
+
* clientData,
|
|
12
|
+
* publicKeyPem,
|
|
13
|
+
* previousSignCount,
|
|
14
|
+
* );
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
1
19
|
export { verifyAssertion } from "./src/assertion.js";
|
|
2
20
|
export type { AppInfo, AssertionResult } from "./src/assertion.js";
|
|
3
21
|
export { AssertionError, AssertionErrorCode } from "./src/errors.js";
|
|
22
|
+
export { withAssertion } from "./src/with-assertion.js";
|
|
23
|
+
export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
|
|
24
|
+
export type { AssertionContext, DeviceKey, ExtractAssertionFn, WithAssertionOptions, } from "./src/with-assertion.js";
|
|
4
25
|
//# sourceMappingURL=assertion.d.ts.map
|
package/esm/assertion.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertion.d.ts","sourceRoot":"","sources":["../src/assertion.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"assertion.d.ts","sourceRoot":"","sources":["../src/assertion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGrE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC"}
|
package/esm/assertion.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight assertion-only entry point. Avoids pulling in `asn1js` and
|
|
3
|
+
* `@noble/curves`, keeping the bundle minimal for assertion-only use cases.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest/assertion";
|
|
7
|
+
*
|
|
8
|
+
* const { signCount } = await verifyAssertion(
|
|
9
|
+
* { appId: "TEAMID.com.example.app" },
|
|
10
|
+
* assertion,
|
|
11
|
+
* clientData,
|
|
12
|
+
* publicKeyPem,
|
|
13
|
+
* previousSignCount,
|
|
14
|
+
* );
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
2
19
|
export { verifyAssertion } from "./src/assertion.js";
|
|
3
20
|
export { AssertionError, AssertionErrorCode } from "./src/errors.js";
|
|
21
|
+
// withAssertion wrapper
|
|
22
|
+
export { withAssertion } from "./src/with-assertion.js";
|
|
23
|
+
export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
|
package/esm/attestation.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full attestation entry point including certificate chain verification
|
|
3
|
+
* dependencies (`asn1js`, `@noble/curves`).
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest/attestation";
|
|
7
|
+
*
|
|
8
|
+
* const { publicKeyPem } = await verifyAttestation(
|
|
9
|
+
* { appId: "TEAMID.com.example.app" },
|
|
10
|
+
* keyId,
|
|
11
|
+
* challenge,
|
|
12
|
+
* attestation,
|
|
13
|
+
* );
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
1
18
|
export { verifyAttestation } from "./src/attestation.js";
|
|
2
19
|
export type { AppInfo, AttestationResult, VerifyAttestationOptions, } from "./src/attestation.js";
|
|
3
20
|
export { AttestationError, AttestationErrorCode } from "./src/errors.js";
|
package/esm/attestation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../src/attestation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../src/attestation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/esm/attestation.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Full attestation entry point including certificate chain verification
|
|
3
|
+
* dependencies (`asn1js`, `@noble/curves`).
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest/attestation";
|
|
7
|
+
*
|
|
8
|
+
* const { publicKeyPem } = await verifyAttestation(
|
|
9
|
+
* { appId: "TEAMID.com.example.app" },
|
|
10
|
+
* keyId,
|
|
11
|
+
* challenge,
|
|
12
|
+
* attestation,
|
|
13
|
+
* );
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
2
18
|
export { verifyAttestation } from "./src/attestation.js";
|
|
3
19
|
export { AttestationError, AttestationErrorCode } from "./src/errors.js";
|
package/esm/mod.d.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify Apple App Attest attestations and assertions using WebCrypto.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { verifyAttestation, verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
6
|
+
*
|
|
7
|
+
* const { publicKeyPem } = await verifyAttestation(
|
|
8
|
+
* { appId: "TEAMID.com.example.app" },
|
|
9
|
+
* keyId,
|
|
10
|
+
* challenge,
|
|
11
|
+
* attestation,
|
|
12
|
+
* );
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
1
17
|
export type { AppInfo, AttestationResult, VerifyAttestationOptions, } from "./src/attestation.js";
|
|
2
18
|
export type { AssertionResult } from "./src/assertion.js";
|
|
3
19
|
export { verifyAttestation } from "./src/attestation.js";
|
|
4
20
|
export { verifyAssertion } from "./src/assertion.js";
|
|
5
21
|
export { AssertionError, AssertionErrorCode, AttestationError, AttestationErrorCode, } from "./src/errors.js";
|
|
22
|
+
export { withAssertion } from "./src/with-assertion.js";
|
|
23
|
+
export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
|
|
24
|
+
export type { AssertionContext, DeviceKey, ExtractAssertionFn, WithAssertionOptions, } from "./src/with-assertion.js";
|
|
6
25
|
//# sourceMappingURL=mod.d.ts.map
|
package/esm/mod.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;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,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC"}
|
package/esm/mod.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Verify Apple App Attest attestations and assertions using WebCrypto.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { verifyAttestation, verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
6
|
+
*
|
|
7
|
+
* const { publicKeyPem } = await verifyAttestation(
|
|
8
|
+
* { appId: "TEAMID.com.example.app" },
|
|
9
|
+
* keyId,
|
|
10
|
+
* challenge,
|
|
11
|
+
* attestation,
|
|
12
|
+
* );
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
2
17
|
export { verifyAttestation } from "./src/attestation.js";
|
|
3
18
|
export { verifyAssertion } from "./src/assertion.js";
|
|
4
19
|
export { AssertionError, AssertionErrorCode, AttestationError, AttestationErrorCode, } from "./src/errors.js";
|
|
20
|
+
// withAssertion wrapper
|
|
21
|
+
export { withAssertion } from "./src/with-assertion.js";
|
|
22
|
+
export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
|
package/esm/src/assertion.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
/** Identifies the app whose assertions are being verified. */
|
|
1
2
|
export interface AppInfo {
|
|
3
|
+
/** Apple App ID in the format `TEAMID.bundleId`. */
|
|
2
4
|
appId: string;
|
|
5
|
+
/** Set to `true` when verifying assertions from the development environment. */
|
|
3
6
|
developmentEnv?: boolean;
|
|
4
7
|
}
|
|
8
|
+
/** Successful assertion verification result. */
|
|
5
9
|
export interface AssertionResult {
|
|
10
|
+
/** Updated sign count to persist for the next assertion. */
|
|
6
11
|
signCount: number;
|
|
7
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Verify an Apple App Attest assertion.
|
|
15
|
+
*
|
|
16
|
+
* Validates the CBOR-encoded assertion against the expected app ID,
|
|
17
|
+
* checks the monotonic sign counter, and verifies the ECDSA signature
|
|
18
|
+
* using the device's public key.
|
|
19
|
+
*
|
|
20
|
+
* @throws {AssertionError} If any verification step fails.
|
|
21
|
+
*/
|
|
8
22
|
export declare function verifyAssertion(appInfo: AppInfo, assertion: Uint8Array | string, clientData: Uint8Array | string, publicKeyPem: string, previousSignCount: number): Promise<AssertionResult>;
|
|
9
23
|
//# sourceMappingURL=assertion.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertion.d.ts","sourceRoot":"","sources":["../../src/src/assertion.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,UAAU,GAAG,MAAM,EAC9B,UAAU,EAAE,UAAU,GAAG,MAAM,EAC/B,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,eAAe,CAAC,CA2G1B"}
|
|
1
|
+
{"version":3,"file":"assertion.d.ts","sourceRoot":"","sources":["../../src/src/assertion.ts"],"names":[],"mappings":"AAaA,8DAA8D;AAC9D,MAAM,WAAW,OAAO;IACtB,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,UAAU,GAAG,MAAM,EAC9B,UAAU,EAAE,UAAU,GAAG,MAAM,EAC/B,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,eAAe,CAAC,CA2G1B"}
|
package/esm/src/assertion.js
CHANGED
|
@@ -4,6 +4,15 @@ import { parseAssertionAuthData } from "./authdata.js";
|
|
|
4
4
|
import { derToRaw } from "./der.js";
|
|
5
5
|
import { AssertionError, AssertionErrorCode } from "./errors.js";
|
|
6
6
|
import { concat, constantTimeEqual, decodeBase64Bytes, importPemPublicKey, toBytes, } from "./utils.js";
|
|
7
|
+
/**
|
|
8
|
+
* Verify an Apple App Attest assertion.
|
|
9
|
+
*
|
|
10
|
+
* Validates the CBOR-encoded assertion against the expected app ID,
|
|
11
|
+
* checks the monotonic sign counter, and verifies the ECDSA signature
|
|
12
|
+
* using the device's public key.
|
|
13
|
+
*
|
|
14
|
+
* @throws {AssertionError} If any verification step fails.
|
|
15
|
+
*/
|
|
7
16
|
export async function verifyAssertion(appInfo, assertion, clientData, publicKeyPem, previousSignCount) {
|
|
8
17
|
const assertionBytes = decodeBase64Bytes(assertion);
|
|
9
18
|
const clientDataBytes = toBytes(clientData);
|
package/esm/src/attestation.d.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
/** Identifies the app being attested. */
|
|
1
2
|
export interface AppInfo {
|
|
3
|
+
/** Apple App ID in the format `TEAMID.bundleId`. */
|
|
2
4
|
appId: string;
|
|
5
|
+
/** Set to `true` when verifying attestations from the development environment. */
|
|
3
6
|
developmentEnv?: boolean;
|
|
4
7
|
}
|
|
8
|
+
/** Successful attestation verification result. */
|
|
5
9
|
export interface AttestationResult {
|
|
10
|
+
/** PEM-encoded ECDSA P-256 public key extracted from the attestation. */
|
|
6
11
|
publicKeyPem: string;
|
|
12
|
+
/** Raw App Attest receipt bytes for server-side refresh. */
|
|
7
13
|
receipt: Uint8Array;
|
|
14
|
+
/** Initial sign count (always `0` for attestation). */
|
|
8
15
|
signCount: number;
|
|
9
16
|
}
|
|
17
|
+
/** Options for {@linkcode verifyAttestation}. */
|
|
10
18
|
export interface VerifyAttestationOptions {
|
|
11
|
-
/** Override date for certificate chain validation (for testing with expired certs) */
|
|
19
|
+
/** Override date for certificate chain validation (for testing with expired certs). */
|
|
12
20
|
checkDate?: Date;
|
|
13
21
|
}
|
|
14
22
|
/**
|
|
@@ -32,6 +40,17 @@ export interface AttestationCbor {
|
|
|
32
40
|
};
|
|
33
41
|
authData: Uint8Array;
|
|
34
42
|
}
|
|
43
|
+
/** Decode an Apple App Attest attestation object from raw CBOR bytes. */
|
|
35
44
|
export declare function decodeAttestationCbor(data: Uint8Array): AttestationCbor;
|
|
45
|
+
/**
|
|
46
|
+
* Verify an Apple App Attest attestation.
|
|
47
|
+
*
|
|
48
|
+
* Implements the full server-side verification described in
|
|
49
|
+
* [Apple's documentation](https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server):
|
|
50
|
+
* CBOR decode, certificate chain validation, nonce check, key extraction,
|
|
51
|
+
* AAGUID check, and credential ID verification.
|
|
52
|
+
*
|
|
53
|
+
* @throws {AttestationError} If any verification step fails.
|
|
54
|
+
*/
|
|
36
55
|
export declare function verifyAttestation(appInfo: AppInfo, keyId: string, challenge: Uint8Array | string, attestation: Uint8Array | string, options?: VerifyAttestationOptions): Promise<AttestationResult>;
|
|
37
56
|
//# sourceMappingURL=attestation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../src/src/attestation.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,UAAU,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC,
|
|
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"}
|
package/esm/src/attestation.js
CHANGED
|
@@ -20,10 +20,10 @@ function readCborUint(data, offset) {
|
|
|
20
20
|
}
|
|
21
21
|
if (additional === 26) {
|
|
22
22
|
return {
|
|
23
|
-
value: ((data[offset + 1] << 24)
|
|
24
|
-
(data[offset + 2] << 16)
|
|
25
|
-
(data[offset + 3] << 8)
|
|
26
|
-
data[offset + 4],
|
|
23
|
+
value: ((data[offset + 1] << 24) |
|
|
24
|
+
(data[offset + 2] << 16) |
|
|
25
|
+
(data[offset + 3] << 8) |
|
|
26
|
+
data[offset + 4]) >>> 0,
|
|
27
27
|
end: offset + 5,
|
|
28
28
|
};
|
|
29
29
|
}
|
|
@@ -88,6 +88,7 @@ function findCborTextKey(data, key, startOffset) {
|
|
|
88
88
|
}
|
|
89
89
|
return -1;
|
|
90
90
|
}
|
|
91
|
+
/** Decode an Apple App Attest attestation object from raw CBOR bytes. */
|
|
91
92
|
export function decodeAttestationCbor(data) {
|
|
92
93
|
// Verify top-level is a CBOR map
|
|
93
94
|
const majorType = (data[0] >> 5) & 0x07;
|
|
@@ -154,6 +155,16 @@ export function decodeAttestationCbor(data) {
|
|
|
154
155
|
const { value: authData } = readCborBytes(data, authDataValuePos);
|
|
155
156
|
return { fmt, attStmt: { x5c, receipt }, authData };
|
|
156
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Verify an Apple App Attest attestation.
|
|
160
|
+
*
|
|
161
|
+
* Implements the full server-side verification described in
|
|
162
|
+
* [Apple's documentation](https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server):
|
|
163
|
+
* CBOR decode, certificate chain validation, nonce check, key extraction,
|
|
164
|
+
* AAGUID check, and credential ID verification.
|
|
165
|
+
*
|
|
166
|
+
* @throws {AttestationError} If any verification step fails.
|
|
167
|
+
*/
|
|
157
168
|
export async function verifyAttestation(appInfo, keyId, challenge, attestation, options) {
|
|
158
169
|
// Decode attestation bytes from base64 if string
|
|
159
170
|
let attestationBytes;
|
package/esm/src/errors.d.ts
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
|
+
/** Error codes returned by {@linkcode AttestationError}. */
|
|
1
2
|
export declare enum AttestationErrorCode {
|
|
3
|
+
/** CBOR decoding or structural validation failed. */
|
|
2
4
|
INVALID_FORMAT = "INVALID_FORMAT",
|
|
5
|
+
/** X.509 certificate chain verification failed. */
|
|
3
6
|
INVALID_CERTIFICATE_CHAIN = "INVALID_CERTIFICATE_CHAIN",
|
|
7
|
+
/** Computed nonce does not match the nonce in the leaf certificate. */
|
|
4
8
|
NONCE_MISMATCH = "NONCE_MISMATCH",
|
|
9
|
+
/** RP ID hash does not match SHA-256 of the app ID. */
|
|
5
10
|
RP_ID_MISMATCH = "RP_ID_MISMATCH",
|
|
11
|
+
/** Public key hash does not match the provided key ID. */
|
|
6
12
|
KEY_ID_MISMATCH = "KEY_ID_MISMATCH",
|
|
13
|
+
/** Sign count is not zero (required for attestation). */
|
|
7
14
|
INVALID_COUNTER = "INVALID_COUNTER",
|
|
15
|
+
/** AAGUID does not match the expected environment (production/development). */
|
|
8
16
|
INVALID_AAGUID = "INVALID_AAGUID"
|
|
9
17
|
}
|
|
18
|
+
/** Thrown when attestation verification fails. */
|
|
10
19
|
export declare class AttestationError extends Error {
|
|
20
|
+
/** Machine-readable error code. */
|
|
11
21
|
readonly code: AttestationErrorCode;
|
|
12
22
|
readonly name = "AttestationError";
|
|
13
|
-
constructor(
|
|
23
|
+
constructor(
|
|
24
|
+
/** Machine-readable error code. */
|
|
25
|
+
code: AttestationErrorCode, message: string);
|
|
14
26
|
}
|
|
27
|
+
/** Error codes returned by {@linkcode AssertionError}. */
|
|
15
28
|
export declare enum AssertionErrorCode {
|
|
29
|
+
/** CBOR decoding or structural validation failed. */
|
|
16
30
|
INVALID_FORMAT = "INVALID_FORMAT",
|
|
31
|
+
/** RP ID hash does not match SHA-256 of the app ID. */
|
|
17
32
|
RP_ID_MISMATCH = "RP_ID_MISMATCH",
|
|
33
|
+
/** Sign count was not greater than the previously stored value. */
|
|
18
34
|
COUNTER_NOT_INCREMENTED = "COUNTER_NOT_INCREMENTED",
|
|
19
|
-
|
|
35
|
+
/** ECDSA signature verification failed. */
|
|
36
|
+
SIGNATURE_INVALID = "SIGNATURE_INVALID",
|
|
37
|
+
/** No device key found for the given device ID (used by {@linkcode withAssertion}). */
|
|
38
|
+
DEVICE_NOT_FOUND = "DEVICE_NOT_FOUND",
|
|
39
|
+
/** An internal or storage callback error occurred (used by {@linkcode withAssertion}). */
|
|
40
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
20
41
|
}
|
|
42
|
+
/** Thrown when assertion verification fails. */
|
|
21
43
|
export declare class AssertionError extends Error {
|
|
44
|
+
/** Machine-readable error code. */
|
|
22
45
|
readonly code: AssertionErrorCode;
|
|
23
46
|
readonly name = "AssertionError";
|
|
24
|
-
constructor(
|
|
47
|
+
constructor(
|
|
48
|
+
/** Machine-readable error code. */
|
|
49
|
+
code: AssertionErrorCode, message: string, options?: {
|
|
50
|
+
cause?: unknown;
|
|
51
|
+
});
|
|
25
52
|
}
|
|
26
53
|
//# sourceMappingURL=errors.d.ts.map
|
package/esm/src/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/src/errors.ts"],"names":[],"mappings":"AAEA,oBAAY,oBAAoB;IAC9B,cAAc,mBAAmB;IACjC,yBAAyB,8BAA8B;IACvD,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,eAAe,oBAAoB;IACnC,eAAe,oBAAoB;IACnC,cAAc,mBAAmB;CAClC;AAED,qBAAa,gBAAiB,SAAQ,KAAK;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/src/errors.ts"],"names":[],"mappings":"AAEA,4DAA4D;AAC5D,oBAAY,oBAAoB;IAC9B,qDAAqD;IACrD,cAAc,mBAAmB;IACjC,mDAAmD;IACnD,yBAAyB,8BAA8B;IACvD,uEAAuE;IACvE,cAAc,mBAAmB;IACjC,uDAAuD;IACvD,cAAc,mBAAmB;IACjC,0DAA0D;IAC1D,eAAe,oBAAoB;IACnC,yDAAyD;IACzD,eAAe,oBAAoB;IACnC,+EAA+E;IAC/E,cAAc,mBAAmB;CAClC;AAED,kDAAkD;AAClD,qBAAa,gBAAiB,SAAQ,KAAK;IAGvC,mCAAmC;aACnB,IAAI,EAAE,oBAAoB;IAH5C,SAAkB,IAAI,sBAAsB;;IAE1C,mCAAmC;IACnB,IAAI,EAAE,oBAAoB,EAC1C,OAAO,EAAE,MAAM;CAIlB;AAED,0DAA0D;AAC1D,oBAAY,kBAAkB;IAC5B,qDAAqD;IACrD,cAAc,mBAAmB;IACjC,uDAAuD;IACvD,cAAc,mBAAmB;IACjC,mEAAmE;IACnE,uBAAuB,4BAA4B;IACnD,2CAA2C;IAC3C,iBAAiB,sBAAsB;IACvC,uFAAuF;IACvF,gBAAgB,qBAAqB;IACrC,0FAA0F;IAC1F,cAAc,mBAAmB;CAClC;AAED,gDAAgD;AAChD,qBAAa,cAAe,SAAQ,KAAK;IAGrC,mCAAmC;aACnB,IAAI,EAAE,kBAAkB;IAH1C,SAAkB,IAAI,oBAAoB;;IAExC,mCAAmC;IACnB,IAAI,EAAE,kBAAkB,EACxC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIhC"}
|
package/esm/src/errors.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
// src/errors.ts
|
|
2
|
+
/** Error codes returned by {@linkcode AttestationError}. */
|
|
2
3
|
export var AttestationErrorCode;
|
|
3
4
|
(function (AttestationErrorCode) {
|
|
5
|
+
/** CBOR decoding or structural validation failed. */
|
|
4
6
|
AttestationErrorCode["INVALID_FORMAT"] = "INVALID_FORMAT";
|
|
7
|
+
/** X.509 certificate chain verification failed. */
|
|
5
8
|
AttestationErrorCode["INVALID_CERTIFICATE_CHAIN"] = "INVALID_CERTIFICATE_CHAIN";
|
|
9
|
+
/** Computed nonce does not match the nonce in the leaf certificate. */
|
|
6
10
|
AttestationErrorCode["NONCE_MISMATCH"] = "NONCE_MISMATCH";
|
|
11
|
+
/** RP ID hash does not match SHA-256 of the app ID. */
|
|
7
12
|
AttestationErrorCode["RP_ID_MISMATCH"] = "RP_ID_MISMATCH";
|
|
13
|
+
/** Public key hash does not match the provided key ID. */
|
|
8
14
|
AttestationErrorCode["KEY_ID_MISMATCH"] = "KEY_ID_MISMATCH";
|
|
15
|
+
/** Sign count is not zero (required for attestation). */
|
|
9
16
|
AttestationErrorCode["INVALID_COUNTER"] = "INVALID_COUNTER";
|
|
17
|
+
/** AAGUID does not match the expected environment (production/development). */
|
|
10
18
|
AttestationErrorCode["INVALID_AAGUID"] = "INVALID_AAGUID";
|
|
11
19
|
})(AttestationErrorCode || (AttestationErrorCode = {}));
|
|
20
|
+
/** Thrown when attestation verification fails. */
|
|
12
21
|
export class AttestationError extends Error {
|
|
13
|
-
constructor(
|
|
22
|
+
constructor(
|
|
23
|
+
/** Machine-readable error code. */
|
|
24
|
+
code, message) {
|
|
14
25
|
super(message);
|
|
15
26
|
Object.defineProperty(this, "code", {
|
|
16
27
|
enumerable: true,
|
|
@@ -26,16 +37,28 @@ export class AttestationError extends Error {
|
|
|
26
37
|
});
|
|
27
38
|
}
|
|
28
39
|
}
|
|
40
|
+
/** Error codes returned by {@linkcode AssertionError}. */
|
|
29
41
|
export var AssertionErrorCode;
|
|
30
42
|
(function (AssertionErrorCode) {
|
|
43
|
+
/** CBOR decoding or structural validation failed. */
|
|
31
44
|
AssertionErrorCode["INVALID_FORMAT"] = "INVALID_FORMAT";
|
|
45
|
+
/** RP ID hash does not match SHA-256 of the app ID. */
|
|
32
46
|
AssertionErrorCode["RP_ID_MISMATCH"] = "RP_ID_MISMATCH";
|
|
47
|
+
/** Sign count was not greater than the previously stored value. */
|
|
33
48
|
AssertionErrorCode["COUNTER_NOT_INCREMENTED"] = "COUNTER_NOT_INCREMENTED";
|
|
49
|
+
/** ECDSA signature verification failed. */
|
|
34
50
|
AssertionErrorCode["SIGNATURE_INVALID"] = "SIGNATURE_INVALID";
|
|
51
|
+
/** No device key found for the given device ID (used by {@linkcode withAssertion}). */
|
|
52
|
+
AssertionErrorCode["DEVICE_NOT_FOUND"] = "DEVICE_NOT_FOUND";
|
|
53
|
+
/** An internal or storage callback error occurred (used by {@linkcode withAssertion}). */
|
|
54
|
+
AssertionErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
35
55
|
})(AssertionErrorCode || (AssertionErrorCode = {}));
|
|
56
|
+
/** Thrown when assertion verification fails. */
|
|
36
57
|
export class AssertionError extends Error {
|
|
37
|
-
constructor(
|
|
38
|
-
|
|
58
|
+
constructor(
|
|
59
|
+
/** Machine-readable error code. */
|
|
60
|
+
code, message, options) {
|
|
61
|
+
super(message, options);
|
|
39
62
|
Object.defineProperty(this, "code", {
|
|
40
63
|
enumerable: true,
|
|
41
64
|
configurable: true,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AssertionError } from "./errors.js";
|
|
2
|
+
/** Default HTTP header name for the base64-encoded assertion. */
|
|
3
|
+
export declare const DEFAULT_ASSERTION_HEADER = "X-App-Attest-Assertion";
|
|
4
|
+
/** Default HTTP header name for the device identifier. */
|
|
5
|
+
export declare const DEFAULT_DEVICE_ID_HEADER = "X-App-Attest-Device-Id";
|
|
6
|
+
/** Stored device key material returned by the `getDeviceKey` callback. */
|
|
7
|
+
export type DeviceKey = {
|
|
8
|
+
/** PEM-encoded ECDSA P-256 public key from attestation. */
|
|
9
|
+
publicKeyPem: string;
|
|
10
|
+
/** Last verified sign count for this device. */
|
|
11
|
+
signCount: number;
|
|
12
|
+
};
|
|
13
|
+
/** Context passed to the inner handler after successful assertion verification. */
|
|
14
|
+
export type AssertionContext = {
|
|
15
|
+
/** Device identifier from the request. */
|
|
16
|
+
deviceId: string;
|
|
17
|
+
/** Updated sign count after verification. */
|
|
18
|
+
signCount: number;
|
|
19
|
+
/** Raw request body bytes (the client data that was signed). */
|
|
20
|
+
rawBody: Uint8Array;
|
|
21
|
+
};
|
|
22
|
+
/** Custom function to extract assertion data from an incoming request. */
|
|
23
|
+
export type ExtractAssertionFn = (req: Request) => Promise<{
|
|
24
|
+
assertion: string;
|
|
25
|
+
deviceId: string;
|
|
26
|
+
clientData: Uint8Array;
|
|
27
|
+
}>;
|
|
28
|
+
/** Configuration for the {@linkcode withAssertion} middleware. */
|
|
29
|
+
export type WithAssertionOptions = {
|
|
30
|
+
/** Apple App ID in the format `TEAMID.bundleId`. */
|
|
31
|
+
appId: string;
|
|
32
|
+
/** Set to `true` for development environment attestations. */
|
|
33
|
+
developmentEnv?: boolean;
|
|
34
|
+
/** Retrieve the stored device key for a given device ID. Return `null` if not found. */
|
|
35
|
+
getDeviceKey: (deviceId: string) => Promise<DeviceKey | null>;
|
|
36
|
+
/** Persist the new sign count after successful verification. */
|
|
37
|
+
updateSignCount: (deviceId: string, newSignCount: number) => Promise<void>;
|
|
38
|
+
/** Override the default header-based assertion extraction. */
|
|
39
|
+
extractAssertion?: ExtractAssertionFn;
|
|
40
|
+
/** Custom error response handler. Defaults to JSON error responses. */
|
|
41
|
+
onError?: (error: AssertionError, req: Request) => Response | Promise<Response>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Request handler middleware that verifies App Attest assertions.
|
|
45
|
+
*
|
|
46
|
+
* Wraps a handler function with automatic assertion verification,
|
|
47
|
+
* device key lookup, and sign count management. Returns a new handler
|
|
48
|
+
* that rejects unauthenticated requests with appropriate HTTP error responses.
|
|
49
|
+
*/
|
|
50
|
+
export declare function withAssertion(options: WithAssertionOptions, handler: (req: Request, context: AssertionContext) => Response | Promise<Response>): (req: Request) => Promise<Response>;
|
|
51
|
+
//# sourceMappingURL=with-assertion.d.ts.map
|
|
@@ -0,0 +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,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;CACrB,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,gEAAgE;IAChE,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,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;;;;;;GAMG;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,CAuErC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/with-assertion.ts
|
|
2
|
+
import { verifyAssertion } from "./assertion.js";
|
|
3
|
+
import { AssertionError, AssertionErrorCode } from "./errors.js";
|
|
4
|
+
/** Default HTTP header name for the base64-encoded assertion. */
|
|
5
|
+
export const DEFAULT_ASSERTION_HEADER = "X-App-Attest-Assertion";
|
|
6
|
+
/** Default HTTP header name for the device identifier. */
|
|
7
|
+
export const DEFAULT_DEVICE_ID_HEADER = "X-App-Attest-Device-Id";
|
|
8
|
+
async function defaultExtractAssertion(req) {
|
|
9
|
+
const assertion = req.headers.get(DEFAULT_ASSERTION_HEADER);
|
|
10
|
+
const deviceId = req.headers.get(DEFAULT_DEVICE_ID_HEADER);
|
|
11
|
+
if (!assertion || !deviceId) {
|
|
12
|
+
throw new AssertionError(AssertionErrorCode.INVALID_FORMAT, `Missing ${DEFAULT_ASSERTION_HEADER} or ${DEFAULT_DEVICE_ID_HEADER} header`);
|
|
13
|
+
}
|
|
14
|
+
const clientData = new Uint8Array(await req.arrayBuffer());
|
|
15
|
+
return { assertion, deviceId, clientData };
|
|
16
|
+
}
|
|
17
|
+
function defaultErrorResponse(error) {
|
|
18
|
+
const status = error.code === AssertionErrorCode.INTERNAL_ERROR
|
|
19
|
+
? 500
|
|
20
|
+
: error.code === AssertionErrorCode.INVALID_FORMAT
|
|
21
|
+
? 400
|
|
22
|
+
: 401;
|
|
23
|
+
return new Response(JSON.stringify({ error: error.message, code: error.code }), { status, headers: { "Content-Type": "application/json" } });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Request handler middleware that verifies App Attest assertions.
|
|
27
|
+
*
|
|
28
|
+
* Wraps a handler function with automatic assertion verification,
|
|
29
|
+
* device key lookup, and sign count management. Returns a new handler
|
|
30
|
+
* that rejects unauthenticated requests with appropriate HTTP error responses.
|
|
31
|
+
*/
|
|
32
|
+
export function withAssertion(options, handler) {
|
|
33
|
+
const appInfo = {
|
|
34
|
+
appId: options.appId,
|
|
35
|
+
developmentEnv: options.developmentEnv ?? false,
|
|
36
|
+
};
|
|
37
|
+
const extract = options.extractAssertion ?? defaultExtractAssertion;
|
|
38
|
+
return async (req) => {
|
|
39
|
+
let deviceId;
|
|
40
|
+
let clientData;
|
|
41
|
+
let newSignCount;
|
|
42
|
+
// Steps 1-4: extract, verify, update sign count
|
|
43
|
+
try {
|
|
44
|
+
const extracted = await extract(req);
|
|
45
|
+
deviceId = extracted.deviceId;
|
|
46
|
+
clientData = extracted.clientData;
|
|
47
|
+
let deviceKey;
|
|
48
|
+
try {
|
|
49
|
+
deviceKey = await options.getDeviceKey(deviceId);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw new AssertionError(AssertionErrorCode.INTERNAL_ERROR, "Storage callback failed", { cause: err });
|
|
53
|
+
}
|
|
54
|
+
if (!deviceKey) {
|
|
55
|
+
throw new AssertionError(AssertionErrorCode.DEVICE_NOT_FOUND, "Device not found");
|
|
56
|
+
}
|
|
57
|
+
const result = await verifyAssertion(appInfo, extracted.assertion, clientData, deviceKey.publicKeyPem, deviceKey.signCount);
|
|
58
|
+
try {
|
|
59
|
+
await options.updateSignCount(deviceId, result.signCount);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
throw new AssertionError(AssertionErrorCode.INTERNAL_ERROR, "Failed to update sign count", { cause: err });
|
|
63
|
+
}
|
|
64
|
+
newSignCount = result.signCount;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const error = err instanceof AssertionError
|
|
68
|
+
? err
|
|
69
|
+
: new AssertionError(AssertionErrorCode.INTERNAL_ERROR, String(err), {
|
|
70
|
+
cause: err,
|
|
71
|
+
});
|
|
72
|
+
return options.onError?.(error, req) ?? defaultErrorResponse(error);
|
|
73
|
+
}
|
|
74
|
+
// Step 5: handler — outside try/catch, errors bubble up
|
|
75
|
+
return await handler(req, {
|
|
76
|
+
deviceId,
|
|
77
|
+
signCount: newSignCount,
|
|
78
|
+
rawBody: clientData,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertion-entry.test.d.ts","sourceRoot":"","sources":["../../src/tests/assertion-entry.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"assertion-entry.test.d.ts","sourceRoot":"","sources":["../../src/tests/assertion-entry.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertion.test.d.ts","sourceRoot":"","sources":["../../src/tests/assertion.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"assertion.test.d.ts","sourceRoot":"","sources":["../../src/tests/assertion.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attestation-entry.test.d.ts","sourceRoot":"","sources":["../../src/tests/attestation-entry.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"attestation-entry.test.d.ts","sourceRoot":"","sources":["../../src/tests/attestation-entry.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.test.d.ts","sourceRoot":"","sources":["../../src/tests/attestation.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"attestation.test.d.ts","sourceRoot":"","sources":["../../src/tests/attestation.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authdata.test.d.ts","sourceRoot":"","sources":["../../src/tests/authdata.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"authdata.test.d.ts","sourceRoot":"","sources":["../../src/tests/authdata.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"certificate.test.d.ts","sourceRoot":"","sources":["../../src/tests/certificate.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"certificate.test.d.ts","sourceRoot":"","sources":["../../src/tests/certificate.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cose.test.d.ts","sourceRoot":"","sources":["../../src/tests/cose.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"cose.test.d.ts","sourceRoot":"","sources":["../../src/tests/cose.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"der.test.d.ts","sourceRoot":"","sources":["../../src/tests/der.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"der.test.d.ts","sourceRoot":"","sources":["../../src/tests/der.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/tests/errors.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/tests/errors.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../src/tests/utils.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../src/tests/utils.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-assertion.test.d.ts","sourceRoot":"","sources":["../../src/tests/with-assertion.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|