@bradford-tech/supabase-integrity-attest 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/src/certificate.d.ts.map +1 -1
- package/esm/src/certificate.js +256 -32
- package/esm/src/der.d.ts +2 -2
- package/esm/src/der.d.ts.map +1 -1
- package/esm/src/der.js +12 -12
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"certificate.d.ts","sourceRoot":"","sources":["../../src/src/certificate.ts"],"names":[],"mappings":"
|
|
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"}
|
package/esm/src/certificate.js
CHANGED
|
@@ -1,19 +1,222 @@
|
|
|
1
1
|
// src/certificate.ts
|
|
2
|
-
import * as pkijs from "pkijs";
|
|
3
2
|
import * as asn1js from "asn1js";
|
|
3
|
+
import { p384 } from "@noble/curves/nist.js";
|
|
4
|
+
import { decodeBase64 } from "../deps/jsr.io/@std/encoding/1.0.10/base64.js";
|
|
4
5
|
import { APPLE_APP_ATTESTATION_ROOT_CA_PEM, APPLE_NONCE_EXTENSION_OID, } from "./constants.js";
|
|
6
|
+
import { derToRaw } from "./der.js";
|
|
5
7
|
import { AttestationError, AttestationErrorCode } from "./errors.js";
|
|
8
|
+
import { constantTimeEqual } from "./utils.js";
|
|
9
|
+
// ── OID mappings ────────────────────────────────────────────────────
|
|
10
|
+
/** Signature algorithm OID → hash algorithm name */
|
|
11
|
+
const SIG_ALG_HASH = {
|
|
12
|
+
"1.2.840.10045.4.3.2": "SHA-256",
|
|
13
|
+
"1.2.840.10045.4.3.3": "SHA-384",
|
|
14
|
+
};
|
|
15
|
+
/** Curve OID → WebCrypto namedCurve */
|
|
16
|
+
const CURVE_OID_NAME = {
|
|
17
|
+
"1.2.840.10045.3.1.7": "P-256",
|
|
18
|
+
"1.3.132.0.34": "P-384",
|
|
19
|
+
};
|
|
20
|
+
/** Curve name → ECDSA r||s component size in bytes */
|
|
21
|
+
const CURVE_COMPONENT_SIZE = {
|
|
22
|
+
"P-256": 32,
|
|
23
|
+
"P-384": 48,
|
|
24
|
+
};
|
|
25
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
6
26
|
/**
|
|
7
|
-
*
|
|
27
|
+
* Safely create an ArrayBuffer from a Uint8Array, handling subarrays.
|
|
28
|
+
* If `der` is a subarray, `der.buffer` includes the full underlying buffer,
|
|
29
|
+
* so we must slice to get only the relevant portion.
|
|
8
30
|
*/
|
|
9
|
-
function
|
|
31
|
+
function safeBuffer(der) {
|
|
32
|
+
return der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Slice bytes from a valueBeforeDecodeView, producing an independent copy.
|
|
36
|
+
*/
|
|
37
|
+
function sliceFromView(view) {
|
|
38
|
+
return new Uint8Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse a date from an ASN.1 time element (UTCTime or GeneralizedTime).
|
|
42
|
+
*/
|
|
43
|
+
function parseAsn1Date(element) {
|
|
44
|
+
if (element instanceof asn1js.UTCTime) {
|
|
45
|
+
return element.toDate();
|
|
46
|
+
}
|
|
47
|
+
if (element instanceof asn1js.GeneralizedTime) {
|
|
48
|
+
return element.toDate();
|
|
49
|
+
}
|
|
50
|
+
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "Unexpected ASN.1 time type in certificate validity");
|
|
51
|
+
}
|
|
52
|
+
// ── parseCertificate ────────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Parse an X.509 DER-encoded certificate into structured fields.
|
|
55
|
+
* All byte slices come from the original input buffer.
|
|
56
|
+
*/
|
|
57
|
+
function parseCertificate(der) {
|
|
58
|
+
const buf = safeBuffer(der);
|
|
59
|
+
const asn1 = asn1js.fromBER(buf);
|
|
60
|
+
if (asn1.offset === -1) {
|
|
61
|
+
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "Failed to parse certificate DER");
|
|
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];
|
|
133
|
+
}
|
|
134
|
+
extensions.push({
|
|
135
|
+
oid,
|
|
136
|
+
critical,
|
|
137
|
+
value: new Uint8Array(valueElement.valueBlock.valueHexView),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
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
|
+
}
|
|
156
|
+
// ── extractRawPublicKeyFromSpki ─────────────────────────────────────
|
|
157
|
+
/**
|
|
158
|
+
* Extract the raw public key bytes from a DER-encoded SubjectPublicKeyInfo.
|
|
159
|
+
* Returns the uncompressed EC point (0x04 || x || y).
|
|
160
|
+
*/
|
|
161
|
+
function extractRawPublicKeyFromSpki(spkiDer) {
|
|
162
|
+
const buf = safeBuffer(spkiDer);
|
|
163
|
+
const asn1 = asn1js.fromBER(buf);
|
|
164
|
+
if (asn1.offset === -1) {
|
|
165
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, "Failed to parse SPKI DER");
|
|
166
|
+
}
|
|
167
|
+
const spkiSeq = asn1.result;
|
|
168
|
+
const publicKeyBitString = spkiSeq.valueBlock.value[1];
|
|
169
|
+
return new Uint8Array(publicKeyBitString.valueBlock.valueHexView);
|
|
170
|
+
}
|
|
171
|
+
// ── verifySignature ─────────────────────────────────────────────────
|
|
172
|
+
/**
|
|
173
|
+
* Verify the signature of a child certificate against the parent's public key.
|
|
174
|
+
*
|
|
175
|
+
* Deno WebCrypto throws "Not implemented" for cross-paired curve+hash
|
|
176
|
+
* (e.g., P-384 key signing with SHA-256). Apple's intermediate (P-384)
|
|
177
|
+
* signs the leaf cert with SHA-256. Use @noble/curves for this case.
|
|
178
|
+
*/
|
|
179
|
+
async function verifySignature(child, parent) {
|
|
180
|
+
const hash = SIG_ALG_HASH[child.signatureAlgorithmOid];
|
|
181
|
+
if (!hash) {
|
|
182
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, `Unsupported signature algorithm OID: ${child.signatureAlgorithmOid}`);
|
|
183
|
+
}
|
|
184
|
+
const namedCurve = CURVE_OID_NAME[parent.publicKeyCurveOid];
|
|
185
|
+
if (!namedCurve) {
|
|
186
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, `Unsupported curve OID: ${parent.publicKeyCurveOid}`);
|
|
187
|
+
}
|
|
188
|
+
const componentSize = CURVE_COMPONENT_SIZE[namedCurve];
|
|
189
|
+
const sigRaw = derToRaw(child.signatureValue, componentSize);
|
|
190
|
+
// Apple's intermediate (P-384 key) signs the leaf cert with SHA-256.
|
|
191
|
+
// Deno WebCrypto throws "Not implemented" for this cross-pairing.
|
|
192
|
+
// Use @noble/curves p384 to verify instead.
|
|
193
|
+
if (namedCurve === "P-384" && hash === "SHA-256") {
|
|
194
|
+
// Pre-hash TBS, then verify with @noble/curves.
|
|
195
|
+
// lowS: false — X.509 signatures don't enforce BIP-62 low-S normalization.
|
|
196
|
+
// prehash: false — we hash manually since the hash algorithm differs from the curve's default.
|
|
197
|
+
const digest = new Uint8Array(await crypto.subtle.digest(hash, child.tbsCertificateDer));
|
|
198
|
+
const rawPubKey = extractRawPublicKeyFromSpki(parent.subjectPublicKeyInfoDer);
|
|
199
|
+
return p384.verify(sigRaw, digest, rawPubKey, {
|
|
200
|
+
prehash: false,
|
|
201
|
+
lowS: false,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// Standard pairing — WebCrypto
|
|
205
|
+
const parentKey = await crypto.subtle.importKey("spki", parent.subjectPublicKeyInfoDer, { name: "ECDSA", namedCurve }, false, ["verify"]);
|
|
206
|
+
return crypto.subtle.verify({ name: "ECDSA", hash }, parentKey, sigRaw, child.tbsCertificateDer);
|
|
207
|
+
}
|
|
208
|
+
// ── parseRootCaDer ──────────────────────────────────────────────────
|
|
209
|
+
/**
|
|
210
|
+
* Decode the Apple App Attestation Root CA from PEM to DER bytes.
|
|
211
|
+
*/
|
|
212
|
+
function parseRootCaDer() {
|
|
10
213
|
const b64 = APPLE_APP_ATTESTATION_ROOT_CA_PEM
|
|
11
214
|
.replace("-----BEGIN CERTIFICATE-----", "")
|
|
12
215
|
.replace("-----END CERTIFICATE-----", "")
|
|
13
216
|
.replace(/\s/g, "");
|
|
14
|
-
|
|
15
|
-
return pkijs.Certificate.fromBER(der);
|
|
217
|
+
return decodeBase64(b64);
|
|
16
218
|
}
|
|
219
|
+
// ── Exported functions ──────────────────────────────────────────────
|
|
17
220
|
/**
|
|
18
221
|
* Verify the x5c certificate chain against the Apple App Attestation Root CA.
|
|
19
222
|
*
|
|
@@ -24,17 +227,41 @@ export async function verifyCertificateChain(x5c, checkDate) {
|
|
|
24
227
|
if (x5c.length === 0) {
|
|
25
228
|
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, "Certificate chain (x5c) is empty");
|
|
26
229
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
230
|
+
// Parse root CA and all x5c certs
|
|
231
|
+
const rootCaDer = parseRootCaDer();
|
|
232
|
+
const rootCa = parseCertificate(rootCaDer);
|
|
233
|
+
const certs = x5c.map((der) => parseCertificate(der));
|
|
234
|
+
// Build chain: [leaf, ..., intermediate, rootCA]
|
|
235
|
+
const chain = [...certs, rootCa];
|
|
236
|
+
// Verify root is self-signed
|
|
237
|
+
if (!constantTimeEqual(rootCa.issuer, rootCa.subject)) {
|
|
238
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, "Root CA is not self-signed (issuer !== subject)");
|
|
239
|
+
}
|
|
240
|
+
const rootSelfSigned = await verifySignature(rootCa, rootCa);
|
|
241
|
+
if (!rootSelfSigned) {
|
|
242
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, "Root CA self-signature verification failed");
|
|
243
|
+
}
|
|
244
|
+
// Verify each child→parent pair in the chain
|
|
245
|
+
for (let i = 0; i < chain.length - 1; i++) {
|
|
246
|
+
const child = chain[i];
|
|
247
|
+
const parent = chain[i + 1];
|
|
248
|
+
// Issuer must match parent's subject
|
|
249
|
+
if (!constantTimeEqual(child.issuer, parent.subject)) {
|
|
250
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, `Certificate ${i} issuer does not match certificate ${i + 1} subject`);
|
|
251
|
+
}
|
|
252
|
+
// Verify signature
|
|
253
|
+
const valid = await verifySignature(child, parent);
|
|
254
|
+
if (!valid) {
|
|
255
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, `Certificate ${i} signature verification failed`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Check validity periods
|
|
259
|
+
const now = checkDate ?? new Date();
|
|
260
|
+
for (let i = 0; i < chain.length; i++) {
|
|
261
|
+
const cert = chain[i];
|
|
262
|
+
if (now < cert.validityNotBefore || now > cert.validityNotAfter) {
|
|
263
|
+
throw new AttestationError(AttestationErrorCode.INVALID_CERTIFICATE_CHAIN, `Certificate ${i} is not valid at ${now.toISOString()} (valid from ${cert.validityNotBefore.toISOString()} to ${cert.validityNotAfter.toISOString()})`);
|
|
264
|
+
}
|
|
38
265
|
}
|
|
39
266
|
}
|
|
40
267
|
/**
|
|
@@ -47,23 +274,19 @@ export async function verifyCertificateChain(x5c, checkDate) {
|
|
|
47
274
|
* @returns The 32-byte nonce
|
|
48
275
|
*/
|
|
49
276
|
export function extractNonceFromCert(certDer) {
|
|
50
|
-
const cert =
|
|
51
|
-
const
|
|
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);
|
|
277
|
+
const cert = parseCertificate(certDer);
|
|
278
|
+
const nonceExt = cert.extensions.find((ext) => ext.oid === APPLE_NONCE_EXTENSION_OID);
|
|
56
279
|
if (!nonceExt) {
|
|
57
280
|
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, `Certificate missing nonce extension (OID ${APPLE_NONCE_EXTENSION_OID})`);
|
|
58
281
|
}
|
|
59
282
|
// Parse the extension value as ASN.1
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
283
|
+
const extBuf = safeBuffer(nonceExt.value);
|
|
284
|
+
const extAsn1 = asn1js.fromBER(extBuf);
|
|
285
|
+
if (extAsn1.offset === -1) {
|
|
63
286
|
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, "Failed to parse nonce extension ASN.1");
|
|
64
287
|
}
|
|
65
288
|
// Navigate: SEQUENCE -> tagged [1] -> OCTET STRING
|
|
66
|
-
const sequence =
|
|
289
|
+
const sequence = extAsn1.result;
|
|
67
290
|
const tagged = sequence.valueBlock.value[0];
|
|
68
291
|
const octetString = tagged.valueBlock.value[0];
|
|
69
292
|
return new Uint8Array(octetString.valueBlock.valueHexView);
|
|
@@ -75,12 +298,13 @@ export function extractNonceFromCert(certDer) {
|
|
|
75
298
|
* @returns 65-byte uncompressed EC point (0x04 || x || y)
|
|
76
299
|
*/
|
|
77
300
|
export async function extractPublicKeyFromCert(certDer) {
|
|
78
|
-
const cert =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
301
|
+
const cert = parseCertificate(certDer);
|
|
302
|
+
const namedCurve = CURVE_OID_NAME[cert.publicKeyCurveOid];
|
|
303
|
+
if (!namedCurve) {
|
|
304
|
+
throw new AttestationError(AttestationErrorCode.INVALID_FORMAT, `Unsupported curve OID: ${cert.publicKeyCurveOid}`);
|
|
305
|
+
}
|
|
306
|
+
// Import SPKI as CryptoKey, then export as raw uncompressed point
|
|
307
|
+
const cryptoKey = await crypto.subtle.importKey("spki", cert.subjectPublicKeyInfoDer, { name: "ECDSA", namedCurve }, true, ["verify"]);
|
|
84
308
|
const rawKey = await crypto.subtle.exportKey("raw", cryptoKey);
|
|
85
309
|
return new Uint8Array(rawKey);
|
|
86
310
|
}
|
package/esm/src/der.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare function derToRaw(der: Uint8Array): Uint8Array;
|
|
2
|
-
export declare function rawToDer(raw: Uint8Array): Uint8Array;
|
|
1
|
+
export declare function derToRaw(der: Uint8Array, componentSize?: number): Uint8Array;
|
|
2
|
+
export declare function rawToDer(raw: Uint8Array, componentSize?: number): Uint8Array;
|
|
3
3
|
//# sourceMappingURL=der.d.ts.map
|
package/esm/src/der.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"der.d.ts","sourceRoot":"","sources":["../../src/src/der.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,
|
|
1
|
+
{"version":3,"file":"der.d.ts","sourceRoot":"","sources":["../../src/src/der.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CACtB,GAAG,EAAE,UAAU,EACf,aAAa,SAAK,GACjB,UAAU,CAoCZ;AAED,wBAAgB,QAAQ,CACtB,GAAG,EAAE,UAAU,EACf,aAAa,SAAK,GACjB,UAAU,CAmBZ"}
|
package/esm/src/der.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/der.ts
|
|
2
|
-
export function derToRaw(der) {
|
|
2
|
+
export function derToRaw(der, componentSize = 32) {
|
|
3
3
|
if (der[0] !== 0x30) {
|
|
4
4
|
throw new Error("Invalid DER signature: expected SEQUENCE tag (0x30)");
|
|
5
5
|
}
|
|
@@ -8,7 +8,7 @@ export function derToRaw(der) {
|
|
|
8
8
|
const lengthBytes = der[1] & 0x7f;
|
|
9
9
|
offset = 2 + lengthBytes;
|
|
10
10
|
}
|
|
11
|
-
const raw = new Uint8Array(
|
|
11
|
+
const raw = new Uint8Array(componentSize * 2);
|
|
12
12
|
if (der[offset] !== 0x02) {
|
|
13
13
|
throw new Error("Invalid DER signature: expected INTEGER tag (0x02) for r");
|
|
14
14
|
}
|
|
@@ -22,16 +22,16 @@ export function derToRaw(der) {
|
|
|
22
22
|
offset++;
|
|
23
23
|
const sLen = der[offset++];
|
|
24
24
|
const sBytes = der.subarray(offset, offset + sLen);
|
|
25
|
-
copyInteger(rBytes, raw, 0);
|
|
26
|
-
copyInteger(sBytes, raw,
|
|
25
|
+
copyInteger(rBytes, raw, 0, componentSize);
|
|
26
|
+
copyInteger(sBytes, raw, componentSize, componentSize);
|
|
27
27
|
return raw;
|
|
28
28
|
}
|
|
29
|
-
export function rawToDer(raw) {
|
|
30
|
-
if (raw.length !==
|
|
31
|
-
throw new Error(`Invalid raw signature: expected
|
|
29
|
+
export function rawToDer(raw, componentSize = 32) {
|
|
30
|
+
if (raw.length !== componentSize * 2) {
|
|
31
|
+
throw new Error(`Invalid raw signature: expected ${componentSize * 2} bytes, got ${raw.length}`);
|
|
32
32
|
}
|
|
33
|
-
const r = encodeInteger(raw.subarray(0,
|
|
34
|
-
const s = encodeInteger(raw.subarray(
|
|
33
|
+
const r = encodeInteger(raw.subarray(0, componentSize));
|
|
34
|
+
const s = encodeInteger(raw.subarray(componentSize, componentSize * 2));
|
|
35
35
|
const seqLen = r.length + s.length;
|
|
36
36
|
const der = new Uint8Array(2 + seqLen);
|
|
37
37
|
der[0] = 0x30;
|
|
@@ -40,16 +40,16 @@ export function rawToDer(raw) {
|
|
|
40
40
|
der.set(s, 2 + r.length);
|
|
41
41
|
return der;
|
|
42
42
|
}
|
|
43
|
-
function copyInteger(src, dst, dstOffset) {
|
|
43
|
+
function copyInteger(src, dst, dstOffset, componentSize) {
|
|
44
44
|
let srcOffset = 0;
|
|
45
45
|
while (srcOffset < src.length - 1 && src[srcOffset] === 0) {
|
|
46
46
|
srcOffset++;
|
|
47
47
|
}
|
|
48
48
|
const len = src.length - srcOffset;
|
|
49
|
-
if (len >
|
|
49
|
+
if (len > componentSize) {
|
|
50
50
|
throw new Error(`Integer too large: ${len} bytes`);
|
|
51
51
|
}
|
|
52
|
-
dst.set(src.subarray(srcOffset), dstOffset + (
|
|
52
|
+
dst.set(src.subarray(srcOffset), dstOffset + (componentSize - len));
|
|
53
53
|
}
|
|
54
54
|
function encodeInteger(value) {
|
|
55
55
|
let start = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bradford-tech/supabase-integrity-attest",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Verify Apple App Attest attestations and assertions using WebCrypto.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"test": "node test_runner.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@noble/curves": "^2.0.1",
|
|
23
24
|
"asn1js": "^3.0.7",
|
|
24
|
-
"cborg": "^4.5.8"
|
|
25
|
-
"pkijs": "^3.3.3"
|
|
25
|
+
"cborg": "^4.5.8"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.9.0",
|