@bradford-tech/supabase-integrity-attest 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,7 +61,6 @@ const supabase = createClient(
61
61
 
62
62
  Deno.serve(withAssertion({
63
63
  appId: Deno.env.get("APP_ATTEST_APP_ID")!,
64
- developmentEnv: Deno.env.get("APP_ATTEST_ENV") === "development",
65
64
  getDeviceKey: async (deviceId) => {
66
65
  const { data } = await supabase
67
66
  .from("device_attestations")
@@ -2,8 +2,6 @@
2
2
  export interface AppInfo {
3
3
  /** Apple App ID in the format `TEAMID.bundleId`. */
4
4
  appId: string;
5
- /** Set to `true` when verifying assertions from the development environment. */
6
- developmentEnv?: boolean;
7
5
  }
8
6
  /** Successful assertion verification result. */
9
7
  export interface AssertionResult {
@@ -1 +1 @@
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"}
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;CACf;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,CAmH1B"}
@@ -14,7 +14,13 @@ import { concat, constantTimeEqual, decodeBase64Bytes, importPemPublicKey, toByt
14
14
  * @throws {AssertionError} If any verification step fails.
15
15
  */
16
16
  export async function verifyAssertion(appInfo, assertion, clientData, publicKeyPem, previousSignCount) {
17
- const assertionBytes = decodeBase64Bytes(assertion);
17
+ let assertionBytes;
18
+ try {
19
+ assertionBytes = decodeBase64Bytes(assertion);
20
+ }
21
+ catch {
22
+ throw new AssertionError(AssertionErrorCode.INVALID_FORMAT, "Invalid base64-encoded assertion");
23
+ }
18
24
  const clientDataBytes = toBytes(clientData);
19
25
  // Step 1: CBOR decode
20
26
  let decoded;
@@ -1 +1 @@
1
- {"version":3,"file":"certificate.d.ts","sourceRoot":"","sources":["../../src/src/certificate.ts"],"names":[],"mappings":"AA+UA;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,UAAU,EAAE,EACjB,SAAS,CAAC,EAAE,IAAI,GACf,OAAO,CAAC,IAAI,CAAC,CAkEf;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,UAAU,GAAG,UAAU,CA6BpE;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,UAAU,CAAC,CAqBrB"}
1
+ {"version":3,"file":"certificate.d.ts","sourceRoot":"","sources":["../../src/src/certificate.ts"],"names":[],"mappings":"AAmWA;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,UAAU,EAAE,EACjB,SAAS,CAAC,EAAE,IAAI,GACf,OAAO,CAAC,IAAI,CAAC,CAkEf;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,UAAU,GAAG,UAAU,CAwCpE;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,UAAU,CAAC,CAqBrB"}
@@ -60,98 +60,105 @@ function parseCertificate(der) {
60
60
  if (asn1.offset === -1) {
61
61
  throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "Failed to parse certificate DER");
62
62
  }
63
- const certSeq = asn1.result;
64
- const certChildren = certSeq.valueBlock.value;
65
- // certChildren: [tbsCertificate, signatureAlgorithm, signatureValue]
66
- const tbsElement = certChildren[0];
67
- const sigAlgElement = certChildren[1];
68
- const sigValueElement = certChildren[2];
69
- // TBS bytes — slice from the element's full encoding (tag + length + value)
70
- const tbsView = tbsElement
71
- .valueBeforeDecodeView;
72
- const tbsCertificateDer = sliceFromView(tbsView);
73
- // Signature algorithm OID
74
- const sigAlgOid = sigAlgElement.valueBlock.value[0].valueBlock
75
- .toString();
76
- // Signature value — unused-bits byte already stripped by asn1js
77
- const signatureValue = new Uint8Array(sigValueElement.valueBlock.valueHexView);
78
- // TBS children check for v3 version tag
79
- const tbsChildren = tbsElement.valueBlock.value;
80
- let offset = 0;
81
- // First child is explicit [0] version tag for v3 certs
82
- const firstChild = tbsChildren[0];
83
- if (firstChild.idBlock.tagClass === 3 && // CONTEXT-SPECIFIC
84
- firstChild.idBlock.tagNumber === 0) {
85
- offset = 1; // Version tag present, shift all indices
86
- }
87
- // TBS layout (with offset): serial, sigAlg, issuer, validity, subject, SPKI, [extensions]
88
- // [offset+0]=serial, [offset+1]=sigAlg, [offset+2]=issuer, [offset+3]=validity,
89
- // [offset+4]=subject, [offset+5]=SPKI
90
- const issuerElement = tbsChildren[offset + 2];
91
- const issuerView = issuerElement
92
- .valueBeforeDecodeView;
93
- const issuer = sliceFromView(issuerView);
94
- const validityElement = tbsChildren[offset + 3];
95
- const validityChildren = validityElement.valueBlock.value;
96
- const validityNotBefore = parseAsn1Date(validityChildren[0]);
97
- const validityNotAfter = parseAsn1Date(validityChildren[1]);
98
- const subjectElement = tbsChildren[offset + 4];
99
- const subjectView = subjectElement
100
- .valueBeforeDecodeView;
101
- const subject = sliceFromView(subjectView);
102
- const spkiElement = tbsChildren[offset + 5];
103
- const spkiView = spkiElement
104
- .valueBeforeDecodeView;
105
- const subjectPublicKeyInfoDer = sliceFromView(spkiView);
106
- // Extract curve OID from SPKI's AlgorithmIdentifier
107
- const spkiSeq = spkiElement;
108
- const spkiAlgId = spkiSeq.valueBlock.value[0];
109
- const curveOidElement = spkiAlgId.valueBlock
110
- .value[1];
111
- const publicKeyCurveOid = curveOidElement.valueBlock.toString();
112
- // Extensions found in explicit [3] tag within TBS
113
- const extensions = [];
114
- for (let i = offset + 6; i < tbsChildren.length; i++) {
115
- const child = tbsChildren[i];
116
- if (child.idBlock.tagClass === 3 && // CONTEXT-SPECIFIC
117
- child.idBlock.tagNumber === 3) {
118
- // This is the extensions wrapper: explicit [3] containing a SEQUENCE of extensions
119
- const extWrapper = child;
120
- const extSeqOfSeq = extWrapper.valueBlock.value[0];
121
- for (const extSeq of extSeqOfSeq.valueBlock.value) {
122
- const extChildren = extSeq.valueBlock.value;
123
- const oid = extChildren[0].valueBlock
124
- .toString();
125
- let critical = false;
126
- let valueElement;
127
- if (extChildren[1] instanceof asn1js.Boolean) {
128
- critical = extChildren[1].getValue();
129
- valueElement = extChildren[2];
130
- }
131
- else {
132
- valueElement = extChildren[1];
63
+ try {
64
+ const certSeq = asn1.result;
65
+ const certChildren = certSeq.valueBlock.value;
66
+ // certChildren: [tbsCertificate, signatureAlgorithm, signatureValue]
67
+ const tbsElement = certChildren[0];
68
+ const sigAlgElement = certChildren[1];
69
+ const sigValueElement = certChildren[2];
70
+ // TBS bytes — slice from the element's full encoding (tag + length + value)
71
+ const tbsView = tbsElement
72
+ .valueBeforeDecodeView;
73
+ const tbsCertificateDer = sliceFromView(tbsView);
74
+ // Signature algorithm OID
75
+ const sigAlgOid = sigAlgElement.valueBlock.value[0].valueBlock
76
+ .toString();
77
+ // Signature value unused-bits byte already stripped by asn1js
78
+ const signatureValue = new Uint8Array(sigValueElement.valueBlock.valueHexView);
79
+ // TBS children — check for v3 version tag
80
+ const tbsChildren = tbsElement.valueBlock.value;
81
+ let offset = 0;
82
+ // First child is explicit [0] version tag for v3 certs
83
+ const firstChild = tbsChildren[0];
84
+ if (firstChild.idBlock.tagClass === 3 && // CONTEXT-SPECIFIC
85
+ firstChild.idBlock.tagNumber === 0) {
86
+ offset = 1; // Version tag present, shift all indices
87
+ }
88
+ // TBS layout (with offset): serial, sigAlg, issuer, validity, subject, SPKI, [extensions]
89
+ // [offset+0]=serial, [offset+1]=sigAlg, [offset+2]=issuer, [offset+3]=validity,
90
+ // [offset+4]=subject, [offset+5]=SPKI
91
+ const issuerElement = tbsChildren[offset + 2];
92
+ const issuerView = issuerElement
93
+ .valueBeforeDecodeView;
94
+ const issuer = sliceFromView(issuerView);
95
+ const validityElement = tbsChildren[offset + 3];
96
+ const validityChildren = validityElement.valueBlock.value;
97
+ const validityNotBefore = parseAsn1Date(validityChildren[0]);
98
+ const validityNotAfter = parseAsn1Date(validityChildren[1]);
99
+ const subjectElement = tbsChildren[offset + 4];
100
+ const subjectView = subjectElement
101
+ .valueBeforeDecodeView;
102
+ const subject = sliceFromView(subjectView);
103
+ const spkiElement = tbsChildren[offset + 5];
104
+ const spkiView = spkiElement
105
+ .valueBeforeDecodeView;
106
+ const subjectPublicKeyInfoDer = sliceFromView(spkiView);
107
+ // Extract curve OID from SPKI's AlgorithmIdentifier
108
+ const spkiSeq = spkiElement;
109
+ const spkiAlgId = spkiSeq.valueBlock.value[0];
110
+ const curveOidElement = spkiAlgId.valueBlock
111
+ .value[1];
112
+ const publicKeyCurveOid = curveOidElement.valueBlock.toString();
113
+ // Extensions found in explicit [3] tag within TBS
114
+ const extensions = [];
115
+ for (let i = offset + 6; i < tbsChildren.length; i++) {
116
+ const child = tbsChildren[i];
117
+ if (child.idBlock.tagClass === 3 && // CONTEXT-SPECIFIC
118
+ child.idBlock.tagNumber === 3) {
119
+ // This is the extensions wrapper: explicit [3] containing a SEQUENCE of extensions
120
+ const extWrapper = child;
121
+ const extSeqOfSeq = extWrapper.valueBlock.value[0];
122
+ for (const extSeq of extSeqOfSeq.valueBlock.value) {
123
+ const extChildren = extSeq.valueBlock.value;
124
+ const oid = extChildren[0].valueBlock
125
+ .toString();
126
+ let critical = false;
127
+ let valueElement;
128
+ if (extChildren[1] instanceof asn1js.Boolean) {
129
+ critical = extChildren[1].getValue();
130
+ valueElement = extChildren[2];
131
+ }
132
+ else {
133
+ valueElement = extChildren[1];
134
+ }
135
+ extensions.push({
136
+ oid,
137
+ critical,
138
+ value: new Uint8Array(valueElement.valueBlock.valueHexView),
139
+ });
133
140
  }
134
- extensions.push({
135
- oid,
136
- critical,
137
- value: new Uint8Array(valueElement.valueBlock.valueHexView),
138
- });
141
+ break;
139
142
  }
140
- break;
141
143
  }
144
+ return {
145
+ tbsCertificateDer,
146
+ signatureAlgorithmOid: sigAlgOid,
147
+ signatureValue,
148
+ issuer,
149
+ subject,
150
+ validityNotBefore,
151
+ validityNotAfter,
152
+ subjectPublicKeyInfoDer,
153
+ publicKeyCurveOid,
154
+ extensions,
155
+ };
156
+ }
157
+ catch (e) {
158
+ if (e instanceof AttestationError)
159
+ throw e;
160
+ throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, `Invalid X.509 certificate structure: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
142
161
  }
143
- return {
144
- tbsCertificateDer,
145
- signatureAlgorithmOid: sigAlgOid,
146
- signatureValue,
147
- issuer,
148
- subject,
149
- validityNotBefore,
150
- validityNotAfter,
151
- subjectPublicKeyInfoDer,
152
- publicKeyCurveOid,
153
- extensions,
154
- };
155
162
  }
156
163
  // ── extractRawPublicKeyFromSpki ─────────────────────────────────────
157
164
  /**
@@ -164,9 +171,16 @@ function extractRawPublicKeyFromSpki(spkiDer) {
164
171
  if (asn1.offset === -1) {
165
172
  throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, "Failed to parse SPKI DER");
166
173
  }
167
- const spkiSeq = asn1.result;
168
- const publicKeyBitString = spkiSeq.valueBlock.value[1];
169
- return new Uint8Array(publicKeyBitString.valueBlock.valueHexView);
174
+ try {
175
+ const spkiSeq = asn1.result;
176
+ const publicKeyBitString = spkiSeq.valueBlock.value[1];
177
+ return new Uint8Array(publicKeyBitString.valueBlock.valueHexView);
178
+ }
179
+ catch (e) {
180
+ if (e instanceof AttestationError)
181
+ throw e;
182
+ throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, `Invalid SPKI structure: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
183
+ }
170
184
  }
171
185
  // ── verifySignature ─────────────────────────────────────────────────
172
186
  /**
@@ -286,10 +300,17 @@ export function extractNonceFromCert(certDer) {
286
300
  throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "Failed to parse nonce extension ASN.1");
287
301
  }
288
302
  // Navigate: SEQUENCE -> tagged [1] -> OCTET STRING
289
- const sequence = extAsn1.result;
290
- const tagged = sequence.valueBlock.value[0];
291
- const octetString = tagged.valueBlock.value[0];
292
- return new Uint8Array(octetString.valueBlock.valueHexView);
303
+ try {
304
+ const sequence = extAsn1.result;
305
+ const tagged = sequence.valueBlock.value[0];
306
+ const octetString = tagged.valueBlock.value[0];
307
+ return new Uint8Array(octetString.valueBlock.valueHexView);
308
+ }
309
+ catch (e) {
310
+ if (e instanceof AttestationError)
311
+ throw e;
312
+ throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, `Invalid nonce extension structure: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
313
+ }
293
314
  }
294
315
  /**
295
316
  * Extract the raw (uncompressed) public key from a DER-encoded certificate.
@@ -46,8 +46,6 @@ export type ExtractAssertionFn = (req: Request) => Promise<{
46
46
  export type WithAssertionOptions = {
47
47
  /** Apple App ID in the format `TEAMID.bundleId`. */
48
48
  appId: string;
49
- /** Set to `true` for development environment attestations. */
50
- developmentEnv?: boolean;
51
49
  /** Retrieve the stored device key for a given device ID. Return `null` if not found. */
52
50
  getDeviceKey: (deviceId: string) => Promise<DeviceKey | null>;
53
51
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"with-assertion.d.ts","sourceRoot":"","sources":["../../src/src/with-assertion.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAsB,MAAM,aAAa,CAAC;AAEjE,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AACjE,0DAA0D;AAC1D,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG;IACtB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,mFAAmF;AACnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,UAAU,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;CACxB,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,MAAM,oBAAoB,GAAG;IACjC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wFAAwF;IACxF,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC9D;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IACtC,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAmCF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,gBAAgB,KACtB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiGrC"}
1
+ {"version":3,"file":"with-assertion.d.ts","sourceRoot":"","sources":["../../src/src/with-assertion.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAsB,MAAM,aAAa,CAAC;AAEjE,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AACjE,0DAA0D;AAC1D,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG;IACtB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,mFAAmF;AACnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,UAAU,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;CACxB,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,MAAM,oBAAoB,GAAG;IACjC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC9D;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IACtC,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAmCF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,gBAAgB,KACtB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAmGrC"}
@@ -35,10 +35,7 @@ function defaultErrorResponse(error) {
35
35
  * concurrent load.
36
36
  */
37
37
  export function withAssertion(options, handler) {
38
- const appInfo = {
39
- appId: options.appId,
40
- developmentEnv: options.developmentEnv ?? false,
41
- };
38
+ const appInfo = { appId: options.appId };
42
39
  const extract = options.extractAssertion ?? defaultExtractAssertion;
43
40
  return async (req) => {
44
41
  let deviceId;
@@ -90,7 +87,13 @@ export function withAssertion(options, handler) {
90
87
  const error = err instanceof AssertionError ? err : new AssertionError(AssertionErrorCode.INTERNAL_ERROR, "Internal error", {
91
88
  cause: err,
92
89
  });
93
- return options.onError?.(error, req) ?? defaultErrorResponse(error);
90
+ try {
91
+ return await options.onError?.(error, req) ??
92
+ defaultErrorResponse(error);
93
+ }
94
+ catch {
95
+ return defaultErrorResponse(error);
96
+ }
94
97
  }
95
98
  // Step 5: handler — outside try/catch, errors bubble up
96
99
  return await handler(req, {
@@ -29,7 +29,18 @@ export type AttestationContext = {
29
29
  /** Custom function to extract attestation data from an incoming request. */
30
30
  export type ExtractAttestationFn = (req: Request) => Promise<{
31
31
  deviceId: string;
32
+ /** Raw challenge bytes for the `consumeChallenge` DB lookup. */
32
33
  challenge: Uint8Array;
34
+ /**
35
+ * The challenge in the exact form the client SDK received it, before
36
+ * any server-side decoding. This is what the client SDK hashed to
37
+ * produce `clientDataHash` — Expo's `attestKeyAsync` and native
38
+ * `DCAppAttestService` wrappers convert this string to UTF-8 bytes
39
+ * and SHA-256 hash them before passing to Apple. The middleware must
40
+ * hash this same string to produce the matching `clientDataHash` for
41
+ * `verifyAttestation`.
42
+ */
43
+ challengeAsSent: string;
33
44
  attestation: Uint8Array;
34
45
  }>;
35
46
  /** Configuration for the {@linkcode withAttestation} middleware. */
@@ -1 +1 @@
1
- {"version":3,"file":"with-attestation.d.ts","sourceRoot":"","sources":["../../src/src/with-attestation.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAErE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,OAAO,EAAE,UAAU,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,kBAAkB,CAAC;CAC7B,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,UAAU,CAAC;IACtB,WAAW,EAAE,UAAU,CAAC;CACzB,CAAC,CAAC;AAEH,oEAAoE;AACpE,MAAM,MAAM,sBAAsB,GAAG;IACnC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,gBAAgB,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D;;;;OAIG;IACH,cAAc,EAAE,CAAC,GAAG,EAAE;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,UAAU,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,gBAAgB,EACvB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAwEF;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,kBAAkB,KACxB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiHrC"}
1
+ {"version":3,"file":"with-attestation.d.ts","sourceRoot":"","sources":["../../src/src/with-attestation.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAErE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,OAAO,EAAE,UAAU,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,kBAAkB,CAAC;CAC7B,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,SAAS,EAAE,UAAU,CAAC;IACtB;;;;;;;;OAQG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,UAAU,CAAC;CACzB,CAAC,CAAC;AAEH,oEAAoE;AACpE,MAAM,MAAM,sBAAsB,GAAG;IACnC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,gBAAgB,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D;;;;OAIG;IACH,cAAc,EAAE,CAAC,GAAG,EAAE;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,UAAU,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,gBAAgB,EACvB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AA8EF;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,kBAAkB,KACxB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAgIrC"}
@@ -36,7 +36,12 @@ async function defaultExtractAttestation(req) {
36
36
  catch {
37
37
  throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "attestation is not valid base64");
38
38
  }
39
- return { deviceId: typed.keyId, challenge, attestation };
39
+ return {
40
+ deviceId: typed.keyId,
41
+ challenge,
42
+ challengeAsSent: typed.challenge,
43
+ attestation,
44
+ };
40
45
  }
41
46
  function defaultErrorResponse(error) {
42
47
  const status = error.code === AttestationErrorCode.INTERNAL_ERROR
@@ -100,13 +105,20 @@ export function withAttestation(options, handler) {
100
105
  if (!challengeOk) {
101
106
  throw new AttestationError(AttestationErrorCode.CHALLENGE_INVALID, "Challenge is missing, expired, or already consumed");
102
107
  }
103
- // Hash the raw challenge to produce clientDataHash. Client SDKs
104
- // (Expo's attestKeyAsync, native DCAppAttestService wrappers) hash
105
- // the challenge with SHA-256 before passing to Apple's attestKey
106
- // API, so the attestation certificate's nonce is computed over the
107
- // hash, not the raw bytes. verifyAttestation expects clientDataHash
108
- // (the hash), not the raw challenge.
109
- const clientDataHash = new Uint8Array(await crypto.subtle.digest("SHA-256", extracted.challenge));
108
+ // Hash the challenge AS THE CLIENT SAW IT — the exact string passed
109
+ // to attestKeyAsync / DCAppAttestService.attestKey. Client SDKs
110
+ // convert this string to UTF-8 bytes and SHA-256 hash them before
111
+ // passing to Apple as clientDataHash. The attestation certificate's
112
+ // nonce is SHA-256(authData || clientDataHash), so we must hash the
113
+ // same string to produce the matching clientDataHash.
114
+ //
115
+ // This is different from the assertion path, which has no encoding
116
+ // layer: the string passed to generateAssertionAsync IS the raw
117
+ // body, and both sides hash identical bytes by definition.
118
+ // Attestation has a transport encoding layer (base64 in a JSON
119
+ // body), so the middleware must hash BEFORE decoding to match what
120
+ // the client SDK hashed.
121
+ const clientDataHash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(extracted.challengeAsSent)));
110
122
  const verifyStart = performance.now();
111
123
  const result = await verifyAttestation(appInfo, deviceId, clientDataHash, extracted.attestation);
112
124
  timings.verifyMs = performance.now() - verifyStart;
@@ -134,7 +146,13 @@ export function withAttestation(options, handler) {
134
146
  const error = err instanceof AttestationError
135
147
  ? err
136
148
  : new AttestationError(AttestationErrorCode.INTERNAL_ERROR, "Internal error", { cause: err });
137
- return options.onError?.(error, req) ?? defaultErrorResponse(error);
149
+ try {
150
+ return await options.onError?.(error, req) ??
151
+ defaultErrorResponse(error);
152
+ }
153
+ catch {
154
+ return defaultErrorResponse(error);
155
+ }
138
156
  }
139
157
  return await handler(req, {
140
158
  deviceId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bradford-tech/supabase-integrity-attest",
3
- "version": "0.5.0",
3
+ "version": "0.8.0",
4
4
  "description": "Verify Apple App Attest attestations and assertions using WebCrypto.",
5
5
  "homepage": "https://integrity-attest.bradford.tech",
6
6
  "repository": {
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "scripts": {},
27
27
  "dependencies": {
28
- "@noble/curves": "^2.0.1",
29
- "asn1js": "^3.0.7",
30
- "cborg": "^4.5.8"
28
+ "@noble/curves": "^2.2.0",
29
+ "asn1js": "^3.0.10",
30
+ "cborg": "^5.1.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^20.9.0"