diff --git a/scripts/build/deps/boringssl.ts b/scripts/build/deps/boringssl.ts index 9703b4cea646..49045eab03d1 100644 --- a/scripts/build/deps/boringssl.ts +++ b/scripts/build/deps/boringssl.ts @@ -24,7 +24,7 @@ import { quote } from "../shell.ts"; import type { Dependency, DirectBuild } from "../source.ts"; import { LIBC_ALLOCATION_SYMBOLS, depSourceDir } from "../source.ts"; -const BORINGSSL_COMMIT = "2288897e2e716330490893d226b4f079f9da9e0c"; +const BORINGSSL_COMMIT = "37fae71009c3a577de38c6796cdee7bdbf6304e1"; export const boringssl: Dependency = { name: "boringssl", diff --git a/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.cpp b/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.cpp index 8cd58670276a..23f8cb051c7a 100644 --- a/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.cpp +++ b/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.cpp @@ -30,13 +30,6 @@ namespace WebCore { -size_t bytesUsedToEncodedLength(uint8_t octet) -{ - if (octet < MaxLengthInOneByte) - return 1; - return octet - MaxLengthInOneByte + 1; -} - static size_t extraBytesNeededForEncodedLength(size_t length) { if (!length) diff --git a/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.h b/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.h index b1961fc3fa4c..dc5ce6d92e70 100644 --- a/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.h +++ b/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.h @@ -43,7 +43,6 @@ inline constexpr unsigned char SequenceMark = 0x30; inline constexpr unsigned char InitialOctet = 0x00; inline constexpr size_t MaxLengthInOneByte = 128; -size_t bytesUsedToEncodedLength(uint8_t); void addEncodedASN1Length(Vector&, size_t); } // namespace WebCore diff --git a/src/jsc/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp b/src/jsc/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp index 237e06ebd21d..aefda22ef300 100644 --- a/src/jsc/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp +++ b/src/jsc/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp @@ -34,27 +34,22 @@ #include #include "CommonCryptoDERUtilities.h" #include "OpenSSLCryptoUniquePtr.h" +#include #include #include namespace WebCore { -// The OID scans below are hand-rolled byte compares and cannot tell a well-formed -// key of another type from malformed bytes, so ask the real parser before a caller -// reports "Invalid key type" rather than "Invalid keyData". -static bool parsesAsSubjectPublicKeyInfo(const Vector& keyData) +static int namedCurveToNID(CryptoKeyOKP::NamedCurve namedCurve) { - const uint8_t* ptr = keyData.begin(); - return !!EvpPKeyPtr(d2i_PUBKEY(nullptr, &ptr, keyData.size())); -} - -static bool parsesAsPrivateKeyInfo(const Vector& keyData) -{ - const uint8_t* ptr = keyData.begin(); - auto p8inf = PKCS8PrivKeyInfoPtr(d2i_PKCS8_PRIV_KEY_INFO(nullptr, &ptr, keyData.size())); - if (!p8inf) - return false; - return !!EvpPKeyPtr(EVP_PKCS82PKEY(p8inf.get())); + switch (namedCurve) { + case CryptoKeyOKP::NamedCurve::X25519: + return EVP_PKEY_X25519; + case CryptoKeyOKP::NamedCurve::Ed25519: + return EVP_PKEY_ED25519; + } + ASSERT_NOT_REACHED(); + return EVP_PKEY_NONE; } bool CryptoKeyOKP::isPlatformSupportedCurve(NamedCurve namedCurve) @@ -94,70 +89,26 @@ std::optional CryptoKeyOKP::platformGeneratePair(CryptoAlgorithmI // For all of the OIDs, the parameters MUST be absent. RefPtr CryptoKeyOKP::importSpki(CryptoAlgorithmIdentifier identifier, NamedCurve namedCurve, Vector&& keyData, bool extractable, CryptoKeyUsageBitmap usages, bool* keyTypeMismatch) { - // FIXME: We should use the underlying crypto library to import PKCS8 OKP keys. - - // Read SEQUENCE - size_t index = 1; - if (keyData.size() < index + 1) + CBS cbs; + CBS_init(&cbs, keyData.begin(), keyData.size()); + EvpPKeyPtr pkey(EVP_parse_public_key(&cbs)); + if (!pkey || CBS_len(&cbs) != 0) return nullptr; - - // Read length and SEQUENCE - // FIXME: Check length is 5 + 1 + 1 + 1 + keyByteSize. - index += bytesUsedToEncodedLength(keyData[index]) + 1; - if (keyData.size() < index + 1) - return nullptr; - - // Read length - // FIXME: Check length is 5. - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 5) - return nullptr; - - // Read OID - // FIXME: spec says this is 1 3 101 11X but WPT tests expect 6 3 43 101 11X. - auto reportKeyTypeMismatch = [&] { - if (keyTypeMismatch && parsesAsSubjectPublicKeyInfo(keyData)) + if (EVP_PKEY_id(pkey.get()) != namedCurveToNID(namedCurve)) { + if (keyTypeMismatch) *keyTypeMismatch = true; - }; - if (keyData[index++] != 6 || keyData[index++] != 3 || keyData[index++] != 43 || keyData[index++] != 101) { - reportKeyTypeMismatch(); return nullptr; } - switch (namedCurve) { - case NamedCurve::X25519: - if (keyData[index++] != 110) { - reportKeyTypeMismatch(); - return nullptr; - } - break; - case NamedCurve::Ed25519: - if (keyData[index++] != 112) { - reportKeyTypeMismatch(); - return nullptr; - } - break; - }; - - // Read BIT STRING - if (keyData.size() < index + 2) - return nullptr; - if (keyData[index++] != 3) - return nullptr; - - // Read length - // FIXME: Check length is keyByteSize + 1. - index += bytesUsedToEncodedLength(keyData[index]); - - if (keyData.size() < index + 1) + size_t rawLen = 0; + if (EVP_PKEY_get_raw_public_key(pkey.get(), nullptr, &rawLen) != 1) return nullptr; - - // Initial octet - if (!!keyData[index]) + Vector raw(rawLen); + if (EVP_PKEY_get_raw_public_key(pkey.get(), raw.begin(), &rawLen) != 1) return nullptr; - ++index; + raw.shrink(rawLen); - return create(identifier, namedCurve, CryptoKeyType::Public, std::span { keyData.begin() + index, keyData.size() - index }, extractable, usages); + return create(identifier, namedCurve, CryptoKeyType::Public, WTF::move(raw), extractable, usages); } constexpr uint8_t OKPOIDFirstByte = 6; @@ -212,8 +163,9 @@ ExceptionOr> CryptoKeyOKP::exportSpki() const return WTF::move(result); } -// Per https://www.ietf.org/rfc/rfc5280.txt -// PrivateKeyInfo ::= SEQUENCE { version INTEGER, privateKeyAlgorithm AlgorithmIdentifier, privateKey OCTET STRING } +// Per https://www.rfc-editor.org/rfc/rfc5958 (OneAsymmetricKey, superseding RFC 5208 PrivateKeyInfo) +// OneAsymmetricKey ::= SEQUENCE { version INTEGER, privateKeyAlgorithm AlgorithmIdentifier, privateKey OCTET STRING, +// attributes [0] IMPLICIT Attributes OPTIONAL, publicKey [1] IMPLICIT BIT STRING OPTIONAL } // AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY DEFINED BY algorithm OPTIONAL } // Per https://www.rfc-editor.org/rfc/rfc8410 // id-X25519 OBJECT IDENTIFIER ::= { 1 3 101 110 } @@ -223,78 +175,29 @@ ExceptionOr> CryptoKeyOKP::exportSpki() const // For all of the OIDs, the parameters MUST be absent. RefPtr CryptoKeyOKP::importPkcs8(CryptoAlgorithmIdentifier identifier, NamedCurve namedCurve, Vector&& keyData, bool extractable, CryptoKeyUsageBitmap usages, bool* keyTypeMismatch) { - // FIXME: We should use the underlying crypto library to import PKCS8 OKP keys. - - // Read SEQUENCE - size_t index = 1; - if (keyData.size() < index + 1) - return nullptr; - - // Read length - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 1) - return nullptr; - - // Read version - index += 3; - if (keyData.size() < index + 1) - return nullptr; - - // Read SEQUENCE - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 1) + // RFC 5958 v2 OneAsymmetricKey support comes from oven-sh/boringssl#10 + const uint8_t* ptr = keyData.begin(); + auto p8 = PKCS8PrivKeyInfoPtr(d2i_PKCS8_PRIV_KEY_INFO(nullptr, &ptr, keyData.size())); + if (!p8 || ptr != keyData.end()) return nullptr; - - // Read length - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 5) + EvpPKeyPtr pkey(EVP_PKCS82PKEY(p8.get())); + if (!pkey) return nullptr; - - // Read OID - auto reportKeyTypeMismatch = [&] { - if (keyTypeMismatch && parsesAsPrivateKeyInfo(keyData)) + if (EVP_PKEY_id(pkey.get()) != namedCurveToNID(namedCurve)) { + if (keyTypeMismatch) *keyTypeMismatch = true; - }; - if (keyData[index++] != OKPOIDFirstByte || keyData[index++] != OKPOIDSecondByte || keyData[index++] != OKPOIDThirdByte || keyData[index++] != OKPOIDFourthByte) { - reportKeyTypeMismatch(); return nullptr; } - switch (namedCurve) { - case NamedCurve::X25519: - if (keyData[index++] != OKPOIDX25519Byte) { - reportKeyTypeMismatch(); - return nullptr; - } - break; - case NamedCurve::Ed25519: - if (keyData[index++] != OKPOIDEd25519Byte) { - reportKeyTypeMismatch(); - return nullptr; - } - break; - }; - - // Read OCTET STRING - if (keyData.size() < index + 2) - return nullptr; - - if (keyData[index++] != 4) - return nullptr; - - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 2) + size_t rawLen = 0; + if (EVP_PKEY_get_raw_private_key(pkey.get(), nullptr, &rawLen) != 1) return nullptr; - - // Read OCTET STRING - if (keyData[index++] != 4) - return nullptr; - - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 1) + Vector raw(rawLen); + if (EVP_PKEY_get_raw_private_key(pkey.get(), raw.begin(), &rawLen) != 1) return nullptr; + raw.shrink(rawLen); - return create(identifier, namedCurve, CryptoKeyType::Private, std::span { keyData.begin() + index, keyData.size() - index }, extractable, usages); + return create(identifier, namedCurve, CryptoKeyType::Private, WTF::move(raw), extractable, usages); } ExceptionOr> CryptoKeyOKP::exportPkcs8() const diff --git a/test/js/node/crypto/crypto.key-objects.test.ts b/test/js/node/crypto/crypto.key-objects.test.ts index 58017268821c..72821c721f06 100644 --- a/test/js/node/crypto/crypto.key-objects.test.ts +++ b/test/js/node/crypto/crypto.key-objects.test.ts @@ -1801,6 +1801,60 @@ function randomProp() { return "prop" + crypto.randomUUID().replace(/-/g, ""); } +// https://github.com/oven-sh/bun/issues/35432 — BoringSSL rejected v2 +// OneAsymmetricKey (publicKey [1] / version 1) on both its template and CBS +// parsers; oven-sh/boringssl#10 makes both tolerate it, matching OpenSSL. +describe("createPrivateKey with RFC 5958 v2 OneAsymmetricKey", () => { + // RFC 8032 section 7.1 test vector 1 + const seed = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"; + const pub = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"; + const pubField = "812100" + pub; + const spki = Buffer.from("302a300506032b6570032100" + pub, "hex"); + const der = (version: number, tail: string) => { + const body = "0201" + version.toString(16).padStart(2, "0") + "300506032b657004220420" + seed + tail; + return Buffer.from("30" + (body.length / 2).toString(16).padStart(2, "0") + body, "hex"); + }; + + const accepts: [string, Buffer][] = [ + ["v2 with attributes and publicKey", der(1, "a000" + pubField)], + ["v2 with publicKey only", der(1, pubField)], + ["v2 with attributes only", der(1, "a000")], + ["v2 with neither optional field", der(1, "")], + ["v1", der(0, "")], + // the publicKey contents are stored, not interpreted, so any width parses + ["v2 with an empty publicKey BIT STRING", der(1, "810100")], + ["v2 with a 10-byte publicKey", der(1, "810b00" + Buffer.alloc(10).toString("hex"))], + ]; + it.each(accepts)("accepts %s", (_name, key) => { + const privateKey = createPrivateKey({ key, format: "der", type: "pkcs8" }); + expect(privateKey.asymmetricKeyType).toBe("ed25519"); + const sig = sign(null, Buffer.from("OneAsymmetricKey"), privateKey); + expect( + verify(null, Buffer.from("OneAsymmetricKey"), createPublicKey({ key: spki, format: "der", type: "spki" }), sig), + ).toBe(true); + }); + + const rejects: [string, Buffer, string][] = [ + ["v1 with publicKey", der(0, pubField), "ERR_OSSL_DECODE_ERROR"], + ["version > v2", der(2, pubField), "ERR_OSSL_DECODE_ERROR"], + ["empty publicKey BIT STRING", der(1, "8100"), "ERR_OSSL_ASN1_STRING_TOO_SHORT"], + ["publicKey BIT STRING with bad padding octet", der(1, "810108"), "ERR_OSSL_ASN1_INVALID_BIT_STRING_BITS_LEFT"], + ["duplicate publicKey", der(1, "a000" + "810100" + "810100"), "ERR_OSSL_ASN1_SEQUENCE_LENGTH_MISMATCH"], + ["publicKey before attributes", der(1, "810100" + "a000"), "ERR_OSSL_ASN1_SEQUENCE_LENGTH_MISMATCH"], + ["unknown trailing [2]", der(1, "a000" + "810100" + "820100"), "ERR_OSSL_ASN1_SEQUENCE_LENGTH_MISMATCH"], + ["malformed attribute body", der(1, "a001ff"), "ERR_OSSL_ASN1_DECODE_ERROR"], + ]; + it.each(rejects)("rejects %s", (_name, key, code) => { + let error: any; + try { + createPrivateKey({ key, format: "der", type: "pkcs8" }); + } catch (e) { + error = e; + } + expect(error?.code).toBe(code); + }); +}); + test("generateKeyPair passes the thrown Error to the callback when key export fails", async () => { const { promise, resolve } = Promise.withResolvers(); // P-224 keygen succeeds, JWK export does not, driving the caught-exception diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 3a2acd1a9759..ab2cbe53269d 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -574,7 +574,7 @@ it("process.versions", () => { // These are the ACTUAL commits built into bun (not derived values, so // bumping a dep requires updating this test too). const expectedVersions = { - boringssl: "2288897e2e716330490893d226b4f079f9da9e0c", + boringssl: "37fae71009c3a577de38c6796cdee7bdbf6304e1", libarchive: "ded82291ab41d5e355831b96b0e1ff49e24d8939", mimalloc: "6e891cbe4790982ca9f3f9a60319a72e61b5d725", picohttpparser: "066d2b1e9ab820703db0837a7255d92d30f0c9f5", diff --git a/test/js/web/crypto/web-crypto.test.ts b/test/js/web/crypto/web-crypto.test.ts index c1fab949a503..b912b23718e3 100644 --- a/test/js/web/crypto/web-crypto.test.ts +++ b/test/js/web/crypto/web-crypto.test.ts @@ -1131,6 +1131,209 @@ describe("OKP spki/pkcs8 cross-curve import", () => { ecdsaPkcs8: "DataError: Invalid key type", }); }); + + it("RSA/EC keys imported as Ed25519 report 'Invalid key type'", async () => { + const rsa = await crypto.subtle.generateKey( + { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, + true, + ["sign", "verify"], + ); + const ec = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); + const importAsEd25519 = async (format: "pkcs8" | "spki", key: CryptoKey) => + rejection( + crypto.subtle.importKey(format, await crypto.subtle.exportKey(format, key), "Ed25519", true, [ + format === "pkcs8" ? "sign" : "verify", + ]), + ); + expect({ + rsaPkcs8: await importAsEd25519("pkcs8", rsa.privateKey), + rsaSpki: await importAsEd25519("spki", rsa.publicKey), + ecPkcs8: await importAsEd25519("pkcs8", ec.privateKey), + ecSpki: await importAsEd25519("spki", ec.publicKey), + }).toEqual({ + rsaPkcs8: "DataError: Invalid key type", + rsaSpki: "DataError: Invalid key type", + ecPkcs8: "DataError: Invalid key type", + ecSpki: "DataError: Invalid key type", + }); + }); + + it("rejects an Ed25519 spki with trailing bytes", async () => { + const ed = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); + const spki = new Uint8Array(await crypto.subtle.exportKey("spki", ed.publicKey)); + const withTrailing = new Uint8Array([...spki, 0xff]); + expect(await rejection(crypto.subtle.importKey("spki", withTrailing, "Ed25519", true, ["verify"]))).toBe( + "DataError: Invalid keyData", + ); + }); +}); + +// https://github.com/oven-sh/bun/issues/35432 — the OKP pkcs8 importer used a +// hand-rolled DER walker that mishandled RFC 5958 v2 OneAsymmetricKey; it now +// goes through BoringSSL's PKCS8_PRIV_KEY_INFO template (oven-sh/boringssl#10). +describe("OKP pkcs8 import of RFC 5958 v2 OneAsymmetricKey", () => { + const fromHex = (hex: string) => Uint8Array.from(Buffer.from(hex, "hex")); + const der = (...parts: string[]) => { + const body = parts.join(""); + const length = body.length / 2; + if (length > 0xff) throw new Error("der helper only encodes lengths up to 0xff"); + const lengthHex = length < 0x80 ? length.toString(16).padStart(2, "0") : "81" + length.toString(16); + return fromHex("30" + lengthHex + body); + }; + + // RFC 8032 section 7.1 test vector 1 + const edSeed = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"; + const edPub = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"; + const edAlgorithm = "300506032b6570"; + // RFC 7748 section 6.1 + const xAlicePriv = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"; + const xAlicePub = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"; + const xBobPub = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"; + const xShared = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"; + const xAlgorithm = "300506032b656e"; + + const version2 = "020101"; + const emptyAttributes = "a000"; + const wrapSeed = (seed: string) => "04220420" + seed; + const publicKeyField = (pub: string) => "812100" + pub; + + const signsAndVerifies = async (pkcs8: Uint8Array) => { + const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, "Ed25519", false, ["sign"]); + const publicKey = await crypto.subtle.importKey("raw", fromHex(edPub), "Ed25519", false, ["verify"]); + const message = new TextEncoder().encode("RFC 5958 OneAsymmetricKey"); + const signature = await crypto.subtle.sign("Ed25519", privateKey, message); + return crypto.subtle.verify("Ed25519", publicKey, signature, message); + }; + + it("Ed25519 with attributes [0] and publicKey [1]", async () => { + const pkcs8 = der(version2, edAlgorithm, wrapSeed(edSeed), emptyAttributes, publicKeyField(edPub)); + expect(await signsAndVerifies(pkcs8)).toBe(true); + }); + + it("Ed25519 with only publicKey [1]", async () => { + expect(await signsAndVerifies(der(version2, edAlgorithm, wrapSeed(edSeed), publicKeyField(edPub)))).toBe(true); + }); + + it("Ed25519 v1 with only attributes [0]", async () => { + expect(await signsAndVerifies(der("020100", edAlgorithm, wrapSeed(edSeed), emptyAttributes))).toBe(true); + }); + + it("Ed25519 v2 with only attributes [0]", async () => { + // non-conformant (v2 without publicKey) but tolerated, matching Node + expect(await signsAndVerifies(der(version2, edAlgorithm, wrapSeed(edSeed), emptyAttributes))).toBe(true); + }); + + it("Ed25519 with a localKeyId attribute", async () => { + // Attribute ::= SEQUENCE { localKeyId OID, SET { OCTET STRING } } + const attributes = "a014" + "3012" + "06092a864886f70d010915" + "3105" + "0403aabbcc"; + expect( + await signsAndVerifies(der(version2, edAlgorithm, wrapSeed(edSeed), attributes, publicKeyField(edPub))), + ).toBe(true); + }); + + it("X25519 with attributes [0] and publicKey [1]", async () => { + const pkcs8 = der(version2, xAlgorithm, wrapSeed(xAlicePriv), emptyAttributes, publicKeyField(xAlicePub)); + const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, "X25519", false, ["deriveBits"]); + const bobKey = await crypto.subtle.importKey("raw", fromHex(xBobPub), "X25519", false, []); + const shared = await crypto.subtle.deriveBits({ name: "X25519", public: bobKey }, privateKey, 256); + expect(Buffer.from(shared).toString("hex")).toBe(xShared); + }); + + it("rejects a CurvePrivateKey that does not fill the privateKey OCTET STRING", async () => { + // inner OCTET STRING claims 32 bytes but the outer one only carries 16 + const truncated = der(version2, edAlgorithm, "04120420" + edSeed.slice(0, 32)); + await expect(crypto.subtle.importKey("pkcs8", truncated, "Ed25519", false, ["sign"])).rejects.toThrow( + "Invalid keyData", + ); + }); + + const expectRejected = async (pkcs8: Uint8Array) => { + await expect(crypto.subtle.importKey("pkcs8", pkcs8, "Ed25519", false, ["sign"])).rejects.toThrow( + "Invalid keyData", + ); + }; + const version1 = "020100"; + + it("rejects bytes outside the declared outer SEQUENCE", async () => { + // the trailing ff is past the 0x2e-byte SEQUENCE + await expectRejected(fromHex("302e" + version1 + edAlgorithm + wrapSeed(edSeed) + "ff")); + }); + + it("rejects a truncated optional field", async () => { + // attributes [0] header claims one content byte that is not present + await expectRejected(der(version1, edAlgorithm, wrapSeed(edSeed), "a001")); + }); + + it("rejects an unknown trailing field", async () => { + await expectRejected(der(version2, edAlgorithm, wrapSeed(edSeed), "8200")); + }); + + it("rejects publicKey [1] before attributes [0]", async () => { + await expectRejected(der(version2, edAlgorithm, wrapSeed(edSeed), publicKeyField(edPub), emptyAttributes)); + }); + + it("rejects duplicate attributes [0]", async () => { + await expectRejected(der(version2, edAlgorithm, wrapSeed(edSeed), emptyAttributes, emptyAttributes)); + }); + + it("rejects duplicate publicKey [1]", async () => { + await expectRejected(der(version2, edAlgorithm, wrapSeed(edSeed), publicKeyField(edPub), publicKeyField(edPub))); + }); + + it("rejects attributes [0] that are not a SET OF Attribute", async () => { + await expectRejected(der(version2, edAlgorithm, wrapSeed(edSeed), "a001ff", publicKeyField(edPub))); + }); + + it("rejects an Attribute missing its type and values", async () => { + await expectRejected(der(version2, edAlgorithm, wrapSeed(edSeed), "a0023000", publicKeyField(edPub))); + }); + + it("rejects a publicKey [1] BIT STRING without the unused-bits octet", async () => { + await expectRejected(der(version2, edAlgorithm, wrapSeed(edSeed), "8100")); + }); + + it("rejects a v1 key carrying publicKey [1]", async () => { + await expectRejected(der(version1, edAlgorithm, wrapSeed(edSeed), publicKeyField(edPub))); + }); + + it("rejects versions above v2", async () => { + await expectRejected(der("020102", edAlgorithm, wrapSeed(edSeed), publicKeyField(edPub))); + }); + + it("rejects AlgorithmIdentifier parameters", async () => { + // RFC 8410: parameters MUST be absent; this one carries a NULL + await expectRejected(der(version1, "300706032b65700500", wrapSeed(edSeed))); + }); + + it("accepts long-form lengths on the SEQUENCE and attributes [0]", async () => { + // one localKeyId attribute with a 120-byte value pushes both lengths into two-byte form + const bigAttributes = + "a0818a" + "308187" + "06092a864886f70d010915" + "317a" + "0478" + Buffer.alloc(120).toString("hex"); + const pkcs8 = der(version2, edAlgorithm, wrapSeed(edSeed), bigAttributes, publicKeyField(edPub)); + expect(pkcs8[1]).toBe(0x81); // outer SEQUENCE length is long-form + expect(await signsAndVerifies(pkcs8)).toBe(true); + }); + + // Strict DER, like Deno and bun's own node:crypto (and unlike Node's + // BER-tolerant OpenSSL): non-minimal lengths and nonzero padding bits + // reject, while the publicKey [1] contents stay uninterpreted. + it("rejects a non-minimal outer SEQUENCE length", async () => { + await expectRejected(fromHex("30812e" + version1 + edAlgorithm + wrapSeed(edSeed))); + }); + + it("rejects a non-minimal privateKey OCTET STRING length", async () => { + await expectRejected(fromHex("302f" + version1 + edAlgorithm + "0481220420" + edSeed)); + }); + + it("rejects a publicKey [1] with nonzero padding bits", async () => { + await expectRejected(der(version2, edAlgorithm, wrapSeed(edSeed), "81020101")); + }); + + it("accepts a publicKey [1] whose contents are not a 32-byte key", async () => { + // the field is stored, not interpreted, matching Node and Deno + const pkcs8 = der(version2, edAlgorithm, wrapSeed(edSeed), "810b00" + Buffer.alloc(10).toString("hex")); + expect(await signsAndVerifies(pkcs8)).toBe(true); + }); }); // importKey's empty-usages guard got Node's message; the same predicate in