diff options
author | Matthew Miller <matthew@millerti.me> | 2022-11-16 21:06:36 -0800 |
---|---|---|
committer | Matthew Miller <matthew@millerti.me> | 2022-11-16 21:06:36 -0800 |
commit | e6972710a09edb96a10cbfa69e9d52d82914e2ec (patch) | |
tree | b5690e3df6571f5141e2f2fdff87aad2f1aa9b1e | |
parent | 684182bfc3ebda7fd335cf4ce5bcfe928e8e6e25 (diff) |
Support leaf cert sig verification
-rw-r--r-- | packages/server/src/helpers/convertCOSEtoPKCS.ts | 2 | ||||
-rw-r--r-- | packages/server/src/helpers/iso/isoCrypto.ts | 232 | ||||
-rw-r--r-- | packages/server/src/helpers/verifySignature.ts | 165 |
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( |