feat: release authentication diagnostics 0.2.0
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user