summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMatthew Miller <matthew@millerti.me>2022-11-16 21:06:36 -0800
committerMatthew Miller <matthew@millerti.me>2022-11-16 21:06:36 -0800
commite6972710a09edb96a10cbfa69e9d52d82914e2ec (patch)
treeb5690e3df6571f5141e2f2fdff87aad2f1aa9b1e
parent684182bfc3ebda7fd335cf4ce5bcfe928e8e6e25 (diff)
Support leaf cert sig verification
-rw-r--r--packages/server/src/helpers/convertCOSEtoPKCS.ts2
-rw-r--r--packages/server/src/helpers/iso/isoCrypto.ts232
-rw-r--r--packages/server/src/helpers/verifySignature.ts165
3 files changed, 303 insertions, 96 deletions
diff --git a/packages/server/src/helpers/convertCOSEtoPKCS.ts b/packages/server/src/helpers/convertCOSEtoPKCS.ts
index 19e4c4c..b9b9d09 100644
--- a/packages/server/src/helpers/convertCOSEtoPKCS.ts
+++ b/packages/server/src/helpers/convertCOSEtoPKCS.ts
@@ -48,6 +48,8 @@ export enum COSECRV {
ED25519 = 6,
}
+export type COSEALG = number;
+
export const COSERSASCHEME: { [key: string]: SigningSchemeHash } = {
'-3': 'pss-sha256',
'-39': 'pss-sha512',
diff --git a/packages/server/src/helpers/iso/isoCrypto.ts b/packages/server/src/helpers/iso/isoCrypto.ts
index bdda68d..42bd0a9 100644
--- a/packages/server/src/helpers/iso/isoCrypto.ts
+++ b/packages/server/src/helpers/iso/isoCrypto.ts
@@ -3,7 +3,7 @@ import { ECDSASigValue } from "@peculiar/asn1-ecc";
import { AsnParser } from '@peculiar/asn1-schema';
import { isoUint8Array, isoBase64URL } from './index';
-import { COSECRV, coseCRV, COSEKEYS, COSEKTY, COSEPublicKey } from '../convertCOSEtoPKCS';
+import { COSECRV, COSEKEYS, COSEKTY, COSEALG, COSEPublicKey } from '../convertCOSEtoPKCS';
/**
* Fill up the provided bytes array with random bytes equal to its length.
@@ -33,7 +33,7 @@ export function getRandomValues(array: Uint8Array): Uint8Array {
* - `"SHA-512"`
*/
export async function digest(data: Uint8Array, algorithm: string): Promise<Uint8Array> {
- algorithm = normalizeAlgorithm(algorithm);
+ algorithm = normalizeSHAAlgorithm(algorithm);
let hashed: ArrayBuffer
if (globalThis.crypto) {
@@ -48,20 +48,22 @@ export async function digest(data: Uint8Array, algorithm: string): Promise<Uint8
}
/**
- * Verify signatures with their public key. Supports EC2 and RSA public key.
+ * Verify signatures with their public key. Supports EC2 and RSA public keys.
*/
-export async function verify(
- publicKey: COSEPublicKey,
+export async function verify({
+ publicKey,
+ coseKty,
+ coseAlg,
+ signature,
+ data,
+}: {
+ publicKey: CryptoKey,
+ coseKty: COSEKTY,
+ coseAlg: COSEALG,
signature: Uint8Array,
- signatureBase: Uint8Array,
-): Promise<boolean> {
- const kty = publicKey.get(COSEKEYS.kty);
-
- if (!kty) {
- throw new Error('Public key was missing kty');
- }
-
- if (kty === COSEKTY.EC2) {
+ data: Uint8Array,
+}): Promise<boolean> {
+ if (coseKty === COSEKTY.EC2) {
// The signature is wrapped in ASN.1 structure, so we need to peel it apart
const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
let rBytes = new Uint8Array(parsedSignature.r);
@@ -77,49 +79,78 @@ export async function verify(
const signatureBytes = isoUint8Array.concat([rBytes, sBytes]);
- const alg = publicKey.get(COSEKEYS.alg);
- const crv = publicKey.get(COSEKEYS.crv);
- const x = publicKey.get(COSEKEYS.x);
- const y = publicKey.get(COSEKEYS.y);
-
- if (!alg) {
- throw new Error('Public key was missing alg');
- }
-
- if (!crv) {
- throw new Error('Public key was missing crv');
- }
+ return verifyECSignature(publicKey, signatureBytes, data, coseAlg);
+ } else if (coseKty === COSEKTY.RSA) {
+ return verifyRSASignature(publicKey, signature, data);
+ }
- if (!x) {
- throw new Error('Public key was missing x');
- }
+ throw new Error(
+ `Signature verification with public key of kty ${coseKty} is not supported by this method`,
+ );
+}
- if (!y) {
- throw new Error('Public key was missing y');
- }
+/**
+ * Import an EC2 or RSA public key from its COSE representation
+ *
+ * @param publicKey A `Map` containing COSE-specific public key properties
+ * @param rsaHashAlgorithm A SHA hashing identifier for use when verifying signatures with the
+ * returned RSA public key (e.g. `"sha1"`, `"sha256"`, etc...), if applicable
+ */
+export async function importKey(publicKey: COSEPublicKey, rsaHashAlgorithm?: string): Promise<CryptoKey> {
+ const kty = publicKey.get(COSEKEYS.kty);
- const subtleCrv = mapCoseCrvToWebCryptoCrv(crv as number);
- const subtleAlg = mapCoseAlgToWebCryptoAlg(alg as number);
+ if (!kty) {
+ throw new Error('Public key was missing kty');
+ }
- const subtlePublicKey = await importECKey(
- subtleCrv,
- x as Uint8Array,
- y as Uint8Array,
- );
+ if (kty === COSEKTY.EC2) {
+ return importECKey(publicKey);
+ }
- return verifyECSignature(subtlePublicKey, signatureBytes, signatureBase, subtleAlg);
+ if (kty === COSEKTY.RSA) {
+ return importRSAKey(publicKey, rsaHashAlgorithm);
}
- return false;
+ throw new Error(`Unable to import public key of kty ${kty}`);
}
/**
- * Import a public key from its corresponding
+ * Import an EC2 public key from its COSE representation
*/
-function importECKey(crv: SubtleCryptoCrv, x: Uint8Array, y: Uint8Array): Promise<CryptoKey> {
+async function importECKey(publicKey: COSEPublicKey): Promise<CryptoKey> {
+ const crv = publicKey.get(COSEKEYS.crv);
+ const x = publicKey.get(COSEKEYS.x);
+ const y = publicKey.get(COSEKEYS.y);
+
+ if (!crv) {
+ throw new Error('EC2 public key was missing crv');
+ }
+
+ if (!x) {
+ throw new Error('EC2 public key was missing x');
+ }
+
+ if (!y) {
+ throw new Error('EC2 public key was missing y');
+ }
+
+ /**
+ * Convert a COSE crv ID into a corresponding string value that WebCrypto APIs expect
+ */
+ let _crv: SubtleCryptoCrv;
+ if (crv === COSECRV.P256) {
+ _crv = 'P-256';
+ } else if (crv === COSECRV.P384) {
+ _crv = 'P-384';
+ } else if (crv === COSECRV.P521) {
+ _crv = 'P-521';
+ } else {
+ throw new Error(`Unexpected COSE crv value of ${crv}`);
+ }
+
const jwk: JsonWebKey = {
kty: "EC",
- crv,
+ crv: _crv,
x: isoBase64URL.fromBuffer(x as Uint8Array),
y: isoBase64URL.fromBuffer(y as Uint8Array),
ext: false,
@@ -127,7 +158,7 @@ function importECKey(crv: SubtleCryptoCrv, x: Uint8Array, y: Uint8Array): Promis
const algorithm: EcKeyImportParams = {
name: 'ECDSA',
- namedCurve: crv,
+ namedCurve: _crv,
};
const extractable = false;
@@ -142,17 +173,94 @@ function importECKey(crv: SubtleCryptoCrv, x: Uint8Array, y: Uint8Array): Promis
}
/**
- *
+ * Verify a signature using an EC2 public key
*/
-function verifyECSignature(
+async function verifyECSignature(
key: CryptoKey,
signature: Uint8Array,
data: Uint8Array,
- alg: SubtleCryptoAlg = 'SHA-256',
+ alg: COSEALG,
): Promise<boolean> {
+ const subtleAlg = mapCoseAlgToWebCryptoAlg(alg);
+
const algorithm: EcdsaParams = {
name: 'ECDSA',
- hash: { name: alg },
+ hash: { name: subtleAlg },
+ };
+ if (globalThis.crypto) {
+ return globalThis.crypto.subtle.verify(algorithm, key, signature, data);
+ } else {
+ return webcrypto.subtle.verify(algorithm, key, signature, data);
+ }
+}
+
+/**
+ * Import an RSA public key from its COSE representation
+ */
+async function importRSAKey(publicKey: COSEPublicKey, hashAlgorithm?: string): Promise<CryptoKey> {
+ const alg = publicKey.get(COSEKEYS.alg);
+ const n = publicKey.get(COSEKEYS.n);
+ const e = publicKey.get(COSEKEYS.e);
+
+ if (!alg) {
+ throw new Error('RSA public key was missing alg');
+ }
+
+ if (!n) {
+ throw new Error('RSA public key was missing n');
+ }
+
+ if (!e) {
+ throw new Error('RSA public key was missing e');
+ }
+
+ const jwk: JsonWebKey = {
+ kty: 'RSA',
+ alg: '',
+ n: isoBase64URL.fromBuffer(n as Uint8Array),
+ e: isoBase64URL.fromBuffer(e as Uint8Array),
+ ext: false,
+ };
+
+ const keyAlgorithm = {
+ name: 'RSASSA-PKCS1-v1_5',
+ // This is actually the digest hash that'll get used by `.verify()`
+ hash: { name: mapCoseAlgToWebCryptoAlg(alg as number) },
+ };
+
+ if (hashAlgorithm) {
+ const normalized = normalizeSHAAlgorithm(hashAlgorithm);
+ keyAlgorithm.hash.name = normalized;
+ }
+
+ if (keyAlgorithm.hash.name === 'SHA-256') {
+ jwk.alg = 'RS256';
+ } else if (keyAlgorithm.hash.name === 'SHA-384') {
+ jwk.alg = 'RS384';
+ } else if (keyAlgorithm.hash.name === 'SHA-512') {
+ jwk.alg = 'RS512';
+ } else if (keyAlgorithm.hash.name === 'SHA-1') {
+ jwk.alg = 'RS1';
+ }
+
+ const extractable = false;
+
+ const keyUsages: KeyUsage[] = ["verify"];
+
+ if (globalThis.crypto) {
+ return globalThis.crypto.subtle.importKey('jwk', jwk, keyAlgorithm, extractable, keyUsages);
+ } else {
+ return webcrypto.subtle.importKey('jwk', jwk, keyAlgorithm, extractable, keyUsages);
+ }
+}
+
+async function verifyRSASignature(
+ key: CryptoKey,
+ signature: Uint8Array,
+ data: Uint8Array,
+): Promise<boolean> {
+ const algorithm = {
+ name: 'RSASSA-PKCS1-v1_5',
};
if (globalThis.crypto) {
return globalThis.crypto.subtle.verify(algorithm, key, signature, data);
@@ -165,12 +273,12 @@ function verifyECSignature(
* Convert algorithms like "SHA1", "sha256", etc... into values like "SHA-1", "SHA-256", etc...
* that `.digest()` will accept
*/
-function normalizeAlgorithm(algorithm: string): SubtleCryptoAlg {
+function normalizeSHAAlgorithm(algorithm: string): SubtleCryptoAlg {
if (/sha\d{1,3}/i.test(algorithm)) {
- algorithm = algorithm.toUpperCase().replace('SHA', 'SHA-');
+ algorithm = algorithm.replace(/sha/i, 'SHA-');
}
- return algorithm as SubtleCryptoAlg;
+ return algorithm.toUpperCase() as SubtleCryptoAlg;
}
/**
@@ -184,30 +292,12 @@ function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
return (bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0);
}
-/**
- * Convert a COSE crv ID into a corresponding string value that WebCrypto APIs expect
- */
-function mapCoseCrvToWebCryptoCrv(crv: number): SubtleCryptoCrv {
- if (crv === COSECRV.P256) {
- return 'P-256';
- }
-
- if (crv === COSECRV.P384) {
- return 'P-384';
- }
-
- if (crv === COSECRV.P521) {
- return 'P-521';
- }
-
- throw new Error(`Unexpected COSE crv value of ${crv}`);
-}
type SubtleCryptoCrv = "P-256" | "P-384" | "P-521";
/**
* Convert a COSE alg ID into a corresponding string value that WebCrypto APIs expect
*/
-function mapCoseAlgToWebCryptoAlg(alg: number): SubtleCryptoAlg {
+function mapCoseAlgToWebCryptoAlg(alg: COSEALG): SubtleCryptoAlg {
if ([-65535].indexOf(alg) >= 0) {
return 'SHA-1';
} else if ([-7, -37, -257].indexOf(alg) >= 0) {
diff --git a/packages/server/src/helpers/verifySignature.ts b/packages/server/src/helpers/verifySignature.ts
index a2a54dc..f4ca876 100644
--- a/packages/server/src/helpers/verifySignature.ts
+++ b/packages/server/src/helpers/verifySignature.ts
@@ -1,9 +1,13 @@
-import crypto from 'crypto';
+/* eslint-disable @typescript-eslint/ban-ts-comment */
import { verify as ed25519Verify } from '@noble/ed25519';
+import { AsnParser } from '@peculiar/asn1-schema';
+import { Certificate } from '@peculiar/asn1-x509';
+import { ECParameters, id_ecPublicKey, id_secp256r1 } from '@peculiar/asn1-ecc';
+import { RSAPublicKey } from '@peculiar/asn1-rsa';
-import { COSEKEYS, COSEKTY, COSEPublicKey } from './convertCOSEtoPKCS';
-import { convertCertBufferToPEM } from './convertCertBufferToPEM';
-import { isoCBOR, isoCrypto } from './iso';
+import { COSECRV, COSEKEYS, COSEKTY, COSEPublicKey } from './convertCOSEtoPKCS';
+import { isoCrypto } from './iso';
+import { decodeCredentialPublicKey } from './decodeCredentialPublicKey';
type VerifySignatureOptsLeafCert = {
signature: Uint8Array;
@@ -25,12 +29,12 @@ type VerifySignatureOptsCredentialPublicKey = {
* @param signature attStmt.sig
* @param signatureBase Bytes that were signed over
* @param publicKey Authenticator's public key as a PEM certificate
- * @param algo Which algorithm to use to verify the signature (default: `'sha256'`)
+ * @param rsaHashAlgorithm Which algorithm to use to verify RSA signatures
*/
export async function verifySignature(
opts: VerifySignatureOptsLeafCert | VerifySignatureOptsCredentialPublicKey,
): Promise<boolean> {
- const { signature, signatureBase, hashAlgorithm = 'sha256' } = opts;
+ const { signature, signatureBase, rsaHashAlgorithm } = opts;
const _isLeafcertOpts = isLeafCertOpts(opts);
const _isCredPubKeyOpts = isCredPubKeyOpts(opts);
@@ -42,29 +46,28 @@ export async function verifySignature(
throw new Error('Must not declare both "leafCert" and "credentialPublicKey"');
}
- let publicKeyPEM = '';
+ let subtlePublicKey: CryptoKey;
+ let kty: COSEKTY;
+ let alg: number;
if (_isCredPubKeyOpts) {
const { publicKey } = opts;
- // Decode CBOR to COSE
- let cosePublicKey;
- try {
- cosePublicKey = isoCBOR.decodeFirst<COSEPublicKey>(publicKey);
- } catch (err) {
- const _err = err as Error;
- throw new Error(`Error decoding public key while converting to PEM: ${_err.message}`);
- }
+ const cosePublicKey = decodeCredentialPublicKey(publicKey);
- const kty = cosePublicKey.get(COSEKEYS.kty);
+ const _kty = cosePublicKey.get(COSEKEYS.kty);
+ const _alg = cosePublicKey.get(COSEKEYS.alg);
- if (!kty) {
+ if (!_kty) {
throw new Error('Public key was missing kty');
}
- // Check key type
- if (kty === COSEKTY.OKP) {
- // Verify Ed25519 slightly differently
+ if (!_alg) {
+ throw new Error('Public key was missing alg');
+ }
+
+ // Verify Ed25519 slightly differently
+ if (_kty === COSEKTY.OKP) {
const x = cosePublicKey.get(COSEKEYS.x);
if (!x) {
@@ -72,17 +75,129 @@ export async function verifySignature(
}
return ed25519Verify(signature, signatureBase, (x as Uint8Array));
+ }
+
+ // Assume we're handling COSEKTY.EC2 or COSEKTY.RSA key from here on
+ subtlePublicKey = await isoCrypto.importKey(cosePublicKey);
+ kty = _kty as COSEKTY;
+ alg = _alg as number;
+ } else if (_isLeafcertOpts) {
+ /**
+ * Time to extract the public key from an X.509 leaf certificate
+ */
+ const { leafCert } = opts;
+
+ const x509 = AsnParser.parse(leafCert, Certificate);
+
+ const { tbsCertificate } = x509;
+ const {
+ subjectPublicKeyInfo,
+ signature: _tbsSignature,
+ } = tbsCertificate;
+
+ // console.log(tbsCertificate);
+
+ const signatureAlgorithm = _tbsSignature.algorithm;
+ const publicKeyAlgorithmID = subjectPublicKeyInfo.algorithm.algorithm;
+
+ if (publicKeyAlgorithmID === id_ecPublicKey) {
+ /**
+ * EC2 Public Key
+ */
+ kty = COSEKTY.EC2;
+
+ if (!subjectPublicKeyInfo.algorithm.parameters) {
+ throw new Error('Leaf cert public key missing parameters (EC2)');
+ }
+
+ const ecParameters = AsnParser.parse(new Uint8Array(subjectPublicKeyInfo.algorithm.parameters), ECParameters);
+
+ let crv = -999;
+ if (ecParameters.namedCurve === id_secp256r1) {
+ crv = COSECRV.P256;
+ } else {
+ throw new Error(
+ `Leaf cert public key contained unexpected namedCurve ${ecParameters.namedCurve} (EC2)`,
+ );
+ }
+
+ const subjectPublicKey = new Uint8Array(subjectPublicKeyInfo.subjectPublicKey)
+
+ let x: Uint8Array;
+ let y: Uint8Array;
+ if (subjectPublicKey[0] === 0x04) {
+ // Public key is in "uncompressed form", so we can split the remaining bytes in half
+ let pointer = 1;
+ const halfLength = (subjectPublicKey.length - 1) / 2;
+ x = subjectPublicKey.slice(pointer, pointer += halfLength);
+ y = subjectPublicKey.slice(pointer);
+ } else {
+ throw new Error('TODO: Figure out how to handle public keys in "compressed form"');
+ }
+
+ const coseEC2PubKey: COSEPublicKey = new Map();
+ coseEC2PubKey.set(COSEKEYS.kty, COSEKTY.EC2);
+ coseEC2PubKey.set(COSEKEYS.crv, crv);
+ coseEC2PubKey.set(COSEKEYS.x, x);
+ coseEC2PubKey.set(COSEKEYS.y, y);
+
+ subtlePublicKey = await isoCrypto.importKey(coseEC2PubKey);
+ alg = -7;
+ } else if (publicKeyAlgorithmID === '1.2.840.113549.1.1.1') {
+ /**
+ * RSA public key
+ */
+ kty = COSEKTY.RSA;
+ const rsaPublicKey = AsnParser.parse(subjectPublicKeyInfo.subjectPublicKey, RSAPublicKey);
+
+ let _alg = -999;
+ if (signatureAlgorithm === '1.2.840.113549.1.1.11') {
+ _alg = -257; // RS256
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.12') {
+ _alg = -258; // RS384
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.13') {
+ _alg = -259; // RS512
+ } else {
+ throw new Error(
+ `Leaf cert contained unexpected signature algorithm ${signatureAlgorithm} (RSA)`,
+ );
+ }
+
+ const coseRSAPubKey: COSEPublicKey = new Map();
+ coseRSAPubKey.set(COSEKEYS.kty, COSEKTY.RSA);
+ coseRSAPubKey.set(COSEKEYS.alg, _alg);
+ coseRSAPubKey.set(COSEKEYS.n, new Uint8Array(rsaPublicKey.modulus));
+ coseRSAPubKey.set(COSEKEYS.e, new Uint8Array(rsaPublicKey.publicExponent));
+
+ subtlePublicKey = await isoCrypto.importKey(coseRSAPubKey, rsaHashAlgorithm);
+ alg = _alg;
} else {
- return isoCrypto.verify(cosePublicKey, signature, signatureBase);
+ throw new Error(`Unexpected leaf cert public key algorithm ${publicKeyAlgorithmID}`);
}
+ } else {
+ throw new Error(
+ 'How did we get here? We were supposed to make sure we were only dealing with one of two possible sets of method arguments!!',
+ );
}
- if (_isLeafcertOpts) {
- const { leafCert } = opts;
- publicKeyPEM = convertCertBufferToPEM(leafCert);
+ if (
+ // @ts-ignore 2454
+ typeof subtlePublicKey === 'undefined'
+ // @ts-ignore 2454
+ || typeof kty === 'undefined'
+ // @ts-ignore 2454
+ || typeof alg === 'undefined'
+ ) {
+ throw new Error('You must import a public key, and determine kty and alg before proceeding');
}
- return crypto.createVerify(hashAlgorithm).update(signatureBase).verify(publicKeyPEM, signature);
+ return isoCrypto.verify({
+ publicKey: subtlePublicKey,
+ coseKty: kty,
+ coseAlg: alg,
+ signature,
+ data: signatureBase,
+ });
}
function isLeafCertOpts(