diff --git a/README.md b/README.md index 0ae026c8..2c60f3d2 100644 --- a/README.md +++ b/README.md @@ -57,19 +57,18 @@ library to convert back and forth between them. | Curve | Encryption | Signature | NodeCrypto | WebCrypto | Constant-Time | |:---------------:|:----------:|:---------:|:----------:|:---------:|:-----------------:| - | curve25519 | ECDH | N/A | No | No | Algorithmically** | - | ed25519 | N/A | EdDSA | No | No | Algorithmically** | - | nistP256 | ECDH | ECDSA | Yes* | Yes* | If native*** | - | nistP384 | ECDH | ECDSA | Yes* | Yes* | If native*** | - | nistP521 | ECDH | ECDSA | Yes* | Yes* | If native*** | - | brainpoolP256r1 | ECDH | ECDSA | Yes* | No | If native*** | - | brainpoolP384r1 | ECDH | ECDSA | Yes* | No | If native*** | - | brainpoolP512r1 | ECDH | ECDSA | Yes* | No | If native*** | - | secp256k1 | ECDH | ECDSA | Yes* | No | If native*** | + | curve25519 | ECDH | N/A | No | Yes* | If native** | + | ed25519 | N/A | EdDSA | No | Yes* | If native** | + | nistP256 | ECDH | ECDSA | Yes* | Yes* | If native** | + | nistP384 | ECDH | ECDSA | Yes* | Yes* | If native** | + | nistP521 | ECDH | ECDSA | Yes* | Yes* | If native** | + | brainpoolP256r1 | ECDH | ECDSA | Yes* | No | If native** | + | brainpoolP384r1 | ECDH | ECDSA | Yes* | No | If native** | + | brainpoolP512r1 | ECDH | ECDSA | Yes* | No | If native** | + | secp256k1 | ECDH | ECDSA | Yes* | No | If native** | - \* when available - \** the curve25519 and ed25519 implementations are algorithmically constant-time, but may not be constant-time after optimizations of the JavaScript compiler - \*** these curves are only constant-time if the underlying native implementation is available and constant-time + \* when available + \** these curves are only constant-time if the underlying native implementation is available and constant-time * If the user's browser supports [native WebCrypto](https://caniuse.com/#feat=cryptography) via the `window.crypto.subtle` API, this will be used. Under Node.js the native [crypto module](https://nodejs.org/api/crypto.html#crypto_crypto) is used. diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 9f5cd1f6..18bdc664 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -20,16 +20,15 @@ * @module crypto/public_key/elliptic/ecdh */ -import nacl from '@openpgp/tweetnacl'; import { CurveWithOID, jwkToRawPublic, rawPublicToJWK, privateToJWK, validateStandardParams, checkPublicPointEnconding } from './oid_curves'; import * as aesKW from '../../aes_kw'; -import { getRandomBytes } from '../../random'; import hash from '../../hash'; import enums from '../../../enums'; import util from '../../../util'; import { b64ToUint8Array } from '../../../encoding/base64'; import * as pkcs5 from '../../pkcs5'; import { getCipherParams } from '../../cipher'; +import { generateEphemeralEncryptionMaterial as ecdhXGenerateEphemeralEncryptionMaterial, recomputeSharedSecret as ecdhXRecomputeSharedSecret } from './ecdh_x'; const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); @@ -92,10 +91,8 @@ async function kdf(hashAlgo, X, length, param, stripLeading = false, stripTraili async function genPublicEphemeralKey(curve, Q) { switch (curve.type) { case 'curve25519Legacy': { - const d = getRandomBytes(32); - const { secretKey, sharedKey } = await genPrivateEphemeralKey(curve, Q, null, d); - let { publicKey } = nacl.box.keyPair.fromSecretKey(secretKey); - publicKey = util.concatUint8Array([new Uint8Array([curve.wireFormatLeadingByte]), publicKey]); + const { sharedSecret: sharedKey, ephemeralPublicKey } = await ecdhXGenerateEphemeralEncryptionMaterial(enums.publicKey.x25519, Q.subarray(1)); + const publicKey = util.concatUint8Array([new Uint8Array([curve.wireFormatLeadingByte]), ephemeralPublicKey]); return { publicKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below } case 'web': @@ -159,7 +156,7 @@ async function genPrivateEphemeralKey(curve, V, Q, d) { switch (curve.type) { case 'curve25519Legacy': { const secretKey = d.slice().reverse(); - const sharedKey = nacl.scalarMult(secretKey, V.subarray(1)); + const sharedKey = await ecdhXRecomputeSharedSecret(enums.publicKey.x25519, V.subarray(1), Q.subarray(1), secretKey); return { secretKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below } case 'web': diff --git a/src/crypto/public_key/elliptic/ecdh_x.js b/src/crypto/public_key/elliptic/ecdh_x.js index 641af020..1dfc96e8 100644 --- a/src/crypto/public_key/elliptic/ecdh_x.js +++ b/src/crypto/public_key/elliptic/ecdh_x.js @@ -11,6 +11,7 @@ import enums from '../../../enums'; import util from '../../../util'; import computeHKDF from '../../hkdf'; import { getCipherParams } from '../../cipher'; +import { b64ToUint8Array, uint8ArrayToB64 } from '../../../encoding/base64'; const HKDF_INFO = { x25519: util.encodeUTF8('OpenPGP X25519'), @@ -24,12 +25,28 @@ const HKDF_INFO = { */ export async function generate(algo) { switch (algo) { - case enums.publicKey.x25519: { - // k stays in little-endian, unlike legacy ECDH over curve25519 - const k = getRandomBytes(32); - const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k); - return { A, k }; - } + case enums.publicKey.x25519: + try { + const webCrypto = util.getWebCrypto(); + const webCryptoKey = await webCrypto.generateKey('X25519', true, ['deriveKey', 'deriveBits']); + + const privateKey = await webCrypto.exportKey('jwk', webCryptoKey.privateKey); + const publicKey = await webCrypto.exportKey('jwk', webCryptoKey.publicKey); + + return { + A: new Uint8Array(b64ToUint8Array(publicKey.x)), + k: b64ToUint8Array(privateKey.d, true) + }; + } catch (err) { + if (err.name !== 'NotSupportedError') { + throw err; + } + // k stays in little-endian, unlike legacy ECDH over curve25519 + const k = getRandomBytes(32); + const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k); + return { A, k }; + } + case enums.publicKey.x448: { const x448 = await util.getNobleCurve(enums.publicKey.x448); const k = x448.utils.randomPrivateKey(); @@ -87,16 +104,14 @@ export async function validateParams(algo, A, k) { * @async */ export async function encrypt(algo, data, recipientA) { + const { ephemeralPublicKey, sharedSecret } = await generateEphemeralEncryptionMaterial(algo, recipientA); + const hkdfInput = util.concatUint8Array([ + ephemeralPublicKey, + recipientA, + sharedSecret + ]); switch (algo) { case enums.publicKey.x25519: { - const ephemeralSecretKey = getRandomBytes(32); - const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA); - const { publicKey: ephemeralPublicKey } = x25519.box.keyPair.fromSecretKey(ephemeralSecretKey); - const hkdfInput = util.concatUint8Array([ - ephemeralPublicKey, - recipientA, - sharedSecret - ]); const cipherAlgo = enums.symmetric.aes128; const { keySize } = getCipherParams(cipherAlgo); const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize); @@ -104,15 +119,6 @@ export async function encrypt(algo, data, recipientA) { return { ephemeralPublicKey, wrappedKey }; } case enums.publicKey.x448: { - const x448 = await util.getNobleCurve(enums.publicKey.x448); - const ephemeralSecretKey = x448.utils.randomPrivateKey(); - const sharedSecret = x448.getSharedSecret(ephemeralSecretKey, recipientA); - const ephemeralPublicKey = x448.getPublicKey(ephemeralSecretKey); - const hkdfInput = util.concatUint8Array([ - ephemeralPublicKey, - recipientA, - sharedSecret - ]); const cipherAlgo = enums.symmetric.aes256; const { keySize } = getCipherParams(enums.symmetric.aes256); const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize); @@ -137,27 +143,20 @@ export async function encrypt(algo, data, recipientA) { * @async */ export async function decrypt(algo, ephemeralPublicKey, wrappedKey, A, k) { + const sharedSecret = await recomputeSharedSecret(algo, ephemeralPublicKey, A, k); + const hkdfInput = util.concatUint8Array([ + ephemeralPublicKey, + A, + sharedSecret + ]); switch (algo) { case enums.publicKey.x25519: { - const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey); - const hkdfInput = util.concatUint8Array([ - ephemeralPublicKey, - A, - sharedSecret - ]); const cipherAlgo = enums.symmetric.aes128; const { keySize } = getCipherParams(cipherAlgo); const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize); return aesKW.unwrap(cipherAlgo, encryptionKey, wrappedKey); } case enums.publicKey.x448: { - const x448 = await util.getNobleCurve(enums.publicKey.x448); - const sharedSecret = x448.getSharedSecret(k, ephemeralPublicKey); - const hkdfInput = util.concatUint8Array([ - ephemeralPublicKey, - A, - sharedSecret - ]); const cipherAlgo = enums.symmetric.aes256; const { keySize } = getCipherParams(enums.symmetric.aes256); const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize); @@ -180,3 +179,108 @@ export function getPayloadSize(algo) { throw new Error('Unsupported ECDH algorithm'); } } + +/** + * Generate shared secret and ephemeral public key for encryption + * @returns {Promise<{ ephemeralPublicKey: Uint8Array, sharedSecret: Uint8Array }>} ephemeral public key (K_A) and shared secret + * @async + */ +export async function generateEphemeralEncryptionMaterial(algo, recipientA) { + switch (algo) { + case enums.publicKey.x25519: + try { + const webCrypto = util.getWebCrypto(); + const jwk = publicKeyToJWK(algo, recipientA); + const ephemeralKeyPair = await webCrypto.generateKey('X25519', true, ['deriveKey', 'deriveBits']); + const recipientPublicKey = await webCrypto.importKey('jwk', jwk, 'X25519', false, []); + const sharedSecretBuffer = await webCrypto.deriveBits( + { name: 'X25519', public: recipientPublicKey }, + ephemeralKeyPair.privateKey, + getPayloadSize(algo) * 8 // in bits + ); + const ephemeralPublicKeyJwt = await webCrypto.exportKey('jwk', ephemeralKeyPair.publicKey); + return { + sharedSecret: new Uint8Array(sharedSecretBuffer), + ephemeralPublicKey: new Uint8Array(b64ToUint8Array(ephemeralPublicKeyJwt.x)) + }; + } catch (err) { + if (err.name !== 'NotSupportedError') { + throw err; + } + const ephemeralSecretKey = getRandomBytes(getPayloadSize(algo)); + const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA); + const { publicKey: ephemeralPublicKey } = x25519.box.keyPair.fromSecretKey(ephemeralSecretKey); + + return { ephemeralPublicKey, sharedSecret }; + } + case enums.publicKey.x448: { + const x448 = await util.getNobleCurve(enums.publicKey.x448); + const ephemeralSecretKey = x448.utils.randomPrivateKey(); + const sharedSecret = x448.getSharedSecret(ephemeralSecretKey, recipientA); + const ephemeralPublicKey = x448.getPublicKey(ephemeralSecretKey); + return { ephemeralPublicKey, sharedSecret }; + } + default: + throw new Error('Unsupported ECDH algorithm'); + } +} + +export async function recomputeSharedSecret(algo, ephemeralPublicKey, A, k) { + switch (algo) { + case enums.publicKey.x25519: + try { + const webCrypto = util.getWebCrypto(); + const privateKeyJWK = privateKeyToJWK(algo, A, k); + const ephemeralPublicKeyJWK = publicKeyToJWK(algo, ephemeralPublicKey); + const privateKey = await webCrypto.importKey('jwk', privateKeyJWK, 'X25519', false, ['deriveKey', 'deriveBits']); + const ephemeralPublicKeyReference = await webCrypto.importKey('jwk', ephemeralPublicKeyJWK, 'X25519', false, []); + const sharedSecretBuffer = await webCrypto.deriveBits( + { name: 'X25519', public: ephemeralPublicKeyReference }, + privateKey, + getPayloadSize(algo) * 8 // in bits + ); + return new Uint8Array(sharedSecretBuffer); + } catch (err) { + if (err.name !== 'NotSupportedError') { + throw err; + } + return x25519.scalarMult(k, ephemeralPublicKey); + } + case enums.publicKey.x448: { + const x448 = await util.getNobleCurve(enums.publicKey.x448); + const sharedSecret = x448.getSharedSecret(k, ephemeralPublicKey); + return sharedSecret; + } + default: + throw new Error('Unsupported ECDH algorithm'); + } +} + + +function publicKeyToJWK(algo, publicKey) { + switch (algo) { + case enums.publicKey.x25519: { + const jwk = { + kty: 'OKP', + crv: 'X25519', + x: uint8ArrayToB64(publicKey, true), + ext: true + }; + return jwk; + } + default: + throw new Error('Unsupported ECDH algorithm'); + } +} + +function privateKeyToJWK(algo, publicKey, privateKey) { + switch (algo) { + case enums.publicKey.x25519: { + const jwk = publicKeyToJWK(algo, publicKey); + jwk.d = uint8ArrayToB64(privateKey, true); + return jwk; + } + default: + throw new Error('Unsupported ECDH algorithm'); + } +} diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index 1e6aac2f..776dfedd 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -25,6 +25,7 @@ import util from '../../../util'; import enums from '../../../enums'; import hash from '../../hash'; import { getRandomBytes } from '../../random'; +import { b64ToUint8Array, uint8ArrayToB64 } from '../../../encoding/base64'; /** @@ -34,11 +35,27 @@ import { getRandomBytes } from '../../random'; */ export async function generate(algo) { switch (algo) { - case enums.publicKey.ed25519: { - const seed = getRandomBytes(getPayloadSize(algo)); - const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed); - return { A, seed }; - } + case enums.publicKey.ed25519: + try { + const webCrypto = util.getWebCrypto(); + const webCryptoKey = await webCrypto.generateKey('Ed25519', true, ['sign', 'verify']); + + const privateKey = await webCrypto.exportKey('jwk', webCryptoKey.privateKey); + const publicKey = await webCrypto.exportKey('jwk', webCryptoKey.publicKey); + + return { + A: new Uint8Array(b64ToUint8Array(publicKey.x)), + seed: b64ToUint8Array(privateKey.d, true) + }; + } catch (err) { + if (err.name !== 'NotSupportedError') { + throw err; + } + const seed = getRandomBytes(getPayloadSize(algo)); + const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed); + return { A, seed }; + } + case enums.publicKey.ed448: { const ed448 = await util.getNobleCurve(enums.publicKey.ed448); const seed = ed448.utils.randomPrivateKey(); @@ -68,11 +85,26 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe throw new Error('Hash algorithm too weak for EdDSA.'); } switch (algo) { - case enums.publicKey.ed25519: { - const secretKey = util.concatUint8Array([privateKey, publicKey]); - const signature = ed25519.sign.detached(hashed, secretKey); - return { RS: signature }; - } + case enums.publicKey.ed25519: + try { + const webCrypto = util.getWebCrypto(); + const jwk = privateKeyToJWK(algo, publicKey, privateKey); + const key = await webCrypto.importKey('jwk', jwk, 'Ed25519', false, ['sign']); + + const signature = new Uint8Array( + await webCrypto.sign('Ed25519', key, hashed) + ); + + return { RS: signature }; + } catch (err) { + if (err.name !== 'NotSupportedError') { + throw err; + } + const secretKey = util.concatUint8Array([privateKey, publicKey]); + const signature = ed25519.sign.detached(hashed, secretKey); + return { RS: signature }; + } + case enums.publicKey.ed448: { const ed448 = await util.getNobleCurve(enums.publicKey.ed448); const signature = ed448.sign(hashed, privateKey); @@ -101,7 +133,19 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { } switch (algo) { case enums.publicKey.ed25519: - return ed25519.sign.detached.verify(hashed, RS, publicKey); + try { + const webCrypto = util.getWebCrypto(); + const jwk = publicKeyToJWK(algo, publicKey); + const key = await webCrypto.importKey('jwk', jwk, 'Ed25519', false, ['verify']); + const verified = await webCrypto.verify('Ed25519', key, RS, hashed); + return verified; + } catch (err) { + if (err.name !== 'NotSupportedError') { + throw err; + } + return ed25519.sign.detached.verify(hashed, RS, publicKey); + } + case enums.publicKey.ed448: { const ed448 = await util.getNobleCurve(enums.publicKey.ed448); return ed448.verify(RS, hashed, publicKey); @@ -125,6 +169,7 @@ export async function validateParams(algo, A, seed) { /** * Derive public point A' from private key * and expect A == A' + * TODO: move to sign-verify using WebCrypto (same as ECDSA) when curve is more widely implemented */ const { publicKey } = ed25519.sign.keyPair.fromSeed(seed); return util.equalsUint8Array(A, publicKey); @@ -164,3 +209,31 @@ export function getPreferredHashAlgo(algo) { throw new Error('Unknown EdDSA algo'); } } + +const publicKeyToJWK = (algo, publicKey) => { + switch (algo) { + case enums.publicKey.ed25519: { + const jwk = { + kty: 'OKP', + crv: 'Ed25519', + x: uint8ArrayToB64(publicKey, true), + ext: true + }; + return jwk; + } + default: + throw new Error('Unsupported EdDSA algorithm'); + } +}; + +const privateKeyToJWK = (algo, publicKey, privateKey) => { + switch (algo) { + case enums.publicKey.ed25519: { + const jwk = publicKeyToJWK(algo, publicKey); + jwk.d = uint8ArrayToB64(privateKey, true); + return jwk; + } + default: + throw new Error('Unsupported EdDSA algorithm'); + } +}; diff --git a/src/crypto/public_key/elliptic/eddsa_legacy.js b/src/crypto/public_key/elliptic/eddsa_legacy.js index b564b176..28c72d4e 100644 --- a/src/crypto/public_key/elliptic/eddsa_legacy.js +++ b/src/crypto/public_key/elliptic/eddsa_legacy.js @@ -26,6 +26,7 @@ import util from '../../../util'; import enums from '../../../enums'; import hash from '../../hash'; import { CurveWithOID, checkPublicPointEnconding } from './oid_curves'; +import { sign as eddsaSign, verify as eddsaVerify } from './eddsa'; /** * Sign a message using the provided legacy EdDSA key @@ -48,8 +49,7 @@ export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed // see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2 throw new Error('Hash algorithm too weak for EdDSA.'); } - const secretKey = util.concatUint8Array([privateKey, publicKey.subarray(1)]); - const signature = nacl.sign.detached(hashed, secretKey); + const { RS: signature } = await eddsaSign(enums.publicKey.ed25519, hashAlgo, message, publicKey.subarray(1), privateKey, hashed); // EdDSA signature params are returned in little-endian format return { r: signature.subarray(0, 32), @@ -75,8 +75,8 @@ export async function verify(oid, hashAlgo, { r, s }, m, publicKey, hashed) { if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(enums.hash.sha256)) { throw new Error('Hash algorithm too weak for EdDSA.'); } - const signature = util.concatUint8Array([r, s]); - return nacl.sign.detached.verify(hashed, signature, publicKey.subarray(1)); + const RS = util.concatUint8Array([r, s]); + return eddsaVerify(enums.publicKey.ed25519, hashAlgo, { RS }, m, publicKey.subarray(1), hashed); } /** * Validate legacy EdDSA parameters diff --git a/src/crypto/public_key/elliptic/oid_curves.js b/src/crypto/public_key/elliptic/oid_curves.js index 079c6f43..a66decdc 100644 --- a/src/crypto/public_key/elliptic/oid_curves.js +++ b/src/crypto/public_key/elliptic/oid_curves.js @@ -20,12 +20,13 @@ * @module crypto/public_key/elliptic/curve */ import nacl from '@openpgp/tweetnacl'; -import { getRandomBytes } from '../../random'; import enums from '../../../enums'; import util from '../../../util'; import { uint8ArrayToB64, b64ToUint8Array } from '../../../encoding/base64'; import OID from '../../../type/oid'; import { UnsupportedError } from '../../../packet/packet'; +import { generate as eddsaGenerate } from './eddsa'; +import { generate as ecdhXGenerate } from './ecdh_x'; const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); @@ -181,18 +182,14 @@ class CurveWithOID { case 'node': return nodeGenKeyPair(this.name); case 'curve25519Legacy': { - const privateKey = getRandomBytes(32); - privateKey[0] = (privateKey[0] & 127) | 64; - privateKey[31] &= 248; - const secretKey = privateKey.slice().reverse(); - const { publicKey: rawPublicKey } = nacl.box.keyPair.fromSecretKey(secretKey); - const publicKey = util.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]), rawPublicKey]); + const { k, A } = await ecdhXGenerate(enums.publicKey.x25519); + const privateKey = k.slice().reverse(); + const publicKey = util.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]), A]); return { publicKey, privateKey }; } case 'ed25519Legacy': { - const privateKey = getRandomBytes(32); - const keyPair = nacl.sign.keyPair.fromSeed(privateKey); - const publicKey = util.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]), keyPair.publicKey]); + const { seed: privateKey, A } = await eddsaGenerate(enums.publicKey.ed25519); + const publicKey = util.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]), A]); return { publicKey, privateKey }; } default: diff --git a/test/general/x25519.js b/test/general/x25519.js index 93bc8c26..e0cc8628 100644 --- a/test/general/x25519.js +++ b/test/general/x25519.js @@ -12,7 +12,9 @@ import util from '../../src/util'; import * as input from './testInputs'; -export default () => (openpgp.config.ci ? describe.skip : describe)('X25519 Cryptography (legacy format)', function () { +const isSafariOrHeadlessWebKit = () => typeof window !== 'undefined' && window.navigator.userAgent.match(/WebKit/) && !window.navigator.userAgent.match(/Chrome/); + +export default () => describe('X25519 Cryptography (legacy format)', function () { const data = { light: { id: '1ecdf026c0245830', @@ -216,7 +218,9 @@ export default () => (openpgp.config.ci ? describe.skip : describe)('X25519 Cryp expect(await result.signatures[0].verified).to.be.true; }); - describe('Ed25519 Test Vectors from RFC8032', function () { + // Safari implements the non-deterministic version of EdDSA (https://cfrg.github.io/draft-irtf-cfrg-det-sigs-with-noise/draft-irtf-cfrg-det-sigs-with-noise.html), + // hence these test vectors do not apply. + (isSafariOrHeadlessWebKit() ? describe.skip : describe)('Ed25519 Test Vectors from RFC8032', function () { // https://tools.ietf.org/html/rfc8032#section-7.1 function testVector(vector) { const curve = new elliptic.CurveWithOID(openpgp.enums.curve.ed25519Legacy);