Merge pull request #1782

This commit is contained in:
larabr 2024-08-21 12:53:13 +02:00 committed by GitHub
commit 79014f00f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 256 additions and 82 deletions

View File

@ -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.

View File

@ -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':

View File

@ -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');
}
}

View File

@ -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');
}
};

View File

@ -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

View File

@ -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:

View File

@ -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);