diff options
Diffstat (limited to 'packages/server/src')
-rw-r--r-- | packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts | 62 | ||||
-rw-r--r-- | packages/server/src/helpers/iso/isoCrypto/verify.ts | 7 |
2 files changed, 52 insertions, 17 deletions
diff --git a/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts b/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts index 3f34c9a..94bb202 100644 --- a/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts +++ b/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts @@ -1,4 +1,5 @@ import { AsnParser, ECDSASigValue } from '../../../deps.ts'; +import { COSECRV } from '../../cose.ts'; import { isoUint8Array } from '../index.ts'; /** @@ -6,18 +7,12 @@ import { isoUint8Array } from '../index.ts'; * * See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types */ -export function unwrapEC2Signature(signature: Uint8Array): Uint8Array { +export function unwrapEC2Signature(signature: Uint8Array, crv: COSECRV): Uint8Array { const parsedSignature = AsnParser.parse(signature, ECDSASigValue); - let rBytes = new Uint8Array(parsedSignature.r); - let sBytes = new Uint8Array(parsedSignature.s); + const n = getSignatureComponentLength(crv); - if (shouldRemoveLeadingZero(rBytes)) { - rBytes = rBytes.slice(1); - } - - if (shouldRemoveLeadingZero(sBytes)) { - sBytes = sBytes.slice(1); - } + const rBytes = toNormalizedBytes(parsedSignature.r, n); + const sBytes = toNormalizedBytes(parsedSignature.s, n); const finalSignature = isoUint8Array.concat([rBytes, sBytes]); @@ -25,12 +20,47 @@ export function unwrapEC2Signature(signature: Uint8Array): Uint8Array { } /** - * Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence - * should be removed based on the following logic: + * ECDSA signatures with in the subtle crypto API expect signatures with `r` and `s` values + * encoded to a specific length depending on the order of the curve. * - * "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" + * See <https://www.w3.org/TR/WebCryptoAPI/#ecdsa-operations> */ -function shouldRemoveLeadingZero(bytes: Uint8Array): boolean { - return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0; +function getSignatureComponentLength(crv: COSECRV): number { + switch (crv) { + case COSECRV.P256: + return 32; + case COSECRV.P384: + return 48; + case COSECRV.P521: + return 66; + default: + throw new Error(`Unexpected COSE crv value of ${crv} (EC2)`); + } +} + +/** + * Converts the ASN.1 integer representation to bytes of a specific length `n`. + * + * DER encodes integers as big-endian byte arrays, with as small as possible representation and + * require leading `0` bytes to disambiguate between negative and positive numbers. This means + * that `r` and `s` can potentially not be the expected length `n` that is needed by the WebCrypto + * subtle API: if there it leading `0`s it can be shorter than expected, and if it has a leading + * `1` bit, it can be one byte longer. + * + * See <https://www.itu.int/rec/T-REC-X.690-202102-I/en> + * See <https://www.w3.org/TR/WebCryptoAPI/#ecdsa-operations> + */ +function toNormalizedBytes(i: ArrayBuffer, n: number): Uint8Array { + const iBytes = new Uint8Array(i); + + const normalizedBytes = new Uint8Array(n); + if (iBytes.length <= n) { + normalizedBytes.set(iBytes, n - iBytes.length); + } else if (iBytes.length === n + 1 && iBytes[0] === 0) { + normalizedBytes.set(iBytes.slice(1)); + } else { + throw new Error("invalid signature component length"); + } + + return normalizedBytes; } diff --git a/packages/server/src/helpers/iso/isoCrypto/verify.ts b/packages/server/src/helpers/iso/isoCrypto/verify.ts index 36d3756..79a07f9 100644 --- a/packages/server/src/helpers/iso/isoCrypto/verify.ts +++ b/packages/server/src/helpers/iso/isoCrypto/verify.ts @@ -2,6 +2,7 @@ import { COSEALG, COSEKEYS, COSEPublicKey, + isCOSECrv, isCOSEPublicKeyEC2, isCOSEPublicKeyOKP, isCOSEPublicKeyRSA, @@ -23,7 +24,11 @@ export function verify(opts: { const { cosePublicKey, signature, data, shaHashOverride } = opts; if (isCOSEPublicKeyEC2(cosePublicKey)) { - const unwrappedSignature = unwrapEC2Signature(signature); + const crv = cosePublicKey.get(COSEKEYS.crv); + if (!isCOSECrv(crv)) { + throw new Error("unknown COSE curve"); + } + const unwrappedSignature = unwrapEC2Signature(signature, crv); return verifyEC2({ cosePublicKey, signature: unwrappedSignature, |