diff options
Diffstat (limited to 'packages/server/src')
-rw-r--r-- | packages/server/src/helpers/iso/isoCrypto/importKey.ts | 164 | ||||
-rw-r--r-- | packages/server/src/helpers/iso/isoCrypto/index.ts | 1 | ||||
-rw-r--r-- | packages/server/src/helpers/iso/isoCrypto/verify.ts | 105 | ||||
-rw-r--r-- | packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts | 115 | ||||
-rw-r--r-- | packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts | 82 |
5 files changed, 220 insertions, 247 deletions
diff --git a/packages/server/src/helpers/iso/isoCrypto/importKey.ts b/packages/server/src/helpers/iso/isoCrypto/importKey.ts index 9e6c82b..b9961d7 100644 --- a/packages/server/src/helpers/iso/isoCrypto/importKey.ts +++ b/packages/server/src/helpers/iso/isoCrypto/importKey.ts @@ -1,164 +1,14 @@ import { webcrypto } from 'node:crypto'; -import { SubtleCryptoCrv } from './structs'; -import { normalizeSHAAlgorithm } from './normalizeSHAAlgorithm'; -import { mapCoseAlgToWebCryptoAlg } from './mapCoseAlgToWebCryptoAlg'; -import { - COSECRV, - COSEKEYS, - COSEKTY, - COSEALG, - COSEPublicKeyEC2, - COSEPublicKeyRSA, - isCOSEAlg, - isCOSEPublicKeyEC2, - isCOSEPublicKeyRSA, -} from '../../convertCOSEtoPKCS'; -import { isoBase64URL } from '../index'; - -/** - * 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: COSEPublicKeyEC2 | COSEPublicKeyRSA, rsaHashAlgorithm?: string): Promise<CryptoKey> { - const kty = publicKey.get(COSEKEYS.kty); - - if (!kty) { - throw new Error('Public key was missing kty'); - } - - if (isCOSEPublicKeyEC2(publicKey)) { - return importECKey(publicKey); - } - - if (isCOSEPublicKeyRSA(publicKey)) { - return importRSAKey(publicKey, rsaHashAlgorithm); - } - - throw new Error(`Unable to import public key of kty ${kty}`); -} - -/** - * Import an EC2 public key from its COSE representation - */ - async function importECKey(publicKey: COSEPublicKeyEC2): 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, - x: isoBase64URL.fromBuffer(x), - y: isoBase64URL.fromBuffer(y), - ext: false, - }; - - const algorithm: EcKeyImportParams = { - name: 'ECDSA', - namedCurve: _crv, - }; - - const extractable = false; - - const keyUsages: KeyUsage[] = ["verify"]; - - if (globalThis.crypto) { - return globalThis.crypto.subtle.importKey('jwk', jwk, algorithm, extractable, keyUsages); - } else { - return webcrypto.subtle.importKey('jwk', jwk, algorithm, extractable, keyUsages); - } -} - -/** - * Import an RSA public key from its COSE representation - */ - async function importRSAKey(publicKey: COSEPublicKeyRSA, 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('Public key was missing alg (RSA)'); - } - - if (!isCOSEAlg(alg)) { - throw new Error(`Public key had invalid alg ${alg} (RSA)`); - } - - if (!n) { - throw new Error('Public key was missing n (RSA)'); - } - - if (!e) { - throw new Error('Public key was missing e (RSA)'); - } - - const jwk: JsonWebKey = { - kty: 'RSA', - alg: '', - n: isoBase64URL.fromBuffer(n), - e: isoBase64URL.fromBuffer(e), - 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) }, - }; - - 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"]; +export async function importKey(opts: { + keyData: JsonWebKey, + algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams, +}): Promise<CryptoKey> { + const { keyData, algorithm } = opts; if (globalThis.crypto) { - return globalThis.crypto.subtle.importKey('jwk', jwk, keyAlgorithm, extractable, keyUsages); + return globalThis.crypto.subtle.importKey('jwk', keyData, algorithm, false, ['verify']); } else { - return webcrypto.subtle.importKey('jwk', jwk, keyAlgorithm, extractable, keyUsages); + return webcrypto.subtle.importKey('jwk', keyData, algorithm, false, ['verify']); } } diff --git a/packages/server/src/helpers/iso/isoCrypto/index.ts b/packages/server/src/helpers/iso/isoCrypto/index.ts index 0dc333f..7850722 100644 --- a/packages/server/src/helpers/iso/isoCrypto/index.ts +++ b/packages/server/src/helpers/iso/isoCrypto/index.ts @@ -1,4 +1,3 @@ export { digest } from './digest'; export { getRandomValues } from './getRandomValues'; -export { importKey } from './importKey'; export { verify } from './verify'; diff --git a/packages/server/src/helpers/iso/isoCrypto/verify.ts b/packages/server/src/helpers/iso/isoCrypto/verify.ts index abcb769..6bf3d42 100644 --- a/packages/server/src/helpers/iso/isoCrypto/verify.ts +++ b/packages/server/src/helpers/iso/isoCrypto/verify.ts @@ -1,101 +1,28 @@ -import { webcrypto } from 'node:crypto'; - -import { ECDSASigValue } from "@peculiar/asn1-ecc"; -import { AsnParser } from '@peculiar/asn1-schema'; - -import { COSEALG, COSEKTY } from '../../convertCOSEtoPKCS'; -import { isoUint8Array } from '../index'; -import { mapCoseAlgToWebCryptoAlg } from './mapCoseAlgToWebCryptoAlg'; +import { COSEALG, COSEKEYS, COSEPublicKey, isCOSEPublicKeyEC2, isCOSEPublicKeyOKP, isCOSEPublicKeyRSA } from '../../cose'; +import { verifyEC2 } from './verifyEC2'; +import { verifyRSA } from './verifyRSA'; /** * Verify signatures with their public key. Supports EC2 and RSA public keys. */ -export async function verify({ - publicKey, - coseKty, - coseAlg, - signature, - data, -}: { - publicKey: CryptoKey, - coseKty: COSEKTY, - coseAlg: COSEALG, +export async function verify(opts: { + cosePublicKey: COSEPublicKey, signature: Uint8Array, data: Uint8Array, + shaHashOverride?: COSEALG, }): 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); - let sBytes = new Uint8Array(parsedSignature.s); - - if (shouldRemoveLeadingZero(rBytes)) { - rBytes = rBytes.slice(1); - } - - if (shouldRemoveLeadingZero(sBytes)) { - sBytes = sBytes.slice(1); - } - - const signatureBytes = isoUint8Array.concat([rBytes, sBytes]); - - return verifyECSignature(publicKey, signatureBytes, data, coseAlg); - } else if (coseKty === COSEKTY.RSA) { - return verifyRSASignature(publicKey, signature, data); + const { cosePublicKey, signature, data, shaHashOverride } = opts; + + if (isCOSEPublicKeyEC2(cosePublicKey)) { + return verifyEC2({ cosePublicKey, signature, data, shaHashOverride }); + } else if (isCOSEPublicKeyRSA(cosePublicKey)) { + return verifyRSA({ cosePublicKey, signature, data, shaHashOverride }); + // } else if (isCOSEPublicKeyOKP(cosePublicKey)) { + // return; } + const kty = cosePublicKey.get(COSEKEYS.kty); throw new Error( - `Signature verification with public key of kty ${coseKty} is not supported by this method`, + `Signature verification with public key of kty ${kty} is not supported by this method`, ); } - -/** - * Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence - * should be removed based on the following logic: - * - * "If the leading byte is 0x0, and the the high order bit on the second byte is not set to 0, - * then remove the leading 0x0 byte" - */ - function shouldRemoveLeadingZero(bytes: Uint8Array): boolean { - return (bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0); -} - -/** - * Verify a signature using an EC2 public key - */ -async function verifyECSignature( - key: CryptoKey, - signature: Uint8Array, - data: Uint8Array, - alg: COSEALG, -): Promise<boolean> { - const subtleAlg = mapCoseAlgToWebCryptoAlg(alg); - - const algorithm: EcdsaParams = { - name: 'ECDSA', - hash: { name: subtleAlg }, - }; - if (globalThis.crypto) { - return globalThis.crypto.subtle.verify(algorithm, key, signature, data); - } else { - return webcrypto.subtle.verify(algorithm, key, signature, data); - } -} - -/** - * - */ -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); - } else { - return webcrypto.subtle.verify(algorithm, key, signature, data); - } -} diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts b/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts new file mode 100644 index 0000000..9e91d3e --- /dev/null +++ b/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts @@ -0,0 +1,115 @@ +import { webcrypto } from 'node:crypto'; +import { ECDSASigValue } from "@peculiar/asn1-ecc"; +import { AsnParser } from '@peculiar/asn1-schema'; + +import { COSEALG, COSECRV, COSEKEYS, COSEPublicKeyEC2 } from "../../cose"; +import { mapCoseAlgToWebCryptoAlg } from "./mapCoseAlgToWebCryptoAlg"; +import { importKey } from './importKey'; +import { isoBase64URL, isoUint8Array } from '../index'; +import { SubtleCryptoCrv } from "./structs"; + +/** + * Verify a signature using an EC2 public key + */ +export async function verifyEC2(opts: { + cosePublicKey: COSEPublicKeyEC2, + signature: Uint8Array, + data: Uint8Array, + shaHashOverride?: COSEALG, +}): Promise<boolean> { + const { cosePublicKey, signature, data, shaHashOverride } = opts; + + // 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); + let sBytes = new Uint8Array(parsedSignature.s); + + if (shouldRemoveLeadingZero(rBytes)) { + rBytes = rBytes.slice(1); + } + + if (shouldRemoveLeadingZero(sBytes)) { + sBytes = sBytes.slice(1); + } + + const finalSignature = isoUint8Array.concat([rBytes, sBytes]); + + // Import the public key + const alg = cosePublicKey.get(COSEKEYS.alg); + const crv = cosePublicKey.get(COSEKEYS.crv); + const x = cosePublicKey.get(COSEKEYS.x); + const y = cosePublicKey.get(COSEKEYS.y); + + if (!alg) { + throw new Error('Public key was missing alg (EC2)'); + } + + if (!crv) { + throw new Error('Public key was missing crv (EC2)'); + } + + if (!x) { + throw new Error('Public key was missing x (EC2)'); + } + + if (!y) { + throw new Error('Public key was missing y (EC2)'); + } + + 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 keyData: JsonWebKey = { + kty: "EC", + crv: _crv, + x: isoBase64URL.fromBuffer(x), + y: isoBase64URL.fromBuffer(y), + ext: false, + }; + + const keyAlgorithm: EcKeyImportParams = { + name: 'ECDSA', + namedCurve: _crv, + }; + + const key = await importKey({ + keyData, + algorithm: keyAlgorithm, + }); + + // Determine which SHA algorithm to use for signature verification + let subtleAlg = mapCoseAlgToWebCryptoAlg(alg); + if (shaHashOverride) { + subtleAlg = mapCoseAlgToWebCryptoAlg(shaHashOverride); + } + + const verifyAlgorithm: EcdsaParams = { + name: 'ECDSA', + hash: { name: subtleAlg }, + }; + + if (globalThis.crypto) { + return globalThis.crypto.subtle.verify(verifyAlgorithm, key, finalSignature, data); + } else { + return webcrypto.subtle.verify(verifyAlgorithm, key, finalSignature, data); + } +} + +/** + * Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence + * should be removed based on the following logic: + * + * "If the leading byte is 0x0, and the the high order bit on the second byte is not set to 0, + * then remove the leading 0x0 byte" + */ +function shouldRemoveLeadingZero(bytes: Uint8Array): boolean { + return (bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0); +} diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts b/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts new file mode 100644 index 0000000..b479515 --- /dev/null +++ b/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts @@ -0,0 +1,82 @@ +import { webcrypto } from 'node:crypto'; + +import { COSEALG, COSEKEYS, COSEPublicKeyRSA, isCOSEAlg } from "../../cose"; +import { mapCoseAlgToWebCryptoAlg } from "./mapCoseAlgToWebCryptoAlg"; +import { importKey } from './importKey'; +import { isoBase64URL } from '../index'; + +/** + * + */ +export async function verifyRSA(opts: { + cosePublicKey: COSEPublicKeyRSA, + signature: Uint8Array, + data: Uint8Array, + shaHashOverride?: COSEALG, +}): Promise<boolean> { + const { cosePublicKey, signature, data, shaHashOverride } = opts; + + const alg = cosePublicKey.get(COSEKEYS.alg); + const n = cosePublicKey.get(COSEKEYS.n); + const e = cosePublicKey.get(COSEKEYS.e); + + if (!alg) { + throw new Error('Public key was missing alg (RSA)'); + } + + if (!isCOSEAlg(alg)) { + throw new Error(`Public key had invalid alg ${alg} (RSA)`); + } + + if (!n) { + throw new Error('Public key was missing n (RSA)'); + } + + if (!e) { + throw new Error('Public key was missing e (RSA)'); + } + + const keyData: JsonWebKey = { + kty: 'RSA', + alg: '', + n: isoBase64URL.fromBuffer(n), + e: isoBase64URL.fromBuffer(e), + ext: false, + }; + + const keyAlgorithm = { + // TODO: Determine this from `alg` so we might support the rarer RSA-PSS + name: 'RSASSA-PKCS1-v1_5', + // This is actually the digest hash that'll get used by `.verify()` + hash: { name: mapCoseAlgToWebCryptoAlg(alg) }, + }; + + if (shaHashOverride) { + keyAlgorithm.hash.name = mapCoseAlgToWebCryptoAlg(shaHashOverride); + } + + if (keyAlgorithm.hash.name === 'SHA-256') { + keyData.alg = 'RS256'; + } else if (keyAlgorithm.hash.name === 'SHA-384') { + keyData.alg = 'RS384'; + } else if (keyAlgorithm.hash.name === 'SHA-512') { + keyData.alg = 'RS512'; + } else if (keyAlgorithm.hash.name === 'SHA-1') { + keyData.alg = 'RS1'; + } + + const key = await importKey({ + keyData, + algorithm: keyAlgorithm, + }); + + const verifyAlgorithm = { + // TODO: Determine this from `alg` so we might support the rarer RSA-PSS + name: 'RSASSA-PKCS1-v1_5', + }; + if (globalThis.crypto) { + return globalThis.crypto.subtle.verify(verifyAlgorithm, key, signature, data); + } else { + return webcrypto.subtle.verify(verifyAlgorithm, key, signature, data); + } +} |