Merge pull request #1687

`crypto-refresh`: minor fixes and updates for X25519/Ed25519 (new format)
This commit is contained in:
larabr 2023-10-10 13:06:09 +02:00 committed by GitHub
commit ed482a17c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 102 additions and 78 deletions

6
openpgp.d.ts vendored
View File

@ -820,7 +820,9 @@ export namespace enums {
dsa = 17,
ecdh = 18,
ecdsa = 19,
/** @deprecated use `eddsaLegacy` instead */
eddsa = 22,
eddsaLegacy = 22,
aedh = 23,
aedsa = 24,
}
@ -829,8 +831,12 @@ export namespace enums {
p256 = 'p256',
p384 = 'p384',
p521 = 'p521',
/** @deprecated use `ed25519Legacy` instead */
ed25519 = 'ed25519',
ed25519Legacy = 'ed25519',
/** @deprecated use `x25519Legacy` instead */
curve25519 = 'curve25519',
x25519Legacy = 'curve25519',
secp256k1 = 'secp256k1',
brainpoolP256r1 = 'brainpoolP256r1',
brainpoolP384r1 = 'brainpoolP384r1',

View File

@ -168,8 +168,7 @@ export function parsePublicKeyParams(algo, bytes) {
const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2;
return { read: read, publicParams: { oid, Q } };
}
case enums.publicKey.eddsa:
case enums.publicKey.ed25519Legacy: {
case enums.publicKey.eddsaLegacy: {
const oid = new OID(); read += oid.read(bytes);
checkSupportedCurve(oid);
let Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2;
@ -224,8 +223,7 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) {
d = util.leftPad(d, curve.payloadSize);
return { read, privateParams: { d } };
}
case enums.publicKey.eddsa:
case enums.publicKey.ed25519Legacy: {
case enums.publicKey.eddsaLegacy: {
const curve = new CurveWithOID(publicParams.oid);
let seed = util.readMPI(bytes.subarray(read)); read += seed.length + 2;
seed = util.leftPad(seed, curve.payloadSize);
@ -331,8 +329,7 @@ export function generateParams(algo, bits, oid) {
privateParams: { d: secret },
publicParams: { oid: new OID(oid), Q }
}));
case enums.publicKey.eddsa:
case enums.publicKey.ed25519Legacy:
case enums.publicKey.eddsaLegacy:
return publicKey.elliptic.generate(oid).then(({ oid, Q, secret }) => ({
privateParams: { seed: secret },
publicParams: { oid: new OID(oid), Q }
@ -401,8 +398,7 @@ export async function validateParams(algo, publicParams, privateParams) {
const { d } = privateParams;
return algoModule.validateParams(oid, Q, d);
}
case enums.publicKey.eddsa:
case enums.publicKey.ed25519Legacy: {
case enums.publicKey.eddsaLegacy: {
const { Q, oid } = publicParams;
const { seed } = privateParams;
return publicKey.elliptic.eddsaLegacy.validateParams(oid, Q, seed);
@ -472,3 +468,20 @@ function checkSupportedCurve(oid) {
throw new UnsupportedError('Unknown curve OID');
}
}
/**
* Get preferred hash algo for a given elliptic algo
* @param {module:enums.publicKey} algo - alrogithm identifier
* @param {module:type/oid} [oid] - curve OID if needed by algo
*/
export function getPreferredCurveHashAlgo(algo, oid) {
switch (algo) {
case enums.publicKey.ecdsa:
case enums.publicKey.eddsaLegacy:
return publicKey.elliptic.getPreferredHashAlgo(oid);
case enums.publicKey.ed25519:
return publicKey.elliptic.eddsa.getPreferredHashAlgo(algo);
default:
throw new Error('Unknown elliptic signing algo');
}
}

View File

@ -27,8 +27,6 @@ export async function generate(algo) {
case enums.publicKey.x25519: {
// k stays in little-endian, unlike legacy ECDH over curve25519
const k = getRandomBytes(32);
k[0] &= 248;
k[31] = (k[31] & 127) | 64;
const { publicKey: A } = nacl.box.keyPair.fromSecretKey(k);
return { A, k };
}

View File

@ -61,9 +61,8 @@ export async function generate(algo) {
* @async
*/
export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashed) {
if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(enums.hash.sha256)) {
// see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2
throw new Error('Hash algorithm too weak: sha256 or stronger is required for EdDSA.');
if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(getPreferredHashAlgo(algo))) {
throw new Error('Hash algorithm too weak for EdDSA.');
}
switch (algo) {
case enums.publicKey.ed25519: {
@ -90,6 +89,9 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe
* @async
*/
export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) {
if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(getPreferredHashAlgo(algo))) {
throw new Error('Hash algorithm too weak for EdDSA.');
}
switch (algo) {
case enums.publicKey.ed25519: {
return nacl.sign.detached.verify(hashed, RS, publicKey);
@ -124,3 +126,12 @@ export async function validateParams(algo, A, seed) {
return false;
}
}
export function getPreferredHashAlgo(algo) {
switch (algo) {
case enums.publicKey.ed25519:
return enums.hash.sha256;
default:
throw new Error('Unknown EdDSA algo');
}
}

View File

@ -47,7 +47,7 @@ nacl.hash = bytes => new Uint8Array(sha512().update(bytes).digest());
export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed) {
if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(enums.hash.sha256)) {
// see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2
throw new Error('Hash algorithm too weak: sha256 or stronger is required for EdDSA.');
throw new Error('Hash algorithm too weak for EdDSA.');
}
const secretKey = util.concatUint8Array([privateKey, publicKey.subarray(1)]);
const signature = nacl.sign.detached(hashed, secretKey);
@ -71,6 +71,9 @@ export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed
* @async
*/
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));
}

View File

@ -92,7 +92,7 @@ const curves = {
},
ed25519: {
oid: [0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01],
keyType: enums.publicKey.eddsa,
keyType: enums.publicKey.eddsaLegacy,
hash: enums.hash.sha512,
node: false, // nodeCurves.ed25519 TODO
payloadSize: 32

View File

@ -46,8 +46,7 @@ export function parseSignatureParams(algo, signature) {
// Algorithm-Specific Fields for legacy EdDSA signatures:
// - MPI of an EC point r.
// - EdDSA value s, in MPI, in the little endian representation
case enums.publicKey.eddsa:
case enums.publicKey.ed25519Legacy: {
case enums.publicKey.eddsaLegacy: {
// When parsing little-endian MPI data, we always need to left-pad it, as done with big-endian values:
// https://www.ietf.org/archive/id/draft-ietf-openpgp-rfc4880bis-10.html#section-3.2-9
let r = util.readMPI(signature.subarray(read)); read += r.length + 2;
@ -103,8 +102,7 @@ export async function verify(algo, hashAlgo, signature, publicParams, data, hash
const s = util.leftPad(signature.s, curveSize);
return publicKey.elliptic.ecdsa.verify(oid, hashAlgo, { r, s }, data, Q, hashed);
}
case enums.publicKey.eddsa:
case enums.publicKey.ed25519Legacy: {
case enums.publicKey.eddsaLegacy: {
const { oid, Q } = publicParams;
// signature already padded on parsing
return publicKey.elliptic.eddsaLegacy.verify(oid, hashAlgo, signature, data, Q, hashed);
@ -158,8 +156,7 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da
const { d } = privateKeyParams;
return publicKey.elliptic.ecdsa.sign(oid, hashAlgo, data, Q, d, hashed);
}
case enums.publicKey.eddsa:
case enums.publicKey.ed25519Legacy: {
case enums.publicKey.eddsaLegacy: {
const { oid, Q } = publicKeyParams;
const { seed } = privateKeyParams;
return publicKey.elliptic.eddsaLegacy.sign(oid, hashAlgo, data, Q, seed, hashed);

View File

@ -43,17 +43,21 @@ export default {
'2b8104000a': 'secp256k1',
'2B8104000A': 'secp256k1',
/** Ed25519 */
/** Ed25519 - deprecated by crypto-refresh (replaced by standaone Ed25519 algo) */
'ed25519Legacy': 'ed25519',
'ED25519': 'ed25519',
/** @deprecated use `ed25519Legacy` instead */
'ed25519': 'ed25519',
'Ed25519': 'ed25519',
'1.3.6.1.4.1.11591.15.1': 'ed25519',
'2b06010401da470f01': 'ed25519',
'2B06010401DA470F01': 'ed25519',
/** Curve25519 */
/** Curve25519 - deprecated by crypto-refresh (replaced by standaone X25519 algo) */
'x25519Legacy': 'curve25519',
'X25519': 'curve25519',
'cv25519': 'curve25519',
/** @deprecated use `x25519Legacy` instead */
'curve25519': 'curve25519',
'Curve25519': 'curve25519',
'1.3.6.1.4.1.3029.1.5.1': 'curve25519',
@ -111,8 +115,11 @@ export default {
ecdsa: 19,
/** EdDSA (Sign only) - deprecated by crypto-refresh (replaced by `ed25519` identifier below)
* [{@link https://tools.ietf.org/html/draft-koch-eddsa-for-openpgp-04|Draft RFC}] */
ed25519Legacy: 22, // NB: this is declared before `eddsa` to translate 22 to 'eddsa' for backwards compatibility
eddsa: 22, // to be deprecated in v6
eddsaLegacy: 22, // NB: this is declared before `eddsa` to translate 22 to 'eddsa' for backwards compatibility
/** @deprecated use `eddsaLegacy` instead */
ed25519Legacy: 22,
/** @deprecated use `eddsaLegacy` instead */
eddsa: 22,
/** Reserved for AEDH */
aedh: 23,
/** Reserved for AEDSA */

View File

@ -197,50 +197,50 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options, conf
const dataToSign = {};
dataToSign.userID = userIDPacket;
dataToSign.key = secretKeyPacket;
const signaturePacket = new SignaturePacket();
signaturePacket.signatureType = enums.signature.certGeneric;
signaturePacket.publicKeyAlgorithm = secretKeyPacket.algorithm;
signaturePacket.hashAlgorithm = await helper.getPreferredHashAlgo(null, secretKeyPacket, undefined, undefined, config);
signaturePacket.keyFlags = [enums.keyFlags.certifyKeys | enums.keyFlags.signData];
signaturePacket.preferredSymmetricAlgorithms = createPreferredAlgos([
const signatureProperties = {};
signatureProperties.signatureType = enums.signature.certGeneric;
signatureProperties.keyFlags = [enums.keyFlags.certifyKeys | enums.keyFlags.signData];
signatureProperties.preferredSymmetricAlgorithms = createPreferredAlgos([
// prefer aes256, aes128, then aes192 (no WebCrypto support: https://www.chromium.org/blink/webcrypto#TOC-AES-support)
enums.symmetric.aes256,
enums.symmetric.aes128,
enums.symmetric.aes192
], config.preferredSymmetricAlgorithm);
if (config.aeadProtect) {
signaturePacket.preferredAEADAlgorithms = createPreferredAlgos([
signatureProperties.preferredAEADAlgorithms = createPreferredAlgos([
enums.aead.eax,
enums.aead.ocb
], config.preferredAEADAlgorithm);
}
signaturePacket.preferredHashAlgorithms = createPreferredAlgos([
signatureProperties.preferredHashAlgorithms = createPreferredAlgos([
// prefer fast asm.js implementations (SHA-256)
enums.hash.sha256,
enums.hash.sha512
], config.preferredHashAlgorithm);
signaturePacket.preferredCompressionAlgorithms = createPreferredAlgos([
signatureProperties.preferredCompressionAlgorithms = createPreferredAlgos([
enums.compression.zlib,
enums.compression.zip,
enums.compression.uncompressed
], config.preferredCompressionAlgorithm);
if (index === 0) {
signaturePacket.isPrimaryUserID = true;
signatureProperties.isPrimaryUserID = true;
}
// integrity protection always enabled
signaturePacket.features = [0];
signaturePacket.features[0] |= enums.features.modificationDetection;
signatureProperties.features = [0];
signatureProperties.features[0] |= enums.features.modificationDetection;
if (config.aeadProtect) {
signaturePacket.features[0] |= enums.features.aead;
signatureProperties.features[0] |= enums.features.aead;
}
if (config.v5Keys) {
signaturePacket.features[0] |= enums.features.v5Keys;
signatureProperties.features[0] |= enums.features.v5Keys;
}
if (options.keyExpirationTime > 0) {
signaturePacket.keyExpirationTime = options.keyExpirationTime;
signaturePacket.keyNeverExpires = false;
signatureProperties.keyExpirationTime = options.keyExpirationTime;
signatureProperties.keyNeverExpires = false;
}
await signaturePacket.sign(secretKeyPacket, dataToSign, options.date);
const signaturePacket = await helper.createSignaturePacket(dataToSign, null, secretKeyPacket, signatureProperties, options.date, undefined, undefined, undefined, config);
return { userIDPacket, signaturePacket };
})).then(list => {

View File

@ -5,8 +5,6 @@
*/
import {
PublicKeyPacket,
PublicSubkeyPacket,
SecretKeyPacket,
SecretSubkeyPacket,
SignaturePacket
@ -88,23 +86,20 @@ export async function createBindingSignature(subkey, primaryKey, options, config
const dataToSign = {};
dataToSign.key = primaryKey;
dataToSign.bind = subkey;
const subkeySignaturePacket = new SignaturePacket();
subkeySignaturePacket.signatureType = enums.signature.subkeyBinding;
subkeySignaturePacket.publicKeyAlgorithm = primaryKey.algorithm;
subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(null, subkey, undefined, undefined, config);
const signatureProperties = { signatureType: enums.signature.subkeyBinding };
if (options.sign) {
subkeySignaturePacket.keyFlags = [enums.keyFlags.signData];
subkeySignaturePacket.embeddedSignature = await createSignaturePacket(dataToSign, null, subkey, {
signatureProperties.keyFlags = [enums.keyFlags.signData];
signatureProperties.embeddedSignature = await createSignaturePacket(dataToSign, null, subkey, {
signatureType: enums.signature.keyBinding
}, options.date, undefined, undefined, undefined, config);
} else {
subkeySignaturePacket.keyFlags = [enums.keyFlags.encryptCommunication | enums.keyFlags.encryptStorage];
signatureProperties.keyFlags = [enums.keyFlags.encryptCommunication | enums.keyFlags.encryptStorage];
}
if (options.keyExpirationTime > 0) {
subkeySignaturePacket.keyExpirationTime = options.keyExpirationTime;
subkeySignaturePacket.keyNeverExpires = false;
signatureProperties.keyExpirationTime = options.keyExpirationTime;
signatureProperties.keyNeverExpires = false;
}
await subkeySignaturePacket.sign(primaryKey, dataToSign, options.date);
const subkeySignaturePacket = await createSignaturePacket(dataToSign, null, primaryKey, signatureProperties, options.date, undefined, undefined, undefined, config);
return subkeySignaturePacket;
}
@ -129,17 +124,11 @@ export async function getPreferredHashAlgo(key, keyPacket, date = new Date(), us
prefAlgo : hashAlgo;
}
}
switch (Object.getPrototypeOf(keyPacket)) {
case SecretKeyPacket.prototype:
case PublicKeyPacket.prototype:
case SecretSubkeyPacket.prototype:
case PublicSubkeyPacket.prototype:
switch (keyPacket.algorithm) {
case enums.publicKey.ecdh:
case enums.publicKey.ecdsa:
case enums.publicKey.eddsa:
prefAlgo = crypto.publicKey.elliptic.getPreferredHashAlgo(keyPacket.publicParams.oid);
}
case enums.publicKey.eddsaLegacy:
case enums.publicKey.ed25519:
prefAlgo = crypto.getPreferredCurveHashAlgo(keyPacket.algorithm, keyPacket.publicParams.oid);
}
return crypto.hash.getHashByteLength(hashAlgo) <= crypto.hash.getHashByteLength(prefAlgo) ?
prefAlgo : hashAlgo;
@ -344,11 +333,11 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) {
} catch (e) {
throw new Error('Unknown curve');
}
if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) {
options.curve = options.sign ? enums.curve.ed25519 : enums.curve.curve25519;
if (options.curve === enums.curve.ed25519Legacy || options.curve === enums.curve.x25519Legacy) {
options.curve = options.sign ? enums.curve.ed25519Legacy : enums.curve.x25519Legacy;
}
if (options.sign) {
options.algorithm = options.curve === enums.curve.ed25519 ? enums.publicKey.eddsa : enums.publicKey.ecdsa;
options.algorithm = options.curve === enums.curve.ed25519Legacy ? enums.publicKey.eddsaLegacy : enums.publicKey.ecdsa;
} else {
options.algorithm = enums.publicKey.ecdh;
}
@ -377,7 +366,7 @@ export function isValidEncryptionKeyPacket(keyPacket, signature) {
return keyAlgo !== enums.publicKey.dsa &&
keyAlgo !== enums.publicKey.rsaSign &&
keyAlgo !== enums.publicKey.ecdsa &&
keyAlgo !== enums.publicKey.eddsa &&
keyAlgo !== enums.publicKey.eddsaLegacy &&
keyAlgo !== enums.publicKey.ed25519 &&
(!signature.keyFlags ||
(signature.keyFlags[0] & enums.keyFlags.encryptCommunication) !== 0 ||
@ -417,7 +406,7 @@ export function checkKeyRequirements(keyPacket, config) {
}
break;
case enums.publicKey.ecdsa:
case enums.publicKey.eddsa:
case enums.publicKey.eddsaLegacy:
case enums.publicKey.ecdh:
if (config.rejectCurves.has(algoInfo.curve)) {
throw new Error(`Support for ${algoInfo.algorithm} keys using curve ${algoInfo.curve} is disabled.`);

View File

@ -178,7 +178,7 @@ n9/quqtmyOtYOA6gXNCw0Fal3iANKBmsPmYI
showComment: true,
preferredCompressionAlgorithm: openpgp.enums.compression.zip,
preferredHashAlgorithm: openpgp.enums.hash.sha512,
rejectPublicKeyAlgorithms: new Set([openpgp.enums.publicKey.eddsa]) // should not matter in this context
rejectPublicKeyAlgorithms: new Set([openpgp.enums.publicKey.eddsaLegacy]) // should not matter in this context
};
const opt2 = { privateKey: origKey, userIDs, config };
const { privateKey: refKeyArmored2 } = await openpgp.reformatKey(opt2);
@ -296,11 +296,11 @@ n9/quqtmyOtYOA6gXNCw0Fal3iANKBmsPmYI
})).to.be.eventually.rejectedWith(/ecdh keys are considered too weak/);
await expect(openpgp.encrypt({
message, encryptionKeys: [key], config: { rejectCurves: new Set([openpgp.enums.curve.curve25519]) }
message, encryptionKeys: [key], config: { rejectCurves: new Set([openpgp.enums.curve.x25519Legacy]) }
})).to.be.eventually.rejectedWith(/Support for ecdh keys using curve curve25519 is disabled/);
const echdEncrypted = await openpgp.encrypt({
message, encryptionKeys: [key], config: { rejectCurves: new Set([openpgp.enums.curve.ed25519]) }
message, encryptionKeys: [key], config: { rejectCurves: new Set([openpgp.enums.curve.ed25519Legacy]) }
});
expect(echdEncrypted).to.match(/---BEGIN PGP MESSAGE---/);
} finally {
@ -366,10 +366,10 @@ n9/quqtmyOtYOA6gXNCw0Fal3iANKBmsPmYI
await expect(openpgp.sign(opt2)).to.be.rejectedWith(/Insecure hash algorithm/);
await expect(openpgp.sign({
message, signingKeys: [key], config: { rejectPublicKeyAlgorithms: new Set([openpgp.enums.publicKey.eddsa]) }
message, signingKeys: [key], config: { rejectPublicKeyAlgorithms: new Set([openpgp.enums.publicKey.eddsaLegacy]) }
})).to.be.eventually.rejectedWith(/eddsa keys are considered too weak/);
await expect(openpgp.sign({
message, signingKeys: [key], config: { rejectCurves: new Set([openpgp.enums.curve.ed25519]) }
message, signingKeys: [key], config: { rejectCurves: new Set([openpgp.enums.curve.ed25519Legacy]) }
})).to.be.eventually.rejectedWith(/Support for eddsa keys using curve ed25519 is disabled/);
});
@ -411,7 +411,7 @@ n9/quqtmyOtYOA6gXNCw0Fal3iANKBmsPmYI
const opt4 = {
message: await openpgp.readMessage({ armoredMessage: signed }),
verificationKeys: [key],
config: { rejectPublicKeyAlgorithms: new Set([openpgp.enums.publicKey.eddsa]) }
config: { rejectPublicKeyAlgorithms: new Set([openpgp.enums.publicKey.eddsaLegacy]) }
};
const { signatures: [sig4] } = await openpgp.verify(opt4);
await expect(sig4.verified).to.be.rejectedWith(/eddsa keys are considered too weak/);
@ -419,7 +419,7 @@ n9/quqtmyOtYOA6gXNCw0Fal3iANKBmsPmYI
const opt5 = {
message: await openpgp.readMessage({ armoredMessage: signed }),
verificationKeys: [key],
config: { rejectCurves: new Set([openpgp.enums.curve.ed25519]) }
config: { rejectCurves: new Set([openpgp.enums.curve.ed25519Legacy]) }
};
const { signatures: [sig5] } = await openpgp.verify(opt5);
await expect(sig5.verified).to.be.eventually.rejectedWith(/Support for eddsa keys using curve ed25519 is disabled/);

View File

@ -4070,7 +4070,7 @@ XvmoLueOOShu01X/kaylMqaT8w==
const subkey = newPrivateKey.subkeys[total];
expect(subkey).to.exist;
expect(subkey.getAlgorithmInfo().algorithm).to.be.equal('ecdh');
expect(subkey.getAlgorithmInfo().curve).to.be.equal(openpgp.enums.curve.curve25519);
expect(subkey.getAlgorithmInfo().curve).to.be.equal(openpgp.enums.curve.x25519Legacy);
await subkey.verify();
});