Refactor ECDSA and ASN.1 helpers to use derBytesToPem; remove redundant functions
This commit is contained in:
parent
4bb2ba5c36
commit
203101c715
@ -130,6 +130,20 @@ export function derToPem(hex, label) {
|
|||||||
bytes[i] = Number.isNaN(v) ? 0 : v;
|
bytes[i] = Number.isNaN(v) ? 0 : v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return derBytesToPem(bytes, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap raw DER bytes in a PEM envelope with LF line endings.
|
||||||
|
*
|
||||||
|
* Canonical helper shared by the key/ECDSA conversion modules; the hex-input
|
||||||
|
* {@link derToPem} above delegates here after parsing.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {string} label
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function derBytesToPem(bytes, label) {
|
||||||
let b64;
|
let b64;
|
||||||
if (typeof Buffer !== "undefined") {
|
if (typeof Buffer !== "undefined") {
|
||||||
b64 = Buffer.from(bytes).toString("base64");
|
b64 = Buffer.from(bytes).toString("base64");
|
||||||
|
|||||||
@ -13,7 +13,9 @@ import { p256, p384, p521 } from "@noble/curves/nist.js";
|
|||||||
import { DER } from "@noble/curves/abstract/weierstrass.js";
|
import { DER } from "@noble/curves/abstract/weierstrass.js";
|
||||||
import { md5, sha1 } from "@noble/hashes/legacy.js";
|
import { md5, sha1 } from "@noble/hashes/legacy.js";
|
||||||
import { sha256, sha384, sha512 } from "@noble/hashes/sha2.js";
|
import { sha256, sha384, sha512 } from "@noble/hashes/sha2.js";
|
||||||
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
||||||
import { AsnParser, AsnSerializer, OctetString } from "@peculiar/asn1-schema";
|
import { AsnParser, AsnSerializer, OctetString } from "@peculiar/asn1-schema";
|
||||||
|
import { derBytesToPem } from "./Asn1.mjs";
|
||||||
import * as ecc from "@peculiar/asn1-ecc";
|
import * as ecc from "@peculiar/asn1-ecc";
|
||||||
const { ECPrivateKey, ECParameters } = ecc;
|
const { ECPrivateKey, ECParameters } = ecc;
|
||||||
const ID_EC_PUBLIC_KEY = ecc.id_ecPublicKey;
|
const ID_EC_PUBLIC_KEY = ecc.id_ecPublicKey;
|
||||||
@ -303,7 +305,7 @@ export function publicKeyToSpkiPem(pair) {
|
|||||||
}),
|
}),
|
||||||
subjectPublicKey: pair.publicKey.slice().buffer,
|
subjectPublicKey: pair.publicKey.slice().buffer,
|
||||||
});
|
});
|
||||||
return derToPem(new Uint8Array(AsnSerializer.serialize(spki)), "PUBLIC KEY");
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(spki)), "PUBLIC KEY");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -328,7 +330,7 @@ export function privateKeyToPkcs8Pem(pair) {
|
|||||||
}),
|
}),
|
||||||
privateKey: new OctetString(AsnSerializer.serialize(ecKey)),
|
privateKey: new OctetString(AsnSerializer.serialize(ecKey)),
|
||||||
});
|
});
|
||||||
return derToPem(new Uint8Array(AsnSerializer.serialize(pkcs8)), "PRIVATE KEY");
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(pkcs8)), "PRIVATE KEY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -549,47 +551,3 @@ function readLength(bytes, offset) {
|
|||||||
return { value, next: offset + 1 + n };
|
return { value, next: offset + 1 + n };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a hex string to a Uint8Array.
|
|
||||||
*
|
|
||||||
* @param {string} hex
|
|
||||||
* @returns {Uint8Array}
|
|
||||||
*/
|
|
||||||
function hexToBytes(hex) {
|
|
||||||
if (hex.length % 2 !== 0) throw new OperationError("Hex string has odd length");
|
|
||||||
const out = new Uint8Array(hex.length / 2);
|
|
||||||
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a Uint8Array to a lowercase hex string.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} bytes
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function bytesToHex(bytes) {
|
|
||||||
let out = "";
|
|
||||||
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap raw DER bytes in a PEM envelope with LF line endings.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} bytes
|
|
||||||
* @param {string} label
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function derToPem(bytes, label) {
|
|
||||||
let b64;
|
|
||||||
if (typeof Buffer !== "undefined") {
|
|
||||||
b64 = Buffer.from(bytes).toString("base64");
|
|
||||||
} else {
|
|
||||||
let bin = "";
|
|
||||||
for (const b of bytes) bin += String.fromCharCode(b);
|
|
||||||
b64 = btoa(bin);
|
|
||||||
}
|
|
||||||
const lines = b64.match(/.{1,64}/g) || [""];
|
|
||||||
return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import { PrivateKeyInfo } from "@peculiar/asn1-pkcs8";
|
|||||||
import { AlgorithmIdentifier, SubjectPublicKeyInfo } from "@peculiar/asn1-x509";
|
import { AlgorithmIdentifier, SubjectPublicKeyInfo } from "@peculiar/asn1-x509";
|
||||||
import { Sequence, Integer, fromBER } from "asn1js";
|
import { Sequence, Integer, fromBER } from "asn1js";
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import { toBase64, fromBase64 } from "./Base64.mjs";
|
||||||
|
import { derBytesToPem } from "./Asn1.mjs";
|
||||||
import {
|
import {
|
||||||
getCurveByName,
|
getCurveByName,
|
||||||
loadEcKey,
|
loadEcKey,
|
||||||
@ -30,8 +32,6 @@ const ID_DSA = "1.2.840.10040.4.1";
|
|||||||
// identifier in RSA SPKI/PKCS#8 envelopes.
|
// identifier in RSA SPKI/PKCS#8 envelopes.
|
||||||
const DER_NULL = new Uint8Array([0x05, 0x00]).buffer;
|
const DER_NULL = new Uint8Array([0x05, 0x00]).buffer;
|
||||||
|
|
||||||
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
||||||
|
|
||||||
|
|
||||||
// ----- public API -----------------------------------------------------------
|
// ----- public API -----------------------------------------------------------
|
||||||
|
|
||||||
@ -98,6 +98,14 @@ export function parseCertPublicKey(certPem) {
|
|||||||
* Convert normalised key info to a JWK object (built in the canonical
|
* Convert normalised key info to a JWK object (built in the canonical
|
||||||
* field order so `JSON.stringify` emits a deterministic string).
|
* field order so `JSON.stringify` emits a deterministic string).
|
||||||
*
|
*
|
||||||
|
* The JWK fields are marshalled by hand rather than via a library: the
|
||||||
|
* conversion spans both RSA (including the CRT private params p/q/dp/dq/qi)
|
||||||
|
* and EC keys, and no current dependency covers both. `@noble/curves` is
|
||||||
|
* EC-only, and Web Crypto's JWK export is async and unavailable for the
|
||||||
|
* MD5/SHA-1 code paths this module supports. Adding a JOSE library purely
|
||||||
|
* for this base64url field mapping would enlarge the dependency surface for
|
||||||
|
* no real benefit. The per-field encoding is delegated to {@link b64url}.
|
||||||
|
*
|
||||||
* @param {object} info
|
* @param {object} info
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
@ -377,7 +385,7 @@ function parsePkcs8(bytes) {
|
|||||||
// EC and everything else delegate to the existing EC loader by
|
// EC and everything else delegate to the existing EC loader by
|
||||||
// re-wrapping the bytes as a PKCS#8 PEM. loadEcKey will surface its
|
// re-wrapping the bytes as a PKCS#8 PEM. loadEcKey will surface its
|
||||||
// own "not an EC key" error for unsupported algorithms.
|
// own "not an EC key" error for unsupported algorithms.
|
||||||
return ecInfoFromLoad(loadEcKey(derToPem(bytes, "PRIVATE KEY")));
|
return ecInfoFromLoad(loadEcKey(derBytesToPem(bytes, "PRIVATE KEY")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -405,7 +413,7 @@ function parseSpki(bytes) {
|
|||||||
const y = parseIntegerBitString(abufToBytes(spki.subjectPublicKey));
|
const y = parseIntegerBitString(abufToBytes(spki.subjectPublicKey));
|
||||||
return { kty: "DSA", isPrivate: false, p, q, g, y };
|
return { kty: "DSA", isPrivate: false, p, q, g, y };
|
||||||
}
|
}
|
||||||
return ecInfoFromLoad(loadEcKey(derToPem(bytes, "PUBLIC KEY")));
|
return ecInfoFromLoad(loadEcKey(derBytesToPem(bytes, "PUBLIC KEY")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -504,7 +512,7 @@ function rsaPkcs8Pem(info) {
|
|||||||
}),
|
}),
|
||||||
privateKey: new OctetString(AsnSerializer.serialize(rsa)),
|
privateKey: new OctetString(AsnSerializer.serialize(rsa)),
|
||||||
});
|
});
|
||||||
return derToPem(new Uint8Array(AsnSerializer.serialize(pkcs8)), "PRIVATE KEY");
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(pkcs8)), "PRIVATE KEY");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -525,7 +533,7 @@ function rsaSpkiPem(info) {
|
|||||||
}),
|
}),
|
||||||
subjectPublicKey: AsnSerializer.serialize(rsa),
|
subjectPublicKey: AsnSerializer.serialize(rsa),
|
||||||
});
|
});
|
||||||
return derToPem(new Uint8Array(AsnSerializer.serialize(spki)), "PUBLIC KEY");
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(spki)), "PUBLIC KEY");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -544,7 +552,7 @@ function dsaSpkiPem(info) {
|
|||||||
}),
|
}),
|
||||||
subjectPublicKey: innerY,
|
subjectPublicKey: innerY,
|
||||||
});
|
});
|
||||||
return derToPem(new Uint8Array(AsnSerializer.serialize(spki)), "PUBLIC KEY");
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(spki)), "PUBLIC KEY");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -604,25 +612,6 @@ export function pemToDer(pem) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap raw DER bytes in a PEM envelope with LF line endings.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} bytes
|
|
||||||
* @param {string} label
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function derToPem(bytes, label) {
|
|
||||||
let b64;
|
|
||||||
if (typeof Buffer !== "undefined") {
|
|
||||||
b64 = Buffer.from(bytes).toString("base64");
|
|
||||||
} else {
|
|
||||||
let bin = "";
|
|
||||||
for (const b of bytes) bin += String.fromCharCode(b);
|
|
||||||
b64 = btoa(bin);
|
|
||||||
}
|
|
||||||
const lines = b64.match(/.{1,64}/g) || [""];
|
|
||||||
return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode a byte array as base64url (no padding).
|
* Encode a byte array as base64url (no padding).
|
||||||
@ -632,40 +621,18 @@ export function derToPem(bytes, label) {
|
|||||||
*/
|
*/
|
||||||
export function b64url(bytes) {
|
export function b64url(bytes) {
|
||||||
if (!(bytes instanceof Uint8Array)) bytes = new Uint8Array(bytes);
|
if (!(bytes instanceof Uint8Array)) bytes = new Uint8Array(bytes);
|
||||||
let out = "";
|
return toBase64(bytes, "A-Za-z0-9-_");
|
||||||
let i = 0;
|
|
||||||
while (i < bytes.length) {
|
|
||||||
const b1 = bytes[i++];
|
|
||||||
const b2 = i < bytes.length ? bytes[i++] : -1;
|
|
||||||
const b3 = i < bytes.length ? bytes[i++] : -1;
|
|
||||||
out += BASE64URL_ALPHABET[b1 >> 2];
|
|
||||||
out += BASE64URL_ALPHABET[((b1 & 0x03) << 4) | (b2 < 0 ? 0 : (b2 >> 4))];
|
|
||||||
if (b2 < 0) break;
|
|
||||||
out += BASE64URL_ALPHABET[((b2 & 0x0f) << 2) | (b3 < 0 ? 0 : (b3 >> 6))];
|
|
||||||
if (b3 < 0) break;
|
|
||||||
out += BASE64URL_ALPHABET[b3 & 0x3f];
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a base64url-encoded string to bytes. Strips standard base64
|
* Decode a base64url-encoded string to bytes. Tolerates standard base64
|
||||||
* padding if present.
|
* padding if present.
|
||||||
*
|
*
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
* @returns {Uint8Array}
|
* @returns {Uint8Array}
|
||||||
*/
|
*/
|
||||||
export function b64urlToBytes(str) {
|
export function b64urlToBytes(str) {
|
||||||
const cleaned = str.replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
|
return new Uint8Array(fromBase64(str, "A-Za-z0-9-_", "byteArray"));
|
||||||
const pad = cleaned.length % 4 === 0 ? "" : "=".repeat(4 - (cleaned.length % 4));
|
|
||||||
const padded = cleaned + pad;
|
|
||||||
if (typeof Buffer !== "undefined") {
|
|
||||||
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
||||||
}
|
|
||||||
const bin = atob(padded);
|
|
||||||
const out = new Uint8Array(bin.length);
|
|
||||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import * as asn1X509 from "@peculiar/asn1-x509";
|
|||||||
import * as ecc from "@peculiar/asn1-ecc";
|
import * as ecc from "@peculiar/asn1-ecc";
|
||||||
import * as rsaSchemas from "@peculiar/asn1-rsa";
|
import * as rsaSchemas from "@peculiar/asn1-rsa";
|
||||||
import { fromBER, Utf8String, BmpString, PrintableString, IA5String } from "asn1js";
|
import { fromBER, Utf8String, BmpString, PrintableString, IA5String } from "asn1js";
|
||||||
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import { fromBase64 } from "./Base64.mjs";
|
import { fromBase64 } from "./Base64.mjs";
|
||||||
import { fromHex } from "./Hex.mjs";
|
import { fromHex } from "./Hex.mjs";
|
||||||
@ -425,17 +426,9 @@ export function asnNameToJson(asnName) {
|
|||||||
|
|
||||||
// ----- byte helpers ---------------------------------------------------------
|
// ----- byte helpers ---------------------------------------------------------
|
||||||
|
|
||||||
/**
|
// bytesToHex is re-exported from @noble/hashes/utils (imported above) so
|
||||||
* Convert a Uint8Array to a lowercase hex string.
|
// operations can continue importing it from this module.
|
||||||
*
|
export { bytesToHex };
|
||||||
* @param {Uint8Array} bytes
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function bytesToHex(bytes) {
|
|
||||||
let out = "";
|
|
||||||
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip a single leading 0x00 byte from a buffer when it's only present to
|
* Strip a single leading 0x00 byte from a buffer when it's only present to
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
||||||
import { cryptNotice } from "../lib/Crypt.mjs";
|
import { cryptNotice } from "../lib/Crypt.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import {
|
import {
|
||||||
@ -102,16 +103,4 @@ function b64url(bytes) {
|
|||||||
return toBase64(bytes, "A-Za-z0-9-_");
|
return toBase64(bytes, "A-Za-z0-9-_");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a byte array to a lowercase hex string.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} bytes
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function bytesToHex(bytes) {
|
|
||||||
let out = "";
|
|
||||||
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GenerateECDSAKeyPair;
|
export default GenerateECDSAKeyPair;
|
||||||
|
|||||||
@ -286,6 +286,22 @@ TestRegister.addTests([
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Published known-answer test pinning correctness against the spec
|
||||||
|
// (not against the previous jsrsasign output). RFC 6979 Appendix
|
||||||
|
// A.2.5: curve P-256 (prime256v1), message "sample", SHA-256. The
|
||||||
|
// public key below is the SPKI form of the (Ux, Uy) point from the
|
||||||
|
// RFC; the r||s signature is the deterministic signature it lists.
|
||||||
|
name: "ECDSA Verify: RFC 6979 A.2.5 P-256/SHA-256 known-answer vector",
|
||||||
|
input: "efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8",
|
||||||
|
expectedOutput: "Verified OK",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
"op": "ECDSA Verify",
|
||||||
|
"args": ["Auto", "SHA-256", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYP7UuiVanTHJYet0xjVtaMBJuJI7\nYfps5mliLmDyn7Z5A/4QCLi8maQa6elWKLxk8vGyDC1+n1F3o8KU1EYimQ==\n-----END PUBLIC KEY-----", "sample", "Raw"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "ECDSA Verify: JSON signature missing r",
|
name: "ECDSA Verify: JSON signature missing r",
|
||||||
input: JSON.stringify({s: JSON.parse(P256.signature.sha256.json).s}),
|
input: JSON.stringify({s: JSON.parse(P256.signature.sha256.json).s}),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user