@bradford-tech/supabase-integrity-attest 0.3.1 → 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 +1 -1
- package/esm/assertion.d.ts +18 -0
- package/esm/assertion.d.ts.map +1 -1
- package/esm/assertion.js +18 -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 +16 -0
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +16 -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 +25 -2
- package/esm/src/errors.d.ts.map +1 -1
- package/esm/src/errors.js +23 -2
- package/esm/src/with-assertion.d.ts +24 -0
- package/esm/src/with-assertion.d.ts.map +1 -1
- package/esm/src/with-assertion.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ 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
14
|
```
|
|
15
15
|
|
|
16
16
|
## Subpath imports
|
package/esm/assertion.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
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";
|
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,4 +1,21 @@
|
|
|
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";
|
|
4
21
|
// withAssertion wrapper
|
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,3 +1,19 @@
|
|
|
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";
|
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,19 @@
|
|
|
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";
|
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,29 +1,52 @@
|
|
|
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",
|
|
35
|
+
/** ECDSA signature verification failed. */
|
|
19
36
|
SIGNATURE_INVALID = "SIGNATURE_INVALID",
|
|
37
|
+
/** No device key found for the given device ID (used by {@linkcode withAssertion}). */
|
|
20
38
|
DEVICE_NOT_FOUND = "DEVICE_NOT_FOUND",
|
|
39
|
+
/** An internal or storage callback error occurred (used by {@linkcode withAssertion}). */
|
|
21
40
|
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
22
41
|
}
|
|
42
|
+
/** Thrown when assertion verification fails. */
|
|
23
43
|
export declare class AssertionError extends Error {
|
|
44
|
+
/** Machine-readable error code. */
|
|
24
45
|
readonly code: AssertionErrorCode;
|
|
25
46
|
readonly name = "AssertionError";
|
|
26
|
-
constructor(
|
|
47
|
+
constructor(
|
|
48
|
+
/** Machine-readable error code. */
|
|
49
|
+
code: AssertionErrorCode, message: string, options?: {
|
|
27
50
|
cause?: unknown;
|
|
28
51
|
});
|
|
29
52
|
}
|
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,17 +37,27 @@ 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}). */
|
|
35
52
|
AssertionErrorCode["DEVICE_NOT_FOUND"] = "DEVICE_NOT_FOUND";
|
|
53
|
+
/** An internal or storage callback error occurred (used by {@linkcode withAssertion}). */
|
|
36
54
|
AssertionErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
37
55
|
})(AssertionErrorCode || (AssertionErrorCode = {}));
|
|
56
|
+
/** Thrown when assertion verification fails. */
|
|
38
57
|
export class AssertionError extends Error {
|
|
39
|
-
constructor(
|
|
58
|
+
constructor(
|
|
59
|
+
/** Machine-readable error code. */
|
|
60
|
+
code, message, options) {
|
|
40
61
|
super(message, options);
|
|
41
62
|
Object.defineProperty(this, "code", {
|
|
42
63
|
enumerable: true,
|
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
import { AssertionError } from "./errors.js";
|
|
2
|
+
/** Default HTTP header name for the base64-encoded assertion. */
|
|
2
3
|
export declare const DEFAULT_ASSERTION_HEADER = "X-App-Attest-Assertion";
|
|
4
|
+
/** Default HTTP header name for the device identifier. */
|
|
3
5
|
export declare const DEFAULT_DEVICE_ID_HEADER = "X-App-Attest-Device-Id";
|
|
6
|
+
/** Stored device key material returned by the `getDeviceKey` callback. */
|
|
4
7
|
export type DeviceKey = {
|
|
8
|
+
/** PEM-encoded ECDSA P-256 public key from attestation. */
|
|
5
9
|
publicKeyPem: string;
|
|
10
|
+
/** Last verified sign count for this device. */
|
|
6
11
|
signCount: number;
|
|
7
12
|
};
|
|
13
|
+
/** Context passed to the inner handler after successful assertion verification. */
|
|
8
14
|
export type AssertionContext = {
|
|
15
|
+
/** Device identifier from the request. */
|
|
9
16
|
deviceId: string;
|
|
17
|
+
/** Updated sign count after verification. */
|
|
10
18
|
signCount: number;
|
|
19
|
+
/** Raw request body bytes (the client data that was signed). */
|
|
11
20
|
rawBody: Uint8Array;
|
|
12
21
|
};
|
|
22
|
+
/** Custom function to extract assertion data from an incoming request. */
|
|
13
23
|
export type ExtractAssertionFn = (req: Request) => Promise<{
|
|
14
24
|
assertion: string;
|
|
15
25
|
deviceId: string;
|
|
16
26
|
clientData: Uint8Array;
|
|
17
27
|
}>;
|
|
28
|
+
/** Configuration for the {@linkcode withAssertion} middleware. */
|
|
18
29
|
export type WithAssertionOptions = {
|
|
30
|
+
/** Apple App ID in the format `TEAMID.bundleId`. */
|
|
19
31
|
appId: string;
|
|
32
|
+
/** Set to `true` for development environment attestations. */
|
|
20
33
|
developmentEnv?: boolean;
|
|
34
|
+
/** Retrieve the stored device key for a given device ID. Return `null` if not found. */
|
|
21
35
|
getDeviceKey: (deviceId: string) => Promise<DeviceKey | null>;
|
|
36
|
+
/** Persist the new sign count after successful verification. */
|
|
22
37
|
updateSignCount: (deviceId: string, newSignCount: number) => Promise<void>;
|
|
38
|
+
/** Override the default header-based assertion extraction. */
|
|
23
39
|
extractAssertion?: ExtractAssertionFn;
|
|
40
|
+
/** Custom error response handler. Defaults to JSON error responses. */
|
|
24
41
|
onError?: (error: AssertionError, req: Request) => Response | Promise<Response>;
|
|
25
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
|
+
*/
|
|
26
50
|
export declare function withAssertion(options: WithAssertionOptions, handler: (req: Request, context: AssertionContext) => Response | Promise<Response>): (req: Request) => Promise<Response>;
|
|
27
51
|
//# sourceMappingURL=with-assertion.d.ts.map
|
|
@@ -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,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AACjE,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,MAAM,MAAM,SAAS,GAAG;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;CACrB,CAAC;AAEF,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,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC9D,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IACtC,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAmCF,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"}
|
|
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"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/with-assertion.ts
|
|
2
2
|
import { verifyAssertion } from "./assertion.js";
|
|
3
3
|
import { AssertionError, AssertionErrorCode } from "./errors.js";
|
|
4
|
+
/** Default HTTP header name for the base64-encoded assertion. */
|
|
4
5
|
export const DEFAULT_ASSERTION_HEADER = "X-App-Attest-Assertion";
|
|
6
|
+
/** Default HTTP header name for the device identifier. */
|
|
5
7
|
export const DEFAULT_DEVICE_ID_HEADER = "X-App-Attest-Device-Id";
|
|
6
8
|
async function defaultExtractAssertion(req) {
|
|
7
9
|
const assertion = req.headers.get(DEFAULT_ASSERTION_HEADER);
|
|
@@ -20,6 +22,13 @@ function defaultErrorResponse(error) {
|
|
|
20
22
|
: 401;
|
|
21
23
|
return new Response(JSON.stringify({ error: error.message, code: error.code }), { status, headers: { "Content-Type": "application/json" } });
|
|
22
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
|
+
*/
|
|
23
32
|
export function withAssertion(options, handler) {
|
|
24
33
|
const appInfo = {
|
|
25
34
|
appId: options.appId,
|