summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts62
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/verify.ts7
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,