244 lines
7.8 KiB
TypeScript
244 lines
7.8 KiB
TypeScript
import { bytesToArrayBuffer } from "../crypto/encoding";
|
|
import { derEcdsaToRaw } from "./verify";
|
|
|
|
export interface DerElement {
|
|
tag: number;
|
|
start: number;
|
|
contentStart: number;
|
|
end: number;
|
|
}
|
|
|
|
export function readDerElement(bytes: Uint8Array, offset: number): DerElement {
|
|
if (offset < 0 || offset + 2 > bytes.length)
|
|
throw new Error("DER value is truncated.");
|
|
const start = offset;
|
|
const tag = bytes[offset++]!;
|
|
let length = bytes[offset++]!;
|
|
if (length & 0x80) {
|
|
const count = length & 0x7f;
|
|
if (count === 0 || count > 4 || offset + count > bytes.length)
|
|
throw new Error("DER length is invalid.");
|
|
length = 0;
|
|
for (let index = 0; index < count; index += 1)
|
|
length = length * 256 + bytes[offset++]!;
|
|
}
|
|
if (length < 0 || offset + length > bytes.length)
|
|
throw new Error("DER content is truncated.");
|
|
return { tag, start, contentStart: offset, end: offset + length };
|
|
}
|
|
|
|
export function derChildren(
|
|
bytes: Uint8Array,
|
|
parent: DerElement,
|
|
): DerElement[] {
|
|
const children: DerElement[] = [];
|
|
let offset = parent.contentStart;
|
|
while (offset < parent.end) {
|
|
const child = readDerElement(bytes, offset);
|
|
if (child.end > parent.end)
|
|
throw new Error("DER child exceeds its parent.");
|
|
children.push(child);
|
|
offset = child.end;
|
|
}
|
|
return children;
|
|
}
|
|
|
|
function certificateTbs(bytes: Uint8Array): DerElement {
|
|
const certificate = readDerElement(bytes, 0);
|
|
if (certificate.tag !== 0x30 || certificate.end !== bytes.length)
|
|
throw new Error("Attestation certificate is not a complete DER sequence.");
|
|
const tbs = derChildren(bytes, certificate)[0];
|
|
if (!tbs || tbs.tag !== 0x30)
|
|
throw new Error("Certificate TBSCertificate is missing.");
|
|
return tbs;
|
|
}
|
|
|
|
export function certificateSpki(bytes: Uint8Array): Uint8Array {
|
|
const fields = derChildren(bytes, certificateTbs(bytes));
|
|
let index = fields[0]?.tag === 0xa0 ? 1 : 0;
|
|
index += 5; // serial, signature, issuer, validity, subject
|
|
const spki = fields[index];
|
|
if (!spki || spki.tag !== 0x30)
|
|
throw new Error("Certificate SubjectPublicKeyInfo is missing.");
|
|
return bytes.slice(spki.start, spki.end);
|
|
}
|
|
|
|
export function certificatePublicKeyBytes(bytes: Uint8Array): Uint8Array {
|
|
const spki = readDerElement(certificateSpki(bytes), 0);
|
|
const children = derChildren(certificateSpki(bytes), spki);
|
|
const bitString = children[1];
|
|
if (
|
|
!bitString ||
|
|
bitString.tag !== 0x03 ||
|
|
bitString.contentStart >= bitString.end ||
|
|
certificateSpki(bytes)[bitString.contentStart] !== 0
|
|
)
|
|
throw new Error("Certificate public-key bit string is invalid.");
|
|
return certificateSpki(bytes).slice(
|
|
bitString.contentStart + 1,
|
|
bitString.end,
|
|
);
|
|
}
|
|
|
|
function containsBytes(certificate: Uint8Array, value: Uint8Array): boolean {
|
|
if (!value.length || value.length > certificate.length) return false;
|
|
outer: for (
|
|
let offset = 0;
|
|
offset <= certificate.length - value.length;
|
|
offset += 1
|
|
) {
|
|
for (let index = 0; index < value.length; index += 1)
|
|
if (certificate[offset + index] !== value[index]) continue outer;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function oidBytes(oid: string): Uint8Array {
|
|
const arcs = oid.split(".").map(Number);
|
|
if (
|
|
arcs.length < 2 ||
|
|
arcs.some((arc) => !Number.isSafeInteger(arc) || arc < 0) ||
|
|
arcs[0]! > 2 ||
|
|
(arcs[0]! < 2 && arcs[1]! > 39)
|
|
)
|
|
throw new Error("Object identifier is invalid.");
|
|
const output = [arcs[0]! * 40 + arcs[1]!];
|
|
for (const arc of arcs.slice(2)) {
|
|
const encoded = [arc & 0x7f];
|
|
let remaining = Math.floor(arc / 128);
|
|
while (remaining) {
|
|
encoded.unshift((remaining & 0x7f) | 0x80);
|
|
remaining = Math.floor(remaining / 128);
|
|
}
|
|
output.push(...encoded);
|
|
}
|
|
return Uint8Array.from(output);
|
|
}
|
|
|
|
export function certificateExtension(
|
|
certificate: Uint8Array,
|
|
oid: string,
|
|
): Uint8Array | undefined {
|
|
const fields = derChildren(certificate, certificateTbs(certificate));
|
|
const wrapper = fields.find((field) => field.tag === 0xa3);
|
|
if (!wrapper) return undefined;
|
|
const sequence = derChildren(certificate, wrapper)[0];
|
|
if (!sequence || sequence.tag !== 0x30)
|
|
throw new Error("Certificate extensions are malformed.");
|
|
const expectedOid = oidBytes(oid);
|
|
for (const extension of derChildren(certificate, sequence)) {
|
|
if (extension.tag !== 0x30)
|
|
throw new Error("Certificate extension is malformed.");
|
|
const parts = derChildren(certificate, extension);
|
|
const name = parts[0];
|
|
const value = parts.at(-1);
|
|
if (!name || name.tag !== 0x06 || !value || value.tag !== 0x04) continue;
|
|
const actualOid = certificate.slice(name.contentStart, name.end);
|
|
if (bytesEqual(actualOid, expectedOid))
|
|
return certificate.slice(value.contentStart, value.end);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function bytesEqual(left: Uint8Array, right: Uint8Array): boolean {
|
|
if (left.length !== right.length) return false;
|
|
return left.every((byte, index) => byte === right[index]);
|
|
}
|
|
|
|
export function extensionContainsBytes(
|
|
certificate: Uint8Array,
|
|
oid: string,
|
|
value: Uint8Array,
|
|
): boolean {
|
|
const extension = certificateExtension(certificate, oid);
|
|
return extension ? containsBytes(extension, value) : false;
|
|
}
|
|
|
|
export interface AttestationAlgorithm {
|
|
importAlgorithm:
|
|
| AlgorithmIdentifier
|
|
| RsaHashedImportParams
|
|
| EcKeyImportParams;
|
|
verifyAlgorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams;
|
|
normalizeSignature(signature: Uint8Array): Uint8Array;
|
|
hash: "SHA-256" | "SHA-384" | "SHA-512";
|
|
}
|
|
|
|
export function attestationAlgorithm(
|
|
coseAlgorithm: number,
|
|
): AttestationAlgorithm {
|
|
if (coseAlgorithm === -7)
|
|
return {
|
|
importAlgorithm: { name: "ECDSA", namedCurve: "P-256" },
|
|
verifyAlgorithm: { name: "ECDSA", hash: "SHA-256" },
|
|
normalizeSignature: (value) => derEcdsaToRaw(value, 32),
|
|
hash: "SHA-256",
|
|
};
|
|
if (coseAlgorithm === -35)
|
|
return {
|
|
importAlgorithm: { name: "ECDSA", namedCurve: "P-384" },
|
|
verifyAlgorithm: { name: "ECDSA", hash: "SHA-384" },
|
|
normalizeSignature: (value) => derEcdsaToRaw(value, 48),
|
|
hash: "SHA-384",
|
|
};
|
|
if (coseAlgorithm === -36)
|
|
return {
|
|
importAlgorithm: { name: "ECDSA", namedCurve: "P-521" },
|
|
verifyAlgorithm: { name: "ECDSA", hash: "SHA-512" },
|
|
normalizeSignature: (value) => derEcdsaToRaw(value, 66),
|
|
hash: "SHA-512",
|
|
};
|
|
if (coseAlgorithm === -257)
|
|
return {
|
|
importAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
verifyAlgorithm: "RSASSA-PKCS1-v1_5",
|
|
normalizeSignature: (value) => value,
|
|
hash: "SHA-256",
|
|
};
|
|
if (coseAlgorithm === -258)
|
|
return {
|
|
importAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" },
|
|
verifyAlgorithm: "RSASSA-PKCS1-v1_5",
|
|
normalizeSignature: (value) => value,
|
|
hash: "SHA-384",
|
|
};
|
|
if (coseAlgorithm === -259)
|
|
return {
|
|
importAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
|
|
verifyAlgorithm: "RSASSA-PKCS1-v1_5",
|
|
normalizeSignature: (value) => value,
|
|
hash: "SHA-512",
|
|
};
|
|
if (coseAlgorithm === -37)
|
|
return {
|
|
importAlgorithm: { name: "RSA-PSS", hash: "SHA-256" },
|
|
verifyAlgorithm: { name: "RSA-PSS", saltLength: 32 },
|
|
normalizeSignature: (value) => value,
|
|
hash: "SHA-256",
|
|
};
|
|
throw new Error(`Unsupported attestation COSE algorithm ${coseAlgorithm}.`);
|
|
}
|
|
|
|
export async function verifyCertificateSignature(
|
|
certificate: Uint8Array,
|
|
coseAlgorithm: number,
|
|
signature: Uint8Array,
|
|
signed: Uint8Array,
|
|
): Promise<boolean> {
|
|
const algorithm = attestationAlgorithm(coseAlgorithm);
|
|
const key = await crypto.subtle.importKey(
|
|
"spki",
|
|
bytesToArrayBuffer(certificateSpki(certificate)),
|
|
algorithm.importAlgorithm,
|
|
false,
|
|
["verify"],
|
|
);
|
|
return crypto.subtle.verify(
|
|
algorithm.verifyAlgorithm,
|
|
key,
|
|
bytesToArrayBuffer(algorithm.normalizeSignature(signature)),
|
|
bytesToArrayBuffer(signed),
|
|
);
|
|
}
|