@simplewebauthn/server 10.0.0 → 10.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
+ import { COSECRV } from '../../cose.js';
1
2
  /**
2
3
  * In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
3
4
  *
4
5
  * See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
5
6
  */
6
- export declare function unwrapEC2Signature(signature: Uint8Array): Uint8Array;
7
+ export declare function unwrapEC2Signature(signature: Uint8Array, crv: COSECRV): Uint8Array;
@@ -1,30 +1,72 @@
1
1
  import { AsnParser, ECDSASigValue } from '../../../deps.js';
2
+ import { COSECRV } from '../../cose.js';
2
3
  import { isoUint8Array } from '../index.js';
3
4
  /**
4
5
  * In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
5
6
  *
6
7
  * See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
7
8
  */
8
- export function unwrapEC2Signature(signature) {
9
+ export function unwrapEC2Signature(signature, crv) {
9
10
  const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
10
- let rBytes = new Uint8Array(parsedSignature.r);
11
- let sBytes = new Uint8Array(parsedSignature.s);
12
- if (shouldRemoveLeadingZero(rBytes)) {
13
- rBytes = rBytes.slice(1);
14
- }
15
- if (shouldRemoveLeadingZero(sBytes)) {
16
- sBytes = sBytes.slice(1);
17
- }
18
- const finalSignature = isoUint8Array.concat([rBytes, sBytes]);
11
+ const rBytes = new Uint8Array(parsedSignature.r);
12
+ const sBytes = new Uint8Array(parsedSignature.s);
13
+ const componentLength = getSignatureComponentLength(crv);
14
+ const rNormalizedBytes = toNormalizedBytes(rBytes, componentLength);
15
+ const sNormalizedBytes = toNormalizedBytes(sBytes, componentLength);
16
+ const finalSignature = isoUint8Array.concat([
17
+ rNormalizedBytes,
18
+ sNormalizedBytes,
19
+ ]);
19
20
  return finalSignature;
20
21
  }
21
22
  /**
22
- * Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence
23
- * should be removed based on the following logic:
23
+ * The SubtleCrypto Web Crypto API expects ECDSA signatures with `r` and `s` values to be encoded
24
+ * to a specific length depending on the order of the curve. This function returns the expected
25
+ * byte-length for each of the `r` and `s` signature components.
26
+ *
27
+ * See <https://www.w3.org/TR/WebCryptoAPI/#ecdsa-operations>
28
+ */
29
+ function getSignatureComponentLength(crv) {
30
+ switch (crv) {
31
+ case COSECRV.P256:
32
+ return 32;
33
+ case COSECRV.P384:
34
+ return 48;
35
+ case COSECRV.P521:
36
+ return 66;
37
+ default:
38
+ throw new Error(`Unexpected COSE crv value of ${crv} (EC2)`);
39
+ }
40
+ }
41
+ /**
42
+ * Converts the ASN.1 integer representation to bytes of a specific length `n`.
43
+ *
44
+ * DER encodes integers as big-endian byte arrays, with as small as possible representation and
45
+ * requires a leading `0` byte to disambiguate between negative and positive numbers. This means
46
+ * that `r` and `s` can potentially not be the expected byte-length that is needed by the
47
+ * SubtleCrypto Web Crypto API: if there are leading `0`s it can be shorter than expected, and if
48
+ * it has a leading `1` bit, it can be one byte longer.
24
49
  *
25
- * "If the leading byte is 0x0, and the the high order bit on the second byte is not set to 0,
26
- * then remove the leading 0x0 byte"
50
+ * See <https://www.itu.int/rec/T-REC-X.690-202102-I/en>
51
+ * See <https://www.w3.org/TR/WebCryptoAPI/#ecdsa-operations>
27
52
  */
28
- function shouldRemoveLeadingZero(bytes) {
29
- return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
53
+ function toNormalizedBytes(bytes, componentLength) {
54
+ let normalizedBytes;
55
+ if (bytes.length < componentLength) {
56
+ // In case the bytes are shorter than expected, we need to pad it with leading `0`s.
57
+ normalizedBytes = new Uint8Array(componentLength);
58
+ normalizedBytes.set(bytes, componentLength - bytes.length);
59
+ }
60
+ else if (bytes.length === componentLength) {
61
+ normalizedBytes = bytes;
62
+ }
63
+ else if (bytes.length === componentLength + 1 && bytes[0] === 0 && (bytes[1] & 0x80) === 0x80) {
64
+ // The bytes contain a leading `0` to encode that the integer is positive. This leading `0`
65
+ // needs to be removed for compatibility with the SubtleCrypto Web Crypto API.
66
+ normalizedBytes = bytes.subarray(1);
67
+ }
68
+ else {
69
+ throw new Error(`Invalid signature component length ${bytes.length}, expected ${componentLength}`);
70
+ }
71
+ return normalizedBytes;
30
72
  }
@@ -1,4 +1,4 @@
1
- import { COSEKEYS, isCOSEPublicKeyEC2, isCOSEPublicKeyOKP, isCOSEPublicKeyRSA, } from '../../cose.js';
1
+ import { COSEKEYS, isCOSECrv, isCOSEPublicKeyEC2, isCOSEPublicKeyOKP, isCOSEPublicKeyRSA, } from '../../cose.js';
2
2
  import { verifyEC2 } from './verifyEC2.js';
3
3
  import { verifyRSA } from './verifyRSA.js';
4
4
  import { verifyOKP } from './verifyOKP.js';
@@ -9,7 +9,11 @@ import { unwrapEC2Signature } from './unwrapEC2Signature.js';
9
9
  export function verify(opts) {
10
10
  const { cosePublicKey, signature, data, shaHashOverride } = opts;
11
11
  if (isCOSEPublicKeyEC2(cosePublicKey)) {
12
- const unwrappedSignature = unwrapEC2Signature(signature);
12
+ const crv = cosePublicKey.get(COSEKEYS.crv);
13
+ if (!isCOSECrv(crv)) {
14
+ throw new Error(`unknown COSE curve ${crv}`);
15
+ }
16
+ const unwrappedSignature = unwrapEC2Signature(signature, crv);
13
17
  return verifyEC2({
14
18
  cosePublicKey,
15
19
  signature: unwrappedSignature,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "module": "./esm/index.js",
3
3
  "main": "./script/index.js",
4
4
  "name": "@simplewebauthn/server",
5
- "version": "10.0.0",
5
+ "version": "10.0.1",
6
6
  "description": "SimpleWebAuthn for Servers",
7
7
  "license": "MIT",
8
8
  "author": "Matthew Miller <matthew@millerti.me>",
@@ -1,6 +1,7 @@
1
+ import { COSECRV } from '../../cose.js';
1
2
  /**
2
3
  * In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
3
4
  *
4
5
  * See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
5
6
  */
6
- export declare function unwrapEC2Signature(signature: Uint8Array): Uint8Array;
7
+ export declare function unwrapEC2Signature(signature: Uint8Array, crv: COSECRV): Uint8Array;
@@ -2,33 +2,75 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.unwrapEC2Signature = void 0;
4
4
  const deps_js_1 = require("../../../deps.js");
5
+ const cose_js_1 = require("../../cose.js");
5
6
  const index_js_1 = require("../index.js");
6
7
  /**
7
8
  * In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
8
9
  *
9
10
  * See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
10
11
  */
11
- function unwrapEC2Signature(signature) {
12
+ function unwrapEC2Signature(signature, crv) {
12
13
  const parsedSignature = deps_js_1.AsnParser.parse(signature, deps_js_1.ECDSASigValue);
13
- let rBytes = new Uint8Array(parsedSignature.r);
14
- let sBytes = new Uint8Array(parsedSignature.s);
15
- if (shouldRemoveLeadingZero(rBytes)) {
16
- rBytes = rBytes.slice(1);
17
- }
18
- if (shouldRemoveLeadingZero(sBytes)) {
19
- sBytes = sBytes.slice(1);
20
- }
21
- const finalSignature = index_js_1.isoUint8Array.concat([rBytes, sBytes]);
14
+ const rBytes = new Uint8Array(parsedSignature.r);
15
+ const sBytes = new Uint8Array(parsedSignature.s);
16
+ const componentLength = getSignatureComponentLength(crv);
17
+ const rNormalizedBytes = toNormalizedBytes(rBytes, componentLength);
18
+ const sNormalizedBytes = toNormalizedBytes(sBytes, componentLength);
19
+ const finalSignature = index_js_1.isoUint8Array.concat([
20
+ rNormalizedBytes,
21
+ sNormalizedBytes,
22
+ ]);
22
23
  return finalSignature;
23
24
  }
24
25
  exports.unwrapEC2Signature = unwrapEC2Signature;
25
26
  /**
26
- * Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence
27
- * should be removed based on the following logic:
27
+ * The SubtleCrypto Web Crypto API expects ECDSA signatures with `r` and `s` values to be encoded
28
+ * to a specific length depending on the order of the curve. This function returns the expected
29
+ * byte-length for each of the `r` and `s` signature components.
30
+ *
31
+ * See <https://www.w3.org/TR/WebCryptoAPI/#ecdsa-operations>
32
+ */
33
+ function getSignatureComponentLength(crv) {
34
+ switch (crv) {
35
+ case cose_js_1.COSECRV.P256:
36
+ return 32;
37
+ case cose_js_1.COSECRV.P384:
38
+ return 48;
39
+ case cose_js_1.COSECRV.P521:
40
+ return 66;
41
+ default:
42
+ throw new Error(`Unexpected COSE crv value of ${crv} (EC2)`);
43
+ }
44
+ }
45
+ /**
46
+ * Converts the ASN.1 integer representation to bytes of a specific length `n`.
47
+ *
48
+ * DER encodes integers as big-endian byte arrays, with as small as possible representation and
49
+ * requires a leading `0` byte to disambiguate between negative and positive numbers. This means
50
+ * that `r` and `s` can potentially not be the expected byte-length that is needed by the
51
+ * SubtleCrypto Web Crypto API: if there are leading `0`s it can be shorter than expected, and if
52
+ * it has a leading `1` bit, it can be one byte longer.
28
53
  *
29
- * "If the leading byte is 0x0, and the the high order bit on the second byte is not set to 0,
30
- * then remove the leading 0x0 byte"
54
+ * See <https://www.itu.int/rec/T-REC-X.690-202102-I/en>
55
+ * See <https://www.w3.org/TR/WebCryptoAPI/#ecdsa-operations>
31
56
  */
32
- function shouldRemoveLeadingZero(bytes) {
33
- return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
57
+ function toNormalizedBytes(bytes, componentLength) {
58
+ let normalizedBytes;
59
+ if (bytes.length < componentLength) {
60
+ // In case the bytes are shorter than expected, we need to pad it with leading `0`s.
61
+ normalizedBytes = new Uint8Array(componentLength);
62
+ normalizedBytes.set(bytes, componentLength - bytes.length);
63
+ }
64
+ else if (bytes.length === componentLength) {
65
+ normalizedBytes = bytes;
66
+ }
67
+ else if (bytes.length === componentLength + 1 && bytes[0] === 0 && (bytes[1] & 0x80) === 0x80) {
68
+ // The bytes contain a leading `0` to encode that the integer is positive. This leading `0`
69
+ // needs to be removed for compatibility with the SubtleCrypto Web Crypto API.
70
+ normalizedBytes = bytes.subarray(1);
71
+ }
72
+ else {
73
+ throw new Error(`Invalid signature component length ${bytes.length}, expected ${componentLength}`);
74
+ }
75
+ return normalizedBytes;
34
76
  }
@@ -12,7 +12,11 @@ const unwrapEC2Signature_js_1 = require("./unwrapEC2Signature.js");
12
12
  function verify(opts) {
13
13
  const { cosePublicKey, signature, data, shaHashOverride } = opts;
14
14
  if ((0, cose_js_1.isCOSEPublicKeyEC2)(cosePublicKey)) {
15
- const unwrappedSignature = (0, unwrapEC2Signature_js_1.unwrapEC2Signature)(signature);
15
+ const crv = cosePublicKey.get(cose_js_1.COSEKEYS.crv);
16
+ if (!(0, cose_js_1.isCOSECrv)(crv)) {
17
+ throw new Error(`unknown COSE curve ${crv}`);
18
+ }
19
+ const unwrappedSignature = (0, unwrapEC2Signature_js_1.unwrapEC2Signature)(signature, crv);
16
20
  return (0, verifyEC2_js_1.verifyEC2)({
17
21
  cosePublicKey,
18
22
  signature: unwrappedSignature,