@bradford-tech/supabase-integrity-attest 0.2.1
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/LICENSE +21 -0
- package/README.md +87 -0
- package/esm/_dnt.test_shims.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assert.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equal.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equals.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/exists.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/fail.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/false.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/match.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/throws.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common64.d.ts +35 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common64.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common64.js +113 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common_detach.d.ts +4 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common_detach.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common_detach.js +13 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_types.d.ts +9 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_types.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_types.js +2 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base64.d.ts +40 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base64.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base64.js +82 -0
- package/esm/deps/jsr.io/@std/internal/1.0.12/build_message.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/internal/1.0.12/diff.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/internal/1.0.12/diff_str.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/internal/1.0.12/format.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/internal/1.0.12/styles.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/internal/1.0.12/types.d.ts.map +1 -0
- package/esm/mod.d.ts +6 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +4 -0
- package/esm/package.json +3 -0
- package/esm/src/assertion.d.ts +9 -0
- package/esm/src/assertion.d.ts.map +1 -0
- package/esm/src/assertion.js +64 -0
- package/esm/src/attestation.d.ts +37 -0
- package/esm/src/attestation.d.ts.map +1 -0
- package/esm/src/attestation.js +242 -0
- package/esm/src/authdata.d.ts +13 -0
- package/esm/src/authdata.d.ts.map +1 -0
- package/esm/src/authdata.js +29 -0
- package/esm/src/certificate.d.ts +25 -0
- package/esm/src/certificate.d.ts.map +1 -0
- package/esm/src/certificate.js +86 -0
- package/esm/src/constants.d.ts +9 -0
- package/esm/src/constants.d.ts.map +1 -0
- package/esm/src/constants.js +56 -0
- package/esm/src/cose.d.ts.map +1 -0
- package/esm/src/der.d.ts +3 -0
- package/esm/src/der.d.ts.map +1 -0
- package/esm/src/der.js +72 -0
- package/esm/src/errors.d.ts +26 -0
- package/esm/src/errors.d.ts.map +1 -0
- package/esm/src/errors.js +52 -0
- package/esm/src/utils.d.ts +13 -0
- package/esm/src/utils.d.ts.map +1 -0
- package/esm/src/utils.js +52 -0
- package/esm/tests/assertion.test.d.ts.map +1 -0
- package/esm/tests/attestation.test.d.ts.map +1 -0
- package/esm/tests/authdata.test.d.ts.map +1 -0
- package/esm/tests/certificate.test.d.ts.map +1 -0
- package/esm/tests/cose.test.d.ts.map +1 -0
- package/esm/tests/der.test.d.ts.map +1 -0
- package/esm/tests/errors.test.d.ts.map +1 -0
- package/esm/tests/fixtures/apple-attestation.d.ts.map +1 -0
- package/esm/tests/fixtures/generate-assertion.d.ts.map +1 -0
- package/esm/tests/utils.test.d.ts.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/assertion.ts
|
|
2
|
+
import { decode } from "cborg";
|
|
3
|
+
import { parseAssertionAuthData } from "./authdata.js";
|
|
4
|
+
import { derToRaw } from "./der.js";
|
|
5
|
+
import { AssertionError, AssertionErrorCode } from "./errors.js";
|
|
6
|
+
import { concat, constantTimeEqual, decodeBase64Bytes, importPemPublicKey, toBytes, } from "./utils.js";
|
|
7
|
+
export async function verifyAssertion(appInfo, assertion, clientData, publicKeyPem, previousSignCount) {
|
|
8
|
+
const assertionBytes = decodeBase64Bytes(assertion);
|
|
9
|
+
const clientDataBytes = toBytes(clientData);
|
|
10
|
+
// Step 1: CBOR decode
|
|
11
|
+
let decoded;
|
|
12
|
+
try {
|
|
13
|
+
decoded = decode(assertionBytes);
|
|
14
|
+
if (!decoded.signature || !decoded.authenticatorData) {
|
|
15
|
+
throw new Error("Missing required fields");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
throw new AssertionError(AssertionErrorCode.INVALID_FORMAT, `Failed to decode assertion: ${e instanceof Error ? e.message : String(e)}`);
|
|
20
|
+
}
|
|
21
|
+
// Step 2: Parse authenticatorData
|
|
22
|
+
let authData;
|
|
23
|
+
try {
|
|
24
|
+
authData = parseAssertionAuthData(decoded.authenticatorData);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
throw new AssertionError(AssertionErrorCode.INVALID_FORMAT, `Invalid authenticatorData: ${e instanceof Error ? e.message : String(e)}`);
|
|
28
|
+
}
|
|
29
|
+
// Step 3: Verify rpIdHash
|
|
30
|
+
const expectedRpIdHash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(appInfo.appId)));
|
|
31
|
+
if (!constantTimeEqual(authData.rpIdHash, expectedRpIdHash)) {
|
|
32
|
+
throw new AssertionError(AssertionErrorCode.RP_ID_MISMATCH, "rpIdHash does not match expected appId");
|
|
33
|
+
}
|
|
34
|
+
// Step 4: Verify counter
|
|
35
|
+
if (authData.signCount <= previousSignCount) {
|
|
36
|
+
throw new AssertionError(AssertionErrorCode.COUNTER_NOT_INCREMENTED, `signCount ${authData.signCount} is not greater than previous ${previousSignCount}`);
|
|
37
|
+
}
|
|
38
|
+
// Step 5: Compute clientDataHash
|
|
39
|
+
const clientDataHash = new Uint8Array(await crypto.subtle.digest("SHA-256", clientDataBytes));
|
|
40
|
+
// Step 6: Build message = authenticatorData || clientDataHash
|
|
41
|
+
const message = concat(decoded.authenticatorData, clientDataHash);
|
|
42
|
+
// Step 7: Convert DER signature to raw r||s
|
|
43
|
+
let signatureRaw;
|
|
44
|
+
try {
|
|
45
|
+
signatureRaw = derToRaw(decoded.signature);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
throw new AssertionError(AssertionErrorCode.INVALID_FORMAT, `Invalid DER signature: ${e instanceof Error ? e.message : String(e)}`);
|
|
49
|
+
}
|
|
50
|
+
// Step 8: Import PEM public key
|
|
51
|
+
let publicKey;
|
|
52
|
+
try {
|
|
53
|
+
publicKey = await importPemPublicKey(publicKeyPem);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
throw new AssertionError(AssertionErrorCode.INVALID_FORMAT, `Invalid public key PEM: ${e instanceof Error ? e.message : String(e)}`);
|
|
57
|
+
}
|
|
58
|
+
// Step 9: Verify ECDSA signature
|
|
59
|
+
const valid = await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, publicKey, signatureRaw, message);
|
|
60
|
+
if (!valid) {
|
|
61
|
+
throw new AssertionError(AssertionErrorCode.SIGNATURE_INVALID, "ECDSA signature verification failed");
|
|
62
|
+
}
|
|
63
|
+
return { signCount: authData.signCount };
|
|
64
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface AppInfo {
|
|
2
|
+
appId: string;
|
|
3
|
+
developmentEnv?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface AttestationResult {
|
|
6
|
+
publicKeyPem: string;
|
|
7
|
+
receipt: Uint8Array;
|
|
8
|
+
signCount: number;
|
|
9
|
+
}
|
|
10
|
+
export interface VerifyAttestationOptions {
|
|
11
|
+
/** Override date for certificate chain validation (for testing with expired certs) */
|
|
12
|
+
checkDate?: Date;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Minimal CBOR decoder for Apple App Attest attestation objects.
|
|
16
|
+
*
|
|
17
|
+
* Apple's CBOR encoding of the attestation object contains a receipt field whose
|
|
18
|
+
* byte-string length header is sometimes incorrect (overstated by ~21 bytes).
|
|
19
|
+
* Standard CBOR libraries (cborg) fail to decode this. We use a lightweight
|
|
20
|
+
* structure-aware parser that handles the known attestation object layout:
|
|
21
|
+
* { "fmt": text, "attStmt": { "x5c": [bstr, ...], "receipt": bstr }, "authData": bstr }
|
|
22
|
+
*
|
|
23
|
+
* The parser locates map keys by scanning for known text-string CBOR keys and
|
|
24
|
+
* extracts values based on their CBOR type headers, which avoids relying on
|
|
25
|
+
* potentially incorrect length fields for opaque byte-string values.
|
|
26
|
+
*/
|
|
27
|
+
export interface AttestationCbor {
|
|
28
|
+
fmt: string;
|
|
29
|
+
attStmt: {
|
|
30
|
+
x5c: Uint8Array[];
|
|
31
|
+
receipt: Uint8Array;
|
|
32
|
+
};
|
|
33
|
+
authData: Uint8Array;
|
|
34
|
+
}
|
|
35
|
+
export declare function decodeAttestationCbor(data: Uint8Array): AttestationCbor;
|
|
36
|
+
export declare function verifyAttestation(appInfo: AppInfo, keyId: string, challenge: Uint8Array | string, attestation: Uint8Array | string, options?: VerifyAttestationOptions): Promise<AttestationResult>;
|
|
37
|
+
//# sourceMappingURL=attestation.d.ts.map
|
|
@@ -0,0 +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,sFAAsF;IACtF,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,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,eAAe,CAyEvE;AAED,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"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// src/attestation.ts
|
|
2
|
+
import { decodeBase64 } from "../deps/jsr.io/@std/encoding/1.0.10/base64.js";
|
|
3
|
+
import { extractNonceFromCert, extractPublicKeyFromCert, verifyCertificateChain, } from "./certificate.js";
|
|
4
|
+
import { AAGUID_DEVELOPMENT, AAGUID_PRODUCTION } from "./constants.js";
|
|
5
|
+
import { AttestationError, AttestationErrorCode } from "./errors.js";
|
|
6
|
+
import { parseAttestationAuthData } from "./authdata.js";
|
|
7
|
+
import { concat, constantTimeEqual, exportKeyToPem, toBytes } from "./utils.js";
|
|
8
|
+
/** Read a CBOR unsigned integer (additional info + following bytes). */
|
|
9
|
+
function readCborUint(data, offset) {
|
|
10
|
+
const additional = data[offset] & 0x1f;
|
|
11
|
+
if (additional < 24)
|
|
12
|
+
return { value: additional, end: offset + 1 };
|
|
13
|
+
if (additional === 24)
|
|
14
|
+
return { value: data[offset + 1], end: offset + 2 };
|
|
15
|
+
if (additional === 25) {
|
|
16
|
+
return {
|
|
17
|
+
value: (data[offset + 1] << 8) | data[offset + 2],
|
|
18
|
+
end: offset + 3,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (additional === 26) {
|
|
22
|
+
return {
|
|
23
|
+
value: ((data[offset + 1] << 24) >>> 0) +
|
|
24
|
+
(data[offset + 2] << 16) +
|
|
25
|
+
(data[offset + 3] << 8) +
|
|
26
|
+
data[offset + 4],
|
|
27
|
+
end: offset + 5,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Unsupported CBOR additional info: ${additional}`);
|
|
31
|
+
}
|
|
32
|
+
/** Read a CBOR text string starting at offset. Returns {value, end}. */
|
|
33
|
+
function readCborText(data, offset) {
|
|
34
|
+
const majorType = (data[offset] >> 5) & 0x07;
|
|
35
|
+
if (majorType !== 3) {
|
|
36
|
+
throw new Error(`Expected CBOR text string (type 3) at offset ${offset}, got type ${majorType}`);
|
|
37
|
+
}
|
|
38
|
+
const { value: len, end: dataStart } = readCborUint(data, offset);
|
|
39
|
+
const text = new TextDecoder().decode(data.slice(dataStart, dataStart + len));
|
|
40
|
+
return { value: text, end: dataStart + len };
|
|
41
|
+
}
|
|
42
|
+
/** Read a CBOR byte string starting at offset. Returns {value, end}. */
|
|
43
|
+
function readCborBytes(data, offset) {
|
|
44
|
+
const majorType = (data[offset] >> 5) & 0x07;
|
|
45
|
+
if (majorType !== 2) {
|
|
46
|
+
throw new Error(`Expected CBOR byte string (type 2) at offset ${offset}, got type ${majorType}`);
|
|
47
|
+
}
|
|
48
|
+
const { value: len, end: dataStart } = readCborUint(data, offset);
|
|
49
|
+
return {
|
|
50
|
+
value: data.slice(dataStart, dataStart + len),
|
|
51
|
+
end: dataStart + len,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Find the position of a CBOR text-string key within a byte sequence.
|
|
56
|
+
* Searches for the CBOR encoding of a text string (type 3 header + UTF-8 bytes).
|
|
57
|
+
*/
|
|
58
|
+
function findCborTextKey(data, key, startOffset) {
|
|
59
|
+
const keyBytes = new TextEncoder().encode(key);
|
|
60
|
+
// Build the expected CBOR encoding: type 3 header + key bytes
|
|
61
|
+
let header;
|
|
62
|
+
if (keyBytes.length < 24) {
|
|
63
|
+
header = new Uint8Array([0x60 | keyBytes.length]);
|
|
64
|
+
}
|
|
65
|
+
else if (keyBytes.length < 256) {
|
|
66
|
+
header = new Uint8Array([0x78, keyBytes.length]);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
header = new Uint8Array([
|
|
70
|
+
0x79,
|
|
71
|
+
keyBytes.length >> 8,
|
|
72
|
+
keyBytes.length & 0xff,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
const needle = new Uint8Array(header.length + keyBytes.length);
|
|
76
|
+
needle.set(header);
|
|
77
|
+
needle.set(keyBytes, header.length);
|
|
78
|
+
for (let i = startOffset; i <= data.length - needle.length; i++) {
|
|
79
|
+
let match = true;
|
|
80
|
+
for (let j = 0; j < needle.length; j++) {
|
|
81
|
+
if (data[i + j] !== needle[j]) {
|
|
82
|
+
match = false;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (match)
|
|
87
|
+
return i;
|
|
88
|
+
}
|
|
89
|
+
return -1;
|
|
90
|
+
}
|
|
91
|
+
export function decodeAttestationCbor(data) {
|
|
92
|
+
// Verify top-level is a CBOR map
|
|
93
|
+
const majorType = (data[0] >> 5) & 0x07;
|
|
94
|
+
if (majorType !== 5) {
|
|
95
|
+
throw new Error("Expected CBOR map at top level");
|
|
96
|
+
}
|
|
97
|
+
// Find "fmt" key and read its value
|
|
98
|
+
const fmtKeyPos = findCborTextKey(data, "fmt", 0);
|
|
99
|
+
if (fmtKeyPos === -1)
|
|
100
|
+
throw new Error('Missing "fmt" key');
|
|
101
|
+
const fmtKeyEnd = fmtKeyPos + 1 + 3; // 0x63 + "fmt"
|
|
102
|
+
const { value: fmt } = readCborText(data, fmtKeyEnd);
|
|
103
|
+
// Find "attStmt" key
|
|
104
|
+
const attStmtKeyPos = findCborTextKey(data, "attStmt", 0);
|
|
105
|
+
if (attStmtKeyPos === -1)
|
|
106
|
+
throw new Error('Missing "attStmt" key');
|
|
107
|
+
// After "attStmt" key: 0x67 + "attStmt" = 8 bytes
|
|
108
|
+
const attStmtValuePos = attStmtKeyPos + 8;
|
|
109
|
+
// attStmt value should be a map
|
|
110
|
+
const attStmtMajor = (data[attStmtValuePos] >> 5) & 0x07;
|
|
111
|
+
if (attStmtMajor !== 5)
|
|
112
|
+
throw new Error("attStmt is not a CBOR map");
|
|
113
|
+
// Find "x5c" key within attStmt
|
|
114
|
+
const x5cKeyPos = findCborTextKey(data, "x5c", attStmtValuePos);
|
|
115
|
+
if (x5cKeyPos === -1)
|
|
116
|
+
throw new Error('Missing "x5c" key in attStmt');
|
|
117
|
+
const x5cValuePos = x5cKeyPos + 4; // 0x63 + "x5c"
|
|
118
|
+
// x5c value is an array
|
|
119
|
+
const x5cMajor = (data[x5cValuePos] >> 5) & 0x07;
|
|
120
|
+
if (x5cMajor !== 4)
|
|
121
|
+
throw new Error("x5c is not a CBOR array");
|
|
122
|
+
const { value: x5cCount, end: x5cFirstItemPos } = readCborUint(data, x5cValuePos);
|
|
123
|
+
// Read each certificate byte string
|
|
124
|
+
const x5c = [];
|
|
125
|
+
let pos = x5cFirstItemPos;
|
|
126
|
+
for (let i = 0; i < x5cCount; i++) {
|
|
127
|
+
const { value: cert, end } = readCborBytes(data, pos);
|
|
128
|
+
x5c.push(cert);
|
|
129
|
+
pos = end;
|
|
130
|
+
}
|
|
131
|
+
// After x5c, find "receipt" key
|
|
132
|
+
const receiptKeyPos = findCborTextKey(data, "receipt", pos);
|
|
133
|
+
if (receiptKeyPos === -1)
|
|
134
|
+
throw new Error('Missing "receipt" key in attStmt');
|
|
135
|
+
// Find "authData" key - search from after the receipt key position
|
|
136
|
+
const authDataKeyPos = findCborTextKey(data, "authData", receiptKeyPos);
|
|
137
|
+
if (authDataKeyPos === -1) {
|
|
138
|
+
throw new Error('Missing "authData" key');
|
|
139
|
+
}
|
|
140
|
+
// The receipt value is the bytes between the receipt key end and the authData key start.
|
|
141
|
+
// Receipt key: 0x67 + "receipt" = 8 bytes
|
|
142
|
+
const receiptValueStart = receiptKeyPos + 8;
|
|
143
|
+
// Read the receipt CBOR header to get the data start offset
|
|
144
|
+
const receiptMajor = (data[receiptValueStart] >> 5) & 0x07;
|
|
145
|
+
if (receiptMajor !== 2)
|
|
146
|
+
throw new Error("receipt is not a CBOR byte string");
|
|
147
|
+
const { end: receiptDataStart } = readCborUint(data, receiptValueStart);
|
|
148
|
+
// The actual receipt data extends to just before the authData key.
|
|
149
|
+
// This handles the case where Apple's CBOR length header is incorrect.
|
|
150
|
+
const receipt = data.slice(receiptDataStart, authDataKeyPos);
|
|
151
|
+
// Read authData value
|
|
152
|
+
// authData key: 0x68 + "authData" = 9 bytes
|
|
153
|
+
const authDataValuePos = authDataKeyPos + 9;
|
|
154
|
+
const { value: authData } = readCborBytes(data, authDataValuePos);
|
|
155
|
+
return { fmt, attStmt: { x5c, receipt }, authData };
|
|
156
|
+
}
|
|
157
|
+
export async function verifyAttestation(appInfo, keyId, challenge, attestation, options) {
|
|
158
|
+
// Decode attestation bytes from base64 if string
|
|
159
|
+
let attestationBytes;
|
|
160
|
+
if (typeof attestation === "string") {
|
|
161
|
+
try {
|
|
162
|
+
attestationBytes = decodeBase64(attestation);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "Failed to decode attestation base64");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
attestationBytes = attestation;
|
|
170
|
+
}
|
|
171
|
+
// Step 1: CBOR decode attestation -> { fmt, attStmt: { x5c, receipt }, authData }
|
|
172
|
+
let decoded;
|
|
173
|
+
try {
|
|
174
|
+
decoded = decodeAttestationCbor(attestationBytes);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "Failed to CBOR-decode attestation object");
|
|
178
|
+
}
|
|
179
|
+
// Step 2: Validate fmt === "apple-appattest"
|
|
180
|
+
if (decoded.fmt !== "apple-appattest") {
|
|
181
|
+
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, `Invalid attestation format: expected "apple-appattest", got "${decoded.fmt}"`);
|
|
182
|
+
}
|
|
183
|
+
const { x5c, receipt } = decoded.attStmt;
|
|
184
|
+
const authData = decoded.authData;
|
|
185
|
+
// Step 3: Verify x5c cert chain exists (length >= 2)
|
|
186
|
+
if (x5c.length < 2) {
|
|
187
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, "Certificate chain (x5c) must contain at least 2 certificates");
|
|
188
|
+
}
|
|
189
|
+
// Step 4: Verify certificate chain
|
|
190
|
+
await verifyCertificateChain(x5c, options?.checkDate);
|
|
191
|
+
// Step 5-6: Compute nonce = SHA-256(authData || challenge)
|
|
192
|
+
// Apple's nonce verification uses the raw challenge bytes concatenated with authData,
|
|
193
|
+
// NOT a hash of the challenge.
|
|
194
|
+
const challengeBytes = toBytes(challenge);
|
|
195
|
+
const nonceInput = concat(authData, challengeBytes);
|
|
196
|
+
const computedNonce = new Uint8Array(await crypto.subtle.digest("SHA-256", nonceInput));
|
|
197
|
+
// Step 7: Extract nonce from leaf cert
|
|
198
|
+
const certNonce = extractNonceFromCert(x5c[0]);
|
|
199
|
+
// Step 8: constantTimeEqual(computedNonce, certNonce)
|
|
200
|
+
if (!constantTimeEqual(computedNonce, certNonce)) {
|
|
201
|
+
throw new AttestationError(AttestationErrorCode.NONCE_MISMATCH, "Computed nonce does not match certificate nonce");
|
|
202
|
+
}
|
|
203
|
+
// Step 9: Extract public key from leaf cert (65 bytes raw)
|
|
204
|
+
const publicKeyRaw = await extractPublicKeyFromCert(x5c[0]);
|
|
205
|
+
// Step 10: SHA-256(publicKeyRaw) must equal base64-decoded keyId
|
|
206
|
+
const publicKeyHash = new Uint8Array(await crypto.subtle.digest("SHA-256", publicKeyRaw));
|
|
207
|
+
const keyIdBytes = decodeBase64(keyId);
|
|
208
|
+
if (!constantTimeEqual(publicKeyHash, keyIdBytes)) {
|
|
209
|
+
throw new AttestationError(AttestationErrorCode.KEY_ID_MISMATCH, "Public key hash does not match keyId");
|
|
210
|
+
}
|
|
211
|
+
// Step 11: Parse authData
|
|
212
|
+
const parsedAuthData = parseAttestationAuthData(authData);
|
|
213
|
+
// Step 12: Verify rpIdHash === SHA-256(appId)
|
|
214
|
+
const appIdHash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(appInfo.appId)));
|
|
215
|
+
if (!constantTimeEqual(parsedAuthData.rpIdHash, appIdHash)) {
|
|
216
|
+
throw new AttestationError(AttestationErrorCode.RP_ID_MISMATCH, "RP ID hash does not match SHA-256 of appId");
|
|
217
|
+
}
|
|
218
|
+
// Step 13: Verify signCount === 0
|
|
219
|
+
if (parsedAuthData.signCount !== 0) {
|
|
220
|
+
throw new AttestationError(AttestationErrorCode.INVALID_COUNTER, `Expected signCount 0 for attestation, got ${parsedAuthData.signCount}`);
|
|
221
|
+
}
|
|
222
|
+
// Step 14: Verify AAGUID matches expected (prod or dev)
|
|
223
|
+
const expectedAaguid = appInfo.developmentEnv
|
|
224
|
+
? AAGUID_DEVELOPMENT
|
|
225
|
+
: AAGUID_PRODUCTION;
|
|
226
|
+
if (!constantTimeEqual(parsedAuthData.aaguid, expectedAaguid)) {
|
|
227
|
+
throw new AttestationError(AttestationErrorCode.INVALID_AAGUID, `AAGUID mismatch: expected ${appInfo.developmentEnv ? "development" : "production"} environment`);
|
|
228
|
+
}
|
|
229
|
+
// Step 15: Verify credentialId === keyIdBytes
|
|
230
|
+
if (!constantTimeEqual(parsedAuthData.credentialId, keyIdBytes)) {
|
|
231
|
+
throw new AttestationError(AttestationErrorCode.KEY_ID_MISMATCH, "Credential ID does not match keyId");
|
|
232
|
+
}
|
|
233
|
+
// Step 16: Export public key as PEM
|
|
234
|
+
const cryptoKey = await crypto.subtle.importKey("raw", publicKeyRaw, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
|
|
235
|
+
const publicKeyPem = await exportKeyToPem(cryptoKey);
|
|
236
|
+
// Step 17: Return result
|
|
237
|
+
return {
|
|
238
|
+
publicKeyPem,
|
|
239
|
+
receipt,
|
|
240
|
+
signCount: 0,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AssertionAuthData {
|
|
2
|
+
rpIdHash: Uint8Array;
|
|
3
|
+
flags: number;
|
|
4
|
+
signCount: number;
|
|
5
|
+
}
|
|
6
|
+
export interface AttestationAuthData extends AssertionAuthData {
|
|
7
|
+
aaguid: Uint8Array;
|
|
8
|
+
credentialId: Uint8Array;
|
|
9
|
+
coseKeyBytes: Uint8Array;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseAssertionAuthData(data: Uint8Array): AssertionAuthData;
|
|
12
|
+
export declare function parseAttestationAuthData(data: Uint8Array): AttestationAuthData;
|
|
13
|
+
//# sourceMappingURL=authdata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authdata.d.ts","sourceRoot":"","sources":["../../src/src/authdata.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,UAAU,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC5D,MAAM,EAAE,UAAU,CAAC;IACnB,YAAY,EAAE,UAAU,CAAC;IACzB,YAAY,EAAE,UAAU,CAAC;CAC1B;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CAgB1E;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,UAAU,GACf,mBAAmB,CAiCrB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/authdata.ts
|
|
2
|
+
export function parseAssertionAuthData(data) {
|
|
3
|
+
if (data.length < 37) {
|
|
4
|
+
throw new Error(`authenticatorData too short: expected at least 37 bytes, got ${data.length}`);
|
|
5
|
+
}
|
|
6
|
+
const rpIdHash = data.slice(0, 32);
|
|
7
|
+
const flags = data[32];
|
|
8
|
+
const signCount = new DataView(data.buffer, data.byteOffset + 33, 4).getUint32(0, false);
|
|
9
|
+
return { rpIdHash, flags, signCount };
|
|
10
|
+
}
|
|
11
|
+
export function parseAttestationAuthData(data) {
|
|
12
|
+
const base = parseAssertionAuthData(data);
|
|
13
|
+
if (data.length < 55) {
|
|
14
|
+
throw new Error(`attestation authenticatorData too short: expected at least 55 bytes, got ${data.length}`);
|
|
15
|
+
}
|
|
16
|
+
const aaguid = data.slice(37, 53);
|
|
17
|
+
const credentialIdLength = new DataView(data.buffer, data.byteOffset + 53, 2).getUint16(0, false);
|
|
18
|
+
if (data.length < 55 + credentialIdLength) {
|
|
19
|
+
throw new Error(`authenticatorData truncated: credentialIdLength=${credentialIdLength} but only ${data.length - 55} bytes remain`);
|
|
20
|
+
}
|
|
21
|
+
const credentialId = data.slice(55, 55 + credentialIdLength);
|
|
22
|
+
const coseKeyBytes = data.slice(55 + credentialIdLength);
|
|
23
|
+
return {
|
|
24
|
+
...base,
|
|
25
|
+
aaguid,
|
|
26
|
+
credentialId,
|
|
27
|
+
coseKeyBytes,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify the x5c certificate chain against the Apple App Attestation Root CA.
|
|
3
|
+
*
|
|
4
|
+
* @param x5c - Array of DER-encoded certificates (index 0 = leaf, last = closest to root)
|
|
5
|
+
* @param checkDate - Date to use for validity checking (defaults to now; override for testing expired certs)
|
|
6
|
+
*/
|
|
7
|
+
export declare function verifyCertificateChain(x5c: Uint8Array[], checkDate?: Date): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Extract the nonce from the Apple App Attest credential certificate.
|
|
10
|
+
*
|
|
11
|
+
* The nonce is stored in an extension with OID 1.2.840.113635.100.8.2.
|
|
12
|
+
* The extension value is ASN.1: SEQUENCE { [1] EXPLICIT { OCTET STRING <nonce> } }
|
|
13
|
+
*
|
|
14
|
+
* @param certDer - DER-encoded leaf certificate
|
|
15
|
+
* @returns The 32-byte nonce
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractNonceFromCert(certDer: Uint8Array): Uint8Array;
|
|
18
|
+
/**
|
|
19
|
+
* Extract the raw (uncompressed) public key from a DER-encoded certificate.
|
|
20
|
+
*
|
|
21
|
+
* @param certDer - DER-encoded certificate
|
|
22
|
+
* @returns 65-byte uncompressed EC point (0x04 || x || y)
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractPublicKeyFromCert(certDer: Uint8Array): Promise<Uint8Array>;
|
|
25
|
+
//# sourceMappingURL=certificate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"certificate.d.ts","sourceRoot":"","sources":["../../src/src/certificate.ts"],"names":[],"mappings":"AAsBA;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,UAAU,EAAE,EACjB,SAAS,CAAC,EAAE,IAAI,GACf,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,UAAU,GAAG,UAAU,CAqCpE;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,UAAU,CAAC,CAkBrB"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/certificate.ts
|
|
2
|
+
import * as pkijs from "pkijs";
|
|
3
|
+
import * as asn1js from "asn1js";
|
|
4
|
+
import { APPLE_APP_ATTESTATION_ROOT_CA_PEM, APPLE_NONCE_EXTENSION_OID, } from "./constants.js";
|
|
5
|
+
import { AttestationError, AttestationErrorCode } from "./errors.js";
|
|
6
|
+
/**
|
|
7
|
+
* Parse the Apple App Attestation Root CA from PEM into a pkijs Certificate.
|
|
8
|
+
*/
|
|
9
|
+
function parseRootCa() {
|
|
10
|
+
const b64 = APPLE_APP_ATTESTATION_ROOT_CA_PEM
|
|
11
|
+
.replace("-----BEGIN CERTIFICATE-----", "")
|
|
12
|
+
.replace("-----END CERTIFICATE-----", "")
|
|
13
|
+
.replace(/\s/g, "");
|
|
14
|
+
const der = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
15
|
+
return pkijs.Certificate.fromBER(der);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Verify the x5c certificate chain against the Apple App Attestation Root CA.
|
|
19
|
+
*
|
|
20
|
+
* @param x5c - Array of DER-encoded certificates (index 0 = leaf, last = closest to root)
|
|
21
|
+
* @param checkDate - Date to use for validity checking (defaults to now; override for testing expired certs)
|
|
22
|
+
*/
|
|
23
|
+
export async function verifyCertificateChain(x5c, checkDate) {
|
|
24
|
+
if (x5c.length === 0) {
|
|
25
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, "Certificate chain (x5c) is empty");
|
|
26
|
+
}
|
|
27
|
+
const rootCa = parseRootCa();
|
|
28
|
+
// Parse all x5c certs
|
|
29
|
+
const certs = x5c.map((der) => pkijs.Certificate.fromBER(der));
|
|
30
|
+
const chainEngine = new pkijs.CertificateChainValidationEngine({
|
|
31
|
+
trustedCerts: [rootCa],
|
|
32
|
+
certs: certs,
|
|
33
|
+
checkDate: checkDate ?? new Date(),
|
|
34
|
+
});
|
|
35
|
+
const result = await chainEngine.verify();
|
|
36
|
+
if (!result.result) {
|
|
37
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, `Certificate chain verification failed: ${result.resultMessage}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extract the nonce from the Apple App Attest credential certificate.
|
|
42
|
+
*
|
|
43
|
+
* The nonce is stored in an extension with OID 1.2.840.113635.100.8.2.
|
|
44
|
+
* The extension value is ASN.1: SEQUENCE { [1] EXPLICIT { OCTET STRING <nonce> } }
|
|
45
|
+
*
|
|
46
|
+
* @param certDer - DER-encoded leaf certificate
|
|
47
|
+
* @returns The 32-byte nonce
|
|
48
|
+
*/
|
|
49
|
+
export function extractNonceFromCert(certDer) {
|
|
50
|
+
const cert = pkijs.Certificate.fromBER(certDer);
|
|
51
|
+
const extensions = cert.extensions;
|
|
52
|
+
if (!extensions) {
|
|
53
|
+
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "Certificate has no extensions");
|
|
54
|
+
}
|
|
55
|
+
const nonceExt = extensions.find((ext) => ext.extnID === APPLE_NONCE_EXTENSION_OID);
|
|
56
|
+
if (!nonceExt) {
|
|
57
|
+
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, `Certificate missing nonce extension (OID ${APPLE_NONCE_EXTENSION_OID})`);
|
|
58
|
+
}
|
|
59
|
+
// Parse the extension value as ASN.1
|
|
60
|
+
const extValue = nonceExt.extnValue.getValue();
|
|
61
|
+
const asn1 = asn1js.fromBER(extValue);
|
|
62
|
+
if (asn1.offset === -1) {
|
|
63
|
+
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "Failed to parse nonce extension ASN.1");
|
|
64
|
+
}
|
|
65
|
+
// Navigate: SEQUENCE -> tagged [1] -> OCTET STRING
|
|
66
|
+
const sequence = asn1.result;
|
|
67
|
+
const tagged = sequence.valueBlock.value[0];
|
|
68
|
+
const octetString = tagged.valueBlock.value[0];
|
|
69
|
+
return new Uint8Array(octetString.valueBlock.valueHexView);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract the raw (uncompressed) public key from a DER-encoded certificate.
|
|
73
|
+
*
|
|
74
|
+
* @param certDer - DER-encoded certificate
|
|
75
|
+
* @returns 65-byte uncompressed EC point (0x04 || x || y)
|
|
76
|
+
*/
|
|
77
|
+
export async function extractPublicKeyFromCert(certDer) {
|
|
78
|
+
const cert = pkijs.Certificate.fromBER(certDer);
|
|
79
|
+
// Get the SubjectPublicKeyInfo and convert to DER
|
|
80
|
+
const spkiDer = cert.subjectPublicKeyInfo.toSchema().toBER(false);
|
|
81
|
+
// Import as SPKI to get a CryptoKey
|
|
82
|
+
const cryptoKey = await crypto.subtle.importKey("spki", spkiDer, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
|
|
83
|
+
// Export as raw (uncompressed point)
|
|
84
|
+
const rawKey = await crypto.subtle.exportKey("raw", cryptoKey);
|
|
85
|
+
return new Uint8Array(rawKey);
|
|
86
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Apple App Attestation Root CA (single cert for both dev and prod environments). */
|
|
2
|
+
export declare const APPLE_APP_ATTESTATION_ROOT_CA_PEM = "-----BEGIN CERTIFICATE-----\nMIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYw\nJAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwK\nQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNa\nFw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlv\nbiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9y\nbmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdh\nNbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9au\nYen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/\nMB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYw\nCgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn\n53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijV\noyFraWVIyd/dganmrduC1bmTBGwD\n-----END CERTIFICATE-----";
|
|
3
|
+
/** Production AAGUID: "appattest" + 7 null bytes (16 bytes total) */
|
|
4
|
+
export declare const AAGUID_PRODUCTION: Uint8Array<ArrayBuffer>;
|
|
5
|
+
/** Development AAGUID: "appattestdevelop" (16 bytes, no nulls) */
|
|
6
|
+
export declare const AAGUID_DEVELOPMENT: Uint8Array<ArrayBuffer>;
|
|
7
|
+
/** OID for the Apple App Attest nonce extension in credential certificates */
|
|
8
|
+
export declare const APPLE_NONCE_EXTENSION_OID = "1.2.840.113635.100.8.2";
|
|
9
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/src/constants.ts"],"names":[],"mappings":"AAEA,sFAAsF;AACtF,eAAO,MAAM,iCAAiC,+yBAapB,CAAC;AAE3B,qEAAqE;AACrE,eAAO,MAAM,iBAAiB,yBAiB5B,CAAC;AAEH,kEAAkE;AAClE,eAAO,MAAM,kBAAkB,yBAiB7B,CAAC;AAEH,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
/** Apple App Attestation Root CA (single cert for both dev and prod environments). */
|
|
3
|
+
export const APPLE_APP_ATTESTATION_ROOT_CA_PEM = `-----BEGIN CERTIFICATE-----
|
|
4
|
+
MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYw
|
|
5
|
+
JAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwK
|
|
6
|
+
QXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNa
|
|
7
|
+
Fw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlv
|
|
8
|
+
biBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9y
|
|
9
|
+
bmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdh
|
|
10
|
+
NbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9au
|
|
11
|
+
Yen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/
|
|
12
|
+
MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYw
|
|
13
|
+
CgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn
|
|
14
|
+
53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijV
|
|
15
|
+
oyFraWVIyd/dganmrduC1bmTBGwD
|
|
16
|
+
-----END CERTIFICATE-----`;
|
|
17
|
+
/** Production AAGUID: "appattest" + 7 null bytes (16 bytes total) */
|
|
18
|
+
export const AAGUID_PRODUCTION = new Uint8Array([
|
|
19
|
+
0x61,
|
|
20
|
+
0x70,
|
|
21
|
+
0x70,
|
|
22
|
+
0x61,
|
|
23
|
+
0x74,
|
|
24
|
+
0x74,
|
|
25
|
+
0x65,
|
|
26
|
+
0x73,
|
|
27
|
+
0x74,
|
|
28
|
+
0x00,
|
|
29
|
+
0x00,
|
|
30
|
+
0x00,
|
|
31
|
+
0x00,
|
|
32
|
+
0x00,
|
|
33
|
+
0x00,
|
|
34
|
+
0x00,
|
|
35
|
+
]);
|
|
36
|
+
/** Development AAGUID: "appattestdevelop" (16 bytes, no nulls) */
|
|
37
|
+
export const AAGUID_DEVELOPMENT = new Uint8Array([
|
|
38
|
+
0x61,
|
|
39
|
+
0x70,
|
|
40
|
+
0x70,
|
|
41
|
+
0x61,
|
|
42
|
+
0x74,
|
|
43
|
+
0x74,
|
|
44
|
+
0x65,
|
|
45
|
+
0x73,
|
|
46
|
+
0x74,
|
|
47
|
+
0x64,
|
|
48
|
+
0x65,
|
|
49
|
+
0x76,
|
|
50
|
+
0x65,
|
|
51
|
+
0x6c,
|
|
52
|
+
0x6f,
|
|
53
|
+
0x70,
|
|
54
|
+
]);
|
|
55
|
+
/** OID for the Apple App Attest nonce extension in credential certificates */
|
|
56
|
+
export const APPLE_NONCE_EXTENSION_OID = "1.2.840.113635.100.8.2";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cose.d.ts","sourceRoot":"","sources":["../../src/src/cose.ts"],"names":[],"mappings":"AAGA,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,CAkBvE;AAED,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,SAAS,CAAC,CASpB"}
|
package/esm/src/der.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"der.d.ts","sourceRoot":"","sources":["../../src/src/der.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAoCpD;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAiBpD"}
|
package/esm/src/der.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/der.ts
|
|
2
|
+
export function derToRaw(der) {
|
|
3
|
+
if (der[0] !== 0x30) {
|
|
4
|
+
throw new Error("Invalid DER signature: expected SEQUENCE tag (0x30)");
|
|
5
|
+
}
|
|
6
|
+
let offset = 2;
|
|
7
|
+
if (der[1] & 0x80) {
|
|
8
|
+
const lengthBytes = der[1] & 0x7f;
|
|
9
|
+
offset = 2 + lengthBytes;
|
|
10
|
+
}
|
|
11
|
+
const raw = new Uint8Array(64);
|
|
12
|
+
if (der[offset] !== 0x02) {
|
|
13
|
+
throw new Error("Invalid DER signature: expected INTEGER tag (0x02) for r");
|
|
14
|
+
}
|
|
15
|
+
offset++;
|
|
16
|
+
const rLen = der[offset++];
|
|
17
|
+
const rBytes = der.subarray(offset, offset + rLen);
|
|
18
|
+
offset += rLen;
|
|
19
|
+
if (der[offset] !== 0x02) {
|
|
20
|
+
throw new Error("Invalid DER signature: expected INTEGER tag (0x02) for s");
|
|
21
|
+
}
|
|
22
|
+
offset++;
|
|
23
|
+
const sLen = der[offset++];
|
|
24
|
+
const sBytes = der.subarray(offset, offset + sLen);
|
|
25
|
+
copyInteger(rBytes, raw, 0);
|
|
26
|
+
copyInteger(sBytes, raw, 32);
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
export function rawToDer(raw) {
|
|
30
|
+
if (raw.length !== 64) {
|
|
31
|
+
throw new Error(`Invalid raw signature: expected 64 bytes, got ${raw.length}`);
|
|
32
|
+
}
|
|
33
|
+
const r = encodeInteger(raw.subarray(0, 32));
|
|
34
|
+
const s = encodeInteger(raw.subarray(32, 64));
|
|
35
|
+
const seqLen = r.length + s.length;
|
|
36
|
+
const der = new Uint8Array(2 + seqLen);
|
|
37
|
+
der[0] = 0x30;
|
|
38
|
+
der[1] = seqLen;
|
|
39
|
+
der.set(r, 2);
|
|
40
|
+
der.set(s, 2 + r.length);
|
|
41
|
+
return der;
|
|
42
|
+
}
|
|
43
|
+
function copyInteger(src, dst, dstOffset) {
|
|
44
|
+
let srcOffset = 0;
|
|
45
|
+
while (srcOffset < src.length - 1 && src[srcOffset] === 0) {
|
|
46
|
+
srcOffset++;
|
|
47
|
+
}
|
|
48
|
+
const len = src.length - srcOffset;
|
|
49
|
+
if (len > 32) {
|
|
50
|
+
throw new Error(`Integer too large: ${len} bytes`);
|
|
51
|
+
}
|
|
52
|
+
dst.set(src.subarray(srcOffset), dstOffset + (32 - len));
|
|
53
|
+
}
|
|
54
|
+
function encodeInteger(value) {
|
|
55
|
+
let start = 0;
|
|
56
|
+
while (start < value.length - 1 && value[start] === 0) {
|
|
57
|
+
start++;
|
|
58
|
+
}
|
|
59
|
+
const needsPadding = value[start] & 0x80;
|
|
60
|
+
const len = value.length - start + (needsPadding ? 1 : 0);
|
|
61
|
+
const result = new Uint8Array(2 + len);
|
|
62
|
+
result[0] = 0x02;
|
|
63
|
+
result[1] = len;
|
|
64
|
+
if (needsPadding) {
|
|
65
|
+
result[2] = 0x00;
|
|
66
|
+
result.set(value.subarray(start), 3);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result.set(value.subarray(start), 2);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|