From cc79face33d9749d13196e086a3113382d6def28 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 12 Jul 2022 19:05:39 +0200 Subject: [PATCH 01/15] Update package.json name and url --- package-lock.json | 4 ++-- package.json | 4 ++-- rollup.config.js | 10 +++++----- test/benchmarks/memory_usage.js | 4 ++-- test/benchmarks/time.js | 2 +- test/initOpenpgp.js | 2 +- test/typescript/definitions.ts | 2 +- tsconfig.json | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e1861dba..bb42f1e41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "openpgp", + "name": "@protontech/openpgp", "version": "6.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "openpgp", + "name": "@protontech/openpgp", "version": "6.1.0", "license": "LGPL-3.0+", "devDependencies": { diff --git a/package.json b/package.json index 6d52e3862..0bf0f8163 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "openpgp", + "name": "@protontech/openpgp", "description": "OpenPGP.js is a Javascript implementation of the OpenPGP protocol. This is defined in RFC 4880.", "version": "6.1.0", "license": "LGPL-3.0+", @@ -114,6 +114,6 @@ }, "repository": { "type": "git", - "url": "https://github.com/openpgpjs/openpgpjs" + "url": "https://github.com/ProtonMail/openpgpjs" } } diff --git a/rollup.config.js b/rollup.config.js index f61c53cce..349b961ab 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -61,8 +61,8 @@ const nodeBuild = { input: 'src/index.js', external: nodeBuiltinModules.concat(nodeDependencies), output: [ - { file: 'dist/node/openpgp.cjs', format: 'cjs', name: pkg.name, banner, intro }, - { file: 'dist/node/openpgp.min.cjs', format: 'cjs', name: pkg.name, banner, intro, plugins: [terser(terserOptions)], sourcemap: true }, + { file: 'dist/node/openpgp.cjs', format: 'cjs', name: 'openpgp', banner, intro }, + { file: 'dist/node/openpgp.min.cjs', format: 'cjs', name: 'openpgp', banner, intro, plugins: [terser(terserOptions)], sourcemap: true }, { file: 'dist/node/openpgp.mjs', format: 'es', banner, intro }, { file: 'dist/node/openpgp.min.mjs', format: 'es', banner, intro, plugins: [terser(terserOptions)], sourcemap: true } ].map(options => ({ ...options, inlineDynamicImports: true })), @@ -85,8 +85,8 @@ const fullBrowserBuild = { input: 'src/index.js', external: nodeBuiltinModules.concat(nodeDependencies), output: [ - { file: 'dist/openpgp.js', format: 'iife', name: pkg.name, banner, intro }, - { file: 'dist/openpgp.min.js', format: 'iife', name: pkg.name, banner, intro, plugins: [terser(terserOptions)], sourcemap: true }, + { file: 'dist/openpgp.js', format: 'iife', name: 'openpgp', banner, intro }, + { file: 'dist/openpgp.min.js', format: 'iife', name: 'openpgp', banner, intro, plugins: [terser(terserOptions)], sourcemap: true }, { file: 'dist/openpgp.mjs', format: 'es', banner, intro }, { file: 'dist/openpgp.min.mjs', format: 'es', banner, intro, plugins: [terser(terserOptions)], sourcemap: true } ].map(options => ({ ...options, inlineDynamicImports: true })), @@ -145,7 +145,7 @@ const testBuild = { plugins: [ alias({ entries: { - openpgp: `./dist/${process.env.npm_config_lightweight ? 'lightweight/' : ''}openpgp.mjs` + '@protontech/openpgp': `./dist/${process.env.npm_config_lightweight ? 'lightweight/' : ''}openpgp.mjs` } }), resolve({ diff --git a/test/benchmarks/memory_usage.js b/test/benchmarks/memory_usage.js index 50673b67d..c7b6b9afa 100644 --- a/test/benchmarks/memory_usage.js +++ b/test/benchmarks/memory_usage.js @@ -4,7 +4,7 @@ import path from 'path'; import { writeFileSync, unlinkSync } from 'fs'; import { fork } from 'child_process'; import { fileURLToPath } from 'url'; -import * as openpgp from 'openpgp'; +import * as openpgp from '@protontech/openpgp'; /** * Benchmark max memory usage recorded during execution of the given function. @@ -18,7 +18,7 @@ const benchmark = async function(fn) { // the code to execute must be written to a file writeFileSync(tmpFileName, ` const assert = require('assert'); -const openpgp = require('openpgp'); +const openpgp = require('@protontech/openpgp'); let maxMemoryComsumption; let activeSampling = false; diff --git a/test/benchmarks/time.js b/test/benchmarks/time.js index 24ba752ac..575378bf8 100644 --- a/test/benchmarks/time.js +++ b/test/benchmarks/time.js @@ -1,6 +1,6 @@ import Benchmark from 'benchmark'; import { readToEnd } from '@openpgp/web-stream-tools'; -import * as openpgp from 'openpgp'; +import * as openpgp from '@protontech/openpgp'; const wrapAsync = func => ({ fn: async deferred => { diff --git a/test/initOpenpgp.js b/test/initOpenpgp.js index 4fd4a360e..3df7f453f 100644 --- a/test/initOpenpgp.js +++ b/test/initOpenpgp.js @@ -3,7 +3,7 @@ * at the top of the test bundle, and that the config is initialised before the tests code runs (incl. that outside of `describe`). */ -import * as openpgp from 'openpgp'; +import * as openpgp from '@protontech/openpgp'; if (typeof window !== 'undefined') { window.openpgp = openpgp; diff --git a/test/typescript/definitions.ts b/test/typescript/definitions.ts index 2d74b77cc..0387d217e 100644 --- a/test/typescript/definitions.ts +++ b/test/typescript/definitions.ts @@ -17,7 +17,7 @@ import { generateSessionKey, encryptSessionKey, decryptSessionKeys, LiteralDataPacket, PacketList, CompressedDataPacket, PublicKeyPacket, PublicSubkeyPacket, SecretKeyPacket, SecretSubkeyPacket, CleartextMessage, WebStream, NodeWebStream, -} from 'openpgp'; +} from '@protontech/openpgp'; (async () => { diff --git a/tsconfig.json b/tsconfig.json index 1b8fdcb00..4b1fc1a11 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "moduleResolution": "Bundler", "allowJs": true, "paths": { - "openpgp": [ "." ] + "@protontech/openpgp": [ "." ] }, } } From d8bdf2bed37312bee7651eb1139fe09d866ef027 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Fri, 19 Jun 2020 16:52:36 +0200 Subject: [PATCH 02/15] Add support for decrypting autoforwarded messages (#1) --- src/crypto/public_key/elliptic/ecdh.js | 4 +- src/enums.js | 12 +++++ src/type/kdf_params.js | 55 ++++++++++++++++--- test/crypto/ecdh.js | 74 ++++++++++++++++++++++++++ test/general/forwarding.js | 43 +++++++++++++++ test/general/index.js | 2 + 6 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 test/general/forwarding.js diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 66937b2cb..b07448a8c 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -50,9 +50,9 @@ function buildEcdhParam(public_algo, oid, kdfParams, fingerprint) { return util.concatUint8Array([ oid.write(), new Uint8Array([public_algo]), - kdfParams.write(), + kdfParams.replacementKDFParams || kdfParams.write(), util.stringToUint8Array('Anonymous Sender '), - fingerprint + kdfParams.replacementFingerprint || fingerprint ]); } diff --git a/src/enums.js b/src/enums.js index 85f0a5d71..1d5b96cfd 100644 --- a/src/enums.js +++ b/src/enums.js @@ -50,6 +50,18 @@ export default { 'brainpoolP512r1': 'brainpoolP512r1' }, + /** KDF parameters flags + * Non-standard extensions (for now) to allow email forwarding + * @enum {Integer} + * @readonly + */ + kdfFlags: { + /** Specify fingerprint to use instead of the recipient's */ + replace_fingerprint: 0x01, + /** Specify custom parameters to use in the KDF digest computation */ + replace_kdf_params: 0x02 + }, + /** A string to key specifier type * @enum {Integer} * @readonly diff --git a/src/type/kdf_params.js b/src/type/kdf_params.js index 84728b3c1..240fb433b 100644 --- a/src/type/kdf_params.js +++ b/src/type/kdf_params.js @@ -26,22 +26,36 @@ import { UnsupportedError } from '../packet/packet'; * Alternative 1) [NIST-SP800-56A] with the KDF hash function that is * SHA2-256 [FIPS-180-3] or stronger is REQUIRED. * @module type/kdf_params - * @private */ +import util from '../util'; +import enums from '../enums'; class KDFParams { /** - * @param {enums.hash} hash - Hash algorithm - * @param {enums.symmetric} cipher - Symmetric algorithm + * @param {Integer} version Version, defaults to 1 + * @param {enums.hash} hash Hash algorithm + * @param {enums.symmetric} cipher Symmetric algorithm + * @param {enums.kdfFlags} flags (v2 only) flags + * @param {Uint8Array} replacementFingerprint (v2 only) fingerprint to use instead of recipient one (v5 keys, the 20 leftmost bytes of the fingerprint) + * @param {Uint8Array} replacementKDFParams (v2 only) serialized KDF params to use in KDF digest computation */ constructor(data) { if (data) { - const { hash, cipher } = data; + const { version, hash, cipher, flags, replacementFingerprint, replacementKDFParams } = data; + this.version = version || 1; this.hash = hash; this.cipher = cipher; + + this.flags = flags; + this.replacementFingerprint = replacementFingerprint; + this.replacementKDFParams = replacementKDFParams; } else { + this.version = null; this.hash = null; this.cipher = null; + this.flags = null; + this.replacementFingerprint = null; + this.replacementKDFParams = null; } } @@ -51,12 +65,27 @@ class KDFParams { * @returns {Number} Number of read bytes. */ read(input) { - if (input.length < 4 || input[0] !== 3 || input[1] !== 1) { + if (input.length < 4 || (input[1] !== 1 && input[1] !== 2)) { throw new UnsupportedError('Cannot read KDFParams'); } + this.version = input[1]; this.hash = input[2]; this.cipher = input[3]; - return 4; + let readBytes = 4; + + if (this.version === 2) { + this.flags = input[readBytes++]; + if (this.flags & enums.kdfFlags.replace_fingerprint) { + this.replacementFingerprint = input.slice(readBytes, readBytes + 20); + readBytes += 20; + } + if (this.flags & enums.kdfFlags.replace_kdf_params) { + const fieldLength = input[readBytes] + 1; // account for length + this.replacementKDFParams = input.slice(readBytes, readBytes + fieldLength); + readBytes += fieldLength; + } + } + return readBytes; } /** @@ -64,7 +93,19 @@ class KDFParams { * @returns {Uint8Array} Array with the KDFParams value */ write() { - return new Uint8Array([3, 1, this.hash, this.cipher]); + if (!this.version || this.version === 1) { + return new Uint8Array([3, 1, this.hash, this.cipher]); + } + + const v2Fields = util.concatUint8Array([ + new Uint8Array([4, 2, this.hash, this.cipher, this.flags]), + this.replacementFingerprint || new Uint8Array(), + this.replacementKDFParams || new Uint8Array() + ]); + + // update length field + v2Fields[0] = v2Fields.length - 1; + return new Uint8Array(v2Fields); } } diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index 72a8892ba..a56f0ee9d 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -362,3 +362,77 @@ export default () => describe('ECDH key exchange @lightweight', function () { }); }); }); + +describe('KDF parameters', function () { + const fingerprint = new Uint8Array([ + 177, 183, 116, 123, 76, 133, 245, 212, 151, 243, + 236, 71, 245, 86, 3, 168, 101, 74, 209, 105 + ]); + + it('Valid serialization', async function () { + const cipher = openpgp.enums.symmetric.aes256; + const hash = openpgp.enums.hash.sha256; + + const v1 = new KDFParams({ cipher, hash }); + const v1Copy = new KDFParams({}); + v1Copy.read(v1.write()); + expect(v1Copy).to.deep.equal(v1); + + const v1Flags0x0 = new KDFParams({ + cipher, + hash, + flags: 0x0 // discarded + }); + const v1Flags0x0Copy = new KDFParams({}); + v1Flags0x0Copy.read(v1Flags0x0.write()); + v1Flags0x0.flags = undefined; + expect(v1Flags0x0Copy).to.deep.equal(v1Flags0x0); + + const v2Flags0x3 = new KDFParams({ + cipher, + hash, + version: 2, + flags: 0x3, + replacementFingerprint: fingerprint, + replacementKDFParams: new Uint8Array([3, 1, cipher, hash]) + }); + const v2Flags0x3Copy = new KDFParams(); + v2Flags0x3Copy.read(v2Flags0x3.write()); + expect(v2Flags0x3Copy).to.deep.equal(v2Flags0x3); + + const v2Flags0x0 = new KDFParams({ + cipher, + hash, + version: 2, + flags: 0x0 + }); + const v2Flags0x0Copy = new KDFParams({}); + v2Flags0x0Copy.read(v2Flags0x0.write()); + + expect(v2Flags0x0Copy).to.deep.equal(v2Flags0x0); + + const v2Flags0x1 = new KDFParams({ + cipher, + hash, + version: 2, + flags: 0x1, + replacementFingerprint: fingerprint + }); + const v2Flags0x1Copy = new KDFParams(); + v2Flags0x1Copy.read(v2Flags0x1.write()); + v2Flags0x1.replacementKDFParams = null; + expect(v2Flags0x1Copy).to.deep.equal(v2Flags0x1); + + const v2Flags0x2 = new KDFParams({ + cipher, + hash, + version: 2, + flags: 0x2, + replacementKDFParams: new Uint8Array([3, 1, cipher, hash]) + }); + const v2Flags0x2Copy = new KDFParams(); + v2Flags0x2Copy.read(v2Flags0x2.write()); + v2Flags0x2.replacementFingerprint = null; + expect(v2Flags0x2Copy).to.deep.equal(v2Flags0x2); + }); +}); diff --git a/test/general/forwarding.js b/test/general/forwarding.js new file mode 100644 index 000000000..f0aa8f471 --- /dev/null +++ b/test/general/forwarding.js @@ -0,0 +1,43 @@ +import { expect } from 'chai'; + +import openpgp from '../initOpenpgp.js'; + +const charlieKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: OpenPGP.js v4.10.4 +Comment: https://openpgpjs.org + +xVgEXqG7KRYJKwYBBAHaRw8BAQdA/q4cs9Pwms3R4trjUd7YyrsRYdQHC9wI +MqLdefob4KUAAQDfy9e8qleM+a1EnPCjDpm69FIY769mo/dpwYlkuI2T/RQt +zSlCb2IgKEZvcndhcmRlZCB0byBDaGFybGllKSA8aW5mb0Bib2IuY29tPsJ4 +BBAWCgAgBQJeobspBgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEACgkQN2cz ++W7U/RnS8AEArtRly8vW6uUSng9EJ0iuIwJpwgZfykSLl/t4u3HTBZ4BALzY +3XsnvKtZZVvaKvFvCUu/2NvC/1yw2wJk9wGbCwEOx3YEXqG7KRIKKwYBBAGX +VQEFAQEHQCGxSJahhDUdTKnlqT3UIn3rXn5i47I4MsG4kSWfTwcOHAIIBwPe +7fJ+kOrMea9aIUeYtGpUzABa9gMBCAcAAP95QjbjU7kyugp39vhi60YW5T8p +Me0kKFCWzmSYzstgGBBbwmEEGBYIAAkFAl6huykCGwwACgkQN2cz+W7U/RkP +WQD+KcU1HKn6PkVJKxg6RS0Q7RcCZwaQ1DyEyjUoneMCRAgA/jUl9uvPAoCS +3+4Wqg9Q//zOwXNImimIPIdpWNXYZJID +=FVvG +-----END PGP PRIVATE KEY BLOCK-----`; + +const fwdCiphertextArmored = `-----BEGIN PGP MESSAGE----- +Version: OpenPGP.js v4.10.4 +Comment: https://openpgpjs.org + +wV4Dog8LAQLriGUSAQdA/I6k0IvGxyNG2SdSDHrv3bZQDWH18OhTWkcmSF0M +Bxcw3w8KMjr2v69ro5cyZztymEXi5RemRx+oPZGKIZ9N5T+26TaOltH7h8eR +Mu4H03Lp0k4BRsjpFNUBL3HsAuMIemNf4369g+szlpuzjNE1KQhQzZbh87AU +T7KAKygwz0EpOWpx2RHtshDy/bZ1EC8Ia4qDAebameIqCU929OmY1uI= +=3iIr +-----END PGP MESSAGE-----`; + +export default () => describe('Forwarding', function() { + it('can decrypt forwarded ciphertext', async function() { + const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); + const msg = await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }); + const result = await openpgp.decrypt({ decryptionKeys: charlieKey, message: msg }); + + expect(result).to.exist; + expect(result.data).to.equal('Hello Bob, hello world'); + }); +}); diff --git a/test/general/index.js b/test/general/index.js index 478a0c5a5..c7af1fe10 100644 --- a/test/general/index.js +++ b/test/general/index.js @@ -4,6 +4,7 @@ import testArmor from './armor.js'; import testPacket from './packet.js'; import testSignature from './signature.js'; import testKey from './key.js'; +import testForwarding from './forwarding.js'; import testOpenPGP from './openpgp.js'; import testConfig from './config.js'; import testOID from './oid.js'; @@ -20,6 +21,7 @@ export default () => describe('General', function () { testPacket(); testSignature(); testKey(); + testForwarding(); testOpenPGP(); testConfig(); testOID(); From 07bedf339292ee199a6931c42cf4ddf5dc8c1043 Mon Sep 17 00:00:00 2001 From: Dan Ristea Date: Thu, 2 Sep 2021 17:50:50 +0200 Subject: [PATCH 03/15] Add symmetric encryption and MAC support To enable stored messages to be protected using symmetric key encryption and validated using message authentication codes, this set of changes adds support for storing symmetric key material as Secret Key Packets, symmetric key encrypted session keys as Public Key Encrypted Session Key Packets, and MAC tags as Signature Packets. Co-authored-by: Konstantinos Andrikopoulos Co-authored-by: Daniel Huigens --- src/crypto/crypto.js | 119 +++++++++++++++++- src/crypto/public_key/hmac.js | 67 ++++++++++ src/crypto/public_key/index.js | 1 + src/crypto/signature.js | 25 +++- src/enums.js | 6 +- src/key/factory.js | 2 + src/key/helper.js | 25 +++- src/key/private_key.js | 18 ++- src/openpgp.js | 10 +- src/packet/public_key.js | 4 +- .../public_key_encrypted_session_key.js | 7 +- src/packet/secret_key.js | 4 +- src/packet/signature.js | 3 +- src/type/enum.js | 78 ++++++++++++ src/type/short_byte_string.js | 32 +++++ test/crypto/crypto.js | 10 +- test/crypto/hmac.js | 34 +++++ test/crypto/index.js | 2 + test/general/key.js | 65 ++++++++++ test/general/packet.js | 113 +++++++++++++++++ test/general/x25519.js | 2 +- 21 files changed, 598 insertions(+), 29 deletions(-) create mode 100644 src/crypto/public_key/hmac.js create mode 100644 src/type/enum.js create mode 100644 src/type/short_byte_string.js create mode 100644 test/crypto/hmac.js diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index 0edf846a0..f63491d50 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -23,16 +23,21 @@ * @module crypto/crypto */ -import { rsa, elliptic, elgamal, dsa } from './public_key'; +import { rsa, elliptic, elgamal, dsa, hmac } from './public_key'; import { getRandomBytes } from './random'; import { getCipherParams } from './cipher'; import ECDHSymkey from '../type/ecdh_symkey'; +import ShortByteString from '../type/short_byte_string'; +import { computeDigest, getHashByteLength } from './hash'; +import config from '../config'; import KDFParams from '../type/kdf_params'; +import { SymAlgoEnum, AEADEnum, HashEnum } from '../type/enum'; import enums from '../enums'; import util from '../util'; import OID from '../type/oid'; import { UnsupportedError } from '../packet/packet'; import ECDHXSymmetricKey from '../type/ecdh_x_symkey'; +import { getAEADMode } from './cipherMode'; /** * Encrypts data using specified algorithm and public key parameters. @@ -40,12 +45,13 @@ import ECDHXSymmetricKey from '../type/ecdh_x_symkey'; * @param {module:enums.publicKey} keyAlgo - Public key algorithm * @param {module:enums.symmetric|null} symmetricAlgo - Cipher algorithm (v3 only) * @param {Object} publicParams - Algorithm-specific public key parameters - * @param {Uint8Array} data - Session key data to be encrypted + * @param {Object} privateParams - Algorithm-specific private key parameters + * @param {Uint8Array} data - Data to be encrypted * @param {Uint8Array} fingerprint - Recipient fingerprint * @returns {Promise} Encrypted session key parameters. * @async */ -export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, data, fingerprint) { +export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, privateParams, data, fingerprint) { switch (keyAlgo) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: { @@ -75,6 +81,21 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, dat const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey }); return { ephemeralPublicKey, C }; } + case enums.publicKey.aead: { + if (!privateParams) { + throw new Error('Cannot encrypt with symmetric key missing private parameters'); + } + const { cipher: algo } = publicParams; + const algoValue = algo.getValue(); + const { keyMaterial } = privateParams; + const aeadMode = config.preferredAEADAlgorithm; + const mode = getAEADMode(config.preferredAEADAlgorithm); + const { ivLength } = mode; + const iv = getRandomBytes(ivLength); + const modeInstance = await mode(algoValue, keyMaterial); + const c = await modeInstance.encrypt(data, iv, new Uint8Array()); + return { aeadMode: new AEADEnum(aeadMode), iv, c: new ShortByteString(c) }; + } default: return []; } @@ -127,6 +148,17 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, return elliptic.ecdhX.decrypt( algo, ephemeralPublicKey, C.wrappedKey, A, k); } + case enums.publicKey.aead: { + const { cipher: algo } = publicKeyParams; + const algoValue = algo.getValue(); + const { keyMaterial } = privateKeyParams; + + const { aeadMode, iv, c } = sessionKeyParams; + + const mode = getAEADMode(aeadMode.getValue()); + const modeInstance = await mode(algoValue, keyMaterial); + return modeInstance.decrypt(c.data, iv, new Uint8Array()); + } default: throw new Error('Unknown public key encryption algorithm.'); } @@ -191,6 +223,13 @@ export function parsePublicKeyParams(algo, bytes) { const A = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(algo)); read += A.length; return { read, publicParams: { A } }; } + case enums.publicKey.hmac: + case enums.publicKey.aead: { + const algo = new SymAlgoEnum(); read += algo.read(bytes); + const digestLength = getHashByteLength(enums.hash.sha256); + const digest = bytes.subarray(read, read + digestLength); read += digestLength; + return { read: read, publicParams: { cipher: algo, digest } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -200,7 +239,7 @@ export function parsePublicKeyParams(algo, bytes) { * Parse private key material in binary form to get the key parameters * @param {module:enums.publicKey} algo - The key algorithm * @param {Uint8Array} bytes - The key material to parse - * @param {Object} publicParams - (ECC only) public params, needed to format some private params + * @param {Object} publicParams - (ECC and symmetric only) public params, needed to format some private params * @returns {{ read: Number, privateParams: Object }} Number of read bytes plus the key parameters referenced by name. */ export function parsePrivateKeyParams(algo, bytes, publicParams) { @@ -248,6 +287,20 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) { const k = util.readExactSubarray(bytes, read, read + payloadSize); read += k.length; return { read, privateParams: { k } }; } + case enums.publicKey.hmac: { + const { cipher: algo } = publicParams; + const keySize = getHashByteLength(algo.getValue()); + const hashSeed = bytes.subarray(read, read + 32); read += 32; + const keyMaterial = bytes.subarray(read, read + keySize); read += keySize; + return { read, privateParams: { hashSeed, keyMaterial } }; + } + case enums.publicKey.aead: { + const { cipher: algo } = publicParams; + const hashSeed = bytes.subarray(read, read + 32); read += 32; + const { keySize } = getCipherParams(algo.getValue()); + const keyMaterial = bytes.subarray(read, read + keySize); read += keySize; + return { read, privateParams: { hashSeed, keyMaterial } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -297,6 +350,20 @@ export function parseEncSessionKeyParams(algo, bytes) { const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read)); return { ephemeralPublicKey, C }; } + // Algorithm-Specific Fields for symmetric AEAD encryption: + // - AEAD algorithm + // - Starting initialization vector + // - Symmetric key encryption of "m" dependent on cipher and AEAD mode prefixed with a one-octet length + // - An authentication tag generated by the AEAD mode. + case enums.publicKey.aead: { + const aeadMode = new AEADEnum(); read += aeadMode.read(bytes.subarray(read)); + const { ivLength } = getAEADMode(aeadMode.getValue()); + + const iv = bytes.subarray(read, read + ivLength); read += ivLength; + const c = new ShortByteString(); read += c.read(bytes.subarray(read)); + + return { aeadMode, iv, c }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -314,7 +381,9 @@ export function serializeParams(algo, params) { enums.publicKey.ed25519, enums.publicKey.x25519, enums.publicKey.ed448, - enums.publicKey.x448 + enums.publicKey.x448, + enums.publicKey.aead, + enums.publicKey.hmac ]); const orderedParams = Object.keys(params).map(name => { const param = params[name]; @@ -329,10 +398,11 @@ export function serializeParams(algo, params) { * @param {module:enums.publicKey} algo - The public key algorithm * @param {Integer} bits - Bit length for RSA keys * @param {module:type/oid} oid - Object identifier for ECC keys + * @param {module:enums.symmetric|enums.hash} symmetric - Hash or cipher algorithm for symmetric keys * @returns {Promise<{ publicParams: {Object}, privateParams: {Object} }>} The parameters referenced by name. * @async */ -export function generateParams(algo, bits, oid) { +export async function generateParams(algo, bits, oid, symmetric) { switch (algo) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: @@ -372,6 +442,14 @@ export function generateParams(algo, bits, oid) { privateParams: { k }, publicParams: { A } })); + case enums.publicKey.hmac: { + const keyMaterial = await hmac.generate(symmetric); + return createSymmetricParams(keyMaterial, new HashEnum(symmetric)); + } + case enums.publicKey.aead: { + const keyMaterial = generateSessionKey(symmetric); + return createSymmetricParams(keyMaterial, new SymAlgoEnum(symmetric)); + } case enums.publicKey.dsa: case enums.publicKey.elgamal: throw new Error('Unsupported algorithm for key generation.'); @@ -380,6 +458,21 @@ export function generateParams(algo, bits, oid) { } } +async function createSymmetricParams(key, algo) { + const seed = getRandomBytes(32); + const bindingHash = await computeDigest(enums.hash.sha256,seed); + return { + privateParams: { + hashSeed: seed, + keyMaterial: key + }, + publicParams: { + cipher: algo, + digest: bindingHash + } + }; +} + /** * Validate algorithm-specific key parameters * @param {module:enums.publicKey} algo - The public key algorithm @@ -434,6 +527,20 @@ export async function validateParams(algo, publicParams, privateParams) { const { k } = privateParams; return elliptic.ecdhX.validateParams(algo, A, k); } + case enums.publicKey.hmac: { + const { cipher: algo, digest } = publicParams; + const { hashSeed, keyMaterial } = privateParams; + const keySize = getHashByteLength(algo.getValue()); + return keySize === keyMaterial.length && + util.equalsUint8Array(digest, await computeDigest(enums.hash.sha256, hashSeed)); + } + case enums.publicKey.aead: { + const { cipher: algo, digest } = publicParams; + const { hashSeed, keyMaterial } = privateParams; + const { keySize } = getCipherParams(algo.getValue()); + return keySize === keyMaterial.length && + util.equalsUint8Array(digest, await computeDigest(enums.hash.sha256, hashSeed)); + } default: throw new Error('Unknown public key algorithm.'); } diff --git a/src/crypto/public_key/hmac.js b/src/crypto/public_key/hmac.js new file mode 100644 index 000000000..f74f20d7a --- /dev/null +++ b/src/crypto/public_key/hmac.js @@ -0,0 +1,67 @@ +import enums from '../../enums'; +import util from '../../util'; + +const supportedHashAlgos = new Set([enums.hash.sha1, enums.hash.sha256, enums.hash.sha512]); + +const webCrypto = util.getWebCrypto(); +const nodeCrypto = util.getNodeCrypto(); + +export async function generate(hashAlgo) { + if (!supportedHashAlgos.has(hashAlgo)) { + throw new Error('Unsupported hash algorithm.'); + } + const hashName = enums.read(enums.webHash, hashAlgo); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const key = await crypto.generateKey( + { + name: 'HMAC', + hash: { name: hashName } + }, + true, + ['sign', 'verify'] + ); + const exportedKey = await crypto.exportKey('raw', key); + return new Uint8Array(exportedKey); +} + +export async function sign(hashAlgo, key, data) { + if (!supportedHashAlgos.has(hashAlgo)) { + throw new Error('Unsupported hash algorithm.'); + } + const hashName = enums.read(enums.webHash, hashAlgo); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const importedKey = await crypto.importKey( + 'raw', + key, + { + name: 'HMAC', + hash: { name: hashName } + }, + false, + ['sign'] + ); + const mac = await crypto.sign('HMAC', importedKey, data); + return new Uint8Array(mac); +} + +export async function verify(hashAlgo, key, mac, data) { + if (!supportedHashAlgos.has(hashAlgo)) { + throw new Error('Unsupported hash algorithm.'); + } + const hashName = enums.read(enums.webHash, hashAlgo); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const importedKey = await crypto.importKey( + 'raw', + key, + { + name: 'HMAC', + hash: { name: hashName } + }, + false, + ['verify'] + ); + return crypto.verify('HMAC', importedKey, mac, data); +} diff --git a/src/crypto/public_key/index.js b/src/crypto/public_key/index.js index f49b4d24e..970c972b4 100644 --- a/src/crypto/public_key/index.js +++ b/src/crypto/public_key/index.js @@ -7,3 +7,4 @@ export * as rsa from './rsa'; export * as elgamal from './elgamal'; export * as elliptic from './elliptic'; export * as dsa from './dsa'; +export * as hmac from './hmac'; diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 3156ebddd..15b933d9a 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -3,9 +3,10 @@ * @module crypto/signature */ -import { elliptic, rsa, dsa } from './public_key'; +import { elliptic, rsa, dsa, hmac } from './public_key'; import enums from '../enums'; import util from '../util'; +import ShortByteString from '../type/short_byte_string'; import { UnsupportedError } from '../packet/packet'; /** @@ -65,7 +66,10 @@ export function parseSignatureParams(algo, signature) { const RS = util.readExactSubarray(signature, read, read + rsSize); read += RS.length; return { read, signatureParams: { RS } }; } - + case enums.publicKey.hmac: { + const mac = new ShortByteString(); read += mac.read(signature.subarray(read)); + return { read, signatureParams: { mac } }; + } default: throw new UnsupportedError('Unknown signature algorithm.'); } @@ -80,12 +84,13 @@ export function parseSignatureParams(algo, signature) { * @param {module:enums.hash} hashAlgo - Hash algorithm * @param {Object} signature - Named algorithm-specific signature parameters * @param {Object} publicParams - Algorithm-specific public key parameters + * @param {Object} privateParams - Algorithm-specific private key parameters * @param {Uint8Array} data - Data for which the signature was created * @param {Uint8Array} hashed - The hashed data * @returns {Promise} True if signature is valid. * @async */ -export async function verify(algo, hashAlgo, signature, publicParams, data, hashed) { +export async function verify(algo, hashAlgo, signature, publicParams, privateParams, data, hashed) { switch (algo) { case enums.publicKey.rsaEncryptSign: case enums.publicKey.rsaEncrypt: @@ -121,6 +126,14 @@ export async function verify(algo, hashAlgo, signature, publicParams, data, hash const { A } = publicParams; return elliptic.eddsa.verify(algo, hashAlgo, signature, data, A, hashed); } + case enums.publicKey.hmac: { + if (!privateParams) { + throw new Error('Cannot verify HMAC signature with symmetric key missing private parameters'); + } + const { cipher: algo } = publicParams; + const { keyMaterial } = privateParams; + return hmac.verify(algo.getValue(), keyMaterial, signature.mac.data, hashed); + } default: throw new Error('Unknown signature algorithm.'); } @@ -176,6 +189,12 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da const { seed } = privateKeyParams; return elliptic.eddsa.sign(algo, hashAlgo, data, A, seed, hashed); } + case enums.publicKey.hmac: { + const { cipher: algo } = publicKeyParams; + const { keyMaterial } = privateKeyParams; + const mac = await hmac.sign(algo.getValue(), keyMaterial, hashed); + return { mac: new ShortByteString(mac) }; + } default: throw new Error('Unknown signature algorithm.'); } diff --git a/src/enums.js b/src/enums.js index 1d5b96cfd..8f39b5eca 100644 --- a/src/enums.js +++ b/src/enums.js @@ -107,7 +107,11 @@ export default { /** Ed25519 (Sign only) */ ed25519: 27, /** Ed448 (Sign only) */ - ed448: 28 + ed448: 28, + /** Persistent symmetric keys: encryption algorithm */ + aead: 100, + /** Persistent symmetric keys: authentication algorithm */ + hmac: 101 }, /** {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC4880, section 9.2} diff --git a/src/key/factory.js b/src/key/factory.js index 955802a3d..e08fa65a7 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -75,6 +75,8 @@ function createKey(packetlist) { * @param {Date} options.date Creation date of the key and the key signatures * @param {Object} config - Full configuration * @param {Array} options.subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}] + * @param {String} options.symmetricHash (optional) hash algorithm for symmetric keys + * @param {String} options.symmetricCipher (optional) cipher algorithm for symmetric keys * sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt * @returns {Promise<{{ key: PrivateKey, revocationCertificate: String }}>} * @async diff --git a/src/key/helper.js b/src/key/helper.js index 50c151b0a..a3eab6c3a 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -17,7 +17,7 @@ export async function generateSecretSubkey(options, config) { const secretSubkeyPacket = new SecretSubkeyPacket(options.date, config); secretSubkeyPacket.packets = null; secretSubkeyPacket.algorithm = enums.write(enums.publicKey, options.algorithm); - await secretSubkeyPacket.generate(options.rsaBits, options.curve); + await secretSubkeyPacket.generate(options.rsaBits, options.curve, options.symmetric); await secretSubkeyPacket.computeFingerprintAndKeyID(); return secretSubkeyPacket; } @@ -26,7 +26,7 @@ export async function generateSecretKey(options, config) { const secretKeyPacket = new SecretKeyPacket(options.date, config); secretKeyPacket.packets = null; secretKeyPacket.algorithm = enums.write(enums.publicKey, options.algorithm); - await secretKeyPacket.generate(options.rsaBits, options.curve, options.config); + await secretKeyPacket.generate(options.rsaBits, options.curve, options.symmetric); await secretKeyPacket.computeFingerprintAndKeyID(); return secretKeyPacket; } @@ -388,6 +388,8 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { options.type = options.type || subkeyDefaults.type; options.curve = options.curve || subkeyDefaults.curve; options.rsaBits = options.rsaBits || subkeyDefaults.rsaBits; + options.symmetricHash = options.symmetricHash || subkeyDefaults.symmetricHash; + options.symmetricCipher = options.symmetricCipher || subkeyDefaults.symmetricCipher; options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime; options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase; options.date = options.date || subkeyDefaults.date; @@ -420,6 +422,23 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { case 'rsa': options.algorithm = enums.publicKey.rsaEncryptSign; break; + case 'symmetric': + if (options.sign) { + options.algorithm = enums.publicKey.hmac; + try { + options.symmetric = enums.write(enums.hash, options.symmetricHash); + } catch (e) { + throw new Error('Unknown hash algorithm'); + } + } else { + options.algorithm = enums.publicKey.aead; + try { + options.symmetric = enums.write(enums.symmetric, options.symmetricCipher); + } catch (e) { + throw new Error('Unknown symmetric algorithm'); + } + } + break; default: throw new Error(`Unsupported key type ${options.type}`); } @@ -435,6 +454,7 @@ export function validateSigningKeyPacket(keyPacket, signature, config) { case enums.publicKey.eddsaLegacy: case enums.publicKey.ed25519: case enums.publicKey.ed448: + case enums.publicKey.hmac: if (!signature.keyFlags && !config.allowMissingKeyFlags) { throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); } @@ -453,6 +473,7 @@ export function validateEncryptionKeyPacket(keyPacket, signature, config) { case enums.publicKey.ecdh: case enums.publicKey.x25519: case enums.publicKey.x448: + case enums.publicKey.aead: if (!signature.keyFlags && !config.allowMissingKeyFlags) { throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); } diff --git a/src/key/private_key.js b/src/key/private_key.js index e82fa118f..2a9b2f292 100644 --- a/src/key/private_key.js +++ b/src/key/private_key.js @@ -39,14 +39,28 @@ class PrivateKey extends PublicKey { toPublic() { const packetlist = new PacketList(); const keyPackets = this.toPacketList(); + let symmetricFound = false; for (const keyPacket of keyPackets) { + if (symmetricFound && + keyPacket.constructor.tag === enums.packet.Signature) { + continue; + } else if (symmetricFound) { + symmetricFound = false; + } switch (keyPacket.constructor.tag) { case enums.packet.secretKey: { + if (keyPacket.algorithm === enums.publicKey.aead || keyPacket.algorithm === enums.publicKey.hmac) { + throw new Error('Cannot create public key from symmetric private'); + } const pubKeyPacket = PublicKeyPacket.fromSecretKeyPacket(keyPacket); packetlist.push(pubKeyPacket); break; } case enums.packet.secretSubkey: { + if (keyPacket.algorithm === enums.publicKey.aead || keyPacket.algorithm === enums.publicKey.hmac) { + symmetricFound = true; + break; + } const pubSubkeyPacket = PublicSubkeyPacket.fromSecretSubkeyPacket(keyPacket); packetlist.push(pubSubkeyPacket); break; @@ -219,10 +233,12 @@ class PrivateKey extends PublicKey { * Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added. * Supports RSA and ECC keys, as well as the newer Curve448 and Curve25519. * Defaults to the algorithm and bit size/curve of the primary key. DSA primary keys default to RSA subkeys. - * @param {ecc|rsa|curve25519|curve448} options.type The subkey algorithm: ECC, RSA, Curve448 or Curve25519 (new format). + * @param {ecc|rsa|curve25519|curve448|symmetric} options.type The subkey algorithm: ECC, RSA, Curve448 or Curve25519 (new format). * Note: Curve448 and Curve25519 are not widely supported yet. * @param {String} options.curve (optional) Elliptic curve for ECC keys * @param {Integer} options.rsaBits (optional) Number of bits for RSA subkeys + * @param {String} options.symmetricCipher (optional) Symmetric algorithm for persistent symmetric aead keys + * @param {String} options.symmetricHash (optional)Hash lgorithm for persistent symmetric hmac keys * @param {Number} options.keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires * @param {Date} options.date (optional) Override the creation date of the key and the key signatures * @param {Boolean} options.sign (optional) Indicates whether the subkey should sign rather than encrypt. Defaults to false diff --git a/src/openpgp.js b/src/openpgp.js index 1b8cfa642..9cb29a73d 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -37,13 +37,15 @@ import { checkKeyRequirements } from './key/helper'; * The generated primary key will have signing capabilities. By default, one subkey with encryption capabilities is also generated. * @param {Object} options * @param {Object|Array} options.userIDs - User IDs as objects: `{ name: 'Jo Doe', email: 'info@jo.com' }` - * @param {'ecc'|'rsa'|'curve448'|'curve25519'} [options.type='ecc'] - The primary key algorithm type: ECC (default for v4 keys), RSA, Curve448 or Curve25519 (new format, default for v6 keys). + * @param {'ecc'|'rsa'|'curve448'|'curve25519'|'symmetric'} [options.type='ecc'] - The primary key algorithm type: ECC (default for v4 keys), RSA, Curve448, Curve25519 (new format, default for v6 keys) or symmetric. * Note: Curve448 and Curve25519 (new format) are not widely supported yet. * @param {String} [options.passphrase=(not protected)] - The passphrase used to encrypt the generated private key. If omitted or empty, the key won't be encrypted. * @param {Number} [options.rsaBits=4096] - Number of bits for RSA keys * @param {String} [options.curve='curve25519Legacy'] - Elliptic curve for ECC keys: * curve25519Legacy (default), nistP256, nistP384, nistP521, secp256k1, * brainpoolP256r1, brainpoolP384r1, or brainpoolP512r1 +* @param {String} options.symmetricCipher (optional) Symmetric algorithm for persistent symmetric aead keys +* @param {String} options.symmetricHash (optional)Hash lgorithm for persistent symmetric hmac keys * @param {Date} [options.date=current date] - Override the creation date of the key and the key signatures * @param {Number} [options.keyExpirationTime=0 (never expires)] - Number of seconds from the key creation time after which the key expires * @param {Array} [options.subkeys=a single encryption subkey] - Options for each subkey e.g. `[{sign: true, passphrase: '123'}]` @@ -55,7 +57,7 @@ import { checkKeyRequirements } from './key/helper'; * @async * @static */ -export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBits = 4096, keyExpirationTime = 0, date = new Date(), subkeys = [{}], format = 'armored', config, ...rest }) { +export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBits = 4096, symmetricHash = 'sha256', symmetricCipher = 'aes256', keyExpirationTime = 0, date = new Date(), subkeys = [{}], format = 'armored', config, ...rest }) { config = { ...defaultConfig, ...config }; checkConfig(config); if (!type && !curve) { type = config.v6Keys ? 'curve25519' : 'ecc'; // default to new curve25519 for v6 keys (legacy curve25519 cannot be used with them) @@ -74,7 +76,7 @@ export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBi throw new Error(`rsaBits should be at least ${config.minRSABits}, got: ${rsaBits}`); } - const options = { userIDs, passphrase, type, rsaBits, curve, keyExpirationTime, date, subkeys }; + const options = { userIDs, passphrase, type, rsaBits, curve, keyExpirationTime, date, subkeys, symmetricHash, symmetricCipher }; try { const { key, revocationCertificate } = await generate(options, config); @@ -82,7 +84,7 @@ export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBi return { privateKey: formatObject(key, format, config), - publicKey: formatObject(key.toPublic(), format, config), + publicKey: type !== 'symmetric' ? formatObject(key.toPublic(), format, config) : null, revocationCertificate }; } catch (err) { diff --git a/src/packet/public_key.js b/src/packet/public_key.js index 4d2235fcc..9f95eb02c 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -264,7 +264,7 @@ class PublicKeyPacket { /** * Returns algorithm information - * @returns {Object} An object of the form {algorithm: String, bits:int, curve:String}. + * @returns {Object} An object of the form {algorithm: String, bits:int, curve:String, symmetric:String}. */ getAlgorithmInfo() { const result = {}; @@ -275,6 +275,8 @@ class PublicKeyPacket { result.bits = util.uint8ArrayBitLength(modulo); } else if (this.publicParams.oid) { result.curve = this.publicParams.oid.getName(); + } else if (this.publicParams.cipher) { + result.symmetric = this.publicParams.cipher.getName(); } return result; } diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 5a751d050..25fa71539 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -182,8 +182,9 @@ class PublicKeyEncryptedSessionKeyPacket { const sessionKeyAlgorithm = this.version === 3 ? this.sessionKeyAlgorithm : null; const fingerprint = key.version === 5 ? key.getFingerprintBytes().subarray(0, 20) : key.getFingerprintBytes(); const encoded = encodeSessionKey(this.version, algo, sessionKeyAlgorithm, this.sessionKey); + const privateParams = algo === enums.publicKey.aead ? key.privateParams : null; this.encrypted = await publicKeyEncrypt( - algo, sessionKeyAlgorithm, key.publicParams, encoded, fingerprint); + algo, sessionKeyAlgorithm, key.publicParams, privateParams, encoded, fingerprint); } /** @@ -230,6 +231,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) { case enums.publicKey.rsaEncryptSign: case enums.publicKey.elgamal: case enums.publicKey.ecdh: + case enums.publicKey.aead: // add checksum return util.concatUint8Array([ new Uint8Array(version === 6 ? [] : [cipherAlgo]), @@ -250,7 +252,8 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: case enums.publicKey.elgamal: - case enums.publicKey.ecdh: { + case enums.publicKey.ecdh: + case enums.publicKey.aead: { // verify checksum in constant time const result = decryptedData.subarray(0, decryptedData.length - 2); const checksum = decryptedData.subarray(decryptedData.length - 2); diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 287f888fa..9d913ce36 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -523,7 +523,7 @@ class SecretKeyPacket extends PublicKeyPacket { } } - async generate(bits, curve) { + async generate(bits, curve, symmetric) { // The deprecated OIDs for Ed25519Legacy and Curve25519Legacy are used in legacy version 4 keys and signatures. // Implementations MUST NOT accept or generate v6 key material using the deprecated OIDs. if (this.version === 6 && ( @@ -532,7 +532,7 @@ class SecretKeyPacket extends PublicKeyPacket { )) { throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${curve}. Generate a key of type 'curve25519' instead`); } - const { privateParams, publicParams } = await generateParams(this.algorithm, bits, curve); + const { privateParams, publicParams } = await generateParams(this.algorithm, bits, curve, symmetric); this.privateParams = privateParams; this.publicParams = publicParams; this.isEncrypted = false; diff --git a/src/packet/signature.js b/src/packet/signature.js index 24963fe0f..43d5d7682 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -781,9 +781,10 @@ class SignaturePacket { } this.params = await this.params; + const privateParams = this.publicKeyAlgorithm === enums.publicKey.hmac ? key.privateParams : null; this[verified] = await signature.verify( - this.publicKeyAlgorithm, this.hashAlgorithm, this.params, key.publicParams, + this.publicKeyAlgorithm, this.hashAlgorithm, this.params, key.publicParams, privateParams, toHash, hash ); diff --git a/src/type/enum.js b/src/type/enum.js new file mode 100644 index 000000000..e697f30c7 --- /dev/null +++ b/src/type/enum.js @@ -0,0 +1,78 @@ +// OpenPGP.js - An OpenPGP implementation in javascript +// Copyright (C) 2015-2016 Decentral +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 3.0 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +/** + * Wrapper for enums + * + * @requires enums + * @module type/enum.js + */ + + +import enums from '../enums'; + +const type_enum = type => class EnumType { + constructor(data) { + if (typeof data === 'undefined') { + this.data = null; + } else { + this.data = data; + } + } + + /** + * Read an enum entry + * @param {Uint8Array} input Where to read the symmetric algo from + */ + read(input) { + const data = input[0]; + this.data = enums.write(type, data); + return 1; + } + + /** + * Write an enum as an integer + * @returns {Uint8Array} An integer representing the algorithm + */ + write() { + return new Uint8Array([this.data]); + } + + /** + * Get the name of the enum entry + * @returns {string} The name string + */ + getName() { + return enums.read(type, this.data); + } + + /** + * Get the integer value of the enum entry + * @returns {Number} The integer value + */ + getValue() { + return this.data; + } +}; +const AEADEnum = type_enum(enums.aead); +const SymAlgoEnum = type_enum(enums.symmetric); +const HashEnum = type_enum(enums.hash); + +export { AEADEnum, SymAlgoEnum, HashEnum }; + +export default type_enum; diff --git a/src/type/short_byte_string.js b/src/type/short_byte_string.js new file mode 100644 index 000000000..1611e16fd --- /dev/null +++ b/src/type/short_byte_string.js @@ -0,0 +1,32 @@ +import util from '../util'; + +class ShortByteString { + constructor(data) { + if (typeof data === 'undefined') { + data = new Uint8Array([]); + } + if (!util.isUint8Array(data)) { + throw new Error('data must be in the form of a Uint8Array'); + } + this.data = data; + this.length = this.data.byteLength; + } + + write() { + return util.concatUint8Array([new Uint8Array([this.length]), this.data]); + } + + read(input) { + if (input.length >= 1) { + const length = input[0]; + if (input.length >= length + 1) { + this.data = input.subarray(1, 1 + length); + this.length = length; + return 1 + length; + } + } + throw new Error('Invalid octet string'); + } +} + +export default ShortByteString; diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index 0fd63fbb0..d6d340920 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -214,7 +214,7 @@ export default () => describe('API functional testing', function() { openpgp.enums.publicKey.rsaEncryptSign, openpgp.enums.hash.sha1, RSAPublicParams, RSAPrivateParams, data, await crypto.computeDigest(2, data) ); const success = await crypto.signature.verify( - openpgp.enums.publicKey.rsaEncryptSign, openpgp.enums.hash.sha1, RSAsignedData, RSAPublicParams, data, await crypto.computeDigest(2, data) + openpgp.enums.publicKey.rsaEncryptSign, openpgp.enums.hash.sha1, RSAsignedData, RSAPublicParams, null, data, await crypto.computeDigest(2, data) ); return expect(success).to.be.true; }); @@ -224,7 +224,7 @@ export default () => describe('API functional testing', function() { openpgp.enums.publicKey.dsa, openpgp.enums.hash.sha1, DSAPublicParams, DSAPrivateParams, data, await crypto.computeDigest(2, data) ); const success = await crypto.signature.verify( - openpgp.enums.publicKey.dsa, openpgp.enums.hash.sha1, DSAsignedData, DSAPublicParams, data, await crypto.computeDigest(2, data) + openpgp.enums.publicKey.dsa, openpgp.enums.hash.sha1, DSAsignedData, DSAPublicParams, null, data, await crypto.computeDigest(2, data) ); return expect(success).to.be.true; @@ -239,7 +239,7 @@ export default () => describe('API functional testing', function() { openpgp.enums.publicKey.ed448, openpgp.enums.hash.sha512, { A }, { seed }, data, toSign ); const success = await crypto.signature.verify( - openpgp.enums.publicKey.ed448, openpgp.enums.hash.sha512, signedData, { A }, data, toSign + openpgp.enums.publicKey.ed448, openpgp.enums.hash.sha512, signedData, { A }, null, data, toSign ); return expect(success).to.be.true; @@ -271,7 +271,7 @@ export default () => describe('API functional testing', function() { it('Asymmetric using RSA with eme_pkcs1 padding', function () { const symmKey = crypto.generateSessionKey(openpgp.enums.symmetric.aes256); - return crypto.publicKeyEncrypt(algoRSA, openpgp.enums.symmetric.aes256, RSAPublicParams, symmKey).then(RSAEncryptedData => { + return crypto.publicKeyEncrypt(algoRSA, openpgp.enums.symmetric.aes256, RSAPublicParams, null, symmKey).then(RSAEncryptedData => { return crypto.publicKeyDecrypt( algoRSA, RSAPublicParams, RSAPrivateParams, RSAEncryptedData ).then(data => { @@ -282,7 +282,7 @@ export default () => describe('API functional testing', function() { it('Asymmetric using Elgamal with eme_pkcs1 padding', function () { const symmKey = crypto.generateSessionKey(openpgp.enums.symmetric.aes256); - return crypto.publicKeyEncrypt(algoElGamal, openpgp.enums.symmetric.aes256, elGamalPublicParams, symmKey).then(ElgamalEncryptedData => { + return crypto.publicKeyEncrypt(algoElGamal, openpgp.enums.symmetric.aes256, elGamalPublicParams, null, symmKey).then(ElgamalEncryptedData => { return crypto.publicKeyDecrypt( algoElGamal, elGamalPublicParams, elGamalPrivateParams, ElgamalEncryptedData ).then(data => { diff --git a/test/crypto/hmac.js b/test/crypto/hmac.js new file mode 100644 index 000000000..61b62e144 --- /dev/null +++ b/test/crypto/hmac.js @@ -0,0 +1,34 @@ +import { use as chaiUse, expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import +chaiUse(chaiAsPromised); + +import { sign, verify } from '../../src/crypto/public_key/hmac'; +import enums from '../../src/enums'; +import util from '../../src/util'; + +export default () => describe('HMAC', function () { + it('Test vectors', async function() { + const vectors = [ + { + algo: enums.hash.sha256, + key: util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), + data: util.hexToUint8Array('4869205468657265'), + expected: util.hexToUint8Array('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7') + }, + { + algo: enums.hash.sha512, + key: util.hexToUint8Array('4a656665'), + data: util.hexToUint8Array('7768617420646f2079612077616e7420666f72206e6f7468696e673f'), + expected: util.hexToUint8Array('164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737') + } + ]; + + await Promise.all(vectors.map(async vec => { + const res = await sign(vec.algo, vec.key, vec.data); + const verified = await verify(vec.algo, vec.key, vec.expected, vec.data); + expect(util.equalsUint8Array(res, vec.expected)).to.be.true; + expect(verified).to.be.true; + })); + }); +}); + diff --git a/test/crypto/index.js b/test/crypto/index.js index a4c73ae5c..ac315a065 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -8,6 +8,7 @@ import testECDH from './ecdh'; import testPKCS5 from './pkcs5'; import testAESKW from './aes_kw'; import testHKDF from './hkdf'; +import testHMAC from './hmac'; import testGCM from './gcm'; import testEAX from './eax'; import testOCB from './ocb'; @@ -25,6 +26,7 @@ export default () => describe('Crypto', function () { testPKCS5(); testAESKW(); testHKDF(); + testHMAC(); testGCM(); testEAX(); testOCB(); diff --git a/test/general/key.js b/test/general/key.js index c2a43dc54..8e5e727f5 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -4725,6 +4725,71 @@ I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== await subkey.verify(); }); + it('create and add a new symmetric subkey to a rsa key', async function() { + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }), + passphrase: 'hello world' + }); + const total = privateKey.getSubkeys().length; + const opt2 = { type: 'symmetric', symmetricCipher: 'aes256' }; + let newPrivateKey = await privateKey.addSubkey(opt2); + const armoredKey = await newPrivateKey.armor(); + newPrivateKey = await openpgp.readKey({ armoredKey: armoredKey }); + const subKey = newPrivateKey.getSubkeys()[total]; + expect(subKey).to.exist; + expect(newPrivateKey.getSubkeys().length).to.be.equal(total + 1); + expect(subKey.getAlgorithmInfo().symmetric).to.be.equal('aes256'); + expect(subKey.keyPacket.publicParams.digest).to.exist.and.to.have.length(32); + expect(subKey.keyPacket.privateParams.keyMaterial).to.exist.and.to.have.length(32); + await subKey.verify(privateKey.primaryKey); + }); + + it('create and add a new rsa key with a symmetric encryption subkey', async function() { + const userId = { name: 'test', email: 'a@b.com' }; + const opt = { rsaBits: 512, userIDs: [userId], subkeys:[{ type: 'symmetric', symmetricCipher: 'aes256' }], format: 'object' }; + + const { privateKey } = await openpgp.generateKey(opt); + const armoredKey = await privateKey.armor(); + const newKey = await openpgp.readKey({ armoredKey: armoredKey }); + const subKey = newKey.getSubkeys()[0]; + expect(newKey.getSubkeys().length).to.be.equal(1); + expect(subKey).to.exist; + expect(subKey.getAlgorithmInfo().symmetric).to.be.equal('aes256'); + expect(subKey.keyPacket.publicParams.cipher).to.exist; + expect(subKey.keyPacket.publicParams.cipher.getValue()).to.be.equal(openpgp.enums.symmetric.aes256); + expect(subKey.keyPacket.publicParams.digest).to.exist.and.to.have.length(32); + expect(subKey.keyPacket.privateParams.keyMaterial).to.exist.and.to.have.length(32); + await subKey.verify(newKey.primaryKey); + }); + + it('create and add a new encrypted rsa key with a symmetric encryption subkey', async function() { + const userId = { name: 'test', email: 'a@b.com' }; + const opt = { rsaBits: 512, userIDs: [userId], subkeys:[{ type: 'symmetric', symmetricCipher: 'aes256' }], format: 'object' }; + + const { privateKey } = await openpgp.generateKey(opt); + const subKey = privateKey.getSubkeys()[0]; + expect(privateKey.getSubkeys().length).to.be.equal(1); + expect(subKey).to.exist; + expect(subKey.getAlgorithmInfo().symmetric).to.be.equal('aes256'); + expect(subKey.keyPacket.publicParams.cipher).to.exist; + expect(subKey.keyPacket.publicParams.cipher.getValue()).to.be.equal(openpgp.enums.symmetric.aes256); + expect(subKey.keyPacket.publicParams.digest).to.exist.and.to.have.length(32); + expect(subKey.keyPacket.privateParams.keyMaterial).to.exist.and.to.have.length(32); + await subKey.verify(privateKey.primaryKey); + }); + + it('create and add a symmetric signing key', async function() { + const opt = { userIDs: [{ name: 'test', email: 'a@b.com' }], type: 'symmetric', symmetricHash: 'sha256', format: 'object' }; + const { privateKey } = await openpgp.generateKey(opt); + const signed = await openpgp.sign({ message: await openpgp.createMessage({ text: 'the data to be signed' }), signingKeys: privateKey, format: 'binary' }); + const message = await openpgp.readMessage({ binaryMessage: signed }); + const { signatures } = await openpgp.verify({ message, verificationKeys: [privateKey], expectSigned: true }); + expect(signatures).to.exist; + expect(signatures.length).to.be.equal(1); + expect(signatures[0].keyID.toHex()).to.be.equal(privateKey.getKeyID().toHex()); + expect(await signatures[0].verified).to.be.true; + }); + it('sign/verify data with the new subkey correctly using curve25519 (legacy format)', async function() { const userID = { name: 'test', email: 'a@b.com' }; const opt = { curve: 'curve25519', userIDs: [userID], format: 'object', subkeys:[] }; diff --git a/test/general/packet.js b/test/general/packet.js index acda172c0..f6d2651ee 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -1225,6 +1225,119 @@ V+HOQJQxXJkVRYa3QrFUehiMzTeqqMdgC6ZqJy7+ }); }); + describe('Persistent symmetric keys', function() { + it('Parse persistent key encrypted session key packet (PKESK encrypted with persistent symmetric key)', () => { + const serializedPacket = new Uint8Array([ + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x64, 0x01, 0x4c, 0x80, 0x3d, 0x9b, 0xb6, + 0x44, 0x13, 0x25, 0x90, 0x24, 0x6e, 0x59, 0x0d, + 0x68, 0xee, 0x1e, 0x23, 0x75, 0x8a, 0x90, 0x85, + 0x78, 0x1b, 0xf7, 0xc4, 0x4d, 0x58, 0xc7, 0x64, + 0xd2, 0xe5, 0xb3, 0x4f, 0xf6, 0x6e, 0xef, 0x53, + 0xc4, 0xc3, 0x76, 0x7b, 0xba, 0xf9, 0x03, 0x86, + 0xee, 0xc0, 0x9d, 0x60, 0x23, 0xa6, 0x8a + ]); + const expectedIV = new Uint8Array([ + 0x4c, 0x80, 0x3d, 0x9b, 0xb6, 0x44, 0x13, 0x25, + 0x90, 0x24, 0x6e, 0x59, 0x0d, 0x68, 0xee, 0x1e + ]); + const expectedCiphertext = new Uint8Array([ + 0x75, 0x8a, 0x90, 0x85, 0x78, 0x1b, 0xf7, 0xc4, + 0x4d, 0x58, 0xc7, 0x64, 0xd2, 0xe5, 0xb3, 0x4f, + 0xf6, 0x6e, 0xef, 0x53, 0xc4, 0xc3, 0x76, 0x7b, + 0xba, 0xf9, 0x03, 0x86, 0xee, 0xc0, 0x9d, 0x60, + 0x23, 0xa6, 0x8a + ]); + const packet = new openpgp.PublicKeyEncryptedSessionKeyPacket(); + packet.read(serializedPacket); + + expect(packet.publicKeyAlgorithm).to.equal(openpgp.enums.publicKey.aead); + expect(packet.encrypted.aeadMode.data).to.equal(openpgp.enums.aead.eax); + expect(util.equalsUint8Array(packet.encrypted.iv, expectedIV)).to.be.true; + expect(util.equalsUint8Array(packet.encrypted.c.data, expectedCiphertext)).to.be.true; + }); + + it('Parse signature packet from persistent symmetric key', () => { + const serializedPacket = new Uint8Array([ + 0x04, 0x00, 0x65, 0x08, 0x00, 0x10, 0x05, 0x02, + 0x60, 0x64, 0x52, 0x28, 0x09, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x6b, 0x1d, 0x40, 0x76, 0xf9, 0x35, 0xf8, + 0xe4, 0xc5, 0x2f, 0x49, 0xce, 0xf7, 0x91, 0x23, + 0xb4, 0x00, 0x3b, 0x77, 0x92, 0x60, 0x2a, 0xfe, + 0x2e, 0x4c, 0xf5, 0x5f, 0x6c, 0x75, 0x80, 0x5a, + 0x3a, 0xf4, 0x38, 0x09, 0x22, 0x97, 0x1a, 0x01, + 0x2e, 0x8a, 0x59, 0x86, 0x26, 0x77, 0x79, 0xe1, + 0xb7, 0xd7, 0x4d, 0x7c, 0xa6, 0x61, 0xe7, 0x00, + 0xf1, 0x4b, 0xa4, 0x5b, 0xc8, 0x3a, 0xe2, 0x35, + 0x9e, 0x67, 0x4a, 0x44 + ]); + + const expectedMAC = new Uint8Array([ + 0x76, 0xf9, 0x35, 0xf8, 0xe4, 0xc5, 0x2f, 0x49, + 0xce, 0xf7, 0x91, 0x23, 0xb4, 0x00, 0x3b, 0x77, + 0x92, 0x60, 0x2a, 0xfe, 0x2e, 0x4c, 0xf5, 0x5f, + 0x6c, 0x75, 0x80, 0x5a, 0x3a, 0xf4, 0x38, 0x09, + 0x22, 0x97, 0x1a, 0x01, 0x2e, 0x8a, 0x59, 0x86, + 0x26, 0x77, 0x79, 0xe1, 0xb7, 0xd7, 0x4d, 0x7c, + 0xa6, 0x61, 0xe7, 0x00, 0xf1, 0x4b, 0xa4, 0x5b, + 0xc8, 0x3a, 0xe2, 0x35, 0x9e, 0x67, 0x4a, 0x44 + ]); + + const packet = new openpgp.SignaturePacket(); + packet.read(serializedPacket); + + const readMAC = packet.params.mac.data; + + expect(packet.publicKeyAlgorithm).to.equal(openpgp.enums.publicKey.hmac); + expect(util.equalsUint8Array(readMAC, expectedMAC)).to.be.true; + }); + + it('encrypt/decrypt using persistent symmetric key', async () => { + const persistentSymmetricKeyPacket = new openpgp.SecretSubkeyPacket(); + persistentSymmetricKeyPacket.version = 4; + persistentSymmetricKeyPacket.algorithm = openpgp.enums.publicKey.aead; + await persistentSymmetricKeyPacket.generate(null, null, openpgp.enums.symmetric.aes256); + await persistentSymmetricKeyPacket.computeFingerprintAndKeyID(); + + const sessionKey = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]); + + const pkesk = openpgp.PublicKeyEncryptedSessionKeyPacket.fromObject({ + version: 3, + encryptionKeyPacket: persistentSymmetricKeyPacket, + sessionKey, + sessionKeyAlgorithm: openpgp.enums.symmetric.aes256 + }); + await pkesk.encrypt(persistentSymmetricKeyPacket); + + const parsedPKESK = new openpgp.PublicKeyEncryptedSessionKeyPacket(); + parsedPKESK.read(pkesk.write()); + await parsedPKESK.decrypt(persistentSymmetricKeyPacket); + expect(util.equalsUint8Array(sessionKey, parsedPKESK.sessionKey)).to.be.true; + }); + + it('throws when encrypting with encrypted symmetric key', async () => { + const persistentSymmetricKeyPacket = new openpgp.SecretSubkeyPacket(); + persistentSymmetricKeyPacket.version = 4; + persistentSymmetricKeyPacket.algorithm = openpgp.enums.publicKey.aead; + await persistentSymmetricKeyPacket.generate(null, null, openpgp.enums.symmetric.aes256); + await persistentSymmetricKeyPacket.computeFingerprintAndKeyID(); + await persistentSymmetricKeyPacket.encrypt('passphrase'); + await persistentSymmetricKeyPacket.clearPrivateParams(); + + const sessionKey = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]); + + const pkesk = openpgp.PublicKeyEncryptedSessionKeyPacket.fromObject({ + version: 3, + encryptionKeyPacket: persistentSymmetricKeyPacket, + sessionKey, + sessionKeyAlgorithm: openpgp.enums.symmetric.aes256 + }); + + await expect(pkesk.encrypt(persistentSymmetricKeyPacket)).to.be.rejectedWith('Cannot encrypt with symmetric key missing private parameters'); + }); + }); + describe('PacketList parsing', function () { it('Ignores unknown packet version with `config.ignoreUnsupportedPackets` enabled', async function() { const armoredSignature = `-----BEGIN PGP SIGNATURE----- diff --git a/test/general/x25519.js b/test/general/x25519.js index e0cc8628f..788f54810 100644 --- a/test/general/x25519.js +++ b/test/general/x25519.js @@ -241,7 +241,7 @@ export default () => describe('X25519 Cryptography (legacy format)', function () expect(R).to.deep.eq(r); expect(S).to.deep.eq(s); }), - signature.verify(22, openpgp.enums.hash.sha256, { r: R, s: S }, publicParams, undefined, data).then(result => { + signature.verify(22, openpgp.enums.hash.sha256, { r: R, s: S }, publicParams, null, undefined, data).then(result => { expect(result).to.be.true; }) ]); From 44327e9c9539f1325586e7649eb13b835a7a2de5 Mon Sep 17 00:00:00 2001 From: larabr Date: Mon, 15 May 2023 18:07:39 +0200 Subject: [PATCH 04/15] Update support for decrypting autoforwarded messages (#6) Update implementation to reflect spec changes to KDF params (v2 -> v255) and new forwarding-related key flag (0x40). --- openpgp.d.ts | 13 ++++ src/config/config.js | 7 ++ src/crypto/public_key/elliptic/ecdh.js | 2 +- src/enums.js | 2 + src/index.js | 2 + src/key/helper.js | 3 +- src/type/kdf_params.js | 52 ++++++-------- test/crypto/ecdh.js | 95 +++++++------------------- test/general/forwarding.js | 66 ++++++++++-------- test/general/key.js | 2 +- 10 files changed, 113 insertions(+), 131 deletions(-) diff --git a/openpgp.d.ts b/openpgp.d.ts index 7a64ea601..46a0d2c2e 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -891,6 +891,7 @@ export namespace enums { encryptStorage = 8, splitPrivateKey = 16, authentication = 32, + forwardedCommunication = 64, sharedPrivateKey = 128 } @@ -937,3 +938,15 @@ export namespace enums { gnu = 101 } } + +interface KDFParamsData { + version: number; + hash: enums.hash; + cipher: enums.symmetric; + replacementFingerprint?: Uint8Array; +} + +export class KDFParams { + constructor(data: KDFParamsData); + write(forReplacementParams?: boolean): Uint8Array; +} diff --git a/src/config/config.js b/src/config/config.js index 9e77710f7..628aed945 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -149,6 +149,13 @@ export default { * @property {Boolean} allowUnauthenticatedStream */ allowUnauthenticatedStream: false, + /** + * Allow decrypting forwarded messages, using keys with 0x40 ('forwarded communication') flag. + * Note: this is related to a **non-standard feature**. + * @memberof module:config + * @property {Boolean} allowForwardedMessages + */ + allowForwardedMessages: false, /** * Minimum RSA key size allowed for key generation and message signing, verification and encryption. * The default is 2047 since due to a bug, previous versions of OpenPGP.js could generate 2047-bit keys instead of 2048-bit ones. diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index b07448a8c..3b8120f3b 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -50,7 +50,7 @@ function buildEcdhParam(public_algo, oid, kdfParams, fingerprint) { return util.concatUint8Array([ oid.write(), new Uint8Array([public_algo]), - kdfParams.replacementKDFParams || kdfParams.write(), + kdfParams.write(true), util.stringToUint8Array('Anonymous Sender '), kdfParams.replacementFingerprint || fingerprint ]); diff --git a/src/enums.js b/src/enums.js index 8f39b5eca..89ff6e4a0 100644 --- a/src/enums.js +++ b/src/enums.js @@ -398,6 +398,8 @@ export default { splitPrivateKey: 16, /** 0x20 - This key may be used for authentication. */ authentication: 32, + /** This key may be used for forwarded communications */ + forwardedCommunication: 64, /** 0x80 - The private component of this key may be in the * possession of more than one person. */ sharedPrivateKey: 128 diff --git a/src/index.js b/src/index.js index 75b320a8d..6d9f5cb71 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,8 @@ export { CleartextMessage, readCleartextMessage, createCleartextMessage } from ' export * from './packet'; +export { default as KDFParams } from './type/kdf_params'; + export * from './encoding/armor'; export { default as enums } from './enums'; diff --git a/src/key/helper.js b/src/key/helper.js index a3eab6c3a..eb1d24af0 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -505,7 +505,8 @@ export function validateDecryptionKeyPacket(keyPacket, signature, config) { return !signature.keyFlags || (signature.keyFlags[0] & enums.keyFlags.encryptCommunication) !== 0 || - (signature.keyFlags[0] & enums.keyFlags.encryptStorage) !== 0; + (signature.keyFlags[0] & enums.keyFlags.encryptStorage) !== 0 || + (config.allowForwardedMessages && (signature.keyFlags[0] & enums.keyFlags.forwardedCommunication) !== 0); } default: return false; diff --git a/src/type/kdf_params.js b/src/type/kdf_params.js index 240fb433b..7a3724742 100644 --- a/src/type/kdf_params.js +++ b/src/type/kdf_params.js @@ -28,34 +28,29 @@ import { UnsupportedError } from '../packet/packet'; * @module type/kdf_params */ import util from '../util'; -import enums from '../enums'; + +const VERSION_FORWARDING = 0xFF; class KDFParams { /** * @param {Integer} version Version, defaults to 1 * @param {enums.hash} hash Hash algorithm * @param {enums.symmetric} cipher Symmetric algorithm - * @param {enums.kdfFlags} flags (v2 only) flags - * @param {Uint8Array} replacementFingerprint (v2 only) fingerprint to use instead of recipient one (v5 keys, the 20 leftmost bytes of the fingerprint) - * @param {Uint8Array} replacementKDFParams (v2 only) serialized KDF params to use in KDF digest computation + * @param {Uint8Array} replacementFingerprint (forwarding only) fingerprint to use instead of recipient one (v5 keys, the 20 leftmost bytes of the fingerprint) */ constructor(data) { if (data) { - const { version, hash, cipher, flags, replacementFingerprint, replacementKDFParams } = data; + const { version, hash, cipher, replacementFingerprint } = data; this.version = version || 1; this.hash = hash; this.cipher = cipher; - this.flags = flags; this.replacementFingerprint = replacementFingerprint; - this.replacementKDFParams = replacementKDFParams; } else { this.version = null; this.hash = null; this.cipher = null; - this.flags = null; this.replacementFingerprint = null; - this.replacementKDFParams = null; } } @@ -65,47 +60,44 @@ class KDFParams { * @returns {Number} Number of read bytes. */ read(input) { - if (input.length < 4 || (input[1] !== 1 && input[1] !== 2)) { + if (input.length < 4 || (input[1] !== 1 && input[1] !== VERSION_FORWARDING)) { throw new UnsupportedError('Cannot read KDFParams'); } + const totalBytes = input[0]; this.version = input[1]; this.hash = input[2]; this.cipher = input[3]; let readBytes = 4; - if (this.version === 2) { - this.flags = input[readBytes++]; - if (this.flags & enums.kdfFlags.replace_fingerprint) { - this.replacementFingerprint = input.slice(readBytes, readBytes + 20); - readBytes += 20; - } - if (this.flags & enums.kdfFlags.replace_kdf_params) { - const fieldLength = input[readBytes] + 1; // account for length - this.replacementKDFParams = input.slice(readBytes, readBytes + fieldLength); - readBytes += fieldLength; - } + if (this.version === VERSION_FORWARDING) { + const fingerprintLength = totalBytes - readBytes + 1; // acount for length byte + this.replacementFingerprint = input.slice(readBytes, readBytes + fingerprintLength); + readBytes += fingerprintLength; } return readBytes; } /** * Write KDFParams to an Uint8Array + * @param {Boolean} [forReplacementParams] - forwarding only: whether to serialize data to use for replacement params * @returns {Uint8Array} Array with the KDFParams value */ - write() { - if (!this.version || this.version === 1) { + write(forReplacementParams) { + if (!this.version || this.version === 1 || forReplacementParams) { return new Uint8Array([3, 1, this.hash, this.cipher]); } - const v2Fields = util.concatUint8Array([ - new Uint8Array([4, 2, this.hash, this.cipher, this.flags]), - this.replacementFingerprint || new Uint8Array(), - this.replacementKDFParams || new Uint8Array() + const forwardingFields = util.concatUint8Array([ + new Uint8Array([ + 3 + this.replacementFingerprint.length, + this.version, + this.hash, + this.cipher + ]), + this.replacementFingerprint ]); - // update length field - v2Fields[0] = v2Fields.length - 1; - return new Uint8Array(v2Fields); + return forwardingFields; } } diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index a56f0ee9d..b370778f1 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -361,78 +361,31 @@ export default () => describe('ECDH key exchange @lightweight', function () { }); }); }); -}); - -describe('KDF parameters', function () { - const fingerprint = new Uint8Array([ - 177, 183, 116, 123, 76, 133, 245, 212, 151, 243, - 236, 71, 245, 86, 3, 168, 101, 74, 209, 105 - ]); - - it('Valid serialization', async function () { - const cipher = openpgp.enums.symmetric.aes256; - const hash = openpgp.enums.hash.sha256; - const v1 = new KDFParams({ cipher, hash }); - const v1Copy = new KDFParams({}); - v1Copy.read(v1.write()); - expect(v1Copy).to.deep.equal(v1); - - const v1Flags0x0 = new KDFParams({ - cipher, - hash, - flags: 0x0 // discarded - }); - const v1Flags0x0Copy = new KDFParams({}); - v1Flags0x0Copy.read(v1Flags0x0.write()); - v1Flags0x0.flags = undefined; - expect(v1Flags0x0Copy).to.deep.equal(v1Flags0x0); - - const v2Flags0x3 = new KDFParams({ - cipher, - hash, - version: 2, - flags: 0x3, - replacementFingerprint: fingerprint, - replacementKDFParams: new Uint8Array([3, 1, cipher, hash]) - }); - const v2Flags0x3Copy = new KDFParams(); - v2Flags0x3Copy.read(v2Flags0x3.write()); - expect(v2Flags0x3Copy).to.deep.equal(v2Flags0x3); - - const v2Flags0x0 = new KDFParams({ - cipher, - hash, - version: 2, - flags: 0x0 - }); - const v2Flags0x0Copy = new KDFParams({}); - v2Flags0x0Copy.read(v2Flags0x0.write()); - - expect(v2Flags0x0Copy).to.deep.equal(v2Flags0x0); - - const v2Flags0x1 = new KDFParams({ - cipher, - hash, - version: 2, - flags: 0x1, - replacementFingerprint: fingerprint - }); - const v2Flags0x1Copy = new KDFParams(); - v2Flags0x1Copy.read(v2Flags0x1.write()); - v2Flags0x1.replacementKDFParams = null; - expect(v2Flags0x1Copy).to.deep.equal(v2Flags0x1); - - const v2Flags0x2 = new KDFParams({ - cipher, - hash, - version: 2, - flags: 0x2, - replacementKDFParams: new Uint8Array([3, 1, cipher, hash]) + describe('KDF parameters', function () { + const fingerprint = new Uint8Array([ + 177, 183, 116, 123, 76, 133, 245, 212, 151, 243, + 236, 71, 245, 86, 3, 168, 101, 74, 209, 105 + ]); + + it('Valid serialization', async function () { + const cipher = openpgp.enums.symmetric.aes256; + const hash = openpgp.enums.hash.sha256; + + const v1 = new KDFParams({ cipher, hash }); + const v1Copy = new KDFParams({}); + v1Copy.read(v1.write()); + expect(v1Copy).to.deep.equal(v1); + + const forwardingFlags = new KDFParams({ + cipher, + hash, + version: 0xFF, + replacementFingerprint: fingerprint + }); + const forwardingFlagsCopy = new KDFParams(); + forwardingFlagsCopy.read(forwardingFlags.write()); + expect(forwardingFlagsCopy).to.deep.equal(forwardingFlags); }); - const v2Flags0x2Copy = new KDFParams(); - v2Flags0x2Copy.read(v2Flags0x2.write()); - v2Flags0x2.replacementFingerprint = null; - expect(v2Flags0x2Copy).to.deep.equal(v2Flags0x2); }); }); diff --git a/test/general/forwarding.js b/test/general/forwarding.js index f0aa8f471..b39fb90b8 100644 --- a/test/general/forwarding.js +++ b/test/general/forwarding.js @@ -3,41 +3,53 @@ import { expect } from 'chai'; import openpgp from '../initOpenpgp.js'; const charlieKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- -Version: OpenPGP.js v4.10.4 -Comment: https://openpgpjs.org - -xVgEXqG7KRYJKwYBBAHaRw8BAQdA/q4cs9Pwms3R4trjUd7YyrsRYdQHC9wI -MqLdefob4KUAAQDfy9e8qleM+a1EnPCjDpm69FIY769mo/dpwYlkuI2T/RQt -zSlCb2IgKEZvcndhcmRlZCB0byBDaGFybGllKSA8aW5mb0Bib2IuY29tPsJ4 -BBAWCgAgBQJeobspBgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEACgkQN2cz -+W7U/RnS8AEArtRly8vW6uUSng9EJ0iuIwJpwgZfykSLl/t4u3HTBZ4BALzY -3XsnvKtZZVvaKvFvCUu/2NvC/1yw2wJk9wGbCwEOx3YEXqG7KRIKKwYBBAGX -VQEFAQEHQCGxSJahhDUdTKnlqT3UIn3rXn5i47I4MsG4kSWfTwcOHAIIBwPe -7fJ+kOrMea9aIUeYtGpUzABa9gMBCAcAAP95QjbjU7kyugp39vhi60YW5T8p -Me0kKFCWzmSYzstgGBBbwmEEGBYIAAkFAl6huykCGwwACgkQN2cz+W7U/RkP -WQD+KcU1HKn6PkVJKxg6RS0Q7RcCZwaQ1DyEyjUoneMCRAgA/jUl9uvPAoCS -3+4Wqg9Q//zOwXNImimIPIdpWNXYZJID -=FVvG + +xVgEZAdtGBYJKwYBBAHaRw8BAQdAcNgHyRGEaqGmzEqEwCobfUkyrJnY8faBvsf9 +R2c5ZzYAAP9bFL4nPBdo04ei0C2IAh5RXOpmuejGC3GAIn/UmL5cYQ+XzRtjaGFy +bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CigQTFggAPAUCZAdtGAmQFXJtmBzDhdcW +IQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbAwIeAQIZAQILBwIVCAIWAAIiAQAAJKYA +/2qY16Ozyo5erNz51UrKViEoWbEpwY3XaFVNzrw+b54YAQC7zXkf/t5ieylvjmA/ +LJz3/qgH5GxZRYAH9NTpWyW1AsdxBGQHbRgSCisGAQQBl1UBBQEBB0CxmxoJsHTW +TiETWh47ot+kwNA1hCk1IYB9WwKxkXYyIBf/CgmKXzV1ODP/mRmtiBYVV+VQk5MF +EAAA/1NW8D8nMc2ky140sPhQrwkeR7rVLKP2fe5n4BEtAnVQEB3CeAQYFggAKgUC +ZAdtGAmQFXJtmBzDhdcWIQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbUAAAl/8A/iIS +zWBsBR8VnoOVfEE+VQk6YAi7cTSjcMjfsIez9FYtAQDKo9aCMhUohYyqvhZjn8aS +3t9mIZPc+zRJtCHzQYmhDg== +=lESj -----END PGP PRIVATE KEY BLOCK-----`; const fwdCiphertextArmored = `-----BEGIN PGP MESSAGE----- -Version: OpenPGP.js v4.10.4 -Comment: https://openpgpjs.org - -wV4Dog8LAQLriGUSAQdA/I6k0IvGxyNG2SdSDHrv3bZQDWH18OhTWkcmSF0M -Bxcw3w8KMjr2v69ro5cyZztymEXi5RemRx+oPZGKIZ9N5T+26TaOltH7h8eR -Mu4H03Lp0k4BRsjpFNUBL3HsAuMIemNf4369g+szlpuzjNE1KQhQzZbh87AU -T7KAKygwz0EpOWpx2RHtshDy/bZ1EC8Ia4qDAebameIqCU929OmY1uI= -=3iIr + +wV4DB27Wn97eACkSAQdA62TlMU2QoGmf5iBLnIm4dlFRkLIg+6MbaatghwxK+Ccw +yGZuVVMAK/ypFfebDf4D/rlEw3cysv213m8aoK8nAUO8xQX3XQq3Sg+EGm0BNV8E +0kABEPyCWARoo5klT1rHPEhelnz8+RQXiOIX3G685XCWdCmaV+tzW082D0xGXSlC +7lM8r1DumNnO8srssko2qIja +=pVRa -----END PGP MESSAGE-----`; export default () => describe('Forwarding', function() { it('can decrypt forwarded ciphertext', async function() { const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); - const msg = await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }); - const result = await openpgp.decrypt({ decryptionKeys: charlieKey, message: msg }); - expect(result).to.exist; - expect(result.data).to.equal('Hello Bob, hello world'); + await expect(openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }), + decryptionKeys: charlieKey + })).to.be.rejectedWith(/Error decrypting message/); + + const result = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }), + decryptionKeys: charlieKey, + config: { allowForwardedMessages: true } + }); + + expect(result.data).to.equal('Message for Bob'); + }); + + it('supports serialising key with KDF params for forwarding', async function() { + const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); + + const serializedKey = charlieKey.write(); + const { data: expectedSerializedKey } = await openpgp.unarmor(charlieKeyArmored); + expect(serializedKey).to.deep.equal(expectedSerializedKey); }); }); diff --git a/test/general/key.js b/test/general/key.js index 8e5e727f5..bbeef1e7d 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -3347,7 +3347,7 @@ QgqsfguR1PqPuJxpXV4bSr6CGAAAAA== })).to.be.rejectedWith(/Unknown curve OID/); }); - it('Parsing ECDH key with unknown kdf param version', async function() { + it.skip('Parsing ECDH key with unknown kdf param version', async function() { // subkey with unknown kdfParam version 255. Parsing should not fail, the subkey should simply dropped const key = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- From 9cf7a384aa3427940079f9e5cea7482886636ee5 Mon Sep 17 00:00:00 2001 From: larabr Date: Wed, 6 Sep 2023 16:39:23 +0200 Subject: [PATCH 05/15] Support generating subkeys with 'forwarded communication' flag to decrypt autoforwarded messages (#8) These subkeys must not have the standard encryption flags (EtEr) set, as they are not supposed to be used for direct messages. Also: - preserve 'forwarded communication' key flag when reformatting - fix bug allowing to decrypt forwarded messages by setting `config.allowInsecureDecryptionWithSigningKeys` instead of `config.allowForwardedMessages` - add TS definition for `config.allowForwardedMessages` --- openpgp.d.ts | 4 +++- src/key/factory.js | 3 ++- src/key/helper.js | 8 ++++++- test/general/forwarding.js | 46 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/openpgp.d.ts b/openpgp.d.ts index 46a0d2c2e..b78cac013 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -328,6 +328,7 @@ interface Config { aeadProtect: boolean; allowUnauthenticatedMessages: boolean; allowUnauthenticatedStream: boolean; + allowForwardedMessages: boolean; minRSABits: number; passwordCollisionCheck: boolean; ignoreUnsupportedPackets: boolean; @@ -713,8 +714,9 @@ interface GenerateKeyOptions { } export type KeyOptions = GenerateKeyOptions; -export interface SubkeyOptions extends Pick { +export interface SubkeyOptions extends Pick { sign?: boolean; + forwarding?: boolean; } export declare class KeyID { diff --git a/src/key/factory.js b/src/key/factory.js index e08fa65a7..238b077f8 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -139,7 +139,8 @@ export async function reformat(options, config) { helper.getLatestValidSignature(subkey.bindingSignatures, secretKeyPacket, enums.signature.subkeyBinding, dataToVerify, null, config) ).catch(() => ({})); return { - sign: bindingSignature.keyFlags && (bindingSignature.keyFlags[0] & enums.keyFlags.signData) + sign: bindingSignature.keyFlags && (bindingSignature.keyFlags[0] & enums.keyFlags.signData), + forwarding: bindingSignature.keyFlags && (bindingSignature.keyFlags[0] & enums.keyFlags.forwardedCommunication) }; })); } diff --git a/src/key/helper.js b/src/key/helper.js index eb1d24af0..e9057e74f 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -94,7 +94,9 @@ export async function createBindingSignature(subkey, primaryKey, options, config signatureType: enums.signature.keyBinding }, options.date, undefined, undefined, undefined, config); } else { - signatureProperties.keyFlags = [enums.keyFlags.encryptCommunication | enums.keyFlags.encryptStorage]; + signatureProperties.keyFlags = options.forwarding ? + [enums.keyFlags.forwardedCommunication] : + [enums.keyFlags.encryptCommunication | enums.keyFlags.encryptStorage]; } if (options.keyExpirationTime > 0) { signatureProperties.keyExpirationTime = options.keyExpirationTime; @@ -395,6 +397,10 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { options.date = options.date || subkeyDefaults.date; options.sign = options.sign || false; + options.forwarding = options.forwarding || false; + if (options.sign && options.forwarding) { + throw new Error('Incompatible options: "sign" and "forwarding" cannot be set together'); + } switch (options.type) { case 'ecc': // NB: this case also handles legacy eddsa and x25519 keys, based on `options.curve` diff --git a/test/general/forwarding.js b/test/general/forwarding.js index b39fb90b8..ad9ec71b0 100644 --- a/test/general/forwarding.js +++ b/test/general/forwarding.js @@ -52,4 +52,50 @@ export default () => describe('Forwarding', function() { const { data: expectedSerializedKey } = await openpgp.unarmor(charlieKeyArmored); expect(serializedKey).to.deep.equal(expectedSerializedKey); }); + + it('generates subkey with forwarding flag (0x40)', async function() { + const { privateKey: armoredKey } = await openpgp.generateKey({ userIDs: { email: 'test@forwarding.it' }, subkeys: [{ forwarding: true }, {}] }); + const privateKey = await openpgp.readKey({ armoredKey }); + + expect(privateKey.subkeys[0].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.forwardedCommunication); + expect(privateKey.subkeys[1].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.encryptCommunication | openpgp.enums.keyFlags.encryptStorage); + }); + + it('reformatting a key preserves its forwarding flags (0x40)', async function() { + // two subkeys, the first with forwarding flag, the second with standard encryption ones + const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZPhkahYJKwYBBAHaRw8BAQdARUPOBft22XPObTCYNRD2VB8ESYHOZsII +XrpUHn2AstUAAQCl30ZHts8cyRRXw7B2595L8RIovkwxhnCRTqe+V92+2BFK +zRQ8dGVzdEBmb3J3YXJkaW5nLml0PsKMBBAWCgA+BYJk+GRqBAsJBwgJkLvy +KUWO/JamAxUICgQWAAIBAhkBApsDAh4BFiEEM00dF5bOjezdbhYlu/IpRY78 +lqYAAP6uAQDt7Xxoh+VUB/xkOX1cj7at7U7zrKAxq7Xh1YbGM+RHKgEAgRoz +UGXKsQigC2KyXGW0nObT8RfUcQIUyrkVdImWiAjHXQRk+GRqEgorBgEEAZdV +AQUBAQdA1E/PrQHG7g8UW7v7fKwgc0x+jTHp8cOa3SGAqd3Pc3gDAQgHAAD/ +TY0mClFVWkDM/W6CnN7pOO36baJ0o1LJAVHucDTbxOgSMMJ4BBgWCAAqBYJk ++GRqCZC78ilFjvyWpgKbQBYhBDNNHReWzo3s3W4WJbvyKUWO/JamAABzegEA +mP3WSG1pceOppv5ncSoZJ9GZoaiXxnkk2TyLvmBQi7kA/1MoAjQDjF3XbX8y +ScSjs3juhSAQ/MnFj8RsDaI7XdIBx10EZPhkahIKKwYBBAGXVQEFAQEHQEyC +E9n5Jo23u9OfoVcUwEfQj4yAMhNBII3j5ePRDaYXAwEIBwAA/2M7YfJN9jV4 +LuiY7ldrWsd875xA5s6I6/8aOtUHuJcYEmPCeAQYFggAKgWCZPhkagmQu/Ip +RY78lqYCmwwWIQQzTR0Xls6N7N1uFiW78ilFjvyWpgAA5KEBAKaoHbyi3wpr +jt2m75fdx10rDOxJDR9H6ilI5ygLWeLsAPoCozX/3KhXLx8WbTe7MFcGl47J +YdgLdgXl0dn/xdXjCQ== +=eC8z +-----END PGP PRIVATE KEY BLOCK-----` }); + + const { privateKey: reformattedKey } = await openpgp.reformatKey({ privateKey, userIDs: { email: 'test@forwarding.it' }, format: 'object' }); + + expect(reformattedKey.subkeys[0].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.forwardedCommunication); + expect(reformattedKey.subkeys[1].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.encryptCommunication | openpgp.enums.keyFlags.encryptStorage); + }); + + it('refuses to encrypt using encryption key with forwarding flag (0x40)', async function() { + const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); + + await expect(openpgp.encrypt({ + message: await openpgp.createMessage({ text: 'abc' }), + encryptionKeys: charlieKey + })).to.be.rejectedWith(/Could not find valid encryption key packet/); + }); }); From bec09a16fa1bba990841f6293af61c2bb945d4c6 Mon Sep 17 00:00:00 2001 From: larabr Date: Wed, 14 Feb 2024 09:59:55 +0100 Subject: [PATCH 06/15] Export Argon2S2K to reuse the WASM module outside of the OpenPGP.js context (#12) --- openpgp.d.ts | 7 +++++++ src/index.js | 1 + 2 files changed, 8 insertions(+) diff --git a/openpgp.d.ts b/openpgp.d.ts index b78cac013..ff15a7ce3 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -941,6 +941,13 @@ export namespace enums { } } +export declare class Argon2S2K { + constructor(config: Config); + salt: Uint8Array; + /** @throws Argon2OutOfMemoryError */ + produceKey(passphrase: string, keySize: number): Promise; +} + interface KDFParamsData { version: number; hash: enums.hash; diff --git a/src/index.js b/src/index.js index 6d9f5cb71..47b46e050 100644 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,7 @@ export { CleartextMessage, readCleartextMessage, createCleartextMessage } from ' export * from './packet'; export { default as KDFParams } from './type/kdf_params'; +export { default as Argon2S2K, Argon2OutOfMemoryError } from './type/s2k/argon2'; export * from './encoding/armor'; From 55f8ab26296f4dc3953aeec7f675e2606eb2f1ab Mon Sep 17 00:00:00 2001 From: larabr Date: Fri, 12 Apr 2024 10:13:36 +0200 Subject: [PATCH 07/15] Add `Argon2S2K.reloadWasmModule()` for manually triggering memory deallocation (#14) Also, make `ARGON2_WASM_MEMORY_THRESHOLD_RELOAD` a static class property, to be able to change its value. --- openpgp.d.ts | 2 ++ src/type/s2k/argon2.js | 25 ++++++++++++++++++++----- test/benchmarks/memory_usage.js | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/openpgp.d.ts b/openpgp.d.ts index ff15a7ce3..2fc4921a6 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -942,6 +942,8 @@ export namespace enums { } export declare class Argon2S2K { + static reloadWasmModule(): void; + static ARGON2_WASM_MEMORY_THRESHOLD_RELOAD: number; constructor(config: Config); salt: Uint8Array; /** @throws Argon2OutOfMemoryError */ diff --git a/src/type/s2k/argon2.js b/src/type/s2k/argon2.js index be357b508..179acf2b3 100644 --- a/src/type/s2k/argon2.js +++ b/src/type/s2k/argon2.js @@ -23,9 +23,26 @@ export class Argon2OutOfMemoryError extends Error { let loadArgonWasmModule; let argon2Promise; // reload wasm module above this treshold, to deallocated used memory -const ARGON2_WASM_MEMORY_THRESHOLD_RELOAD = 2 << 19; +// (cannot be declared as a simple `static` field as its not supported by Safari 14) +let ARGON2_WASM_MEMORY_THRESHOLD_RELOAD = 2 << 19; class Argon2S2K { + static get ARGON2_WASM_MEMORY_THRESHOLD_RELOAD() { + return ARGON2_WASM_MEMORY_THRESHOLD_RELOAD; + } + + static set ARGON2_WASM_MEMORY_THRESHOLD_RELOAD(memoryThreshold) { + ARGON2_WASM_MEMORY_THRESHOLD_RELOAD = memoryThreshold; + } + + static reloadWasmModule() { + if (!loadArgonWasmModule) return; + + // it will be awaited if needed at the next `produceKey` invocation + argon2Promise = loadArgonWasmModule(); + argon2Promise.catch(() => {}); + } + /** * @param {Object} [config] - Full configuration, defaults to openpgp.config */ @@ -125,10 +142,8 @@ class Argon2S2K { }); // a lot of memory was used, reload to deallocate - if (decodedM > ARGON2_WASM_MEMORY_THRESHOLD_RELOAD) { - // it will be awaited if needed at the next `produceKey` invocation - argon2Promise = loadArgonWasmModule(); - argon2Promise.catch(() => {}); + if (decodedM > Argon2S2K.ARGON2_WASM_MEMORY_THRESHOLD_RELOAD) { + Argon2S2K.reloadWasmModule(); } return hash; } catch (e) { diff --git a/test/benchmarks/memory_usage.js b/test/benchmarks/memory_usage.js index c7b6b9afa..206410a42 100644 --- a/test/benchmarks/memory_usage.js +++ b/test/benchmarks/memory_usage.js @@ -335,6 +335,20 @@ class MemoryBenchamrkSuite { await decryptedData.pipeTo(sink); }); + suite.add('openpgp.encrypt/decryptSessionKeys (argon2)', async () => { + const config = { s2kType: openpgp.enums.s2k.argon2 }; + const passwords = 'password'; + const sessionKey = { + algorithm: 'aes128', + data: require('crypto').getRandomValues(new Uint8Array(16)) + }; + const encrypted = await openpgp.encryptSessionKey({ ...sessionKey, passwords, config, format: 'object' }); + assert(encrypted.packets.length === 1); + const skesk = encrypted.packets[0]; + assert(skesk.s2k.type === 'argon2'); + await openpgp.decryptSessionKeys({ message: encrypted, passwords }); + }); + const stats = await suite.run(); // Print JSON stats to stdout console.log(JSON.stringify(stats, null, 4)); From 7cf978092b18b8ea2c9ab961447ed2ed67ef5456 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:17:12 +0100 Subject: [PATCH 08/15] Only add SHA3 preferences to v6 keys To ensure compatibility with older mobile clients, which may not support verifying SHA3 message signatures --- src/key/factory.js | 3 +-- test/general/key.js | 12 ++++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/key/factory.js b/src/key/factory.js index 238b077f8..af1430f08 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -219,8 +219,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options, conf signatureProperties.preferredHashAlgorithms = createPreferredAlgos([ enums.hash.sha512, enums.hash.sha256, - enums.hash.sha3_512, - enums.hash.sha3_256 + ...(secretKeyPacket.version === 6 ? [enums.hash.sha3_512, enums.hash.sha3_256] : []) ], config.preferredHashAlgorithm); signatureProperties.preferredCompressionAlgorithms = createPreferredAlgos([ enums.compression.uncompressed, diff --git a/test/general/key.js b/test/general/key.js index bbeef1e7d..e6861f5ef 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -2261,7 +2261,11 @@ function versionSpecificTests() { ]); } const hash = openpgp.enums.hash; - expect(selfSignature.preferredHashAlgorithms).to.eql([hash.sha512, hash.sha256, hash.sha3_512, hash.sha3_256]); + expect(selfSignature.preferredHashAlgorithms).to.eql( + openpgp.config.v6Keys ? + [hash.sha512, hash.sha256, hash.sha3_512, hash.sha3_256] : + [hash.sha512, hash.sha256] + ); const compr = openpgp.enums.compression; expect(selfSignature.preferredCompressionAlgorithms).to.eql([compr.uncompressed, compr.zlib, compr.zip]); @@ -2313,7 +2317,11 @@ function versionSpecificTests() { ]); } const hash = openpgp.enums.hash; - expect(selfSignature.preferredHashAlgorithms).to.eql([hash.sha224, hash.sha512, hash.sha256, hash.sha3_512, hash.sha3_256]); + expect(selfSignature.preferredHashAlgorithms).to.eql( + openpgp.config.v6Keys ? + [hash.sha224, hash.sha512, hash.sha256, hash.sha3_512, hash.sha3_256] : + [hash.sha224, hash.sha512, hash.sha256] + ); const compr = openpgp.enums.compression; expect(selfSignature.preferredCompressionAlgorithms).to.eql([compr.zlib, compr.uncompressed, compr.zip]); From 9fe278241a0f59c40d6d511ecfd172f9947db5db Mon Sep 17 00:00:00 2001 From: larabr Date: Fri, 19 Apr 2024 16:10:03 +0200 Subject: [PATCH 09/15] Temporarily add `config.ignoreSEIPDv2FeatureFlag` for compatibility (#15) SEIPDv2 is a more secure and faster choice, but it is not necessarily compatible with other libs and our mobile apps. Co-authored-by: Daniel Huigens --- openpgp.d.ts | 1 + src/config/config.js | 8 ++++++++ src/key/helper.js | 2 +- test/general/openpgp.js | 11 +++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpgp.d.ts b/openpgp.d.ts index 2fc4921a6..664040424 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -326,6 +326,7 @@ interface Config { showVersion: boolean; showComment: boolean; aeadProtect: boolean; + ignoreSEIPDv2FeatureFlag: boolean; allowUnauthenticatedMessages: boolean; allowUnauthenticatedStream: boolean; allowForwardedMessages: boolean; diff --git a/src/config/config.js b/src/config/config.js index 628aed945..def9cda1c 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -50,6 +50,14 @@ export default { * @property {Boolean} aeadProtect */ aeadProtect: false, + /** + * Whether to disable encrypton using SEIPDv2 even if the encryption keys include the SEIPDv2 feature flag. + * If true, SEIPDv1 (i.e. no AEAD) packets are always used instead. + * SEIPDv2 is a more secure and faster choice, but it is not necessarily compatible with other libs and our mobile apps. + * @memberof module:config + * @property {Boolean} ignoreSEIPDv2FeatureFlag + */ + ignoreSEIPDv2FeatureFlag: false, /** * When reading OpenPGP v4 private keys (e.g. those generated in OpenPGP.js when not setting `config.v5Keys = true`) * which were encrypted by OpenPGP.js v5 (or older) using `config.aeadProtect = true`, diff --git a/src/key/helper.js b/src/key/helper.js index e9057e74f..311f96f18 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -229,7 +229,7 @@ export async function getPreferredCompressionAlgo(keys = [], date = new Date(), export async function getPreferredCipherSuite(keys = [], date = new Date(), userIDs = [], config = defaultConfig) { const selfSigs = await Promise.all(keys.map((key, i) => key.getPrimarySelfSignature(date, userIDs[i], config))); const withAEAD = keys.length ? - selfSigs.every(selfSig => selfSig.features && (selfSig.features[0] & enums.features.seipdv2)) : + !config.ignoreSEIPDv2FeatureFlag && selfSigs.every(selfSig => selfSig.features && (selfSig.features[0] & enums.features.seipdv2)) : config.aeadProtect; if (withAEAD) { diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 3eb42d70b..3b684ffd9 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -2406,6 +2406,17 @@ k0mXubZvyl4GBg== expect(seipd).to.be.instanceOf(openpgp.SymEncryptedIntegrityProtectedDataPacket); expect(seipd.version).to.equal(2); expect(seipd.aeadAlgorithm).to.equal(openpgp.enums.aead.ocb); + + const encryptedWithoutAEAD = await openpgp.encrypt({ + message: await openpgp.createMessage({ text: 'test' }), + encryptionKeys: [v4PrivateKeyWithOCBPref, v6PrivateKeyWithOCBPref], + format: 'object', + config: { ignoreSEIPDv2FeatureFlag: true } + }); + + const seipdV1 = encryptedWithoutAEAD.packets[2]; + expect(seipdV1).to.be.instanceOf(openpgp.SymEncryptedIntegrityProtectedDataPacket); + expect(seipdV1.version).to.equal(1); }); it('should support encrypting to a key without features (missing SEIPDv1 feature)', async function () { From 0ebf5797a502f5dba48d632060b900c244a005f9 Mon Sep 17 00:00:00 2001 From: larabr Date: Wed, 26 Jun 2024 14:04:10 +0200 Subject: [PATCH 10/15] Use noble-ed25519 over tweetnacl for signature verification (#16) Much faster than tweetnacl, and no constant-timeness required. We are not using v2 for now, despite being smaller, because it relies on bigint literals, and it requires polyfilling the WebCrypto lib manually in Node < 19. --- package-lock.json | 13 ++++++++++++ package.json | 1 + rollup.config.js | 21 +++++++++++++++++++ src/crypto/public_key/elliptic/eddsa.js | 12 ++++++----- .../public_key/elliptic/eddsa_legacy.js | 4 ++-- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb42f1e41..30c31f512 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@noble/ciphers": "^1.0.0", "@noble/curves": "^1.6.0", + "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", @@ -956,6 +957,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@noble/hashes": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", diff --git a/package.json b/package.json index 0bf0f8163..c25a430db 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "devDependencies": { "@noble/ciphers": "^1.0.0", "@noble/curves": "^1.6.0", + "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", diff --git a/rollup.config.js b/rollup.config.js index 349b961ab..2da87fa25 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -100,6 +100,13 @@ const fullBrowserBuild = { commonjs({ ignore: nodeBuiltinModules.concat(nodeDependencies) }), + replace({ + include: 'node_modules/@noble/ed25519/**', + // Rollup ignores the `browser: { crypto: false }` directive in package.json, since `exports` are present, + // hence we need to manually drop it. + "import * as nodeCrypto from 'crypto'": 'const nodeCrypto = null', + delimiters: ['', ''] + }), replace({ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, "import { createRequire } from 'module';": 'const createRequire = () => () => {}', @@ -127,6 +134,13 @@ const lightweightBrowserBuild = { commonjs({ ignore: nodeBuiltinModules.concat(nodeDependencies) }), + replace({ + include: 'node_modules/@noble/ed25519/**', + // Rollup ignores the `browser: { crypto: false }` directive in package.json, since `exports` are present, + // hence we need to manually drop it. + "import * as nodeCrypto from 'crypto'": 'const nodeCrypto = null', + delimiters: ['', ''] + }), replace({ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, "import { createRequire } from 'module';": 'const createRequire = () => () => {}', @@ -158,6 +172,13 @@ const testBuild = { ignore: nodeBuiltinModules.concat(nodeDependencies), requireReturnsDefault: 'preferred' }), + replace({ + include: 'node_modules/@noble/ed25519/**', + // Rollup ignores the `browser: { crypto: false }` directive in package.json, since `exports` are present, + // hence we need to manually drop it. + "import * as nodeCrypto from 'crypto'": 'const nodeCrypto = null', + delimiters: ['', ''] + }), replace({ "import { createRequire } from 'module';": 'const createRequire = () => () => {}', delimiters: ['', ''] diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index 79d68db4d..d3114fce5 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -20,7 +20,9 @@ * @module crypto/public_key/elliptic/eddsa */ -import ed25519 from '@openpgp/tweetnacl'; +import naclEd25519 from '@openpgp/tweetnacl'; // better constant-timeness as it uses Uint8Arrays over BigInts +import { verify as nobleEd25519Verify } from '@noble/ed25519'; + import util from '../../../util'; import enums from '../../../enums'; import { getHashByteLength } from '../../hash'; @@ -52,7 +54,7 @@ export async function generate(algo) { throw err; } const seed = getRandomBytes(getPayloadSize(algo)); - const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed); + const { publicKey: A } = naclEd25519.sign.keyPair.fromSeed(seed); return { A, seed }; } @@ -104,7 +106,7 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe throw err; } const secretKey = util.concatUint8Array([privateKey, publicKey]); - const signature = ed25519.sign.detached(hashed, secretKey); + const signature = naclEd25519.sign.detached(hashed, secretKey); return { RS: signature }; } @@ -149,7 +151,7 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { if (err.name !== 'NotSupportedError') { throw err; } - return ed25519.sign.detached.verify(hashed, RS, publicKey); + return nobleEd25519Verify(RS, hashed, publicKey); } case enums.publicKey.ed448: { @@ -177,7 +179,7 @@ export async function validateParams(algo, A, seed) { * 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); + const { publicKey } = naclEd25519.sign.keyPair.fromSeed(seed); return util.equalsUint8Array(A, publicKey); } diff --git a/src/crypto/public_key/elliptic/eddsa_legacy.js b/src/crypto/public_key/elliptic/eddsa_legacy.js index 682f53bba..e4eb8d95b 100644 --- a/src/crypto/public_key/elliptic/eddsa_legacy.js +++ b/src/crypto/public_key/elliptic/eddsa_legacy.js @@ -21,7 +21,7 @@ * @module crypto/public_key/elliptic/eddsa_legacy */ -import nacl from '@openpgp/tweetnacl'; +import naclEd25519 from '@openpgp/tweetnacl'; // better constant-timeness as it uses Uint8Arrays over BigInts import util from '../../../util'; import enums from '../../../enums'; import { getHashByteLength } from '../../hash'; @@ -101,7 +101,7 @@ export async function validateParams(oid, Q, k) { * Derive public point Q' = dG from private key * and expect Q == Q' */ - const { publicKey } = nacl.sign.keyPair.fromSeed(k); + const { publicKey } = naclEd25519.sign.keyPair.fromSeed(k); const dG = new Uint8Array([0x40, ...publicKey]); // Add public key prefix return util.equalsUint8Array(Q, dG); From f1f7ca972f2d7230cd8a627c116aec20d339129a Mon Sep 17 00:00:00 2001 From: larabr Date: Mon, 25 Nov 2024 11:24:48 +0100 Subject: [PATCH 11/15] PQC: Implement draft RFC for ML-KEM with X25519 (#10) Implements Draft 6 with custom KEM combiner (https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/06/) Co-authored-by: Daniel Huigens --- package-lock.json | 27 +++ package.json | 1 + src/crypto/crypto.js | 61 ++++++- src/crypto/public_key/index.js | 1 + src/crypto/public_key/post_quantum/index.js | 5 + .../public_key/post_quantum/kem/ecc_kem.js | 62 +++++++ .../public_key/post_quantum/kem/index.js | 2 + src/crypto/public_key/post_quantum/kem/kem.js | 55 ++++++ .../public_key/post_quantum/kem/ml_kem.js | 72 ++++++++ src/enums.js | 2 + src/key/helper.js | 11 +- src/packet/public_key.js | 4 + .../public_key_encrypted_session_key.js | 12 +- src/packet/secret_key.js | 7 +- test/crypto/crypto.js | 8 +- test/crypto/index.js | 2 + test/crypto/postQuantum.js | 158 ++++++++++++++++++ test/crypto/validate.js | 34 +++- test/general/key.js | 11 ++ 19 files changed, 519 insertions(+), 16 deletions(-) create mode 100644 src/crypto/public_key/post_quantum/index.js create mode 100644 src/crypto/public_key/post_quantum/kem/ecc_kem.js create mode 100644 src/crypto/public_key/post_quantum/kem/index.js create mode 100644 src/crypto/public_key/post_quantum/kem/kem.js create mode 100644 src/crypto/public_key/post_quantum/kem/ml_kem.js create mode 100644 test/crypto/postQuantum.js diff --git a/package-lock.json b/package-lock.json index 30c31f512..9643bcc63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@noble/curves": "^1.6.0", "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", + "@noble/post-quantum": "^0.2.1", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/tweetnacl": "^1.0.4-1", @@ -981,6 +982,32 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/post-quantum": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.2.1.tgz", + "integrity": "sha512-ImgfMp9notXSEocz464o1AefYfFWEkkszKMGO+ZiTn73yIBFeNyEHKQUMS+SheJwSNymldSts6YyVcQDjcnVVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.6.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index c25a430db..ae557f97d 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@noble/curves": "^1.6.0", "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", + "@noble/post-quantum": "^0.2.1", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/tweetnacl": "^1.0.4-1", diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index f63491d50..264739657 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -23,7 +23,7 @@ * @module crypto/crypto */ -import { rsa, elliptic, elgamal, dsa, hmac } from './public_key'; +import { rsa, elliptic, elgamal, dsa, hmac, postQuantum } from './public_key'; import { getRandomBytes } from './random'; import { getCipherParams } from './cipher'; import ECDHSymkey from '../type/ecdh_symkey'; @@ -96,6 +96,12 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, pri const c = await modeInstance.encrypt(data, iv, new Uint8Array()); return { aeadMode: new AEADEnum(aeadMode), iv, c: new ShortByteString(c) }; } + case enums.publicKey.pqc_mlkem_x25519: { + const { eccPublicKey, mlkemPublicKey } = publicParams; + const { eccCipherText, mlkemCipherText, wrappedKey } = await postQuantum.kem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data); + const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey }); + return { eccCipherText, mlkemCipherText, C }; + } default: return []; } @@ -115,8 +121,8 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, pri * @throws {Error} on sensitive decryption error, unless `randomPayload` is given * @async */ -export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, sessionKeyParams, fingerprint, randomPayload) { - switch (algo) { +export async function publicKeyDecrypt(keyAlgo, publicKeyParams, privateKeyParams, sessionKeyParams, fingerprint, randomPayload) { + switch (keyAlgo) { case enums.publicKey.rsaEncryptSign: case enums.publicKey.rsaEncrypt: { const { c } = sessionKeyParams; @@ -146,7 +152,7 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, throw new Error('AES session key expected'); } return elliptic.ecdhX.decrypt( - algo, ephemeralPublicKey, C.wrappedKey, A, k); + keyAlgo, ephemeralPublicKey, C.wrappedKey, A, k); } case enums.publicKey.aead: { const { cipher: algo } = publicKeyParams; @@ -159,6 +165,12 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, const modeInstance = await mode(algoValue, keyMaterial); return modeInstance.decrypt(c.data, iv, new Uint8Array()); } + case enums.publicKey.pqc_mlkem_x25519: { + const { eccSecretKey, mlkemSecretKey } = privateKeyParams; + const { eccPublicKey, mlkemPublicKey } = publicKeyParams; + const { eccCipherText, mlkemCipherText, C } = sessionKeyParams; + return postQuantum.kem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, C.wrappedKey); + } default: throw new Error('Unknown public key encryption algorithm.'); } @@ -230,6 +242,11 @@ export function parsePublicKeyParams(algo, bytes) { const digest = bytes.subarray(read, read + digestLength); read += digestLength; return { read: read, publicParams: { cipher: algo, digest } }; } + case enums.publicKey.pqc_mlkem_x25519: { + const eccPublicKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccPublicKey.length; + const mlkemPublicKey = util.readExactSubarray(bytes, read, read + 1184); read += mlkemPublicKey.length; + return { read, publicParams: { eccPublicKey, mlkemPublicKey } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -242,7 +259,7 @@ export function parsePublicKeyParams(algo, bytes) { * @param {Object} publicParams - (ECC and symmetric only) public params, needed to format some private params * @returns {{ read: Number, privateParams: Object }} Number of read bytes plus the key parameters referenced by name. */ -export function parsePrivateKeyParams(algo, bytes, publicParams) { +export async function parsePrivateKeyParams(algo, bytes, publicParams) { let read = 0; switch (algo) { case enums.publicKey.rsaEncrypt: @@ -301,6 +318,12 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) { const keyMaterial = bytes.subarray(read, read + keySize); read += keySize; return { read, privateParams: { hashSeed, keyMaterial } }; } + case enums.publicKey.pqc_mlkem_x25519: { + const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccSecretKey.length; + const mlkemSeed = util.readExactSubarray(bytes, read, read + 64); read += mlkemSeed.length; + const { mlkemSecretKey } = await postQuantum.kem.mlkemExpandSecretSeed(algo, mlkemSeed); + return { read, privateParams: { eccSecretKey, mlkemSecretKey, mlkemSeed } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -364,6 +387,12 @@ export function parseEncSessionKeyParams(algo, bytes) { return { aeadMode, iv, c }; } + case enums.publicKey.pqc_mlkem_x25519: { + const eccCipherText = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccCipherText.length; + const mlkemCipherText = util.readExactSubarray(bytes, read, read + 1088); read += mlkemCipherText.length; + const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read)); + return { eccCipherText, mlkemCipherText, C }; // eccCipherText || mlkemCipherText || len(C) || C + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -383,9 +412,19 @@ export function serializeParams(algo, params) { enums.publicKey.ed448, enums.publicKey.x448, enums.publicKey.aead, - enums.publicKey.hmac + enums.publicKey.hmac, + enums.publicKey.pqc_mlkem_x25519, ]); + + const excludedFields = { + [enums.publicKey.pqc_mlkem_x25519]: new Set(['mlkemSecretKey']) // only `mlkemSeed` is serialized + }; + const orderedParams = Object.keys(params).map(name => { + if (excludedFields[algo]?.has(name)) { + return new Uint8Array(); + } + const param = params[name]; if (!util.isUint8Array(param)) return param.write(); return algosWithNativeRepresentation.has(algo) ? param : util.uint8ArrayToMPI(param); @@ -450,6 +489,11 @@ export async function generateParams(algo, bits, oid, symmetric) { const keyMaterial = generateSessionKey(symmetric); return createSymmetricParams(keyMaterial, new SymAlgoEnum(symmetric)); } + case enums.publicKey.pqc_mlkem_x25519: + return postQuantum.kem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSeed, mlkemSecretKey, mlkemPublicKey }) => ({ + privateParams: { eccSecretKey, mlkemSeed, mlkemSecretKey }, + publicParams: { eccPublicKey, mlkemPublicKey } + })); case enums.publicKey.dsa: case enums.publicKey.elgamal: throw new Error('Unsupported algorithm for key generation.'); @@ -541,6 +585,11 @@ export async function validateParams(algo, publicParams, privateParams) { return keySize === keyMaterial.length && util.equalsUint8Array(digest, await computeDigest(enums.hash.sha256, hashSeed)); } + case enums.publicKey.pqc_mlkem_x25519: { + const { eccSecretKey, mlkemSeed } = privateParams; + const { eccPublicKey, mlkemPublicKey } = publicParams; + return postQuantum.kem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed); + } default: throw new Error('Unknown public key algorithm.'); } diff --git a/src/crypto/public_key/index.js b/src/crypto/public_key/index.js index 970c972b4..67403de5c 100644 --- a/src/crypto/public_key/index.js +++ b/src/crypto/public_key/index.js @@ -8,3 +8,4 @@ export * as elgamal from './elgamal'; export * as elliptic from './elliptic'; export * as dsa from './dsa'; export * as hmac from './hmac'; +export * as postQuantum from './post_quantum'; diff --git a/src/crypto/public_key/post_quantum/index.js b/src/crypto/public_key/post_quantum/index.js new file mode 100644 index 000000000..982b28be7 --- /dev/null +++ b/src/crypto/public_key/post_quantum/index.js @@ -0,0 +1,5 @@ +import * as kem from './kem/index'; + +export { + kem +}; diff --git a/src/crypto/public_key/post_quantum/kem/ecc_kem.js b/src/crypto/public_key/post_quantum/kem/ecc_kem.js new file mode 100644 index 000000000..a133c18a1 --- /dev/null +++ b/src/crypto/public_key/post_quantum/kem/ecc_kem.js @@ -0,0 +1,62 @@ +import * as ecdhX from '../../elliptic/ecdh_x'; +import { computeDigest } from '../../../hash'; +import util from '../../../../util'; +import enums from '../../../../enums'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { A, k } = await ecdhX.generate(enums.publicKey.x25519); + return { + eccPublicKey: A, + eccSecretKey: k + }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function encaps(eccAlgo, eccRecipientPublicKey) { + switch (eccAlgo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { ephemeralPublicKey: eccCipherText, sharedSecret: eccSharedSecret } = await ecdhX.generateEphemeralEncryptionMaterial(enums.publicKey.x25519, eccRecipientPublicKey); + const eccKeyShare = await computeDigest(enums.hash.sha3_256, util.concatUint8Array([ + eccSharedSecret, + eccCipherText, + eccRecipientPublicKey + ])); + return { + eccCipherText, + eccKeyShare + }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function decaps(eccAlgo, eccCipherText, eccSecretKey, eccPublicKey) { + switch (eccAlgo) { + case enums.publicKey.pqc_mlkem_x25519: { + const eccSharedSecret = await ecdhX.recomputeSharedSecret(enums.publicKey.x25519, eccCipherText, eccPublicKey, eccSecretKey); + const eccKeyShare = await computeDigest(enums.hash.sha3_256, util.concatUint8Array([ + eccSharedSecret, + eccCipherText, + eccPublicKey + ])); + return eccKeyShare; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function validateParams(algo, eccPublicKey, eccSecretKey) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: + return ecdhX.validateParams(enums.publicKey.x25519, eccPublicKey, eccSecretKey); + default: + throw new Error('Unsupported KEM algorithm'); + } +} diff --git a/src/crypto/public_key/post_quantum/kem/index.js b/src/crypto/public_key/post_quantum/kem/index.js new file mode 100644 index 000000000..399750ad7 --- /dev/null +++ b/src/crypto/public_key/post_quantum/kem/index.js @@ -0,0 +1,2 @@ +export { generate, encrypt, decrypt, validateParams } from './kem'; +export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem'; diff --git a/src/crypto/public_key/post_quantum/kem/kem.js b/src/crypto/public_key/post_quantum/kem/kem.js new file mode 100644 index 000000000..1a78de5de --- /dev/null +++ b/src/crypto/public_key/post_quantum/kem/kem.js @@ -0,0 +1,55 @@ +import * as eccKem from './ecc_kem'; +import * as mlKem from './ml_kem'; +import * as aesKW from '../../../aes_kw'; +import util from '../../../../util'; +import enums from '../../../../enums'; +import { computeDigest } from '../../../hash'; + +export async function generate(algo) { + const { eccPublicKey, eccSecretKey } = await eccKem.generate(algo); + const { mlkemPublicKey, mlkemSeed, mlkemSecretKey } = await mlKem.generate(algo); + + return { eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed, mlkemSecretKey }; +} + +export async function encrypt(algo, eccPublicKey, mlkemPublicKey, sessioneKeyData) { + const { eccKeyShare, eccCipherText } = await eccKem.encaps(algo, eccPublicKey); + const { mlkemKeyShare, mlkemCipherText } = await mlKem.encaps(algo, mlkemPublicKey); + const kek = await multiKeyCombine(algo, eccKeyShare, eccCipherText, eccPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey); + const wrappedKey = await aesKW.wrap(enums.symmetric.aes256, kek, sessioneKeyData); // C + return { eccCipherText, mlkemCipherText, wrappedKey }; +} + +export async function decrypt(algo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, encryptedSessionKeyData) { + const eccKeyShare = await eccKem.decaps(algo, eccCipherText, eccSecretKey, eccPublicKey); + const mlkemKeyShare = await mlKem.decaps(algo, mlkemCipherText, mlkemSecretKey); + const kek = await multiKeyCombine(algo, eccKeyShare, eccCipherText, eccPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey); + const sessionKey = await aesKW.unwrap(enums.symmetric.aes256, kek, encryptedSessionKeyData); + return sessionKey; +} + +async function multiKeyCombine(algo, ecdhKeyShare, ecdhCipherText, ecdhPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey) { + // LAMPS-aligned and NIST compatible combiner, proposed in: https://mailarchive.ietf.org/arch/msg/openpgp/NMTCy707LICtxIhP3Xt1U5C8MF0/ + // 2a. KDF(mlkemSS || tradSS || tradCT || tradPK || Domain) + // where Domain is "Domain" for LAMPS, and "mlkemCT || mlkemPK || algId" for OpenPGP + const encData = util.concatUint8Array([ + mlkemKeyShare, + ecdhKeyShare, + ecdhCipherText, + ecdhPublicKey, + // domSep + mlkemCipherText, + mlkemPublicKey, + new Uint8Array([algo]) + ]); + + const kek = await computeDigest(enums.hash.sha3_256, encData); + return kek; +} + +export async function validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed) { + const eccValidationPromise = eccKem.validateParams(algo, eccPublicKey, eccSecretKey); + const mlkemValidationPromise = mlKem.validateParams(algo, mlkemPublicKey, mlkemSeed); + const valid = await eccValidationPromise && await mlkemValidationPromise; + return valid; +} diff --git a/src/crypto/public_key/post_quantum/kem/ml_kem.js b/src/crypto/public_key/post_quantum/kem/ml_kem.js new file mode 100644 index 000000000..3789459c6 --- /dev/null +++ b/src/crypto/public_key/post_quantum/kem/ml_kem.js @@ -0,0 +1,72 @@ +import enums from '../../../../enums'; +import util from '../../../../util'; +import { getRandomBytes } from '../../../random'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const mlkemSeed = getRandomBytes(64); + const { mlkemSecretKey, mlkemPublicKey } = await expandSecretSeed(algo, mlkemSeed); + + return { mlkemSeed, mlkemSecretKey, mlkemPublicKey }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +/** + * Expand ML-KEM secret seed and retrieve the secret and public key material + * @param {module:enums.publicKey} algo - Public key algorithm + * @param {Uint8Array} seed - secret seed to expand + * @returns {Promise<{ mlkemPublicKey: Uint8Array, mlkemSecretKey: Uint8Array }>} + */ +export async function expandSecretSeed(algo, seed) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { ml_kem768 } = await import('@noble/post-quantum/ml-kem'); + const { publicKey: encapsulationKey, secretKey: decapsulationKey } = ml_kem768.keygen(seed); + + return { mlkemPublicKey: encapsulationKey, mlkemSecretKey: decapsulationKey }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function encaps(algo, mlkemRecipientPublicKey) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { ml_kem768 } = await import('@noble/post-quantum/ml-kem'); + const { cipherText: mlkemCipherText, sharedSecret: mlkemKeyShare } = ml_kem768.encapsulate(mlkemRecipientPublicKey); + + return { mlkemCipherText, mlkemKeyShare }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function decaps(algo, mlkemCipherText, mlkemSecretKey) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { ml_kem768 } = await import('@noble/post-quantum/ml-kem'); + const mlkemKeyShare = ml_kem768.decapsulate(mlkemCipherText, mlkemSecretKey); + + return mlkemKeyShare; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function validateParams(algo, mlkemPublicKey, mlkemSeed) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { mlkemPublicKey: expectedPublicKey } = await expandSecretSeed(algo, mlkemSeed); + return util.equalsUint8Array(mlkemPublicKey, expectedPublicKey); + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} diff --git a/src/enums.js b/src/enums.js index 89ff6e4a0..59ed7e5d6 100644 --- a/src/enums.js +++ b/src/enums.js @@ -108,6 +108,8 @@ export default { ed25519: 27, /** Ed448 (Sign only) */ ed448: 28, + /** Post-quantum ML-KEM-768 + X25519 (Encrypt only) */ + pqc_mlkem_x25519: 105, /** Persistent symmetric keys: encryption algorithm */ aead: 100, /** Persistent symmetric keys: authentication algorithm */ diff --git a/src/key/helper.js b/src/key/helper.js index 311f96f18..10acbd202 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -403,6 +403,13 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { } switch (options.type) { + case 'pqc': + if (options.sign) { + throw new Error('Post-quantum signing algorithms are not yet supported.'); + } else { + options.algorithm = enums.publicKey.pqc_mlkem_x25519; + } + break; case 'ecc': // NB: this case also handles legacy eddsa and x25519 keys, based on `options.curve` try { options.curve = enums.write(enums.curve, options.curve); @@ -480,6 +487,7 @@ export function validateEncryptionKeyPacket(keyPacket, signature, config) { case enums.publicKey.x25519: case enums.publicKey.x448: case enums.publicKey.aead: + case enums.publicKey.pqc_mlkem_x25519: if (!signature.keyFlags && !config.allowMissingKeyFlags) { throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); } @@ -502,7 +510,8 @@ export function validateDecryptionKeyPacket(keyPacket, signature, config) { case enums.publicKey.elgamal: case enums.publicKey.ecdh: case enums.publicKey.x25519: - case enums.publicKey.x448: { + case enums.publicKey.x448: + case enums.publicKey.pqc_mlkem_x25519: { const isValidSigningKeyPacket = !signature.keyFlags || (signature.keyFlags[0] & enums.keyFlags.signData) !== 0; if (isValidSigningKeyPacket && config.allowInsecureDecryptionWithSigningKeys) { // This is only relevant for RSA keys, all other signing algorithms cannot decrypt diff --git a/src/packet/public_key.js b/src/packet/public_key.js index 9f95eb02c..cb316cbad 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -138,6 +138,10 @@ class PublicKeyPacket { ) { throw new Error('Legacy curve25519 cannot be used with v6 keys'); } + // The composite ML-KEM + ECDH schemes MUST be used only with v6 keys. + if (this.version !== 6 && this.algorithm === enums.publicKey.pqc_mlkem_x25519) { + throw new Error('Unexpected key version: ML-KEM algorithms can only be used with v6 keys'); + } this.publicParams = publicParams; pos += read; diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 25fa71539..c4d060d5d 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -21,6 +21,12 @@ import enums from '../enums'; import util from '../util'; import { UnsupportedError } from './packet'; +const algosWithV3CleartextSessionKeyAlgorithm = new Set([ + enums.publicKey.x25519, + enums.publicKey.x448, + enums.publicKey.pqc_mlkem_x25519 +]); + /** * Public-Key Encrypted Session Key Packets (Tag 1) * @@ -128,7 +134,7 @@ class PublicKeyEncryptedSessionKeyPacket { } this.publicKeyAlgorithm = bytes[offset++]; this.encrypted = parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset)); - if (this.publicKeyAlgorithm === enums.publicKey.x25519 || this.publicKeyAlgorithm === enums.publicKey.x448) { + if (algosWithV3CleartextSessionKeyAlgorithm.has(this.publicKeyAlgorithm)) { if (this.version === 3) { this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm); } else if (this.encrypted.C.algorithm !== null) { @@ -211,7 +217,7 @@ class PublicKeyEncryptedSessionKeyPacket { if (this.version === 3) { // v3 Montgomery curves have cleartext cipher algo - const hasEncryptedAlgo = this.publicKeyAlgorithm !== enums.publicKey.x25519 && this.publicKeyAlgorithm !== enums.publicKey.x448; + const hasEncryptedAlgo = !algosWithV3CleartextSessionKeyAlgorithm.has(this.publicKeyAlgorithm); this.sessionKeyAlgorithm = hasEncryptedAlgo ? sessionKeyAlgorithm : this.sessionKeyAlgorithm; if (sessionKey.length !== getCipherParams(this.sessionKeyAlgorithm).keySize) { @@ -240,6 +246,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) { ]); case enums.publicKey.x25519: case enums.publicKey.x448: + case enums.publicKey.pqc_mlkem_x25519: return sessionKeyData; default: throw new Error('Unsupported public key algorithm'); @@ -288,6 +295,7 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { } case enums.publicKey.x25519: case enums.publicKey.x448: + case enums.publicKey.pqc_mlkem_x25519: return { sessionKeyAlgorithm: null, sessionKey: decryptedData diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 9d913ce36..042c368c8 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -221,7 +221,7 @@ class SecretKeyPacket extends PublicKeyPacket { } } try { - const { read, privateParams } = parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); + const { read, privateParams } = await parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); if (read < cleartext.length) { throw new Error('Error reading MPIs'); } @@ -479,7 +479,7 @@ class SecretKeyPacket extends PublicKeyPacket { } try { - const { privateParams } = parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); + const { privateParams } = await parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); this.privateParams = privateParams; } catch (err) { throw new Error('Error reading MPIs'); @@ -532,6 +532,9 @@ class SecretKeyPacket extends PublicKeyPacket { )) { throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${curve}. Generate a key of type 'curve25519' instead`); } + if (this.version !== 6 && this.algorithm === enums.publicKey.pqc_mlkem_x25519) { + throw new Error(`Cannot generate v${this.version} keys of type 'pqc'. Generate a v6 key instead`); + } const { privateParams, publicParams } = await generateParams(this.algorithm, bits, curve, symmetric); this.privateParams = privateParams; this.publicParams = publicParams; diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index d6d340920..be7c6c44a 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -6,7 +6,7 @@ import openpgp from '../initOpenpgp.js'; import * as crypto from '../../src/crypto'; import util from '../../src/util.js'; -export default () => describe('API functional testing', function() { +export default () => describe('API functional testing', async function() { const RSAPublicKeyMaterial = util.concatUint8Array([ new Uint8Array([0x08,0x00,0xac,0x15,0xb3,0xd6,0xd2,0x0f,0xf0,0x7a,0xdd,0x21,0xb7, 0xbf,0x61,0xfa,0xca,0x93,0x86,0xc8,0x55,0x5a,0x4b,0xa6,0xa4,0x1a, @@ -196,15 +196,15 @@ export default () => describe('API functional testing', function() { const algoRSA = openpgp.enums.publicKey.rsaEncryptSign; const RSAPublicParams = crypto.parsePublicKeyParams(algoRSA, RSAPublicKeyMaterial).publicParams; - const RSAPrivateParams = crypto.parsePrivateKeyParams(algoRSA, RSAPrivateKeyMaterial).privateParams; + const RSAPrivateParams = (await crypto.parsePrivateKeyParams(algoRSA, RSAPrivateKeyMaterial)).privateParams; const algoDSA = openpgp.enums.publicKey.dsa; const DSAPublicParams = crypto.parsePublicKeyParams(algoDSA, DSAPublicKeyMaterial).publicParams; - const DSAPrivateParams = crypto.parsePrivateKeyParams(algoDSA, DSAPrivateKeyMaterial).privateParams; + const DSAPrivateParams = (await crypto.parsePrivateKeyParams(algoDSA, DSAPrivateKeyMaterial)).privateParams; const algoElGamal = openpgp.enums.publicKey.elgamal; const elGamalPublicParams = crypto.parsePublicKeyParams(algoElGamal, elGamalPublicKeyMaterial).publicParams; - const elGamalPrivateParams = crypto.parsePrivateKeyParams(algoElGamal, elGamalPrivateKeyMaterial).privateParams; + const elGamalPrivateParams = (await crypto.parsePrivateKeyParams(algoElGamal, elGamalPrivateKeyMaterial)).privateParams; const data = util.stringToUint8Array('foobar'); diff --git a/test/crypto/index.js b/test/crypto/index.js index ac315a065..4f90ebf32 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -14,6 +14,7 @@ import testEAX from './eax'; import testOCB from './ocb'; import testRSA from './rsa'; import testValidate from './validate'; +import testPQC from './postQuantum'; export default () => describe('Crypto', function () { testBigInteger(); @@ -32,4 +33,5 @@ export default () => describe('Crypto', function () { testOCB(); testRSA(); testValidate(); + testPQC(); }); diff --git a/test/crypto/postQuantum.js b/test/crypto/postQuantum.js new file mode 100644 index 000000000..2f58ae0d5 --- /dev/null +++ b/test/crypto/postQuantum.js @@ -0,0 +1,158 @@ +import { use as chaiUse, expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import +chaiUse(chaiAsPromised); + +import openpgp from '../initOpenpgp.js'; +import { generateParams, publicKeyEncrypt, publicKeyDecrypt } from '../../src/crypto/crypto.js'; + +export default () => describe('PQC', function () { + it('ML-KEM + X25519 - Generate/encrypt/decrypt', async function () { + const sessionKey = { data: new Uint8Array(16).fill(1), algorithm: 'aes128' }; + + const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.pqc_mlkem_x25519); + const encryptedSessionKeyParams = await publicKeyEncrypt(openpgp.enums.publicKey.pqc_mlkem_x25519, undefined, publicParams, null, sessionKey.data); + const decryptedSessionKey = await publicKeyDecrypt(openpgp.enums.publicKey.pqc_mlkem_x25519, publicParams, privateParams, encryptedSessionKeyParams); + expect(decryptedSessionKey).to.deep.equal(sessionKey.data); + }); + + it('ML-KEM + X25519 - private key is correctly serialized using the seed instead of the expanded secret key material', async function () { + const armoredKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +zdmQk3zZBuwE/KAXTAY= +-----END PGP PRIVATE KEY BLOCK-----`; + + const { data: expectedBinaryKey } = await openpgp.unarmor(armoredKey); + + const privateKey = await openpgp.readKey({ armoredKey }); + expect(privateKey.write()).to.deep.equal(expectedBinaryKey); + }); + + it('ML-KEM + X25519 - Test vector', async function () { + const armoredMessage = `-----BEGIN PGP MESSAGE----- + +wcPUA7w9P/Y4tvvTaUZQqpafFZ6WRHDl5y8FJcqxWgQ8kK3P++vG3p2aW0Vb +BuxUbAzplFMVjybzf8n0qa/92R6IeFKWMilaM+iAf81VyTIBv12lfQy27RFx +itM0v/CAWrEvPf1RkB0ZnEX9lKhIfBp7QRSVEp2QOh3pWtYQ6AkS0JtOGeiI +befPigAVtmDlnYHYYEFbrKlE8pvLF2bMCJkHredQue7AmsGLpnXZpna19rvG +3kB38YiM6777jxW8yb7h77jOZgnclJXYkSHly7LKa6z6omOYqn6rA5izbE0e +s/yqucighBVnjiwNEwrPlQbogh6EtRsGXkYLPGRSVSM2MCSsigl2Xs2cgWJV +7outF2Qr8BauBRqHW5ScJF/yAmVP7NfQ9PfoUPMiHFJE8U6ffow6lnNWzhn2 +Wqp8GkBccTmzwD/6XXf23yKGO6AIc1xvNpE99QBzTkPslPuXDiocMHfAXpbW +HzVyOwXu/tQmRNe+ISNpQsMmQDUM05kq0diazBXOiZGRMRFndEpa1Qhhgj7V +4GgLyr/HdylNbzbiUGFjovN8/pUwO6APu1jU/CmFEUrjjOFpXaOLZ6svt5mW +iNlVNfWVq35CrJdWVFInLjD/GmsQNSiPe34LcDS3TVRzAhS5HehTVrkFDOD+ +e7wBnHFKDquYTzc3pqP3gHmzCLueUfo3Z6cTgXz6T3+RlRfZDNQzLZ7XHC3W +VWuwjViv0mQncau6bCnhhCK4VwAqXCvjy9m598cqKQZ5Sf3DAGY1+ZGqbID+ +0zCdVKC0v4eUEGg2D9K1f1XiTfaJv6AO0sS5RqjAZxBpw2/m5ruxMyJxqTE1 +h0GZLEY1ZQKxe9E0bDvTi0ogbYicLEokgRyKN/ojZE+hwpA7msjLVV7E0tDg +rz8jxp4AwQ8O/FXX/X0Dp04QsZgJhdwEYt4HPTogSff/NaC8KNSTzxEvJIMa +N+dPAp3Ypz7pGU6y73odwBCJ58nAPaI06f3Ev4gWJai8sUxeHe6amQX+SYqL +jbTP838PgbRbNCwBUiH2I5WX2bdIeRbEiQerVr7f76Kpdo2FC93A8VlW/wyK +VYWINPBfB/ktLAzmohR9wq+E+uMCOnyz6+vE1Bf2fbljGBSBjyUwED07tlDd +B9EvUlLgr7TAiideveSXHAx0G0qK5NBzfoY3Ca3WETiqkDTWpBSPDQUsbXrF +u2QUJKIcXcQptuiC+nKFFbDoFvCJzgcuRS/H1ZyOcI0dgmx9T1GGrID65R0z +FqdpenxdaHncrMmOlYI/cwyrfhHV0wZB6Y0XAv5Ujwsfg0fdCxJclkAI1k/x +N2xIhjwFdh62sKZqCJgfgKlrkSv4DDUVAooR/yHIIKpeJQgC2ZUfL3XLNycJ +eBf5V5ZmyRhIVgAThhb4cGVtNahP0IuIHk7eMdALC5Yhl+PTlSlhqVkbyi05 +Ae7dcaTW/KAEzZw2mqvVQX2MABaShv49reFhsCfI7EWbdI0CpTQbrCxp9mTI +Lw2nVxF5SGApCQtVJ+cajQ48p2MNwYMbsnMU9oEjy89nXZfU0G3CBRnAnhUP +Sw5WYSDSOgGTtzkIxvyRfaLftZRNfT/5XmvtpCqVWwBKHJ1/qcLEIRWfenqT +M/UQCkZrgB2Ry+4Oc1hgibIUzOM= +=8HM/ +-----END PGP MESSAGE-----`; + + const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +zdmQk3zZBuwE/KAXTAY= +-----END PGP PRIVATE KEY BLOCK-----` + }); + + const { data: decryptedData } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage }), + decryptionKeys: privateKey + }); + expect(decryptedData).to.equal('Testing\n'); + }); +}); diff --git a/test/crypto/validate.js b/test/crypto/validate.js index 8ed63089c..7c8209268 100644 --- a/test/crypto/validate.js +++ b/test/crypto/validate.js @@ -81,7 +81,7 @@ async function cloneKeyPacket(key) { } async function generatePrivateKeyObject(options) { - const config = { rejectCurves: new Set() }; + const config = { rejectCurves: new Set(), ...options.config }; const { privateKey } = await openpgp.generateKey({ ...options, userIDs: [{ name: 'Test', email: 'test@test.com' }], format: 'object', config }); return privateKey; } @@ -314,6 +314,38 @@ export default () => { }); }); + describe('PQC parameter validation', function() { + let pqcEncryptionSubkey; + before(async () => { + const key = await generatePrivateKeyObject({ type: 'symmetric', subkeys: [{ type: 'pqc' }], config: { v6Keys: true } }); + pqcEncryptionSubkey = key.subkeys[0]; + }); + + async function cloneSubeyPacket(subkey) { + const subkeyPacket = new openpgp.SecretSubkeyPacket(); + await subkeyPacket.read(subkey.keyPacket.write()); + return subkeyPacket; + } + + it('generated params are valid', async function() { + await expect(pqcEncryptionSubkey.keyPacket.validate()).to.not.be.rejected; + }); + + it('detect invalid ML-KEM public key part', async function() { + const keyPacket = await cloneSubeyPacket(pqcEncryptionSubkey); + const { mlkemPublicKey } = keyPacket.publicParams; + mlkemPublicKey[0]++; + await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + + it('detect invalid ECC-KEM key part', async function() { + const keyPacket = await cloneSubeyPacket(pqcEncryptionSubkey); + const { eccPublicKey } = keyPacket.publicParams; + eccPublicKey[0]++; + await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + }); + describe('DSA parameter validation', function() { let dsaKey; before(async () => { diff --git a/test/general/key.js b/test/general/key.js index e6861f5ef..4b48c46e9 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -4612,6 +4612,17 @@ I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== expect(v6Key.subkeys).to.have.length(1); }); + it('should throw when trying to add a ML-KEM PQC key to a v4 key', async function() { + const v4Key = await openpgp.decryptKey({ + privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }), + passphrase: 'hello world' + }); + expect(v4Key.keyPacket.version).to.equal(4); + expect(v4Key.subkeys).to.have.length(1); + await expect(v4Key.addSubkey({ type: 'pqc', sign: false })).to.be.rejectedWith(/Cannot generate v4 keys of type 'pqc'/); + expect(v4Key.subkeys).to.have.length(1); + }); + it('should throw when trying to encrypt a subkey separately from key', async function() { const privateKey = await openpgp.decryptKey({ privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }), From 8e497f419b9abaaeb489bfd5787742d05a0d21ea Mon Sep 17 00:00:00 2001 From: larabr Date: Mon, 25 Nov 2024 14:42:03 +0100 Subject: [PATCH 12/15] PQC: Implement draft RFC for ML-DSA with Ed25519 (#13) Implements Draft 6 (https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/06/). Also, chunk ML-KEM and ML-DSA together in lightweight bundle. Noble-curves had to be updated to v1.7.0 to ensure the same version of noble-hashes is used as noble-post-quantum, making it possible to reuse the sha3 code/chunk across libs. --- .github/test-suite/config.json.template | 3 +- openpgp.d.ts | 8 +- package-lock.json | 31 +- package.json | 2 +- src/crypto/crypto.js | 25 +- src/crypto/public_key/post_quantum/index.js | 4 +- .../public_key/post_quantum/kem/ml_kem.js | 6 +- .../post_quantum/noble_post_quantum.ts | 10 + .../post_quantum/signature/ecc_dsa.js | 46 + .../post_quantum/signature/index.js | 2 + .../post_quantum/signature/ml_dsa.js | 69 ++ .../post_quantum/signature/signature.js | 70 ++ src/crypto/signature.js | 17 +- src/enums.js | 3 + src/key/helper.js | 11 +- src/packet/public_key.js | 8 +- src/packet/secret_key.js | 5 +- test/crypto/ecdh.js | 4 +- test/crypto/postQuantum.js | 1051 +++++++++++++++++ test/crypto/validate.js | 30 +- test/general/key.js | 6 +- 21 files changed, 1361 insertions(+), 50 deletions(-) create mode 100644 src/crypto/public_key/post_quantum/noble_post_quantum.ts create mode 100644 src/crypto/public_key/post_quantum/signature/ecc_dsa.js create mode 100644 src/crypto/public_key/post_quantum/signature/index.js create mode 100644 src/crypto/public_key/post_quantum/signature/ml_dsa.js create mode 100644 src/crypto/public_key/post_quantum/signature/signature.js diff --git a/.github/test-suite/config.json.template b/.github/test-suite/config.json.template index 8e9a9d3ff..32c74d177 100644 --- a/.github/test-suite/config.json.template +++ b/.github/test-suite/config.json.template @@ -4,7 +4,8 @@ "id": "sop-openpgpjs-branch", "path": "__SOP_OPENPGPJS__", "env": { - "OPENPGPJS_PATH": "__OPENPGPJS_BRANCH__" + "OPENPGPJS_PATH": "__OPENPGPJS_BRANCH__", + "OPENPGPJS_CUSTOM_PROFILES": "{\"generate-key\": { \"post-quantum\": { \"description\": \"generate post-quantum v6 keys (relying on ML-DSA + ML-KEM)\", \"options\": { \"type\": \"pqc\", \"config\": { \"v6Keys\": true } } } } }" } }, { diff --git a/openpgp.d.ts b/openpgp.d.ts index 664040424..8b8a44ce4 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -704,7 +704,7 @@ export type EllipticCurveName = 'ed25519Legacy' | 'curve25519Legacy' | 'nistP256 interface GenerateKeyOptions { userIDs: MaybeArray; passphrase?: string; - type?: 'ecc' | 'rsa' | 'curve25519' | 'curve448'; + type?: 'ecc' | 'rsa' | 'curve25519' | 'curve448' | 'pqc'; curve?: EllipticCurveName; rsaBits?: number; keyExpirationTime?: number; @@ -825,7 +825,7 @@ export namespace enums { aeadEncryptedData = 20 } - export type publicKeyNames = 'rsaEncryptSign' | 'rsaEncrypt' | 'rsaSign' | 'elgamal' | 'dsa' | 'ecdh' | 'ecdsa' | 'eddsaLegacy' | 'aedh' | 'aedsa' | 'ed25519' | 'x25519' | 'ed448' | 'x448'; + export type publicKeyNames = 'rsaEncryptSign' | 'rsaEncrypt' | 'rsaSign' | 'elgamal' | 'dsa' | 'ecdh' | 'ecdsa' | 'eddsaLegacy' | 'aedh' | 'aedsa' | 'ed25519' | 'x25519' | 'ed448' | 'x448' | 'pqc_mlkem_x25519' | 'pqc_mldsa_ed25519'; export enum publicKey { rsaEncryptSign = 1, rsaEncrypt = 2, @@ -840,7 +840,9 @@ export namespace enums { x25519 = 25, x448 = 26, ed25519 = 27, - ed448 = 28 + ed448 = 28, + pqc_mlkem_x25519 = 105, + pqc_mldsa_ed25519 = 107 } export enum curve { diff --git a/package-lock.json b/package-lock.json index 9643bcc63..7ad70eaae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "LGPL-3.0+", "devDependencies": { "@noble/ciphers": "^1.0.0", - "@noble/curves": "^1.6.0", + "@noble/curves": "^1.7.0", "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", "@noble/post-quantum": "^0.2.1", @@ -944,12 +944,13 @@ } }, "node_modules/@noble/curves": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", - "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", + "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", "dev": true, + "license": "MIT", "dependencies": { - "@noble/hashes": "1.5.0" + "@noble/hashes": "1.6.0" }, "engines": { "node": "^14.21.3 || >=16" @@ -971,10 +972,11 @@ ] }, "node_modules/@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", "dev": true, + "license": "MIT", "engines": { "node": "^14.21.3 || >=16" }, @@ -995,19 +997,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", - "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index ae557f97d..cad19c9fd 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "devDependencies": { "@noble/ciphers": "^1.0.0", - "@noble/curves": "^1.6.0", + "@noble/curves": "^1.7.0", "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", "@noble/post-quantum": "^0.2.1", diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index 264739657..fcdd32caa 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -247,6 +247,11 @@ export function parsePublicKeyParams(algo, bytes) { const mlkemPublicKey = util.readExactSubarray(bytes, read, read + 1184); read += mlkemPublicKey.length; return { read, publicParams: { eccPublicKey, mlkemPublicKey } }; } + case enums.publicKey.pqc_mldsa_ed25519: { + const eccPublicKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.ed25519)); read += eccPublicKey.length; + const mldsaPublicKey = util.readExactSubarray(bytes, read, read + 1952); read += mldsaPublicKey.length; + return { read, publicParams: { eccPublicKey, mldsaPublicKey } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -324,6 +329,12 @@ export async function parsePrivateKeyParams(algo, bytes, publicParams) { const { mlkemSecretKey } = await postQuantum.kem.mlkemExpandSecretSeed(algo, mlkemSeed); return { read, privateParams: { eccSecretKey, mlkemSecretKey, mlkemSeed } }; } + case enums.publicKey.pqc_mldsa_ed25519: { + const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.ed25519)); read += eccSecretKey.length; + const mldsaSeed = util.readExactSubarray(bytes, read, read + 32); read += mldsaSeed.length; + const { mldsaSecretKey } = await postQuantum.signature.mldsaExpandSecretSeed(algo, mldsaSeed); + return { read, privateParams: { eccSecretKey, mldsaSecretKey, mldsaSeed } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -414,10 +425,12 @@ export function serializeParams(algo, params) { enums.publicKey.aead, enums.publicKey.hmac, enums.publicKey.pqc_mlkem_x25519, + enums.publicKey.pqc_mldsa_ed25519 ]); const excludedFields = { - [enums.publicKey.pqc_mlkem_x25519]: new Set(['mlkemSecretKey']) // only `mlkemSeed` is serialized + [enums.publicKey.pqc_mlkem_x25519]: new Set(['mlkemSecretKey']), // only `mlkemSeed` is serialized + [enums.publicKey.pqc_mldsa_ed25519]: new Set(['mldsaSecretKey']) // only `mldsaSeed` is serialized }; const orderedParams = Object.keys(params).map(name => { @@ -494,6 +507,11 @@ export async function generateParams(algo, bits, oid, symmetric) { privateParams: { eccSecretKey, mlkemSeed, mlkemSecretKey }, publicParams: { eccPublicKey, mlkemPublicKey } })); + case enums.publicKey.pqc_mldsa_ed25519: + return postQuantum.signature.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }) => ({ + privateParams: { eccSecretKey, mldsaSeed, mldsaSecretKey }, + publicParams: { eccPublicKey, mldsaPublicKey } + })); case enums.publicKey.dsa: case enums.publicKey.elgamal: throw new Error('Unsupported algorithm for key generation.'); @@ -590,6 +608,11 @@ export async function validateParams(algo, publicParams, privateParams) { const { eccPublicKey, mlkemPublicKey } = publicParams; return postQuantum.kem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed); } + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccSecretKey, mldsaSeed } = privateParams; + const { eccPublicKey, mldsaPublicKey } = publicParams; + return postQuantum.signature.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed); + } default: throw new Error('Unknown public key algorithm.'); } diff --git a/src/crypto/public_key/post_quantum/index.js b/src/crypto/public_key/post_quantum/index.js index 982b28be7..cf803ef88 100644 --- a/src/crypto/public_key/post_quantum/index.js +++ b/src/crypto/public_key/post_quantum/index.js @@ -1,5 +1,7 @@ import * as kem from './kem/index'; +import * as signature from './signature'; export { - kem + kem, + signature }; diff --git a/src/crypto/public_key/post_quantum/kem/ml_kem.js b/src/crypto/public_key/post_quantum/kem/ml_kem.js index 3789459c6..0651547b0 100644 --- a/src/crypto/public_key/post_quantum/kem/ml_kem.js +++ b/src/crypto/public_key/post_quantum/kem/ml_kem.js @@ -24,7 +24,7 @@ export async function generate(algo) { export async function expandSecretSeed(algo, seed) { switch (algo) { case enums.publicKey.pqc_mlkem_x25519: { - const { ml_kem768 } = await import('@noble/post-quantum/ml-kem'); + const { ml_kem768 } = await import('../noble_post_quantum'); const { publicKey: encapsulationKey, secretKey: decapsulationKey } = ml_kem768.keygen(seed); return { mlkemPublicKey: encapsulationKey, mlkemSecretKey: decapsulationKey }; @@ -37,7 +37,7 @@ export async function expandSecretSeed(algo, seed) { export async function encaps(algo, mlkemRecipientPublicKey) { switch (algo) { case enums.publicKey.pqc_mlkem_x25519: { - const { ml_kem768 } = await import('@noble/post-quantum/ml-kem'); + const { ml_kem768 } = await import('../noble_post_quantum'); const { cipherText: mlkemCipherText, sharedSecret: mlkemKeyShare } = ml_kem768.encapsulate(mlkemRecipientPublicKey); return { mlkemCipherText, mlkemKeyShare }; @@ -50,7 +50,7 @@ export async function encaps(algo, mlkemRecipientPublicKey) { export async function decaps(algo, mlkemCipherText, mlkemSecretKey) { switch (algo) { case enums.publicKey.pqc_mlkem_x25519: { - const { ml_kem768 } = await import('@noble/post-quantum/ml-kem'); + const { ml_kem768 } = await import('../noble_post_quantum'); const mlkemKeyShare = ml_kem768.decapsulate(mlkemCipherText, mlkemSecretKey); return mlkemKeyShare; diff --git a/src/crypto/public_key/post_quantum/noble_post_quantum.ts b/src/crypto/public_key/post_quantum/noble_post_quantum.ts new file mode 100644 index 000000000..de77098ef --- /dev/null +++ b/src/crypto/public_key/post_quantum/noble_post_quantum.ts @@ -0,0 +1,10 @@ +/** + * This file is needed to dynamic import noble-post-quantum libs. + * Separate dynamic imports are not convenient as they result in multiple chunks, + * which ultimately share a lot of code and need to be imported together + * when it comes to Proton's ML-DSA + ML-KEM keys. + */ + +export { ml_kem768 } from '@noble/post-quantum/ml-kem'; +export { ml_dsa65 } from '@noble/post-quantum/ml-dsa'; + diff --git a/src/crypto/public_key/post_quantum/signature/ecc_dsa.js b/src/crypto/public_key/post_quantum/signature/ecc_dsa.js new file mode 100644 index 000000000..9bf3dfbe4 --- /dev/null +++ b/src/crypto/public_key/post_quantum/signature/ecc_dsa.js @@ -0,0 +1,46 @@ +import * as eddsa from '../../elliptic/eddsa'; +import enums from '../../../../enums'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { A, seed } = await eddsa.generate(enums.publicKey.ed25519); + return { + eccPublicKey: A, + eccSecretKey: seed + }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest) { + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { RS: eccSignature } = await eddsa.sign(enums.publicKey.ed25519, hashAlgo, null, eccPublicKey, eccSecretKey, dataDigest); + + return { eccSignature }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature) { + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: + return eddsa.verify(enums.publicKey.ed25519, hashAlgo, { RS: eccSignature }, null, eccPublicKey, dataDigest); + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function validateParams(algo, eccPublicKey, eccSecretKey) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: + return eddsa.validateParams(enums.publicKey.ed25519, eccPublicKey, eccSecretKey); + default: + throw new Error('Unsupported signature algorithm'); + } +} diff --git a/src/crypto/public_key/post_quantum/signature/index.js b/src/crypto/public_key/post_quantum/signature/index.js new file mode 100644 index 000000000..a1b1dc436 --- /dev/null +++ b/src/crypto/public_key/post_quantum/signature/index.js @@ -0,0 +1,2 @@ +export { generate, sign, verify, validateParams, getRequiredHashAlgo } from './signature'; +export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa'; diff --git a/src/crypto/public_key/post_quantum/signature/ml_dsa.js b/src/crypto/public_key/post_quantum/signature/ml_dsa.js new file mode 100644 index 000000000..0165e71e6 --- /dev/null +++ b/src/crypto/public_key/post_quantum/signature/ml_dsa.js @@ -0,0 +1,69 @@ +import enums from '../../../../enums'; +import util from '../../../../util'; +import { getRandomBytes } from '../../../random'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const mldsaSeed = getRandomBytes(32); + const { mldsaSecretKey, mldsaPublicKey } = await expandSecretSeed(algo, mldsaSeed); + + return { mldsaSeed, mldsaSecretKey, mldsaPublicKey }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +/** + * Expand ML-DSA secret seed and retrieve the secret and public key material + * @param {module:enums.publicKey} algo - Public key algorithm + * @param {Uint8Array} seed - secret seed to expand + * @returns {Promise<{ mldsaPublicKey: Uint8Array, mldsaSecretKey: Uint8Array }>} + */ +export async function expandSecretSeed(algo, seed) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { ml_dsa65 } = await import('../noble_post_quantum'); + const { secretKey: mldsaSecretKey, publicKey: mldsaPublicKey } = ml_dsa65.keygen(seed); + + return { mldsaSecretKey, mldsaPublicKey }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function sign(algo, mldsaSecretKey, dataDigest) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { ml_dsa65 } = await import('../noble_post_quantum'); + const mldsaSignature = ml_dsa65.sign(mldsaSecretKey, dataDigest); + return { mldsaSignature }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function verify(algo, mldsaPublicKey, dataDigest, mldsaSignature) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { ml_dsa65 } = await import('../noble_post_quantum'); + return ml_dsa65.verify(mldsaPublicKey, dataDigest, mldsaSignature); + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function validateParams(algo, mldsaPublicKey, mldsaSeed) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { mldsaPublicKey: expectedPublicKey } = await expandSecretSeed(algo, mldsaSeed); + return util.equalsUint8Array(mldsaPublicKey, expectedPublicKey); + } + default: + throw new Error('Unsupported signature algorithm'); + } +} diff --git a/src/crypto/public_key/post_quantum/signature/signature.js b/src/crypto/public_key/post_quantum/signature/signature.js new file mode 100644 index 000000000..fcdfc745c --- /dev/null +++ b/src/crypto/public_key/post_quantum/signature/signature.js @@ -0,0 +1,70 @@ +import enums from '../../../../enums'; +import * as mldsa from './ml_dsa'; +import * as eccdsa from './ecc_dsa'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccSecretKey, eccPublicKey } = await eccdsa.generate(algo); + const { mldsaSeed, mldsaSecretKey, mldsaPublicKey } = await mldsa.generate(algo); + return { eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, dataDigest) { + if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) { + // The signature hash algo MUST be set to the specified algorithm, see + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + throw new Error('Unexpected hash algorithm for PQC signature'); + } + + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccSignature } = await eccdsa.sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest); + const { mldsaSignature } = await mldsa.sign(signatureAlgo, mldsaSecretKey, dataDigest); + + return { eccSignature, mldsaSignature }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function verify(signatureAlgo, hashAlgo, eccPublicKey, mldsaPublicKey, dataDigest, { eccSignature, mldsaSignature }) { + if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) { + // The signature hash algo MUST be set to the specified algorithm, see + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + throw new Error('Unexpected hash algorithm for PQC signature'); + } + + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const eccVerifiedPromise = eccdsa.verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature); + const mldsaVerifiedPromise = mldsa.verify(signatureAlgo, mldsaPublicKey, dataDigest, mldsaSignature); + const verified = await eccVerifiedPromise && await mldsaVerifiedPromise; + return verified; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export function getRequiredHashAlgo(signatureAlgo) { + // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: + return enums.hash.sha3_256; + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed) { + const eccValidationPromise = eccdsa.validateParams(algo, eccPublicKey, eccSecretKey); + const mldsaValidationPromise = mldsa.validateParams(algo, mldsaPublicKey, mldsaSeed); + const valid = await eccValidationPromise && await mldsaValidationPromise; + return valid; +} diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 15b933d9a..2be2bcd26 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -3,7 +3,7 @@ * @module crypto/signature */ -import { elliptic, rsa, dsa, hmac } from './public_key'; +import { elliptic, rsa, dsa, hmac, postQuantum } from './public_key'; import enums from '../enums'; import util from '../util'; import ShortByteString from '../type/short_byte_string'; @@ -70,6 +70,12 @@ export function parseSignatureParams(algo, signature) { const mac = new ShortByteString(); read += mac.read(signature.subarray(read)); return { read, signatureParams: { mac } }; } + case enums.publicKey.pqc_mldsa_ed25519: { + const eccSignatureSize = 2 * elliptic.eddsa.getPayloadSize(enums.publicKey.ed25519); + const eccSignature = util.readExactSubarray(signature, read, read + eccSignatureSize); read += eccSignature.length; + const mldsaSignature = util.readExactSubarray(signature, read, read + 3309); read += mldsaSignature.length; + return { read, signatureParams: { eccSignature, mldsaSignature } }; + } default: throw new UnsupportedError('Unknown signature algorithm.'); } @@ -134,6 +140,10 @@ export async function verify(algo, hashAlgo, signature, publicParams, privatePar const { keyMaterial } = privateParams; return hmac.verify(algo.getValue(), keyMaterial, signature.mac.data, hashed); } + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccPublicKey, mldsaPublicKey } = publicParams; + return postQuantum.signature.verify(algo, hashAlgo, eccPublicKey, mldsaPublicKey, hashed, signature); + } default: throw new Error('Unknown signature algorithm.'); } @@ -195,6 +205,11 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da const mac = await hmac.sign(algo.getValue(), keyMaterial, hashed); return { mac: new ShortByteString(mac) }; } + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccPublicKey } = publicKeyParams; + const { eccSecretKey, mldsaSecretKey } = privateKeyParams; + return postQuantum.signature.sign(algo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, hashed); + } default: throw new Error('Unknown signature algorithm.'); } diff --git a/src/enums.js b/src/enums.js index 59ed7e5d6..c1b3f31e9 100644 --- a/src/enums.js +++ b/src/enums.js @@ -110,6 +110,9 @@ export default { ed448: 28, /** Post-quantum ML-KEM-768 + X25519 (Encrypt only) */ pqc_mlkem_x25519: 105, + /** Post-quantum ML-DSA-64 + Ed25519 (Sign only) */ + pqc_mldsa_ed25519: 107, + /** Persistent symmetric keys: encryption algorithm */ aead: 100, /** Persistent symmetric keys: authentication algorithm */ diff --git a/src/key/helper.js b/src/key/helper.js index 10acbd202..15b81ba1f 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -9,7 +9,7 @@ import { SignaturePacket } from '../packet'; import enums from '../enums'; -import { getPreferredCurveHashAlgo, getHashByteLength } from '../crypto'; +import { getPreferredCurveHashAlgo, getHashByteLength, publicKey } from '../crypto'; import util from '../util'; import defaultConfig from '../config'; @@ -117,6 +117,12 @@ export async function createBindingSignature(subkey, primaryKey, options, config * @async */ export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date = new Date(), targetUserIDs = [], config) { + if (signingKeyPacket.algorithm === enums.publicKey.pqc_mldsa_ed25519) { + // For PQC, the returned hash algo MUST be set to the specified algorithm, see + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + return publicKey.postQuantum.signature.getRequiredHashAlgo(signingKeyPacket.algorithm); + } + /** * If `preferredSenderAlgo` appears in the prefs of all recipients, we pick it; otherwise, we use the * strongest supported algo (`defaultAlgo` is always implicitly supported by all keys). @@ -405,7 +411,7 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { switch (options.type) { case 'pqc': if (options.sign) { - throw new Error('Post-quantum signing algorithms are not yet supported.'); + options.algorithm = enums.publicKey.pqc_mldsa_ed25519; } else { options.algorithm = enums.publicKey.pqc_mlkem_x25519; } @@ -468,6 +474,7 @@ export function validateSigningKeyPacket(keyPacket, signature, config) { case enums.publicKey.ed25519: case enums.publicKey.ed448: case enums.publicKey.hmac: + case enums.publicKey.pqc_mldsa_ed25519: if (!signature.keyFlags && !config.allowMissingKeyFlags) { throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); } diff --git a/src/packet/public_key.js b/src/packet/public_key.js index cb316cbad..df7c07087 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -138,9 +138,13 @@ class PublicKeyPacket { ) { throw new Error('Legacy curve25519 cannot be used with v6 keys'); } + // The composite ML-DSA + EdDSA schemes MUST be used only with v6 keys. // The composite ML-KEM + ECDH schemes MUST be used only with v6 keys. - if (this.version !== 6 && this.algorithm === enums.publicKey.pqc_mlkem_x25519) { - throw new Error('Unexpected key version: ML-KEM algorithms can only be used with v6 keys'); + if (this.version !== 6 && ( + this.algorithm === enums.publicKey.pqc_mldsa_ed25519 || + this.algorithm === enums.publicKey.pqc_mlkem_x25519 + )) { + throw new Error('Unexpected key version: ML-DSA and ML-KEM algorithms can only be used with v6 keys'); } this.publicParams = publicParams; pos += read; diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 042c368c8..a397569bc 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -532,7 +532,10 @@ class SecretKeyPacket extends PublicKeyPacket { )) { throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${curve}. Generate a key of type 'curve25519' instead`); } - if (this.version !== 6 && this.algorithm === enums.publicKey.pqc_mlkem_x25519) { + if (this.version !== 6 && ( + this.algorithm === enums.publicKey.pqc_mldsa_ed25519 || + this.algorithm === enums.publicKey.pqc_mlkem_x25519 + )) { throw new Error(`Cannot generate v${this.version} keys of type 'pqc'. Generate a v6 key instead`); } const { privateParams, publicParams } = await generateParams(this.algorithm, bits, curve, symmetric); diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index b370778f1..6796d75bd 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -267,9 +267,9 @@ export default () => describe('ECDH key exchange @lightweight', function () { for (const { vector } of vectors) { const lowOrderPoint = util.hexToUint8Array(vector); const { A: K_A, k: a } = await elliptic_curves.ecdhX.generate(openpgp.enums.publicKey.x448); - await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x448, data, lowOrderPoint)).to.be.rejectedWith(/Invalid private or public key received|expected valid u|low order point/); + await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x448, data, lowOrderPoint)).to.be.rejectedWith(/invalid private or public key received|expected valid u|low order point/); const dummyWrappedKey = new Uint8Array(32); // expected to be unused - await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x448, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejectedWith(/Invalid private or public key received|expected valid u|low order point/); + await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x448, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejectedWith(/invalid private or public key received|expected valid u|low order point/); } }); diff --git a/test/crypto/postQuantum.js b/test/crypto/postQuantum.js index 2f58ae0d5..5d93a84d9 100644 --- a/test/crypto/postQuantum.js +++ b/test/crypto/postQuantum.js @@ -1,9 +1,593 @@ +/* eslint-disable max-lines */ import { use as chaiUse, expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import chaiUse(chaiAsPromised); import openpgp from '../initOpenpgp.js'; import { generateParams, publicKeyEncrypt, publicKeyDecrypt } from '../../src/crypto/crypto.js'; +import { sign, verify } from '../../src/crypto/signature.js'; + +// Test vector from https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#appendix-A.2.1 +const mldsaEd25519AndMlkemX25519PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +-----END PGP PRIVATE KEY BLOCK-----`; + +const mldsaEd25519AndMlkemX25519PublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xscKBlHQxoBrAAAHwGy44RkYq173huDlFbFTF0FbPOsQdZRherJYVBKlJKH8+IJe +miR3kgJaw42LGYryBJuCAavtf1M+Nyh3IR06sEJ226n+LLb72/uGCrlqRlUQkSbz +/EHjiOogerpgiuz6D8gHDqdNlwvtuO0Cw+CbCFAsDbSOp8zqwd3Qq1poCSLueLc1 +DIJe8TkZrmqbLA1LMyQAvT7ALQjGAP33wzKB1fGDv8napjWSiCgT03EdvRa39pEZ +I7Inu5y0RkYdsTKt03HkcApB3XM8n4xolj3NYrUc7f1Mqqlz+5d5ydcFRmnos+Cm +fDzj6cdaseT9IV8BfHjVNMXpozpxkr8Dwaz7a2SrSnJIp9N2k3nlCVMAGYj4qyWO +dmRjevSvJbPccX7ar4V7g+a/wr7Ec+beB+J3ya87SXI1PYj7wc6wIPCZRqxk+X9Z +JLESYmoOV3eQQwdOgjiWp5xWAn7LJ+MBC6cFcQRMfFPDuZlbwv2gQ6vX2HTpHqaO +oE86wKXYfpHqgTukExxFdx7A6JlAcxYi57g0xfCllxDD9wXDirsBMX4eGK/d+/1c +E9Cf7yTJQlQfL+Dtpsqr3qfrGHvxiwxVmzsp7yM9J75ZZoQpNC22jUgOG4U9r5N5 +Op30/340M1ysMKzVgduz+tE3ewU8Mm8lNe/TU5R2ZN5TDV5cT1O70MLzjPhECfRU +pKCyhnBVidNLZ7GGOmYQyj2zXcmQ0cM1By/ohWz2fnOxH702Y+QdRv0r52hlvCY9 +RoNnlax5tEN7zE8cVgLVfVN0UXU+klVSlHfLPMy4gZK2/s7LylVcabRhTE9MG3hU +fbtpA0KPQU4v8BartJNOwGAXx1qpCxppDZHWVnp8xO5cVhLENe0CoUn8S9wO2M6B +ggbn+35F7pMONASGH3uELt4XzibzNGFJCmUUPOOHfFPwrqtfjvYkPySLTTp21j47 +2ab+FfVBFBkK1FOlnz3ufA2U/0+LvRAIfdJIfRzES+Gx8g5u2eh7VO8pwmw2zexW +3Cp/4LNisLyg/vv+UgMdWfh66A5r6ssXN9MT8oytWcMcsng0FlmCVKehbea/8ziO +rVJ9RdGaiAv2EF1mm+5ZDi9asOemwdzX39277NfFYXcBpigqFEqMVbhoUGQu635Y +eZbNBuTSlq7s7hVgGzMszeImuv0cVFLkk5z/JvYkbO/cuGFcN9xYhHXj1R/6DNqf +JyRAKhBheMkxGNVSV+7Ot5V1mRMGpt3UmnkowooTGHux69oYFRx8gBvMNs/+Kbub +n+dgPA2fbwCR8kWc6jvewU4g0fjNiSZgFuM4EYMMAeEAZTFNw4Cm8TWZ6Wmo1Hax +aJVvnJWRGXF4TP4I6lX3am7dMiofK5Og4BxymDabzJKo8uiOYXkyUUTfWfzDGdpZ +bF+hW4qcvE650KP+Bf4rOOSfOJ6SFt2y0srSJpR+H3EfLk76WpDqiy1DlBpV8E74 +sEJK7CcwzsaSyxpUzk4N+KJAa7ahkL1/Rw9ZFf0PEkquMIOoK0MXlQ24jlTDCAD5 +7ynzhLqWfxhj6KNRbS2M8wVrf7WBjgjHlRIaWZFbpC6nUP0wqNXIG7ZlMfRQQjHH +jTlPFFe8kcq9Xcw3oKZmln4yHhstR960DFYGELvTR+9ozFiYoxgAwdJC7748EtvD +ASXWZqhQ1g3lXEDHbJ8PAdU3GxeYSeUiC80Hro/0uf20tLGU3kdyfbmMjBrnDzWF +bOwcm/76V15YIgFiOENXWivesqNY4eZ4MSB7ZcJm3wdvJ2ZsUzcsVmj0x7bxgld5 +hsl4uEJxKgVwUf0Hg8AJQJ/p0PjFiwWF9wttOZynrSfamWur/kmmhKPS0NqPFEXE +I70eMuPVCGI8oLvfK93RqZqGUsaURVrsDNH2fvx5sh1/iaXtl3fTcZyrcLYUck83 +4sY/KUi+/NHfZpj82EhVjOvQ/2f0tqADLeYXy1bFXE/T/6ooN/eEV8yKSwL43mjB +q9iaURTOFOqftc7Bv+IzOxCDjpYIoQKmHa8VeDSEMURBfn22amDA900dgwbEMpaP ++L/SrxkcIzYie//XYePcyhGjAuIQine/+vAE9fBZ4zS0tnoAsktlD7S0EPNKrlfi +H7kHNh9ueRwzOxUHomMzRzTet8zY3ZxXXYAqD9W6LiUbnK+4a7LtsF+xRSL9XsBN +YevQFSZ2i6d8Xoqx6hRB4GVGCopQSAixwnSqoYw1x9tL+u/rYS1hNeEFN06MBvry +CfKvy4ZeGXI3Pd0xyyqAcFyF7biYxWPoNkm8mQEvuKdAFvSXRQd9lrkcH+QEkiML +eJUw2jPVxG39r8wJ1yrHzPsizQWUb+s99OO4heuhw7PTeIIT7cg217iAJL+GrkCb +oEn6BdFr96D8kn4SHGbyW51ZvTmwIAfpOqp3Lt4ph4//N3HJ1yhVmF4rtgjGSoJ9 +nJIngjMb20dRis55Pe1n7w5CXM+VTo+YUTKokiBLNtIlNRHtWdvE6C29se2Zs0Tt +hNn4QzQNXWh4k3o1zD51V8FLUz6hROGelleVf8MPcKtYPIJn1JDRnhgfT3bNnbh0 +Dp8AKmeymVAKVPTU8mQDHmVQy4V6DGQ6Enn7sBXzKobOEGpI3+JFYnd3Deu2dT/2 +Fh0ildKLKNiZW21YjRMGBJcf8mySmg2XBoWX0hTCzMwGH2sMAAAAQAWCUdDGgAML +CQcDFQoIAhYAApsDAh4JIqEGvyYrJBdwAqyK5dxtpHwFbSKrmQbUfQeVK3XDWAIZ +AcoFJwkCBwIAAAAAq9gQSFUCQFY6OmE5d4M+wa5Gewj4Nh0m+5BRgHk7tMWj9f+b +N2LHBV0XPmKybmFtcpIO4zTJopJVrKOgMKyGXQhtuERlw2HndacJQxF3zI2uXQe1 +ifdzzbnWmEZfU5U1gc7m/tq9ufZi9srdjm9uj0FSA/hr72AZLVZG/Qvh8Raw+AMa +wv9LSrrjf2QzGAtdjtQtaZCJ2aVU9DAd9EM+sYcjVefoHHqgiITS76+ACQG/RLA2 +/Xi6DYoVOn98QXMD5UAzTK1JjtgLiLwvAlpuLH/WK9hIQfXob468cs3TDsKexi2U +X+0xV57wR24DRjph4Zr0Y/tc2hfP7/QfM7djBp3AvBlFGLSgKcoD4c6KVdqzyCsJ +tgNDuQQR+VaUJcXM3IJuAIZ765CiUPvZw2QC3N9p49v0JAdQytZbXyZhl60fglIW +WuujJqh2ae2l1PZpYD1TvLCgewQVwPlZDbmSczJzdzIzQU3/FK4SazbXLscb6V1+ +V3XX4Co5Ap/tdaXNHHcDHPJXEmSeyPXd+JcZDyVQ5KcPq9q1M5Etgb8OcVN6HUt1 +rU7UuUjw7mddMyPsX2oznUIa3ZEyPMpb50zTR2BKucEYLMs2ZwZ7ZbdYz8HHl4eP +27HyaYAleEKuTg1dqMIo1o9lEkMPReB8pr6019BfBBQWdhz/YXZNXSDaqvOERkWU +d1gkd8HLOW1vPRCFIvHxjFGRlxj5X2Q7x3/msu7Q43/JbTuI/Z9+ljw1pilXO1pl +TfmFsmfhPeKPyde8h728ZroYdKPlzM39OKILsJQim49+r2OvirVEhPIRfWEBccRk +4f1s/PbExbDXQum9hnhZPUwsGl8Tpo+CncD/jo4mN/08I9ErsjGKJ7w72whbPSX2 +VuXkAwxjWrYvXAqu3/REIZU0Z6Db4t3t3nxRFggZfF4zsI+qiPySpP4vk4rnWl+Q +bGHrbljgw0XRnjl1eaTCINr2jm/1QE0u/PtPzVXvaN+MG7PcGi0nAhDG1VqoX+EY +YaCUuVB3SQ5bgOQTPhAkfhIeQVEoMHySe3oV11fpUt0TKX2xI+KWWa2Z6wflrz1D +nRfxclwxK4hBawdyUjjbX0cmatXwPhi/0NTLEyjn39LhCgMk/sYGIFu/Si0Mhggc +zo9ibybQbsiFNP9Itwi3ruEL8JeOrd//CUFf7ifJo6cHxEsJpk7RQvjXXs7K9tQY +cIcJj5VBLfffGhuuTJ6ZllpolMasizgYA6JsCaYdM4IhDEvyR5PcxqfV0xoGuU3S +8EkU0ZekY3vZKFx6BgvZaJKWyM6KnEu8MkN0Vb+2FO+5/WF+uyKJfvO+M2vzpinD +Xc4q2caDSRniNmR+v/tfY3zTX11NHYPCsd7GOsJaXXT1xvE4D7T0SV7S7RDj4OBN ++JPkGDWYhhVOEtRqO66ZeqFVI6vRsafb98qgxwvF8AQ7LMRDwdu9b3JKUnFd7C0f +eTmf4vdZkINNFzrVRyLdyIk7dcPi0qPhm+yXzWf8egfOgQ/VwfkuaRL2kt6wTt9e +bf1KN7V3WwQOcHhGr/yTgYi0kbxMYrwa1CNxFkssLkdSeHJKQi6PWGo21RlgFP4m +YKMNOFyIPxSnTZeRnkuOOCZSAHWaIlEa6YgA4xbXFtBU8UaJGfNTWN88qTh762Ft +intkpkOzDeAoLBFoCv6jqs8yE+mQiia6Ao2HmOYJx1iYaTbDxcANCrs56UWdyfkz +0qE7Qwqgqk2YURjj9mBWwoadmV+Z7NiO8qWuj+x44OgWzNVlXbVtL4VqSF9vUCng +lyzYJ2noWclWhyuuuBqNawJQsdrpdc0xdJHnB1LLXW2BrR98QO0zvOhqIWTji9jd +DXHftXzEe+0LF6JyIm8GPTAwUtRdAxO1BpcMP/MQfd/ke+AhOzX2IyZhL/Aivbfc +FSR3O8YMaEckZeEADhi+yrSmFhil6UqHUi30REtVro+kdsbnHT+zPAYSloxv6w37 +hS5becBRO8UpWD9USJqtTRJ3BjhlB5oDY33pF/jxI/cQZbaBv3Sbb1aolxOgkUgg +6sXPNWMlJBn7SB+YLOSAQ5sCt2h1nJZ286QN1aV6VzGI0IIgcJpXC/7jxfpvrTIm +VtqL2+RZva38rpiC6Rf9iy7gwkoO9pk37CE5l0PvdZS9TDOw3axlc1Co1SpDjyvB +JPWWn+W99sTsE3dopWGyuJ28UvMH3xm/mTP9lbJz+5CBfMLT+YANG9zlj9z7FxJB +H2t+hw/oQN63QQAgieRuK6D4xO5kdj7IW8Y7NuLiMNuCeFBbPCVNmo0dH+nbgHZ4 +L8Z40h9A12Yn06fBFWSZV9ZGqzw2jYZ6yB25FaCOPZC3BTOyCF9gNLcX/HsyY0/D +oyoLcFIeyAdLEPqDPy7dOcLUzVINCVY0X0TZpEAvL/e5pDBt3NpBzApYUKYVCQ7m +SC3iZ/NlAa0L2ma8SlHxllDlG64t3IF+X/mYFc5pvgiCLJ+Qb38tikxrEjWZw/tP +35jWEOG8DCgWsuLwlJ6TMwSaESye3lmjTpmAfikIuqz0rkJyYyoW7GW5j4hgPxO0 +/VRWbmzUat5s2S7vCFWs5RD2qe5MMzCTHFd0jnyIVBL4VCamfcTiAbXQfzUzHl3V +dup2KLC2INktej2c2pLFuDHjulY+ltZCia9s+cRgsqEXb+Z68vbNjiPVOpO2noYs +RFebC9U024fUKUtBeyvVJhZnksZKZ33uIzRkC9pzB13RxVf9bhpCl9AnetsIy41o +ChIkJDuUQ7yq2Tggsd1dELRiG/RlfHFzDNguRD8LN7waDiY4FXECufj8OmWbRoqp +yc1jexwJFN3wfbnecmLpyf3Rk7/hWEVWoJ8XQkPk9Ynrr/wIVd/zU9YzPVJVB/K+ +1u1U9ac5i93kb1K1oDnREmgrImlbfEju+L6EqYNOvpgyPbHL8Gcn7UlyZQUCRAk2 +csGu1b9Id+4X82LXNC/wjl3ZK9g25QW0qb7iEkQ0DVDJVpgdni+n+cPOkhtpCvD9 +u7b81Gzxz4DzI5JBhAcVcDEyLFGVnJPpCi8LzPHmjKEZXVIzF5Inug7I488fANSE +ttRD+cjYVYb+QFzjMWI46F4umIKmMY8v+R9LOCvc1Fpara2rQaoEkyfC9RLXea9z +k/o6VEAN4PkTnPj3y4wYUqXS+oDkLKFhDOWQoa7OHBQgE1xp9gz6e5wPkag5Jj+E +bvrLuFWLr7EDkx9rKHrS52Z3qZkgev8f4gOwoIM4kp9Mo8/gvvFknI/yEe23Rkda +pdpaLeLs5NTUxZaPVv9pXM5i9kMBSXAesI+QZzcymT4nIAeZCXbzUHoc9VuIdqUD +RSWR7siPVmDdKpa3ka8+8m49tlfBOPT8WtHPpEicqfBAnWZnaNSoBUIaJv3LU9fo +8h8BLKsnHWX4VNv5qfT8mjKBFNU64dkox3AuD8xKbd6xgaIfMUWpyrkLzHHw6YXH +VpWkWAdDU9VpckqLlFOqJKq8hi4NRAXMhW1GWOCYkmfnx5/0we+U4EocS7WraHKC +8HMRbh7E0ObrqMwjlwiFRuWEYYIxC36bbEJG7tkfJTm67J6r2Rf3jgRplS1PA4W2 +mcTQYTQ0Qqf6QnJQ+NzXtKJPfuVkF/UZaAhT7XcvfP00+yb9seHX+H2UIAZ6rIvO +e/OH8KbH3Frf5+YZi0CJ+w/zHxWbE1ne8ZPJAZp8d4yzAC0vCZQymtkq9L+cDdgD +aP69HpiL1/RSHg7liQzJG+2AZCRDb2A8iYPiAZnD+JnOiTh7W/14OJawuPVWZYrY +7y19J25WI3jAJ1ezur6p+G5NmlZoCtiFvDmEEWg95twQPkD2vs5PW4q+zjse/zFo +ReGmVJtCs9T+U9HGaLbHLYteA7IlIw+8XEVZkHDW8XMkcZVOBbKcpJ+5gszpcGR5 +XpRNf9D0KsD9MXyYGtS9MNfNaopZF5yFVz0m9we7vqAN7nZBVNIHIWXYNsjKaeQS +pyOwn0lmk9L9yb0dRHVwr+00oGqxWgM8I5DlJqJCUA05RngdHhtb1m7d8B4OP0Yc +0hYQBlDUpsBgF8666sO1qBM73V9fadfo3O3CgaZLGPIV104jhAaChocqaX40Rlb2 +hJrDeCqFwl8oJ78yp17pvm1Y2YTQH+p1deYodm9Q3emfukA74M/zXoMxonmIapJr +Zn+1pO+9lZoWW+sYWYWXlzGk7YXjQXXo77F9lX5VnazHNtjkwn2RbcKrFa3NxpfM +ybov2013eoKfM2xit7B40SpMByVhmeL6HeLFaqXiHqBm3KM5vMOSLPXDSX4idXtc +NgDzlxaDvO55+DoW1c5ZslaZEvK7J7tgUNNoel0TEZF+spA7BurnByRs/e3kIKYu +5t7IWvMO09DH5PBvbf3aiN5V8+22JX/hPdfaA/wibZWMlyrvm/AoENyrdwo6fTaO +IaTLg7G9fKk8E5mbzaya/gTUPX9jfjf/BkVt6bl6egEdX2GnqegKVKKrtb7S3gwj +KDhNgI+ZmrjQ7jaMxf9MXWGbzfImQlpkdAAAAAAAAAAAAAAAAAAHDxsfJSrNLlBR +QyB1c2VyIChUZXN0IEtleSkgPHBxYy10ZXN0LWtleUBleGFtcGxlLmNvbT7CzLgG +E2sMAAAALAWCUdDGgAIZASKhBr8mKyQXcAKsiuXcbaR8BW0iq5kG1H0HlSt1w1gC +GQHKAAAAAPJjEKH2vbCxOnVz6yo4ofGv2PAC6L1Y635O6BSXvXCojWo8gycKqAGl +NUproxxClmkDa3U6HEx02iFa+brfyV6uL/nzEN07d61+K9J1ZDhaAbUAOE6DibKt +itg9kPa9bv1V4LSOdd3r2qo3JR4IPNdS+Snb7o77M7HbA7ziWSrBduxtF7mGuiIJ +txEDNtt6heYAIExUt1cO02UO8p1r7xcQXqH2x46Hf/B6api/J1j/4uEtP78wkG6K +tNE69Gw9NRe2PTKvKFp3jayq2I6pkjf5KvV9G13z9MvxyRjnTARTcVcqNNwFhvQv +4t1lRXRJxeYmkMFKLnbdZeBdSWAUUKBofy1dP6zJOk11iycofDRZ2eqpWBeejuGW +hdjpCKFilrybBSNLB831v7qqW6d5NpKu1kCngqfDf1GCP5d+JePsPk2aRZ8zzvqy +kvyWLvenmrQrH7yKw9l4N4X8HdLWIPq/iVkaUVO2lHkAJxZ5Z6ciG557++Gdu6uz +M5hdfyD6xzLxEHHJ7MBw9zQ/UI5h7LRAu17PIvAsxCIroTECyx9w2GAbSjCQXdh9 +P7zamnCprnBHpOStJRcVjvC4tDfCfrOUIBSD+g4hMIaMhqP6C6zALn6mbMHr1hLT +RqV99tMjnNX5QH52z1Dr/P/+9ZqVlA3R7td7Yw2buJyPdFRGKamxihHepQxFhOvB +43uEZexzC2O1jIaMxquBdis5NqvvtdtJQ2kVvSBEYw6Kg3bjC0b6XmoTGn5fBEQH +rHVvFOz9TZzZDnF+vQHC2G7Tv6QDldxz8MR0SzMkP7WPo7KdFwNFWVvoofKsCN3L +NcoWnwpeLcYnpD2hX6YktQKr9s01o30/ICA7hiJ2Hzqp4U4XD6g2Cn+E8RfU3ZJm +QzXVJchrNyN2cD31vN3uKB/Qb9/iQ4DGH/rwmYqKNmfba6dRDa0djPKh7FcbgAO1 +8I6i32UB1U31wg3YSZtCmokCgsEsfn/7XI781yj0Dy64/iXwPavO02fG7pLdwJ5n +mLseghGxyNyB92GWCREsP8BFAttEP9Hs64Xp+PpwYxX+ZgrVvfv5bM3Mi9SU8I3B +mASiMJz3Hb+ouuEH51WROKVZSJecLhfgkGekVvyFdqgP3rldidaBEegkJhOgxeq9 +cck8DUwyWheVxh6kFM0J8evOzJQQmwdQ5HkVTp5XJgQMvDV62t3s7Obd1fLW+Q2h +Bu6ODy1qh7eZKfu3DEbHlzS/HP1zqkQJKphQqIw0AAO11+I/Vd11NOuNqB6TrjW6 +UmXWFM2v3WqJpDmYIuz6WlVUB/2cXcCO0/jPoPaTcuKuZ9VoG1KFBie+OrWQfzeD +hX5CdIoG7E6LRh9PsqT4/9TM8LI5aMGHajIGCHsi3OhsR+9NwgS0rn8qZjmTqrvq +L646JY4C4Z2Bnuk/xpEmqjkAok0xQZNWAX9OGJ7IIER0ThD1Lvbvjm5j/jg4cdjJ +SvZc9ifXwR8oJsf3YKFkH7ZlfWKg3eU/olrIovu+EaFEIVEjWjDqfzP5OENNB2Z/ +12lYrwllSdDxhGr/5VedO5KnKy9NrfJJdykCo2UbGQADZcKLVlNFVS0MhO4s5G5n +/1p+md2iHbXZW10Gt5oLR4pFYM9i1Lw5BP42RVXfHSpvR4FTJ00s4/nXhTd5xjdc +uKT/7tNQ+wlgINMRke/WhAcm1HRHDyxi0lEpyazoeur0ZMJ4GHV4IwU/nzqTN4uS +TLt5EdhFu/OICCHmeb/7zAJDvJqSuYqNJGT9U1zFSA1aGl6nRlhtkq/0OD9Z2Nw0 +pAI2du0/F7XA9ERfxYMTZFU62i6H/bm4+nJ9UwQcvdTLx7l7aRMY3q6l7FZqIpqr +zhK+AzTYfyH31XNDG9BrrUni4vH+QKQ5kgCGzD5iirwLTYNI97hwT3Q0kJAJbau0 +az2cGvjEELwud6GEcfc/L6BCjS8frzA9eFYK3BirUuKppkrq0BlmA1DFsUMzx2Xt ++AAv3nu7cvQAr6gxpFS5DeYIGZFyrNO//L9QCTKLXek+rm11O40GVv+ztAjuVoc7 +DcXQ3nttGDrAiGjdNvQyEImLcwnZ/CD/6TUiS7gEvBeotd4zaWaI4n6btP3V+qXk +FujfoDRzk9RtTqI17S9ENJFnCPxOmO9o2tchqttnk7fGHP3ZXalGtX+tyl+05C51 +/UVrIr/3FWCvSoditLRMUqMEei0qFERA/SxCv28JE9eCXmfD7XBcWgmXTteubG3x +yWrpttxkXbNqBf1HqVz30sV1XeKPHz4VGzInuhJes/ziSn+LsYTDCcTnAadoAM1p +LEHhJdGMHTZWCl166Mz8xRWdHnM8HJZriAQCU0rqfSUPrMnuhUKWnOiyBYtkcPd+ +65XCZWjN3f0CCNn7A5XZ3RAA6xrZRTsez+MtOAQM6vng3oabChPJwNIxGBum3H5/ +iw5WWsr+PjQ4xG+qOyZEAw2dSvtOXB/ez+5uDqBrKxGSCzQ6qeIZ1posKEV0GEXV +uUEzPyBjh3Z+gXH7hTLESI/vQK1J8YKdAUXOuwFwBLALYxQKWOMeGH/nnIR98Tbb +KEjBYrQitB42fXFd0DT7uUd8Df7zGv6hPbblu9eL5FrOxf74gqVcTXtn1UCLgm5A +cG57gf4H9lA3JsoiI8bXaW2PylZtYGtZ/z3ldShdQd+YI6ViLkEYrWOiJfbE5tyy +1Mp6wzPcVi0zFW1qVqpCczdMLyHzkd8skysvpYGoKPJSjWEieYMhzM8HsuXSVMZ6 +jM4vn3wwn4A3x5+Gl1wKVkbJEVwkXP60eeWgknKNu4swwMZ87gdL+vn9uEJnYqjX +S7rk/b8f9bg+Du99ovynCSj9ywYjVNqKTtWe6ak+/B8CWLVKXgEvJla+ynFVfie9 +E755maHIyq4tjAwBqsBD0ZuSCpKD1tLrxJZzQajHe6Qtd7rHKo81mqdmpX3+cgm5 +bSJcki2ZD7prwwQMBMRaqrzG7W3RgEvnUZAyOJr6NBIDAJqKNUtj4USAjeO7ISX7 +ZCWrEKUIPnjQcCQzFk7H2w9dNEtTn9Oc/5zI9vk/YAobnweo8SqrJAJLywpKq2by +NkMDfBu7Z1qjXPY9q/hd+y9Q4VulqAHQvQB57vghvJpdhR6wjS1oCKRcrRYql4Iz +ugHu3rVCdvlj33KX6Mqsa/1hwqNd6lPJdFVDeuiQvRgdAyF/SpfBofehfpvkSf+I +gmSbIeQWI1QDlRL2LllJHmk6ktZJ3OTUIZ8ttPX/wLF8H8PzeH+YI+y6/qXq73JK +9hIL0mz/KsENA7/gajBUxqK8TWCdKqQC23lxkdLCEiZ0+IEVhP8XZdmb/wnRR1Iv +wG+bjPHBnzCp3rtbNGvrAG3vmnBmFaZVFUDQSmObj944rHt5vuLCjuSzcxWH7yzU +xLtnBQ263Qfd43/KHVODycW9PeaPc77rwq4ksl/VU/m6xpUr7HtVWUFLfPHoAwBK +GLTOQcgkvwrn+8Kboih1OMmZ4cPtcvL5XdVB3GwkkFpTWxk7VOnU70IuC+un6iCa +1LpmRz06pEYvTiXITjM0GE2QLbNvQIkKsGnSUlwfNAKLtLLKP4jJCZMUgyXbsPZo +buJb3CBXwG9pPdZYqi997qptUBiXEKnynVypqaJpaj8t59VIqOSbHyTsrIn5Eptn +xAg+N/QbwZKc6Gl/yD3ChyOiHflf/Y/bnnMnKfcDs4kAzeO2fEB8JM7Diqb8vGwF +tDSoxcyyP9QNAb/BaeXuWBIhLsUi2paDsQOSjFeVKxI+8uI0al20vHn2bV+aUAsj +Th++5LGHPJZEp4aFziO/T9LwAlx9E2DGqdw8wOS8MN5Ogs/gcqx5gJUw6zxb+f0/ +dkFAQokQebH/G6K83TAlXovnml5BaZ23ELLt2L7oDt7iestXxLp1acKCAn3iub26 +FC8V6Ht1UFDUnP1WaaGF4CQy2X+U1MH6ExlZ7KGc8jC2v02EXfnTFTYcqhIldBJl +W7RKFrdUNfoplK3orJyZRE2wA5PoYJXYeiozyRi0LUb8fH6wEBNRnRQDjZLa6MX4 +vwGBraabcgo3kpjIBJ387Csro2V35WU7A/svcmeZ4sskwQDGbQZZHznUPT3OSIaa +2AA6HgN9LZK4druc+0vyTnE3SMe3cIDU+2aXXf+EaHpqa4SL+xjvSHXLujPZi4uN ++Uel6chvORxE222SUhY6t1naNPw0b27sF0ASUl4MMIwiyoizWek2rV4qqSEtCTvg +AFVea45jL6Uv9cSrD8BzEnO36sZQdRM3ujrhXb9LpfO9ocMxB2Yozvpy3ecDvt7Q +I0zgf37HPylfQ2GSFIXaudDb9vfMPy/fNO9lwp8tAnHn7HtAXO2vBKkgf7oti/vr +AUPp3VtQrk9oclM0R6bXDf1EefDvTqX3a+NL9gdtGfy+q2JVW5Et4kix4vP7mqSF +Uvfj6ReIxeRqOmyvTIarLF9FdS61Ny6/B5QoLjdqsLry/gtKdH6LyOP8GCdHjJ+1 +xszm/guHx8nSAwoTGiUyaZCS8/0KRlCapwAAAAAAAAAACBAaHyovzsQKBlHQxoBp +AAAEwC5vpHgHg/y02hzkOQ6+ZH9+u1sjjRDrg4EfmhSAeogdCqBaDwEUEVuK3Mip +jfkKf6CYBDhbT3CrVWwvFlAiaqMTZmykhcGYgbNeG9YJWpyQFca4oja6Ztku1MUH +fjyR4/J1j7wdY0w6YuBYiIw53UVIBSeh/SijdpogwArN3+AWeDjO5vMjW7YEpUU3 +eWFNakm+DJapkduw9KfAmEl+ZegHI5lX/PtXTthLYhs+GkARf0mUdWGwfNa/lyiT +RiKeIbEq9FIjcUEMmAwaXWdIvNrBJ2dsg0IkQDE9yLkw6AFhI3oeZ0pFyjhk/XDD +onW2WRp9YCS8kyw9OSSByeSKd9CeBachnMOreBuUmvYBxwUiWsK2F4eaMgYdpihi +y6vLt7NgjDKOdjqsPhEAYeFCy6Ev5UJ/mOY009qbqmGJycpsgLyLpkqM2+dYWFgT +cYynw6Ad5mAQEzcFs2kL2BITHaS/AaoD3XCL1cYdbKRqKgOPzmyuhzZybHkoHBC2 +jcQY4zo1lcSrZrgqOsNbjaMnAgDJ+jF+1AvLoQESdViMQNS3NPJm5MOwhZIAV8Io +AQg+fLeKveM56sSAjaFqVqsVoWCcqYMdqHom0lYs30uabyylumQcAGugGBWKIoCU +/EOjx1ZzPxAtd+VP/LSCUUBAnMFus/vL2fzEH7BFT2RasKUZXJsQxQJz85cFuqZd +O1vJz5VIaxWDsOVJawIELaUoDYaAVuJg0ldJB0kLvOJGwhw628ggScNgVCu2uImp +jZVFMgJkY4d6JoWLJMGneJkmyNti4io2BBmaTyhLpyiAGeuXDwIKOSXM44lfLedo +c8s+AflfwFgdSbxYRWuJC4OF4PWQdroutSARljVULKEc/QoWPYSYV+hPA/xyXko3 +LKo7lKknnZl5s6J2LvKbb2p6CcpvYKazyuxQXlgZBmVmkUsXfqVD0OCFXWExIPQs +PHQ10FSK0zKPeZZh8RUHAQJD1QEFSJXFACxvfZw0s4FMyQItn0HIF2ogFcGmj0dA +HChPVre9RiIhyoJy6BqAEDnPSREBVGaDtUNefGiCLHemYiN2uUvGxqzIXYaGkfBj +6+ah7KkwKYuUJtaHArk5JRQ2wwanXjaQoFQqHwxxPoJ4XLQtvbe5UoLBzWxRCoQR +HuVn4aaFcENPGBp5aiaY3yFQuFScfsaEi+h6FlCo9ggTbSK5D2tOgwlUxkZjk5S+ +LHe4hQoU6ZAf9HdLzsNr3ZULyCRigivH9oguRdoXnxO2CcdETPu3WgQfA3OH7QgX +JDaH8NGj9/lqF0JnO1AA1KkT15uZtpOYSCzM5GJpqgCW6QRDFlCLSixrXzN5fflx +7ow2YVEnNIYAE2gvVaORuBinQ5QhwUFzWHhd3YpzbSAfQbcKNqkairCx64p6AAJM +Rbosd/tK8omiTAS8JIkRd/kIt+rDKgtYkWQFRURyjrVkunmwI6SrQMIw1SsBIyFl +IJWp0Cd4kQsyCUMB9RZgsIt91YAvwJZ4SNGuAAmNoyO44ZI5/nHA+pEmp6d93Xut +v9cPN4p4ApMdX7QaxLCc3qY+C9zMMsWk9fC7kfNU/OGNfZiexLM2Tm+57JxZ0I/3 +c5i+CTkeCqVTVevvaWRbUTF6EHzCzLgGGGsMAAAALAWCUdDGgAKbDCKhBr8mKyQX +cAKsiuXcbaR8BW0iq5kG1H0HlSt1w1gCGQHKAAAAAE4REJBeK4w6BgxSX5H9c9AQ +u2CHzklqkYzjcuYVYiTqZfmGpwudE3u171PNzppoBZv3NUVHJdEoEeHhg8bGwpi+ +zbAxv6foQqp1+YvPeZqcKjAPwODlB5pXM0fF33GZm6dNaGKq07MnAaKeGMp5N5HZ +LDyfc4TdCk3oCfJrwZ1aEAEsDmKwtrujdqJWdgzlXsvMJMRE9im5GuadIIbkNIMF +56GnQvzmZsLZFZ5SYEfyn1RqCTC8LvLmLUv3PiPomURReIXT77FZy13FHczLjZMW +MsnPwHsnFcqRsmgx27vKBdKSUXKPI7w0wte9zUNEHvCN95xc32CLI1pIjFoYq/5V +XLrdjoo2CDR8POamYnrmQGhVrxOI/RDJS5PTB9Zb+QHgqKY9DEOG/2VT64kn0eiw +40E2PZdTF8dS6KA+arWKdiiBwF3iHzaGAUX0EoJ6BFxgyXF8G15ABZL7G1is96Oh +pDpWzlfVgZGxAJq2jFY870QrD39/EocfSpUP0kRbIL2a6xt3omwPhPPfDUVNnMug +9n0km10ITZ6+gjN5PzTFqnI1w6+uCcjzwH//9y6W1fUgY+Cl8rxCskvZkDEWMkIh +RVSsLK4Gzb6yhvLH0cG02ikJuv1ERYagtAVi9pOPliFlX3QNvIWopfKhoDsHjB0O +M3TlqREdTH2J9YlviJ96uyfpp4h9qdBj+9Z7uCcxz2nE5YNsh2cKGMawCPGjWJMn +csOVkPmc7k7oe0fSwh4dECAaiLysJaP7WtfoaT+68afqcmnm0oMf6+w2Kvh4gpUn +jMaj7QRT5cmg5Oem0UMEtAQnNCcAH1ButBAbsGu4V38R3C/TS82mowQ/VkxQG6tp +nNdFOgFOWYjxizoAXHdZxnGTKfzjJU3sZSumZWBVKKfa8PIkMbh1re85MrXNKhrj +S+Du3ZElz0fygmx3kcfEOGPuI9qq3T/x31zVfB2vXnXDgW4GQqyGsIj+KAw1/npl +MYn9KkSYVapu2MZ3s6m2X6pdo8NFAFyO4h2r+UdIHr03KZ5mXj62Bk+391vPIGow +Wnv2ENNeK4tBpZUY+ZP56OPY97ZvZhuoFqec9JkCDr3bZeQsf3qrFG7hjiOTNUoU +jUjYTiChSRevydJuE4W9x24ddB+oBKr4zBTjStZzrJ9GKpdxxCT3SkMrx95mOsuI +sNDSUgCZXGuVXHLCXmhi1hh+AoJcGc117Oh0J1VIcvYBQzvLSMC0t5ZAKB2XcDqV +I8ocUdsXA/v7Z4EAESDi21Ln7Cw2Rh/+0GkrADwv7srrYuYQK3/1OqUvL1MmKvCZ +5QUoqq+KMzCXF7Zoum4IGf701Hba+8bOhN/6Ohf2BkDdrYe1bieG8yvsGSzXdy9w +SHt0dfAV6KVKgk+mbciFBKbvplLstMmoagC+IKUIF6OA2uQhsZrnVbq/mTc5G5RD +/8YFe5kj/iE8bhdQnRTdiaCsYW0rrfgTle4g5F3xVKXLTaC52NK4y8sZAEXHm+EP +Xo+odH24XDh6nCNbkXRjq6R9pm7/LmRe8NV6nnV0yMGCzEqvEIe3UyQnauGemwwr +YAT4CzYgBVjnqESomwg35+439aZomHLiAnSA94AeQqON0/nvrCs8i4WChmDKk/5A +ZeaSsJCKliKYq88tv7uklCvMK/M0vYw8f3g5l+iWTzjM3mhs+VZ3sA/WlXXc/b6w +c6aQRH2gyuaXvoAqRzZBGmlYKd8XvGR7NeY2gp9mwVZ4vT/WbQUzD1PowtI5uPN6 +UNPL4HpzYRYnIR0pLNozIY9S0/PT20W4lTv63shs0LxnjOZQBHZuriaGL9Y3tRjK +ja58uQYjfHdkcMdu24CnKglZRykzYjmriU7ixSOdJ0ezTeoA/ECBG/Z+rXBQqVRd ++qXfqUSildqmDZ26dD9IQf+Iydku3o5tBwl8WjmDzILR2XOrDhj7B9Pz0DFgynxf +AM8BZpH8G+z7jLswm7LCCAliHLb5pQmPwqlbl+CE2Qlc2grfp0T3H5pwdcxymaMC +sXl3DF9ZoNwPCbozckQ9Qf6RpTC8Nm20r0wQlTF4LQuN409ZHGohdOUzCDW+6XYv +W/ykzsZFYBuOPmjvLp55hEk5LmT9sQ2jxJIX3/znkGDD8hFWNpgxsaDqBSn8i+M1 +0locMkaKys56dd5gr/unObrFMsWEE6nB8Rep7V9jao9Q+NpT8e2QbqQlMwuErxUd +kOyIFi0hp1zFNybEe/xwq3WriYBsS+MCEb9PesaHKwa3JcpaskaCSdnjEUjIhSI2 +HAkIfK6uLdZZ5tU3W3Xi03qdqB9i+xYKkCsA0ED0ZAVh/z4rw0KSaws2TogUyFkw +fC5+ie/aJAQd2Vnh6L1ZM5D3rxb7HizIGrHvpYgvgEiSLmB5Md2ALxLA9PX8mWz6 +O6/dJt2aUUzbRVCO50CnhPt9yktdx4m1JnKozvW/rKv4SwJ4T8omIpnr1g870Wn9 +JtCfaRjGqhVLUqVQqC13AHLTjRBJqeJCtp3gaJSgas5MchsLOww5kOZoeClTUSCr +WRSlk85RFKrq1hmkVBOvnsytaytS5U/lcuamG2Cw1KFsipmwJH/FLCjxUiHkbsdh +1ZCAxQzjhvMkUCnPI7rJBhWZPBlM1KyqytOvMPTNUcPA9AFj1Ks0UqABgLSWSoyf +iFm2FAZ/fDC4qDZmRkfrRnOvLOt2eVlKri6hKu2ag+MHUJbZilj65ryRC9RxmIs3 +jfWGr6UxH9Sim4kTxEWhfWccvNx4G/0yPyuDcKmjAXF1uqMZEte6a42S86LN/5Ma +erGMqgk4iT6hDa3LPA2WOsv+lNaWESVOYT4jHZNBUFhgRJzDre+vgniqyJpZBzWc +YMY8zcQz7PiCOXPm+XNLIr2tKeZAGuC4uiHqPSG4RGXtK05GG0af7VofZbSsySp/ +hJ8RAAt+sAi1XlSpE4G7th6SSeMvrQwndkPO7N/8JCTlVCrm7MvZEw7uPs8wN+Qz +TyXcuMY7dtSBcXPrSJZFdRloCV2ueVgfGfX5qfL3xz5yjlWuDookqGrgT8gvN7dU +bn5hLltGy0EZZRkg+6jhqS+LLSoNmWHnU2ndzyjhntIknmAstlLsgUEljBDELOdj +ioelnauM57gIajtMk45hXyxtvmX9ElEdZG4nA7zLStEHUGX73QGwV3IY0DPunz23 +ztrctmku8az6krOmcFjTLi4OzETxe5ypqsUsc8LZ95IqKKCZYUdGCQfBm0PVPlap +Ci3rLh2uM5nxzwvZLYX2SeAoMxS8YckUEtkLsinAuVAI4ESYCgQqt46x1HsE4r14 +Y10FDfPiJVLvlKKQzPiZD3LJ1WNWMbinubcoorBQWQF+ir1fPmr1kdbvtjVbXp2k +M+HY894swnUwOfHF8fM1vCCbSbWDk9J3psvhrcF5dDiMn2Jx0UiU+JbQDhm2O9hy +yBVUh49yfU5m3TLuOfSxDo3MaYUgHHZ4XgQpSDbdDegs/JINxTlqU71jP51MLf1s +xu46EiurJIqY5nOuIBgq6WXISjaImkQxXuRgbM0Qe9GRnOMCv6ldEXz56XM9Sscm +qsaaZVeQNaSrbqRwJONpIzYmXdMkVBklq7Hn0SyAHi+BIzsuBq9O19y85y9xzqVw +DGUz8IZ5kRNGuO5mgq/IEDEMeEntLKmKDkv0lDCk3c6QYvXtGsvn0A8l2sFDynnZ +BJCA8X6PJtJhKSlHUx44oFEwLd7IfBlNHXEAeMcAYLcBv4n632kvOc6DkP9gKI80 +k/nHh9OJJ/Pf8ymleXC52Ah91PkcXtDqISdxZtRGWsE9Ic1JcjiI1Dwc4gLn3DiT +Gk93oLwbMHOBVwXxnusqbprY9WzajBr+Rt4cbH9G0Gev4AUUA61Mzw00lhxKz1yb +GSdmzp6ohAljkPba8X7H4Q3JNsQ9Npb7DUsfjHxQj4qd2MaQmhFAeRfEUSOQSj7r +ADgYkkjkmCZr2kzUYowpWhe20jzfb5CGW/eNrjiZdzWw3ViENke1BKrrrAwmTrU4 +gFjC3K7SBBarOBDzFZ80BmcdXrzFoLVr5Lbvjs9EMDxdzLdwEtslxdN92J9q1Bsn +W1a+uJY72mL84R5dL4ywVF+TcJdhvs5fZzIr1+P08JAdBUWEXdm7IoMHEAONPzsc +m0EZ9RW8MGgaCa+gwS4XjLEh7kSN/LQC0DXDUS+09gbJlVVbte3TaHSpZTt7Kl6t +9KtXHlPXU6sl3ursFByqzcqnLSdd46NjJVqQB+TjiKglpWIn9x+jMgb+bkKK/dwy +eTaWZ7FCkSIDmfnKe2+tiDs+0ZqbbgaHYfLpWe+y8u7GfakRAehUKedjRFvjCqKI +s2tlR4r8Pc0coNAAfasPRc2iCIOl/E6Q8Psp0MDIR7or8z/OgjUEafVEBVZML9/z +u4djz5Ql0U7RxmOoFnZ+TC1rnnozQBcBNVgFntlM3dC/5wjTzH0K/cDIofaYbadc +WmFbdK/B+gwukZuv6/ADssjK6jFXXnCtvOghLjtOVKDo/QsSLzhDpNsAAAAAAAAA +AAAAAAAAAAAABQwRGCAn +-----END PGP PUBLIC KEY BLOCK-----`; export default () => describe('PQC', function () { it('ML-KEM + X25519 - Generate/encrypt/decrypt', async function () { @@ -155,4 +739,471 @@ zdmQk3zZBuwE/KAXTAY= }); expect(decryptedData).to.equal('Testing\n'); }); + + it('ML-DSA + Ed25519 - Generate/sign/verify', async function () { + const digest = new Uint8Array(32).fill(1); + const hashAlgo = openpgp.enums.hash.sha3_256; + + const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.pqc_mldsa_ed25519); + const signature = await sign(openpgp.enums.publicKey.pqc_mldsa_ed25519, hashAlgo, publicParams, privateParams, null, digest); + const verified = await verify(openpgp.enums.publicKey.pqc_mldsa_ed25519, hashAlgo, signature, publicParams, null, null, digest); + expect(verified).to.be.true; + }); + + it('ML-DSA + Ed25519 - private key is correctly serialized using the seed instead of the expanded secret key material', async function () { + const armoredKey = mldsaEd25519AndMlkemX25519PrivateKey; + + const { data: expectedBinaryKey } = await openpgp.unarmor(armoredKey); + + const privateKey = await openpgp.readKey({ armoredKey }); + expect(privateKey.write()).to.deep.equal(expectedBinaryKey); + }); + + it('ML-DSA + Ed25519 - Test vector', async function () { + const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey }); + const publicKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PublicKey }); + + // `getSigningKey()` internally verifies the ML-DSA binding sigs + const signingKey1 = await privateKey.getSigningKey(); + const signingKey2 = await publicKey.getSigningKey(); + + expect(signingKey1.getKeyID().equals(signingKey2.getKeyID())).to.be.true; + }); + + it('ML-DSA + Ed25519 - throws on unexpected signature algorithm', async () => { + // The signature hash algo MUST be set to the specified algorithm, see + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + const privateKeyWithInvalidSignatures = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +QkebRJCZxZG/RCVh66PXOpv3JKQY2VscDzf7x22FDxuCDiKR72Mi15XydCi6fnQI +ug1/j6olqArvt51WpBoM/aN5uD7fuzqcv5OaCXuhyck6lhLRfp0/YRaYdlciihOb +1jHtdO4545qpeb/qETayE7R88zrUQLkiNsjdGdxHgs+MHl64KFC3D6mDvkqnFdqb +7Nd4FxDCBGv5ktQlKFKhGWtrR7PWnOYif6JqzzgsxWnC1nmVEAeWtw17s4tuke4c +Rh5owUNJBZZygKBJsn37H5cXoZHw8uzmESNxgMxw9zVUoeOc+l4eX5ynM+FxmXhx +IhOH1sQluFb1MpQglVPBrWzz/2NKvV9tsQq+lISpoewliGawL6Vh7X8qyvzad1Lm +ofEp0p9RrUwl/iGveCR0bdX11D5P/JyetN97OmQoLGmI9FWWrCZ46rlqMuH6+53i +ejXDUGXOnn0D27y3MdmFtklIMOMNapEt5Bz7iFbtA0uxPBD6TjlYwP5l6FEzjVyE +/KeOyv+oVZzTFsKW2mfyfPrj2N+DyPrMknZzI2joNGUkKfcuRcO681OU6tX3bj01 +sTWo7F6wg4ZQdT6Aje2ryScj24eHAjbGRqI7VQGvDx/kYmSXdrbhHG4Hnmk6mhG5 +NO1kR6Wu8EioJ2wCjfxBmzHGu4RP2+pWji8KIVeIy3LYfyKpgP8jxx62VbGYrUV4 +Iag+OPBukfe3QLhr3p5iMnoPI0ihwNYmCzCvJ76IKLmfjwchvT/W0a29syQuKcNa +ofjSihIlUs3ug1T/ZG3Xyl7NF2h0YFHnF7Rpm3WRSZQsePbMGmwiEck0UFP/Z/DP +Fe1V8lkH3hEty4NtMUqtn5F1VHalFs4C5UYKHtBRuZvqjvr0xnbZDFouCc7m4Vjp +lzsS1tSjLl6EAyonAbbQ3vCtTn3v9k8Ro8J1p9mRHA1NdBCTI9J8Lkml+f1IIn8g +VdHTTiYcqDxM/zRPmC5ve6fAD+BFh7qvDkIGqpB0FIkHnstjp2/XFaxJ1Xso7e0Q +Yo30tzVyBFNu8KQF8Okeh2Mz6M2oKn8GInjeDMWD9DxlZwHOnlcWuXFeurj79G5Y +iUffg9el18KCwIMrnDD48xUqv1gYDRy/1LAp3AE3nnm5cIPq1Rp4Nsi8DQBOW/ZL +J7HW1IXs6o2U7Hytlx+KLZ2a3ETlZ5Nu86OwZYhErsLxDd3wF/85NYUzMxvO5uAV +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 +-----END PGP PRIVATE KEY BLOCK-----` }); + + await expect(privateKeyWithInvalidSignatures.getSigningKey()).to.be.rejectedWith(/Unexpected hash algorithm for PQC signature/); + }); + + it('ML-DSA + ML-KEM - Test vector: decrypt/verify', async function () { + const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey }); + + const publicKey = privateKey.toPublic(); + + // NB: this is NOT the test vector from draft-ietf-openpgp-pqc-06, + // since we use a different KEM combiner + const armoredMessage = `-----BEGIN PGP MESSAGE----- + +wcPtBiEGSLlLzi+XcXiPX+t0Ei1ZmYnEAMwPSRCLyY4Op5ReSDhpHX4UyzuK +/tcNcefLVJCTJdJ0gC2Ncaq4nLayjf7XfRv3NyJhgOEgqejpBiGE87htXE5G +SkdzggBtJjvqgnsyDENvpcJxXLM6nekhH604VgCm0gsymGA1yXYmB71/nN1Q +L9rBv9oS7CDmz37y/zO/0x0dx9JIHd1hC6jwlYD3J9Q02+Kdi8YCZuP4ND1m +dEY3GTuyAGZE6L7fDMt1YJF2ibRMxBE44dIPxk4Fptlzt+YvlDs6T/PfH8XP +U/u/qxJE+A7m76aNSEtUK82CLsSiGeH6EKJmqEd8qGCr7N/EQJQF9tDodBbM +BH9eYzwnWO70cqIt39QkBF+yMzr+LbF1GFn5Qgfc9o/SKyDfMbqxL9Brfkvk +7tF6oibLajbFAR/NLIZ2a69otFJFw5+nAeVNVv65XYbjUO1XFlPH3L9i4sAy +9hL+UapWfJLfMijs2T157K8ijKM1nmCpJNgkaUFYy0CvnrARBuJ5Yy3nukHV +xIlexA8stFnbTbfBTiBILzeWTM+7PUqey7ouUs0UE6zUbSpwZOvESZMGHF8w +mBFEIkVaAiCBekMLXu4oFNl43QGZ9p+kFAOrC4cIvQ5Tfi98WrIgCkEk5NUe +TPWiJTtrTlWoBJbYG+7oSoKY4Nl25RWThxIhMBRSz9IUqVb2vrsBdZJrV4aP +6bJ2EhAFLjCcxShKU79mnhvRn+XTfjLPEjrqf5x+KbP8vwPznfiezxTKnmIy +BEZk6Tm5+106/fZGM7+cj9TwGKJsAoSF5Moe1qrLYA26S1Us6OEdTHVjo/Ua +Du3bHrPpNri9JGTKOKKu+Nxhyje/ZG5WNOyLgIS7p7ytmFkxp+weSpZAT4yz +6FeyVwCvHTp8KgrLJQaiQfvVuD7HkweNUq44LjjktPXTSp8LEi6A7X9T36sG +nTSfANJtTqqnUYlmPFyzU7djYcReiQ9F2t+yTwtnQdDIm/WpWTkrZzZFmn+Q +Px7ry/Oi9QDzIU37aiXR3deQ37Ajh9NrNUr4PG54rIwUbVwZf93+yCCrYI7o +953eSeS+jx/LKcSNcEJFCOsuStN3aHj45C3qzr+vrYEtQ9/aUeJrG00qfTXw +zFulHIVv0+fzswSA1QCbLduPvbTtuLWDkNr+kbI2hdNB977yqpH+92KhzkPU +jAgCirTuGOl1Ehx2NLMr4YhRsq9TwrgGy/u3DaOpZPTlF9WgbynrlPAOhUIB +gjbvF0lHhQR17SDmEdQ+fLUj13oh5on9Kb3TybHBfuJzybzOml9MpqcHsKLU +jzFNfK4NwLtYwXYPXBXPXU3JFNNCYozzFWrpM0l3CDPgUQC8k7PbhlMzXTQq +g5asEILt3gVev35y9NtWKwACeAoLHZMYq6N085igBJbOhSqMb2tgzapPvOyR +AoX6hGfMRPmmUInt9oBDxH8CkUkdE589Bg7cnV62o8Xcur9cyQLcANOb5SW9 +GJq+QCUbwNsu+bdR7TDHw3+key1t6Onb14+b53bKJvKA+igt1yiXRcSoNin8 +MXJoys+p5jbn118hP+jTAqIXfdUVS14xYD8BaXPY0usCCQIM1kbz5+aLR7mJ +8CR7kOJxvPaGHlQdbqJUhPoCoSoLbygWp81LD+qB1FTLH+NSRXhRpyMyImAC +wD7FF6+Ba5wwNzxBJAxhlOj6ypGjhIiyB6LQ9VsyL1fPQhQNVgw99SlLkScb +afipBrbvUNeRNrnAQQXAGnRUDBQxlL8Psz91fWxcFdOu0IXDfgMiw2HhT9aw +T9wETN/0GJzBriieplRXAW3kG9O1SOH67ZHeYNbg5J3q3DkYzBrSPVFg3xze +oGpxWc0x7y3t0kEdZMrspkCXDjSP2f738iGfLgT4krlSn5Xr3294kv7V72hp +giYdUngFXaQ9/EU0ipjVRHEoCDua6Z8vecCA9xXqee1bFxxQZRQaeROso3m/ +dV9iCF4N83lz6gAHX78noKpmqy0LqLLGJmZb79GspINTIsyCZjaO4/sHbPno +ZPlVh84Pr8mxzzH2IF8PD1e8en9ZqSpUC1Q0aBgypW3g+Ss5j8rjTqzTzS4t +ky72Y6L+qP8/a6bzrU88fsOeqRTTxyFHu4oydRmdbgrN/cvhltS+6FuAgRs4 +wDauasyX0QA8FglHJ6OcFyD5KNgRMDkEIonxG2ggkswZxqByv0rAN+7oGO8v +DDdVajUmkcx5gDBGuWgKkpPSbOPnvGtSkUEOmRsLn+oNZcPumVzMrq2pzU/G +Icqko8Ons09h34Z1yLZO6jMJD0cwppDqkOWbcKKQZZXJ+CianYkyucyHhMce +SI6tcwfqgg9FfoYxFPRKbhlNmhLxRvr5FPSYMOdixIXTBOKiS32yEQjZcH4O +n03PySLzZ3mMpFcCmiSkd5bqZcoknRDbUUjxmasJJBFMs0l5ps0pEABtFlgz +WSbrzbvGX7uwXFqem80MWlVJtVtN5608bPqwsiKIYXB81hSXLcEG4DYpmApo ++G8A4nz2xY11l/dvfE2am2ooH2lQelsx5KnC90Rte2YNdjAtJUFJyFDmU820 +UagJpbQ4vU8m48dZR2XBdQI/0Ag8AlJNqUuT49XRfevrMJUca3E0dKn9g/oS +Q6uevD48hckSyDZ/JbSjZoXV5T7xg6Tivksns8Tm1d19cargLw5+w+GTMK62 +JMEBSutW1X0KvewwngpQ4m64XEx1Wh4kzp0Wb/O/r6m/e11qqjyiMfOZEZwG +EYznOcIAeOAtdjj5dQiXAXZhyq1z/Z30uWVRAQ94jIK5wXRwR7ez6uINV7FU +2BHs9NqJg/oviayfn+VWhHgHKR3Z51jIx4DNCGjbGrnBVeHSuQv3BZvmnsFc +V2+NerHdwLsslKm5mE+FDt4h3fZNHHApb6mHuh1b75Jc61q62EKaJElSsQgq +vP4E25WcRQ00YxeSUIPeOEVeGQvTmIwKCMmDt2dxVfbwTvaIsombIDs33Yw4 +JdaQyiHvwCcChDQlhAYDPQwFV9GJSEkwPlRQsCxgdZE+WhIDqxIM2IF5BMF1 +AZxpQlK105tDcnu06Fmbfc4blmLKZ3mNuQA/KCvvznclsbMNCou6utEAzh9W +5bI8j9i0yx/sG6GHY6awEaGSMFr6O2t6mcouckeWh7KZ5wqsGUnP9A3JgFwE +g50f86rHegjhqzkWOeF024YzhlCV9wseEZfBLEISU1h8NiaP4iKq5bi8Apo+ +AoBuYFVz5d/1e7bF81wxmo76z6TtEoC+WjwHHDp4YJh3LMq5/bdf4gjbVgbO +iWNaxho1JoPdZra3Uc/lL4EHpPyEzv92IY420dGS5I/swXodTPB1ul9b0NfG +rhhS5mwaPO1l2O9SiKbUv5gINzfpD329pQlDgurk04D52Jl2Uir1fa4o45r3 +4Od9R6micuzLWF2Iguw3psYakeQ4WS+uiDQPCtlWqo20mhSFRol/t8tXKNeH +M8PVF9pEWGGg/KWDliHQIEb/21yR67x+qqplmYcaPxAq6yj7BveX9qlEWb+v +Ly7YLv4QReQoNl0ouFgQ7zi9rEOuceM/iuNs+wbbhzXr+LVoxYbFWtir1X+M +eFhtV4BtjzyZjfIfWrnu0EcXX4IUtG3X9B3E5AhGauhzLNCGG4hQKEes2pUq +b+d0Jjgnj2ADX2bog6JPVXMS1LIVKxWHcQsKmdK0rkGw5wrrWxi4/H8OVOz8 +XPt6GB2p0OYuh24A5ZvX4Mhu1FqoobggbF36nYksxSkJL9rwbOT9J+cmUvD8 +QhRNGMW8K03/41V577GsM0o8bJWcqp1ABFW0lGWZTw3Pkcmfd74btkNtZMkk ++gC5droxZDufyJOPfJxR7413MunyurSZL4foCJU+VI21sHmieC4m6uiSyUIC +6mbzB6MNmy9n+mrjZsEVBCcUF1idkhwbQgARqq6FVyCBdqEq6lWv+DzyVVyX +Q6h7QiqiJKrIpXLy8Y7KwYQ3ZfFD9AANIECVHXEpjeP2tkFX5TXlmwYYci7r +mhYvvuxqaBZ9eb8wymcYoFixY2E+xnB6v+cFmt3IayHItNHOyg+jUN//r4u7 +0KuX/TS+yLT1x6VZa/5uFZ7+5T/W/DIgFjk9eazz5o+I8PwzVoMYce2Owriw +ZMxi+IuwT2flEM83aYs0byxLfco9tEVUCyUYrEjtcoVWsyvYmFV/hpl09jEb +8ny9iep+tp89fypbknZuS1k8Xl5FfJG1tuwMcYmxfBM36Yhgtfr3qPWVoDjY +uLtfW0kES/F8gKEfcFHYvtYFUPMDe+2BckVvfdDMRJH0Zc7bG2qtlyyjTgB2 +Ftc4uyqtByzpMsVFFugKknFqBwCthkteP1DCcfU9NIC4iRcRx2Go2vEDfM6m +uZ6X/2UrG1xHq3aXPDcTHnVQW+hFoCxYIHeR8ubKX2TK2ntx9BITKc3aqzwi +m9hHqKG2fgq9wDpNqdX3KZJoY+T7/xpT1YUjDzpgWJ0Pss/0DT1Vu61cG660 +vcwgngqlWcBegsDp3oEAm8ie8gOKka+d+Vb1ZsfKdFekWMQHh1ezbPLzMDgf +OmaKlD6avbnwEOHq7NlrKPOed69ntQBBr7eNlo4g23x2H5rqCKepzhVTPU8S +WPVrs1xUR4o8fxchd1ofDJtCPANBqily3nMKujxxZCysK9NlyfPtZsZrt6Lj ++BFp3GfspLSuE6Am4xG1bD8pQnkOp69Pu91CPr1FAxlmxS67EK1EeICnFCc9 +LuVAdm70xslgXXPa94eH+WvyP9CVMQNhHgZrJsTvD84DH7O8xliQgWH/15Dx +NaEnyKNvnHjLyYdCD35BMPRkCqM0hX5DvmNoYXacYdetCkD6UqgWHDGr/CuM +Oub9Z2+SM34ki/H6i79XRm3fjRGTTRBk7Bae4MW1OBezsRa7qVBNjzdAIPfJ +n3uo3V4MkwQU9pE95EtfThQwTacCc3RGd/42D6BGo+xzcDgbVnVGOVGEougW +1AnWUAIJjyWgPz4u5DYN3K9j15qQROvPuZe5uJgdxWiMH4B5sIyGu7gOvDCy +gELV7udBCDYauJ77mgsOh1JlC/ZBLZfUNUZlImrPgyuLK+K0JhxQIAEn95F9 +mEYFdi2hyWiHU7Af3VYq8WlWwwuu/5kbUZSyxDCURBwfZ0Q5YD/lNwoTvEJ2 +WLw1eog/kD/PM0/5ElUcwXmjtil4lxwQXRmZr6aV5mWGPJ34MrjMA4P0DLWT +AbAWNYmFq3O/9RoeLKsuYVA5eXz6ZrCAa0kjoq/Z7XmmpvXd5AtVrJRImKOK +byd/ru0Sre5K0p727GyHMAWJ7SEZraboC7Hh5ZxNM7j6N52pRjtV4j396xf0 +suoZmxpz3qaAIGUX7oAMD6xIqBNvcd3/UXosZfIvW3sE2OM+qL7rLkSg3oh4 +i7GCVDE+Glkq2Ok3xR3m+/l1u3T5okN2zKku2RQ1292RvHTv3OFez3MnSeNN +ECH8cnezNYxHjuwvTUqHcSBs3EU/Ggx9vGuP990IGnE8koRyz+REHiQUgBFo +BHKdmGNqH4AOjdBBf1XU75b/rX374rCWylLLQMa4/7NcoVJzVzeAtFjGfz/4 +zB+Hf+B6F6TQop5SPfrI+J0wrxQbrK/Q7rVyifrzf+TlngKsV6rYM6zFGwWa +p2bQHUf9MExiDQehQ9MhnWnJID/qezXpmtcALD/OLkPLpKxxYPZDOyfYsP/L +kH71msDQPA+1FaAy6+neGuzvU1WhG/X+WyKvkzHDSJsDBKzg8O6XESl0DWnV +geHGxLHWgbvK5cZnnlkkfbPHn8OCSKwjcriYhGKcPS7rJYlahI94uTNMpyMY +7QiQKFR5oaIAN/NwoIKkH9SUjAw582e32+u0U7Yikg5HGCPDwzQFKH92gXqI +Bb7u7F6JvyHNTDDULm51abXIHPF/tO4CB3/4dq3gE+iabvNViUsQBJdHkfgB +FLbpLDvvrm0CQHRbzDuI0llbIYVQyh4PYwC/fO8PhYKAjRSp62jCCnT/adz6 +lNO2gqnIJumw4ggQ4AbuhIVNsOF6XFx0xQnaVhCiJ/jAKRUfJZ2aNU10Xa7Z +4gTBfu/uN7eqtOHc2mCtZ1j7dA+iq0/nvyasJufmDEribXatcmu/1XfsRXRn +yZYxEgv/+KkbE43L5xEPyvUNH2xNj8zBEKdI70T98ubLdzSuKPQEkYoPujkj +ASfFHkeY6ZlbroRDPm+bSLk6RHROVMljmjxnUvMe3EP082f4rV7ZGiZu6727 +06cP+8/QqLkIvHG4zQNlQzkNhD5+c+XL75fz0beShsER4GjGbfaqVrU4xm91 +qmlZypsMpdbgxtBPw8+DwB3117IW8o+B+bNq1Vlif23RSrwCAMCKbOXvUgok +6LfQlrZ3DteWPwEVWDEBfKKhFyTzzgk= +-----END PGP MESSAGE-----`; + + const { data: decryptedData, signatures: [{ verified }] } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage }), + decryptionKeys: privateKey, + verificationKeys: publicKey + }); + expect(decryptedData).to.equal('Testing\n'); + expect(await verified).to.be.true; + }); + + it('ML-DSA + ML-KEM - encrypt/sign and decrypt/verify', async function () { + const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey }); + const publicKey = privateKey.toPublic(); + + const plaintext = 'Testing\n'; + const encrypted = await openpgp.encrypt({ message: await openpgp.createMessage({ text: plaintext }), signingKeys: privateKey, encryptionKeys: publicKey }); + const { data, signatures: [{ verified }] } = await openpgp.decrypt({ message: await openpgp.readMessage({ armoredMessage: encrypted }), verificationKeys: publicKey, decryptionKeys: privateKey }); + expect(data).to.equal(plaintext); + await expect(verified).to.eventually.be.true; + }); }); diff --git a/test/crypto/validate.js b/test/crypto/validate.js index 7c8209268..ca448c74e 100644 --- a/test/crypto/validate.js +++ b/test/crypto/validate.js @@ -315,31 +315,41 @@ export default () => { }); describe('PQC parameter validation', function() { + let pqcSigningKey; let pqcEncryptionSubkey; before(async () => { - const key = await generatePrivateKeyObject({ type: 'symmetric', subkeys: [{ type: 'pqc' }], config: { v6Keys: true } }); - pqcEncryptionSubkey = key.subkeys[0]; + pqcSigningKey = await generatePrivateKeyObject({ type: 'pqc', config: { v6Keys: true } }); + pqcEncryptionSubkey = pqcSigningKey.subkeys[0]; }); - async function cloneSubeyPacket(subkey) { - const subkeyPacket = new openpgp.SecretSubkeyPacket(); - await subkeyPacket.read(subkey.keyPacket.write()); - return subkeyPacket; - } - it('generated params are valid', async function() { + await expect(pqcSigningKey.keyPacket.validate()).to.not.be.rejected; await expect(pqcEncryptionSubkey.keyPacket.validate()).to.not.be.rejected; }); it('detect invalid ML-KEM public key part', async function() { - const keyPacket = await cloneSubeyPacket(pqcEncryptionSubkey); + const keyPacket = await cloneKeyPacket(pqcEncryptionSubkey); const { mlkemPublicKey } = keyPacket.publicParams; mlkemPublicKey[0]++; await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); }); it('detect invalid ECC-KEM key part', async function() { - const keyPacket = await cloneSubeyPacket(pqcEncryptionSubkey); + const keyPacket = await cloneKeyPacket(pqcEncryptionSubkey); + const { eccPublicKey } = keyPacket.publicParams; + eccPublicKey[0]++; + await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + + it('detect invalid ML-DSA public key part', async function() { + const keyPacket = await cloneKeyPacket(pqcSigningKey); + const { mldsaPublicKey } = keyPacket.publicParams; + mldsaPublicKey[0]++; + await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + + it('detect invalid ECC part', async function() { + const keyPacket = await cloneKeyPacket(pqcSigningKey); const { eccPublicKey } = keyPacket.publicParams; eccPublicKey[0]++; await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); diff --git a/test/general/key.js b/test/general/key.js index 4b48c46e9..ded152832 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -4612,15 +4612,19 @@ I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== expect(v6Key.subkeys).to.have.length(1); }); - it('should throw when trying to add a ML-KEM PQC key to a v4 key', async function() { + it('should throw when trying to add a ML-KEM or ML-DSA PQC key to a v4 key', async function() { const v4Key = await openpgp.decryptKey({ privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }), passphrase: 'hello world' }); expect(v4Key.keyPacket.version).to.equal(4); expect(v4Key.subkeys).to.have.length(1); + // try adding an ML-KEM subkey await expect(v4Key.addSubkey({ type: 'pqc', sign: false })).to.be.rejectedWith(/Cannot generate v4 keys of type 'pqc'/); expect(v4Key.subkeys).to.have.length(1); + // try adding an ML-DSA subkey + await expect(v4Key.addSubkey({ type: 'pqc', sign: true })).to.be.rejectedWith(/Cannot generate v4 keys of type 'pqc'/); + expect(v4Key.subkeys).to.have.length(1); }); it('should throw when trying to encrypt a subkey separately from key', async function() { From 55de9c0dc4708e622e2afdc0ae5434a533a8f271 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:07:55 +0100 Subject: [PATCH 13/15] Palemoon patch: treat `'SyntaxError'` as `'NotSupportedError'` on EdDSA WebCrypto operations Fallback to JS code instead of throwing. --- src/crypto/public_key/elliptic/eddsa.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index d3114fce5..5c6b7998e 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -50,7 +50,11 @@ export async function generate(algo) { seed: b64ToUint8Array(privateKey.d, true) }; } catch (err) { - if (err.name !== 'NotSupportedError' && err.name !== 'OperationError') { // Temporary (hopefully) fix for WebKit on Linux + if ( + err.name !== 'NotSupportedError' && + err.name !== 'OperationError' && // Temporary (hopefully) fix for WebKit on Linux + err.name !== 'SyntaxError' // Temporary fix for Palemoon throwing 'SyntaxError' + ) { throw err; } const seed = getRandomBytes(getPayloadSize(algo)); @@ -102,7 +106,7 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe return { RS: signature }; } catch (err) { - if (err.name !== 'NotSupportedError') { + if (err.name !== 'NotSupportedError' && err.name !== 'SyntaxError') { // Temporary fix for Palemoon throwing 'SyntaxError' throw err; } const secretKey = util.concatUint8Array([privateKey, publicKey]); @@ -148,7 +152,7 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { const verified = await webCrypto.verify('Ed25519', key, RS, hashed); return verified; } catch (err) { - if (err.name !== 'NotSupportedError') { + if (err.name !== 'NotSupportedError' && err.name !== 'SyntaxError') { // Temporary fix for Palemoon throwing 'SyntaxError' throw err; } return nobleEd25519Verify(RS, hashed, publicKey); From 299342946d4f4dafd37cf7ef3fb770151f9b091a Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:22:41 +0100 Subject: [PATCH 14/15] PQC: switch to latest proposed variant for draft 7 of ML-KEM with X25519 Which also drops ECDH-KEM CCA-conversion. This variant is yet to be adopted under draft 7, hence official test vectors aren't available for now. See https://github.com/openpgp-pqc/draft-openpgp-pqc/pull/161 . --- .../public_key/post_quantum/kem/ecc_kem.js | 17 +- src/crypto/public_key/post_quantum/kem/kem.js | 5 +- test/crypto/postQuantum.js | 272 +++++++++--------- 3 files changed, 142 insertions(+), 152 deletions(-) diff --git a/src/crypto/public_key/post_quantum/kem/ecc_kem.js b/src/crypto/public_key/post_quantum/kem/ecc_kem.js index a133c18a1..1233eba8a 100644 --- a/src/crypto/public_key/post_quantum/kem/ecc_kem.js +++ b/src/crypto/public_key/post_quantum/kem/ecc_kem.js @@ -1,6 +1,4 @@ import * as ecdhX from '../../elliptic/ecdh_x'; -import { computeDigest } from '../../../hash'; -import util from '../../../../util'; import enums from '../../../../enums'; export async function generate(algo) { @@ -20,12 +18,8 @@ export async function generate(algo) { export async function encaps(eccAlgo, eccRecipientPublicKey) { switch (eccAlgo) { case enums.publicKey.pqc_mlkem_x25519: { - const { ephemeralPublicKey: eccCipherText, sharedSecret: eccSharedSecret } = await ecdhX.generateEphemeralEncryptionMaterial(enums.publicKey.x25519, eccRecipientPublicKey); - const eccKeyShare = await computeDigest(enums.hash.sha3_256, util.concatUint8Array([ - eccSharedSecret, - eccCipherText, - eccRecipientPublicKey - ])); + const { ephemeralPublicKey: eccCipherText, sharedSecret: eccKeyShare } = await ecdhX.generateEphemeralEncryptionMaterial(enums.publicKey.x25519, eccRecipientPublicKey); + return { eccCipherText, eccKeyShare @@ -39,12 +33,7 @@ export async function encaps(eccAlgo, eccRecipientPublicKey) { export async function decaps(eccAlgo, eccCipherText, eccSecretKey, eccPublicKey) { switch (eccAlgo) { case enums.publicKey.pqc_mlkem_x25519: { - const eccSharedSecret = await ecdhX.recomputeSharedSecret(enums.publicKey.x25519, eccCipherText, eccPublicKey, eccSecretKey); - const eccKeyShare = await computeDigest(enums.hash.sha3_256, util.concatUint8Array([ - eccSharedSecret, - eccCipherText, - eccPublicKey - ])); + const eccKeyShare = await ecdhX.recomputeSharedSecret(enums.publicKey.x25519, eccCipherText, eccPublicKey, eccSecretKey); return eccKeyShare; } default: diff --git a/src/crypto/public_key/post_quantum/kem/kem.js b/src/crypto/public_key/post_quantum/kem/kem.js index 1a78de5de..1626eb038 100644 --- a/src/crypto/public_key/post_quantum/kem/kem.js +++ b/src/crypto/public_key/post_quantum/kem/kem.js @@ -31,7 +31,7 @@ export async function decrypt(algo, eccCipherText, mlkemCipherText, eccSecretKey async function multiKeyCombine(algo, ecdhKeyShare, ecdhCipherText, ecdhPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey) { // LAMPS-aligned and NIST compatible combiner, proposed in: https://mailarchive.ietf.org/arch/msg/openpgp/NMTCy707LICtxIhP3Xt1U5C8MF0/ // 2a. KDF(mlkemSS || tradSS || tradCT || tradPK || Domain) - // where Domain is "Domain" for LAMPS, and "mlkemCT || mlkemPK || algId" for OpenPGP + // where Domain is "Domain" for LAMPS, and "mlkemCT || mlkemPK || algId || const" for OpenPGP const encData = util.concatUint8Array([ mlkemKeyShare, ecdhKeyShare, @@ -40,7 +40,8 @@ async function multiKeyCombine(algo, ecdhKeyShare, ecdhCipherText, ecdhPublicKey // domSep mlkemCipherText, mlkemPublicKey, - new Uint8Array([algo]) + new Uint8Array([algo]), + util.encodeUTF8('OpenPGPCompositeKDFv1') ]); const kek = await computeDigest(enums.hash.sha3_256, encData); diff --git a/test/crypto/postQuantum.js b/test/crypto/postQuantum.js index 5d93a84d9..9614cd450 100644 --- a/test/crypto/postQuantum.js +++ b/test/crypto/postQuantum.js @@ -655,35 +655,35 @@ zdmQk3zZBuwE/KAXTAY= it('ML-KEM + X25519 - Test vector', async function () { const armoredMessage = `-----BEGIN PGP MESSAGE----- -wcPUA7w9P/Y4tvvTaUZQqpafFZ6WRHDl5y8FJcqxWgQ8kK3P++vG3p2aW0Vb -BuxUbAzplFMVjybzf8n0qa/92R6IeFKWMilaM+iAf81VyTIBv12lfQy27RFx -itM0v/CAWrEvPf1RkB0ZnEX9lKhIfBp7QRSVEp2QOh3pWtYQ6AkS0JtOGeiI -befPigAVtmDlnYHYYEFbrKlE8pvLF2bMCJkHredQue7AmsGLpnXZpna19rvG -3kB38YiM6777jxW8yb7h77jOZgnclJXYkSHly7LKa6z6omOYqn6rA5izbE0e -s/yqucighBVnjiwNEwrPlQbogh6EtRsGXkYLPGRSVSM2MCSsigl2Xs2cgWJV -7outF2Qr8BauBRqHW5ScJF/yAmVP7NfQ9PfoUPMiHFJE8U6ffow6lnNWzhn2 -Wqp8GkBccTmzwD/6XXf23yKGO6AIc1xvNpE99QBzTkPslPuXDiocMHfAXpbW -HzVyOwXu/tQmRNe+ISNpQsMmQDUM05kq0diazBXOiZGRMRFndEpa1Qhhgj7V -4GgLyr/HdylNbzbiUGFjovN8/pUwO6APu1jU/CmFEUrjjOFpXaOLZ6svt5mW -iNlVNfWVq35CrJdWVFInLjD/GmsQNSiPe34LcDS3TVRzAhS5HehTVrkFDOD+ -e7wBnHFKDquYTzc3pqP3gHmzCLueUfo3Z6cTgXz6T3+RlRfZDNQzLZ7XHC3W -VWuwjViv0mQncau6bCnhhCK4VwAqXCvjy9m598cqKQZ5Sf3DAGY1+ZGqbID+ -0zCdVKC0v4eUEGg2D9K1f1XiTfaJv6AO0sS5RqjAZxBpw2/m5ruxMyJxqTE1 -h0GZLEY1ZQKxe9E0bDvTi0ogbYicLEokgRyKN/ojZE+hwpA7msjLVV7E0tDg -rz8jxp4AwQ8O/FXX/X0Dp04QsZgJhdwEYt4HPTogSff/NaC8KNSTzxEvJIMa -N+dPAp3Ypz7pGU6y73odwBCJ58nAPaI06f3Ev4gWJai8sUxeHe6amQX+SYqL -jbTP838PgbRbNCwBUiH2I5WX2bdIeRbEiQerVr7f76Kpdo2FC93A8VlW/wyK -VYWINPBfB/ktLAzmohR9wq+E+uMCOnyz6+vE1Bf2fbljGBSBjyUwED07tlDd -B9EvUlLgr7TAiideveSXHAx0G0qK5NBzfoY3Ca3WETiqkDTWpBSPDQUsbXrF -u2QUJKIcXcQptuiC+nKFFbDoFvCJzgcuRS/H1ZyOcI0dgmx9T1GGrID65R0z -FqdpenxdaHncrMmOlYI/cwyrfhHV0wZB6Y0XAv5Ujwsfg0fdCxJclkAI1k/x -N2xIhjwFdh62sKZqCJgfgKlrkSv4DDUVAooR/yHIIKpeJQgC2ZUfL3XLNycJ -eBf5V5ZmyRhIVgAThhb4cGVtNahP0IuIHk7eMdALC5Yhl+PTlSlhqVkbyi05 -Ae7dcaTW/KAEzZw2mqvVQX2MABaShv49reFhsCfI7EWbdI0CpTQbrCxp9mTI -Lw2nVxF5SGApCQtVJ+cajQ48p2MNwYMbsnMU9oEjy89nXZfU0G3CBRnAnhUP -Sw5WYSDSOgGTtzkIxvyRfaLftZRNfT/5XmvtpCqVWwBKHJ1/qcLEIRWfenqT -M/UQCkZrgB2Ry+4Oc1hgibIUzOM= -=8HM/ +wcPUA7w9P/Y4tvvTafGcykbHOZebb4UEiUzy7MdOx0v3nPOv8JfjdcbC3Ucz +4couZg5vJI7En+EBaT8f2aU+ZTmCmuZMVy5BHp9YKLhGmSU6tT3eUrhh63Z9 +IC44aS0m0vH/4f1vpsJmbpuPLxi+Q6DS4Sl4UOr0U8FOXzcb6uGMhWtOOdwI +iPZKgVpjgzwzxM1xh44hJz0iRSWKPPnFg9mbIHVPtuDMDNUYdOkRQhyQKYQE +z/ok0X9jCFKmAvKHpNjl4lFCxwcscJ5q5OYPO9I2BrJMyK2oyHmtgCojSvW1 +hSTeVNoXO0cwfQzWSNBQvBQEcuQ5gGKkQtbkBGds2LUmf2PASbZb8sZkN6EQ +8zdljhU3r4mHjTdNedTi8bN00/VQwsKFeN928f9k6SZDCU0XQxq/wZKmDhnI +vhvd5GbzFgeFSiPV+WY2sajZvh2cPuz0SGhW91ue0h3F2yKEzSQrloxZvV6Q +aFHWtBdzwx/Tx4oAvLvbBjV5qKC/C5f4RTNBoZ1doa4y8jxeeBEXBR7K7+mz +sS5GkVv5tpI1Z8UO/I450XdSYFS+dESgpZNLwy3Hu4TFMAxH78jjN21byr7S +Nx0ZmYrqQcfrW9m4YFsqpn50CJfbzRven84tgM9LqONIufCFhPv6K57MPSWR +l/YAut3fT2Mt/Bc3RS0xqiH7pi+/RxzKzY2nJ1takd2r4uWFLU3oLZBZrgck +M9+ZW9wwlgLOt5zmG+zZG9yfnpCBQIdmEJ31mH+j7oGemoM7r9UuLhbizlEE +Nx/TtsEXpuKTbzsrxjp9zVhgoF0ivV9mKWuGY1yGYaeT73RbyIzxVYjJTPsZ +Z9AWXNOmaRTFmrHHstQGz6AwcAZXoQ3Od2hiZ9BtgQ92Q5YZICFH3cbjCIEU +Pvj0f/1ruhHO20Q+uUcACAWRjWD9r1ijMnY1ke5GA+LZNXstEnzZPCqrhk5Z +NIFhuu+o47fakDvciMEqbdk6L2r620GLFG6F7cOO0vm/SrOm4drrvIpTbUe4 +DrgXhQuusUiXHKm4oB1/zNTEOpL4wqXLyyqcsN3EWXb3j+ZMwo+hm/ynEvPl +Gj76dvaJx5hRAtga4ZD8L6E1LS82bMmQKTp3uBJ3vcBxhKsSAYXO3ZQWeq0X +YSyvJiyy7We7gsjjN7RcYm8tOohoGvr2thf3LhN7hTBJWU1js7yq6UlAY+0A +so0dbkne00G77pRsy+nRYjVOFTm+57U/7UUTKsDtZf2Vsrv/nfAK2l507WAJ +8oioHhaDb4kxR92UR+cPkdqNzeMFEShgkRTRNG8d6cyRBa5/sYPtuSrKB0VX +S/t2UXgFWqTWPzcRv/NTDq89+yY5UvwfSFlRaKa32DHcUYq9L77Wq3bEZJiE +KYWFPla4cz1PnckU6DGUXxMAe2KrJ/ALhnTXavwLptqKSktgMS6FfBT8NJTX +Cf691DI9fikwHXZVGEKdh2CQpyZkJCoe+qOhKHEwrErMHHaajYYBHkGk6P9y +UifBYpdwZcspCcv6z78hlprAKcSAnXRKj2zIP5z36dbTN3z+vEa8HKM3lfAz +9Vbc34PSOgHX0jeD+RNwY7bt13n8MmNwsEUIUyHl/ivKTqhp2MKGnc622oXb +MdhqSPQkCscEVIT6fTh/vIyMTs0= +=OmpP -----END PGP MESSAGE-----`; const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- @@ -1078,113 +1078,113 @@ fJWWnLW7yOAAAAAAAAAAAAAAAAAAAAAAAAAACgwOExkk // since we use a different KEM combiner const armoredMessage = `-----BEGIN PGP MESSAGE----- -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 -6LfQlrZ3DteWPwEVWDEBfKKhFyTzzgk= +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 +jXoyrGhDDakI/1XygauU5DpNuhgApoA= -----END PGP MESSAGE-----`; const { data: decryptedData, signatures: [{ verified }] } = await openpgp.decrypt({ From 4b8af0d5d36243c03df9fb4d1f46fffbb8d3a201 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:44:40 +0100 Subject: [PATCH 15/15] WIP: PQC: Implement draft RFC for SLH-DSA (shake128s variant only) Implements Draft 6 (https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/06/). NB: signing is currently too slow to be usable (10+ seconds). --- package-lock.json | 23 +- package.json | 2 +- src/crypto/crypto.js | 40 +- src/crypto/public_key/post_quantum/index.js | 32 +- .../signature.js => ml_dsa/combiner.js} | 2 +- .../{signature => ml_dsa}/ecc_dsa.js | 0 .../{signature => ml_dsa}/index.js | 4 +- .../ml_dsa.js => ml_dsa/ml_dsa_pure.js} | 0 .../post_quantum/{kem => ml_kem}/ecc_kem.js | 0 .../post_quantum/{kem => ml_kem}/index.js | 4 +- .../{kem/kem.js => ml_kem/kem_combiner.js} | 2 +- .../{kem/ml_kem.js => ml_kem/ml_kem_pure.js} | 0 .../post_quantum/noble_post_quantum.ts | 1 - .../public_key/post_quantum/slh_dsa/index.js | 75 ++ src/crypto/signature.js | 16 +- src/enums.js | 3 +- src/key/helper.js | 11 +- src/packet/public_key.js | 9 +- src/packet/secret_key.js | 7 +- test/crypto/postQuantum.js | 1090 +++++++++++++++++ 20 files changed, 1276 insertions(+), 45 deletions(-) rename src/crypto/public_key/post_quantum/{signature/signature.js => ml_dsa/combiner.js} (98%) rename src/crypto/public_key/post_quantum/{signature => ml_dsa}/ecc_dsa.js (100%) rename src/crypto/public_key/post_quantum/{signature => ml_dsa}/index.js (77%) rename src/crypto/public_key/post_quantum/{signature/ml_dsa.js => ml_dsa/ml_dsa_pure.js} (100%) rename src/crypto/public_key/post_quantum/{kem => ml_kem}/ecc_kem.js (100%) rename src/crypto/public_key/post_quantum/{kem => ml_kem}/index.js (84%) rename src/crypto/public_key/post_quantum/{kem/kem.js => ml_kem/kem_combiner.js} (98%) rename src/crypto/public_key/post_quantum/{kem/ml_kem.js => ml_kem/ml_kem_pure.js} (100%) create mode 100644 src/crypto/public_key/post_quantum/slh_dsa/index.js diff --git a/package-lock.json b/package-lock.json index 7ad70eaae..a64524545 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@noble/curves": "^1.7.0", "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", - "@noble/post-quantum": "^0.2.1", + "@noble/post-quantum": "^0.4.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/tweetnacl": "^1.0.4-1", @@ -985,13 +985,26 @@ } }, "node_modules/@noble/post-quantum": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.2.1.tgz", - "integrity": "sha512-ImgfMp9notXSEocz464o1AefYfFWEkkszKMGO+ZiTn73yIBFeNyEHKQUMS+SheJwSNymldSts6YyVcQDjcnVVg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.4.0.tgz", + "integrity": "sha512-pkTt+KEmgKWPo9/MbJtZRnX4AqNNZ5Ui6DwpKfr7WDSxP5b8pxf/SC1Nblzr8LHs+3gc+GEeUvq54u7zg1RHFw==", "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.6.0" + "@noble/hashes": "1.7.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" diff --git a/package.json b/package.json index cad19c9fd..c648bca5b 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@noble/curves": "^1.7.0", "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", - "@noble/post-quantum": "^0.2.1", + "@noble/post-quantum": "^0.4.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/tweetnacl": "^1.0.4-1", diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index fcdd32caa..82da08fb8 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -39,6 +39,8 @@ import { UnsupportedError } from '../packet/packet'; import ECDHXSymmetricKey from '../type/ecdh_x_symkey'; import { getAEADMode } from './cipherMode'; +export { isPostQuantumAlgo } from './public_key/post_quantum'; + /** * Encrypts data using specified algorithm and public key parameters. * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms. @@ -98,7 +100,7 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, pri } case enums.publicKey.pqc_mlkem_x25519: { const { eccPublicKey, mlkemPublicKey } = publicParams; - const { eccCipherText, mlkemCipherText, wrappedKey } = await postQuantum.kem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data); + const { eccCipherText, mlkemCipherText, wrappedKey } = await postQuantum.mlkem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data); const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey }); return { eccCipherText, mlkemCipherText, C }; } @@ -169,7 +171,7 @@ export async function publicKeyDecrypt(keyAlgo, publicKeyParams, privateKeyParam const { eccSecretKey, mlkemSecretKey } = privateKeyParams; const { eccPublicKey, mlkemPublicKey } = publicKeyParams; const { eccCipherText, mlkemCipherText, C } = sessionKeyParams; - return postQuantum.kem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, C.wrappedKey); + return postQuantum.mlkem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, C.wrappedKey); } default: throw new Error('Unknown public key encryption algorithm.'); @@ -252,6 +254,10 @@ export function parsePublicKeyParams(algo, bytes) { const mldsaPublicKey = util.readExactSubarray(bytes, read, read + 1952); read += mldsaPublicKey.length; return { read, publicParams: { eccPublicKey, mldsaPublicKey } }; } + case enums.publicKey.pqc_slhdsa_shake128s: { + const slhdsaPublicKey = util.readExactSubarray(bytes, read, read + 32); read += slhdsaPublicKey.length; + return { read, publicParams: { slhdsaPublicKey } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -326,15 +332,20 @@ export async function parsePrivateKeyParams(algo, bytes, publicParams) { case enums.publicKey.pqc_mlkem_x25519: { const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccSecretKey.length; const mlkemSeed = util.readExactSubarray(bytes, read, read + 64); read += mlkemSeed.length; - const { mlkemSecretKey } = await postQuantum.kem.mlkemExpandSecretSeed(algo, mlkemSeed); + const { mlkemSecretKey } = await postQuantum.mlkem.mlkemExpandSecretSeed(algo, mlkemSeed); return { read, privateParams: { eccSecretKey, mlkemSecretKey, mlkemSeed } }; } case enums.publicKey.pqc_mldsa_ed25519: { const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.ed25519)); read += eccSecretKey.length; const mldsaSeed = util.readExactSubarray(bytes, read, read + 32); read += mldsaSeed.length; - const { mldsaSecretKey } = await postQuantum.signature.mldsaExpandSecretSeed(algo, mldsaSeed); + const { mldsaSecretKey } = await postQuantum.mldsa.mldsaExpandSecretSeed(algo, mldsaSeed); return { read, privateParams: { eccSecretKey, mldsaSecretKey, mldsaSeed } }; } + case enums.publicKey.pqc_slhdsa_shake128s: { + const slhdsaSecretKey = util.readExactSubarray(bytes, read, read + 64); read += slhdsaSecretKey.length; + + return { read, privateParams: { slhdsaSecretKey } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -425,7 +436,8 @@ export function serializeParams(algo, params) { enums.publicKey.aead, enums.publicKey.hmac, enums.publicKey.pqc_mlkem_x25519, - enums.publicKey.pqc_mldsa_ed25519 + enums.publicKey.pqc_mldsa_ed25519, + enums.publicKey.pqc_slhdsa_shake128s ]); const excludedFields = { @@ -503,15 +515,20 @@ export async function generateParams(algo, bits, oid, symmetric) { return createSymmetricParams(keyMaterial, new SymAlgoEnum(symmetric)); } case enums.publicKey.pqc_mlkem_x25519: - return postQuantum.kem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSeed, mlkemSecretKey, mlkemPublicKey }) => ({ + return postQuantum.mlkem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSeed, mlkemSecretKey, mlkemPublicKey }) => ({ privateParams: { eccSecretKey, mlkemSeed, mlkemSecretKey }, publicParams: { eccPublicKey, mlkemPublicKey } })); case enums.publicKey.pqc_mldsa_ed25519: - return postQuantum.signature.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }) => ({ + return postQuantum.mldsa.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }) => ({ privateParams: { eccSecretKey, mldsaSeed, mldsaSecretKey }, publicParams: { eccPublicKey, mldsaPublicKey } })); + case enums.publicKey.pqc_slhdsa_shake128s: + return postQuantum.slhdsa.generate(algo).then(({ slhdsaSecretKey, slhdsaPublicKey }) => ({ + privateParams: { slhdsaSecretKey }, + publicParams: { slhdsaPublicKey } + })); case enums.publicKey.dsa: case enums.publicKey.elgamal: throw new Error('Unsupported algorithm for key generation.'); @@ -606,12 +623,17 @@ export async function validateParams(algo, publicParams, privateParams) { case enums.publicKey.pqc_mlkem_x25519: { const { eccSecretKey, mlkemSeed } = privateParams; const { eccPublicKey, mlkemPublicKey } = publicParams; - return postQuantum.kem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed); + return postQuantum.mlkem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed); } case enums.publicKey.pqc_mldsa_ed25519: { const { eccSecretKey, mldsaSeed } = privateParams; const { eccPublicKey, mldsaPublicKey } = publicParams; - return postQuantum.signature.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed); + return postQuantum.mldsa.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed); + } + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slhdsaSecretKey } = privateParams; + const { slhdsaPublicKey } = publicParams; + return postQuantum.slhdsa.validateParams(algo, slhdsaPublicKey, slhdsaSecretKey); } default: throw new Error('Unknown public key algorithm.'); diff --git a/src/crypto/public_key/post_quantum/index.js b/src/crypto/public_key/post_quantum/index.js index cf803ef88..6aa2b574d 100644 --- a/src/crypto/public_key/post_quantum/index.js +++ b/src/crypto/public_key/post_quantum/index.js @@ -1,7 +1,31 @@ -import * as kem from './kem/index'; -import * as signature from './signature'; +import * as mlkem from './ml_kem'; +import * as mldsa from './ml_dsa'; +import * as slhdsa from './slh_dsa'; +import enums from '../../../enums'; export { - kem, - signature + mlkem, + mldsa, + slhdsa }; + +const pqcAlgos = new Set([ + enums.publicKey.pqc_mldsa_ed25519, + enums.publicKey.pqc_mlkem_x25519, + enums.publicKey.pqc_slhdsa_shake128s +]); + +export function isPostQuantumAlgo(algo) { + return pqcAlgos.has(algo); +} + +export function getRequiredHashAlgo(signatureAlgo) { + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: + return mldsa.getRequiredHashAlgo(signatureAlgo); + case enums.publicKey.pqc_slhdsa_shake128s: + return slhdsa.getRequiredHashAlgo(signatureAlgo); + default: + throw new Error('Unsupported signature algorithm'); + } +} diff --git a/src/crypto/public_key/post_quantum/signature/signature.js b/src/crypto/public_key/post_quantum/ml_dsa/combiner.js similarity index 98% rename from src/crypto/public_key/post_quantum/signature/signature.js rename to src/crypto/public_key/post_quantum/ml_dsa/combiner.js index fcdfc745c..c5a378f05 100644 --- a/src/crypto/public_key/post_quantum/signature/signature.js +++ b/src/crypto/public_key/post_quantum/ml_dsa/combiner.js @@ -1,5 +1,5 @@ import enums from '../../../../enums'; -import * as mldsa from './ml_dsa'; +import * as mldsa from './ml_dsa_pure'; import * as eccdsa from './ecc_dsa'; export async function generate(algo) { diff --git a/src/crypto/public_key/post_quantum/signature/ecc_dsa.js b/src/crypto/public_key/post_quantum/ml_dsa/ecc_dsa.js similarity index 100% rename from src/crypto/public_key/post_quantum/signature/ecc_dsa.js rename to src/crypto/public_key/post_quantum/ml_dsa/ecc_dsa.js diff --git a/src/crypto/public_key/post_quantum/signature/index.js b/src/crypto/public_key/post_quantum/ml_dsa/index.js similarity index 77% rename from src/crypto/public_key/post_quantum/signature/index.js rename to src/crypto/public_key/post_quantum/ml_dsa/index.js index a1b1dc436..6fafb6936 100644 --- a/src/crypto/public_key/post_quantum/signature/index.js +++ b/src/crypto/public_key/post_quantum/ml_dsa/index.js @@ -1,2 +1,2 @@ -export { generate, sign, verify, validateParams, getRequiredHashAlgo } from './signature'; -export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa'; +export { generate, sign, verify, validateParams, getRequiredHashAlgo } from './combiner'; +export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa_pure'; diff --git a/src/crypto/public_key/post_quantum/signature/ml_dsa.js b/src/crypto/public_key/post_quantum/ml_dsa/ml_dsa_pure.js similarity index 100% rename from src/crypto/public_key/post_quantum/signature/ml_dsa.js rename to src/crypto/public_key/post_quantum/ml_dsa/ml_dsa_pure.js diff --git a/src/crypto/public_key/post_quantum/kem/ecc_kem.js b/src/crypto/public_key/post_quantum/ml_kem/ecc_kem.js similarity index 100% rename from src/crypto/public_key/post_quantum/kem/ecc_kem.js rename to src/crypto/public_key/post_quantum/ml_kem/ecc_kem.js diff --git a/src/crypto/public_key/post_quantum/kem/index.js b/src/crypto/public_key/post_quantum/ml_kem/index.js similarity index 84% rename from src/crypto/public_key/post_quantum/kem/index.js rename to src/crypto/public_key/post_quantum/ml_kem/index.js index 399750ad7..29c8ba96d 100644 --- a/src/crypto/public_key/post_quantum/kem/index.js +++ b/src/crypto/public_key/post_quantum/ml_kem/index.js @@ -1,2 +1,2 @@ -export { generate, encrypt, decrypt, validateParams } from './kem'; -export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem'; +export { generate, encrypt, decrypt, validateParams } from './kem_combiner'; +export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem_pure'; diff --git a/src/crypto/public_key/post_quantum/kem/kem.js b/src/crypto/public_key/post_quantum/ml_kem/kem_combiner.js similarity index 98% rename from src/crypto/public_key/post_quantum/kem/kem.js rename to src/crypto/public_key/post_quantum/ml_kem/kem_combiner.js index 1626eb038..9d5e1319c 100644 --- a/src/crypto/public_key/post_quantum/kem/kem.js +++ b/src/crypto/public_key/post_quantum/ml_kem/kem_combiner.js @@ -1,5 +1,5 @@ import * as eccKem from './ecc_kem'; -import * as mlKem from './ml_kem'; +import * as mlKem from './ml_kem_pure'; import * as aesKW from '../../../aes_kw'; import util from '../../../../util'; import enums from '../../../../enums'; diff --git a/src/crypto/public_key/post_quantum/kem/ml_kem.js b/src/crypto/public_key/post_quantum/ml_kem/ml_kem_pure.js similarity index 100% rename from src/crypto/public_key/post_quantum/kem/ml_kem.js rename to src/crypto/public_key/post_quantum/ml_kem/ml_kem_pure.js diff --git a/src/crypto/public_key/post_quantum/noble_post_quantum.ts b/src/crypto/public_key/post_quantum/noble_post_quantum.ts index de77098ef..041030442 100644 --- a/src/crypto/public_key/post_quantum/noble_post_quantum.ts +++ b/src/crypto/public_key/post_quantum/noble_post_quantum.ts @@ -7,4 +7,3 @@ export { ml_kem768 } from '@noble/post-quantum/ml-kem'; export { ml_dsa65 } from '@noble/post-quantum/ml-dsa'; - diff --git a/src/crypto/public_key/post_quantum/slh_dsa/index.js b/src/crypto/public_key/post_quantum/slh_dsa/index.js new file mode 100644 index 000000000..517c26e7d --- /dev/null +++ b/src/crypto/public_key/post_quantum/slh_dsa/index.js @@ -0,0 +1,75 @@ +import enums from '../../../../enums'; +import { getRandomBytes } from '../../../random'; + + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slh_dsa_shake_128s } = await import('@noble/post-quantum/slh-dsa'); + const { secretKey: slhdsaSecretKey, publicKey: slhdsaPublicKey } = slh_dsa_shake_128s.keygen(); + + return { slhdsaSecretKey, slhdsaPublicKey }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function sign(signatureAlgo, hashAlgo, slhdsaSecretKey, dataDigest) { + if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) { + // The signature hash algo MUST be set to the specified algorithm, see + // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#section-6.1.1 + throw new Error('Unexpected hash algorithm for PQC signature'); + } + + switch (signatureAlgo) { + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slh_dsa_shake_128s } = await import('@noble/post-quantum/slh-dsa'); + const slhdsaSignature = slh_dsa_shake_128s.sign(slhdsaSecretKey, dataDigest); + return { slhdsaSignature }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function verify(signatureAlgo, hashAlgo, slhdsaPublicKey, dataDigest, { slhdsaSignature }) { + if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) { + // The signature hash algo MUST be set to the specified algorithm, see + // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#section-6.1.1 + throw new Error('Unexpected hash algorithm for PQC signature'); + } + + switch (signatureAlgo) { + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slh_dsa_shake_128s } = await import('@noble/post-quantum/slh-dsa'); + return slh_dsa_shake_128s.verify(slhdsaPublicKey, dataDigest, slhdsaSignature); + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function validateParams(algo, slhdsaPublicKey, slhdsaSecretKey) { + switch (algo) { + case enums.publicKey.pqc_slhdsa_shake128s: { + // TODO check if more performant validation is possible via public key re-derivation + const randomBytes = getRandomBytes(16); + const { slhdsaSignature } = await sign(algo, slhdsaSecretKey, randomBytes); + const trialSignatureVerified = await verify(algo, slhdsaPublicKey, randomBytes, slhdsaSignature); + return trialSignatureVerified; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export function getRequiredHashAlgo(signatureAlgo) { + // See https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#section-6.1.1 + switch (signatureAlgo) { + case enums.publicKey.pqc_slhdsa_shake128s: + return enums.hash.sha3_256; + default: + throw new Error('Unsupported signature algorithm'); + } +} diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 2be2bcd26..24966a46a 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -76,6 +76,10 @@ export function parseSignatureParams(algo, signature) { const mldsaSignature = util.readExactSubarray(signature, read, read + 3309); read += mldsaSignature.length; return { read, signatureParams: { eccSignature, mldsaSignature } }; } + case enums.publicKey.pqc_slhdsa_shake128s: { + const slhdsaSignature = util.readExactSubarray(signature, read, read + 7856); read += slhdsaSignature.length; + return { read, signatureParams: { slhdsaSignature } }; + } default: throw new UnsupportedError('Unknown signature algorithm.'); } @@ -142,7 +146,11 @@ export async function verify(algo, hashAlgo, signature, publicParams, privatePar } case enums.publicKey.pqc_mldsa_ed25519: { const { eccPublicKey, mldsaPublicKey } = publicParams; - return postQuantum.signature.verify(algo, hashAlgo, eccPublicKey, mldsaPublicKey, hashed, signature); + return postQuantum.mldsa.verify(algo, hashAlgo, eccPublicKey, mldsaPublicKey, hashed, signature); + } + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slhdsaPublicKey } = publicParams; + return postQuantum.slhdsa.verify(algo, hashAlgo, slhdsaPublicKey, hashed, signature); } default: throw new Error('Unknown signature algorithm.'); @@ -208,7 +216,11 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da case enums.publicKey.pqc_mldsa_ed25519: { const { eccPublicKey } = publicKeyParams; const { eccSecretKey, mldsaSecretKey } = privateKeyParams; - return postQuantum.signature.sign(algo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, hashed); + return postQuantum.mldsa.sign(algo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, hashed); + } + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slhdsaSecretKey } = privateKeyParams; + return postQuantum.slhdsa.sign(algo, hashAlgo, slhdsaSecretKey, hashed); } default: throw new Error('Unknown signature algorithm.'); diff --git a/src/enums.js b/src/enums.js index c1b3f31e9..a6930e075 100644 --- a/src/enums.js +++ b/src/enums.js @@ -112,7 +112,8 @@ export default { pqc_mlkem_x25519: 105, /** Post-quantum ML-DSA-64 + Ed25519 (Sign only) */ pqc_mldsa_ed25519: 107, - + /** Post-quantum SLH-DSA-128S (Sign only) */ + pqc_slhdsa_shake128s: 109, /** Persistent symmetric keys: encryption algorithm */ aead: 100, /** Persistent symmetric keys: authentication algorithm */ diff --git a/src/key/helper.js b/src/key/helper.js index 15b81ba1f..68331edfe 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -9,7 +9,7 @@ import { SignaturePacket } from '../packet'; import enums from '../enums'; -import { getPreferredCurveHashAlgo, getHashByteLength, publicKey } from '../crypto'; +import { getPreferredCurveHashAlgo, getHashByteLength, publicKey, isPostQuantumAlgo } from '../crypto'; import util from '../util'; import defaultConfig from '../config'; @@ -117,10 +117,10 @@ export async function createBindingSignature(subkey, primaryKey, options, config * @async */ export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date = new Date(), targetUserIDs = [], config) { - if (signingKeyPacket.algorithm === enums.publicKey.pqc_mldsa_ed25519) { - // For PQC, the returned hash algo MUST be set to the specified algorithm, see - // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. - return publicKey.postQuantum.signature.getRequiredHashAlgo(signingKeyPacket.algorithm); + // For PQC, the returned hash algo MUST be set to the specified algorithm, see + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + if (isPostQuantumAlgo(signingKeyPacket.algorithm)) { + return publicKey.postQuantum.getRequiredHashAlgo(signingKeyPacket.algorithm); } /** @@ -475,6 +475,7 @@ export function validateSigningKeyPacket(keyPacket, signature, config) { case enums.publicKey.ed448: case enums.publicKey.hmac: case enums.publicKey.pqc_mldsa_ed25519: + case enums.publicKey.pqc_slhdsa_shake128s: if (!signature.keyFlags && !config.allowMissingKeyFlags) { throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); } diff --git a/src/packet/public_key.js b/src/packet/public_key.js index df7c07087..42b0f13a3 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -17,7 +17,7 @@ import KeyID from '../type/keyid'; import defaultConfig from '../config'; -import { computeDigest, parsePublicKeyParams, serializeParams } from '../crypto'; +import { computeDigest, isPostQuantumAlgo, parsePublicKeyParams, serializeParams } from '../crypto'; import enums from '../enums'; import util from '../util'; import { UnsupportedError } from './packet'; @@ -140,11 +140,8 @@ class PublicKeyPacket { } // The composite ML-DSA + EdDSA schemes MUST be used only with v6 keys. // The composite ML-KEM + ECDH schemes MUST be used only with v6 keys. - if (this.version !== 6 && ( - this.algorithm === enums.publicKey.pqc_mldsa_ed25519 || - this.algorithm === enums.publicKey.pqc_mlkem_x25519 - )) { - throw new Error('Unexpected key version: ML-DSA and ML-KEM algorithms can only be used with v6 keys'); + if (this.version !== 6 && isPostQuantumAlgo(this.algorithm)) { + throw new Error('Unexpected key version: post-quantum algorithms can only be used with v6 keys'); } this.publicParams = publicParams; pos += read; diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index a397569bc..0704d81f3 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -17,7 +17,7 @@ import PublicKeyPacket from './public_key'; import { newS2KFromConfig, newS2KFromType } from '../type/s2k'; -import { computeDigest, getCipherParams, parsePrivateKeyParams, serializeParams, generateParams, validateParams, getRandomBytes, cipherMode } from '../crypto'; +import { computeDigest, getCipherParams, parsePrivateKeyParams, serializeParams, generateParams, validateParams, getRandomBytes, cipherMode, isPostQuantumAlgo } from '../crypto'; import enums from '../enums'; import util from '../util'; import defaultConfig from '../config'; @@ -532,10 +532,7 @@ class SecretKeyPacket extends PublicKeyPacket { )) { throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${curve}. Generate a key of type 'curve25519' instead`); } - if (this.version !== 6 && ( - this.algorithm === enums.publicKey.pqc_mldsa_ed25519 || - this.algorithm === enums.publicKey.pqc_mlkem_x25519 - )) { + if (this.version !== 6 && isPostQuantumAlgo(this.algorithm)) { throw new Error(`Cannot generate v${this.version} keys of type 'pqc'. Generate a v6 key instead`); } const { privateParams, publicParams } = await generateParams(this.algorithm, bits, curve, symmetric); diff --git a/test/crypto/postQuantum.js b/test/crypto/postQuantum.js index 9614cd450..831dac2b8 100644 --- a/test/crypto/postQuantum.js +++ b/test/crypto/postQuantum.js @@ -589,6 +589,1068 @@ WmFbdK/B+gwukZuv6/ADssjK6jFXXnCtvOghLjtOVKDo/QsSLzhDpNsAAAAAAAAA AAAAAAAAAAAABQwRGCAn -----END PGP PUBLIC KEY BLOCK-----`; +const slhdsaPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +9a62xlfyV+zkm+ZJy/tiWJ9x31uTupvXeZq8QixWnaqZT1xbvy3Ip4nmDZ3GSUDZ +1OePc/R8CQGt2kddKb+HAPoEsL3U+1PNoKGuYmTPFDzeVGroNfHVBawKr56Frl7x +ptghYMHGZ5L87bpaQVnRMcD6RU3LXlAk8orkNTo3+2aY+qGnDvWHqonJ49hl7MIS +xYQ85EzMUIpXeZQrcHNcIYTQHMdOTSXXEZgWEMx4GwGXBO+FYKffM2G10t2/TmCK +dsjALcHAa7+vP1Pixy2CQ6TFsl2kfcveMWxF6j0mfkhu6Wt30w3n+qcyKu6jrKPL +8JueQsimrhIyn3a2MMbLWkmPYh4w/MAcOdycumLN4r6TfZNPrR8wMzKyjqtYLopH +deOD3BdYNDJlYEbP2FOmc/NYZL12NNyeJBVVUIcuJEBj4gqLNUIzXOSN876IdF/g +EbKN8BMLT0Fi0h7md/DzduDRP/3uUw3oEPAhIbsADL6etx8ixPmcpUJMNM6wpyBy +CWfkezmjhZeM8Iuw+WVmQHBeW88cfdJtWMQ6cn5F3/7+X/+XAwI7ECCQp2xax+ZI +fgB3BLnb3viEKzuZPq7U65YnzvKUu5shseXxU5TaPxaU/Y7gpihe0zux4xGAGekp +yc51zkqzHOw+2b0svCTCq6Wla/2E4bL9oNuhisKBfzIES0QwdQUOXQU/AzkzvGEO +iYXnsfvucws7sUp+xe7w/mmL/Qpuw9g2d8vYeIKS/NZN1qvTTIg/6Mih63OGARCu +vn8RqQX4A57NuZN4hSvz6cRFAxwPqwecW3cJ6kMznLWQRcggnXracBGNWapWj5xj +cl0gHmYHN33GFv602vglGrVJq/ovwkTrmdpjG9OtChWg7FPWRWL4W+gckazRRZyF +Xc4SxFzka8xdlHXpx839DAp5czgZ9J1aQ0PlyCMHh021KH1GcjmmmR98fVakO9Un +4eswJDUfVYm/dm5L3Ahu2bQnzsh/vEyqeaUBrTNT/yKxbGmSVzmZhcEn7WiZmQwj +eOZkmA6fhBbaEY41o8Z3Px9+A+Hvm3hO9iQHobYzz3n1A7lG6HfmCEtONhMbl9Et +AQzYPbg7z8Wn/8Wh6e9pl7+zR2hSNYww/F0JXilAsb9S4qTt8fe0Uk5wHYMyIOJb +8N8qhXnletvT6P0QUrftmvBREQdpZj9Va2jGyrbDKjJmS5CKUlYDvEAg5pjmm7Of +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 +=jk4+ +-----END PGP PRIVATE KEY BLOCK-----`; + +const slhdsaPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xioGZ5JgOW0AAAAgCxlLhHeA5kTjbLaXp8T9x0F1jFaaUBFp6VqdiDFpf3zC3k8G +H20MAAAAQAWCZ5JgOQMLCQcDFQwIAhYAApsDAh4JIqEGTql6tbroVIidNBuKEOLe +ZtYLQeCNBQAUY6K57CM5FDgFJwkCBwIAAAAAhxQQShNlrYCegDbPJ3wt9yQopwl9 +XvAEAWMY7J3tpYFbHw5XH8ScWjrxijmefCkLuuMrYHDZgV2E9V19JcQ9aIM9ag0K +Au7Q0NbNk9qHHys6D5QJXxbfQTGQia0b0zgOdWcyixNmUfbgmjq/Sf2Ye827VXZ0 +cvkiBjCnlR7EpTSpu4aMKaNpJJZGVexlZRsAldLo3x0b2ik9ZPCgON3KiUziJIuH +SGDodr7bQxMuxXNiS2ubOLM6RBDEp4eABCbgf/+rdG/xVvXtbnfLIICADapgyR+n +z/23ZD1EmVgaul6NdPZjGNKproT+gqAxJdpR5zxVQBRjJiwhaaQzO2pqBM+W+0PK +l9Djj5kaYUW4hNog0ucDhE37/CrSdyaN1gbMZZO9YSiBasxtLKw78x0QA37P4uCp +FJsrGD4+JBc9u6ufLXa5kClDOYBVYdmJJr+HLuN039dOfaiUH7Zy6+aQ+3BPTzNb +FrLuUq5dIfjwlGebWjmQMZFG/fPIBWyQ0e4HN4QS48qvPlWpoIKTLpGtn8fEcwaX +tAsHjjmHkPDyc2UoKfsynxXD8IW9P7DuC2kesYy0RtB4EFp6/KWfEeTVDX28iHRd +651eBuzGLVplPGSDQlMt3U2XVhEPHro770ioSbpwjNK9RdJ3Ev7ku12Hw9LJtOD6 +yeW2i8HwrCh8PU+kw+ZHe2lVtJTgGdOhqXVvmI2b6Ohqrr8HOJIVpRQ+1O4NE6Xa +NroKc1g9svCjHdXETmCP/HMb2u2ZJFcVwZTh8fyUUOlAD2IS9Fzm7O4u81x3dA06 +hq8pY5xiTKC0+RBIpKNrsuilOdVKPTDsqkcMtHiR493sGkdiVdAg0CBXgT8ylePa +in7du3CzaMyXWqjSL3d66uTjoe9sZZGOu+IGdA30buhEI4y5aqfqVkNn+bsfPjNi +60rqYMhOLpyCSSVGmjiMNsFPLu7KM1HBLp5D3uXVRht7XI4BcCAlC1HnXw471xWN +YubSSXHUAJzOmtlYXvWTqF8fv+V1VKwaFA7dvJq0WFetsrBruIr1XEB7zl17eL6z +e44Su99BM9cE2dwF5S6L8F74wSskfcdNn+bcb+JjN6HCUHM9f1AoPZUbUXmhCP5Z +SaeW0geFNlmvtS/R6IS20rU9gSCjPwGKeSAQBPCcMq5KhdYCY4ynfvKStxgMxXGz +ZnjFTzl/hbhhe8pe2fDbnAkZ7sqC3QiCO9ldDof+co5bgAXgNI+P97ICHTUNTO3t +1BWKjtl6v2xfcyJqtXvpDEVZp2H8TvpjoGQJiJYP+0PRPTv3v+JiBc1AufdmOars +YcfUlKnNSvBA/cyEuK6kL3ytHMt4kwRJnaAWulc4RmZC7Wmh7nlBdHfOYc3LyA3x +d+aIt7ugHFcJZWW51PSCbK4pQWeRIRNzYOzAR+U7Y6EgBkaqU+tMskXkPz6SNv7G +JVtBfvU6paqVX0WIQVPaCG9PDzZ52TqqO9xxg+m70soyxHjJ80VcV+jt9tjPbY6v +QuoxwXWHxnsm69FIdlQfvgW0s49JNuxtCkzthgmyO+7n2urh7xlK1NU6hsvWCL8l +U78uEiaGbYY4vLnZ6Q3TJiSMc/QTMTFnQxzcr8LRgjtqynbpMqtOCmy+r12c0sNZ +cWurHp5wOHTnJ8RCRN6cZdtV8OVpltCF1lMpOUxCCypaWfAMoHQgpXiL5eapGVPD +zjwdmw6eCnCsH2CltNBjOwrdoSY0tEvj/uG2aR2f0UH4SXKURBp/0fPG3xbpkSke +FbGDblh+LD+etcLF6kd0ZZNLqEHGrj74bneaPWyo+Xr0xWTA80tlAPLEkHmxzWEL +xnNowyZ6xbQk9NfbDzXXBoZuNuGsvoFAAAviIylWH5qWOORuTVA9FWUVhF0yxDNW +Z5DCfuv2EnzB2qJrXI5BoupB2uxOrVEXbxzkKXEzWXhhE0Juzad1k/vyJ8H/reaJ +MnvGrCp1N7rYzhJsqBB0LOxvsTAnTAL/S63tTPQKWz/Lb3AkQQskuaeIJK8GSpdN +l0oLrtk7Qo5K5Fm/rg+tdAFGkuIVH7mzN4wDlFsR7MJ7apzGIwvjHscZzeCrk0VB +vxPt2WGtXKy2KR76g6m/N60BDdhXkZXS63NZtJlwz1CLAAS6ohvOGCS6btJayq8h +pjDKE5nKfgNZfbHOMcG/zgPi/u0tn3IIr1rkNE4YX3rqivuFg3Hp87GfJJd66LM7 +BVeGF23o7rypUA76qGvxCz4Af2bI4YZnQ3YaF4jJQwAIaaCSx3Dexu+PZrPsomay +yF7YRHF+YUTVKbO0EcabGduY/5ezAIIWQiBCJdQGMDxz8GzcRyk51tlJ5rw9dZ3i +WC/5jQiGtPHHiZMk9rXgnC/IqC9EuH+IuEr1XfkPufuv3cbyA1wOVop6pOUXv53q +duDzOrE4OlW9C1K3zCFPwBItX3IWsLL1I7H68Z805vvEASU394fPMcOBFk5Pasin +gkBKbzfVmZ8Czs1wAXsmed4abV3MMfEzgq7oiSPajYsMO6umlnj7JXMZJiHTBb/m +6jNDei4LseA3qPazneRpO5eXyJW04D29iiDPJ7wxqTAN7Ymhve400rOVW7Joyp73 +xDK0QbyqzLYvbCu7wpnm3NDfJQWm455mAjboEr2m2re3kPq+BwmWyjbKH5kYgYqt +vsQT1sFwhkwL49YgOKd9IIzNb1Wp4WwaU4bif+G1aageWAhBEIDtCIbj/KMMWx9e +rfgyGEXc1RHwKaJfan5+T4Jfg0P7+94d7dYtwjQjfpbWC48gpbrVLJjNoxqUiMks +zaMlMslnDeI/NwDNtjRPBmtM912Upjma9IGUfxc7TJFzkMN2UDiGNKmprzs9fUE0 +rlzwLYIJGuf1irE8m7O8k/IZfa2sDkIiFMxAtg74W4v3oZKIpfxhNtsWP/fnObET +mfsvI+1GwYLfPZeix6kuxD/QOzogDY6tw9InnFQ7+VK4/+WuPlus0g73pdiZasfH +2c2bH5PuKR+OlT6HhL05T3aGQ1+903gkWcn2KxM2yCogagqnyvD0W3H3v6EbcCoI +RnU1hlnrqJWF8Brck2tqAE5e/iQbH2Efqc7py/2qVU/34Owef9AcLdJbTO/hZd+L +ZGk1QeePB2pDxa7AGrHjlMEDZ9Lbnbh82H0leOoNuXqZ+2VHg3pKxFgruyMxALpB +X9Im0HW7BwQIqbkWXkdT3y0C075x2t60+jCvPvcD/pUz1WwUuJh2BFi1CXLnpRpz +fZgfiKPdwoQHHaRLTLcJYrVfbdtCAx/U+Wp77BiuIqg+BHqxnQEAdncdFW8dba+Y +DrUHRY3Bp1rIOgVUhSzrcLX3X8iEofdLbMWuDxAtjGUyF1t9x/FqEB6+5qejF0oQ +2QUV35ZVtZLV337sahsE4S8hb8lz3VVUwCDIffA3TT7+OOPzgWBznCvdu1Pim/OM +ACKstHJsq5FkpiblaHnnH+0em9E7yYKuSKQ2t3cYQKxvbHkXP1rgLBTuFZNkHN6+ +u9MGiv1F4YH5i1JOfZL2D6quz0rhaCd2gkl6RgWxZta/jS2ppz/HXFcplE3PPitY +PeseI+Rg4t8+BDPl/2owiF6IfU2evkOdKE5IsqXbja8KHevhtKtUX/YZ8uMzm0Yr +8k4wrnMAsW7nWDWb/CwMQc3pE+yXtMjf5Rt8C9c/ad9Mq/Sp/aBMbJTpqMBuRQ4n +zRArrtiB5QDusCpM8ny55BnPmo+/9fjihw9wRTpVZ9KB+Nivjcb3jqzqtwjn8UNH +NSPH9Y8EyWE0W+W0YidCQpXlUZsIAxW8WHjIC+2D2xGEqETAwqPvWvm7/IATY+3a +obxC+d1uJl3Cz34NcuScUxkY+lHl36E5R6/PitloVJVO7JPLzC3FAszHB2FOD6dK +w4JW8pCqmJdHPmSDbqcPfvMtRh488+nbVQeCfFAEJoLOKcmH7Z83T82l7z1iAOFD +qeWIunk+01xmATe2AtgtkO6HC4r+Umz+4IhGEWG6Nx6fPbJGnAa/9RfHWiPx4+8Q +U205XkJMF7fKozYwDesCbQGw1Xyw0tXD1XKFIbvnqQk3oUMc4K1ZWqxEGer2f1F5 +Ymr8rrGzqX6WC9qRh3kXLZZyJR61EiyDSMvd5pBXaRK4tWaDEiLkwGZXrbGkwikY +ZAST4ablphlBVFipUEMK/YcF29eEFtHDnOewpJS97lG6XQ0OZvVMWfP9YPtYIQhD +OSZ9RlbuGU6dffc1sHKoTScBId6P1Cqrmf2T+Kl6l4kuImYyZ7gIf1zpAnvQZpiw +1F8fNuimSlCzQoQvsww18ATaEOuX6ejOZeJE79zYMoxcx1QpvXdod+x+goC89P+W +EtyswgJuU9miN2darcrJjKX5ZMffGYrupHu6DMAwkcuU6r37k0hQejkF+rb7sxze +PV7dqlx8iNLbu7eI/x6+2My3ydYAedz/JlWvnaCNjN+33JZ1vkL2oVMwrAfNPiRX +n9F2t7BK3hCkOjepFgBNY+kuL2Wiagt6jg+L0gG2YdOkvlc1BHpWqwRc16jxSVCB +4mB8q2iUKPuo3Fg0znGTpGQABZACQCWqd9AnFLtF3hTDzfh1nXQHSnjACRE3QIlx +Mw6KdKB4tp4AQFLY0Bw4KX8blZH/BoiI4sWRbvpV0wxZvBVT29seDTnnQ2eUFhMt +/N5HONFlv/4mFFEQmb+kLWapmS7PEgar4v+RAndlLxCBETq7zQ0QJp27hiKeLCfF +IqC3zG7Ll4HJEhImYLpJ57F+h29Na/KwZ0wMmp0aLuNwbs8au0MXes0PzqDutryv +JeuN/zmN5gNwlNGqwyYVz/6ic6JdjNU4dbrx9/7RpJ9kL0VhvDTtUHcTQZMRS5mh +P60DSNNPq1ZwVCgY/I3zF6AvKBDLXfI3CgI2jNadQ2rE/Ecjk+2Xbuvc9N36B9Y9 +Py4sUilt68H5M4Af/3uXqhCC6USVegzKpKU5cdQzC8YmTh/rjuFcfcrZVMd7zIW4 +2fNyO+jOZc+TRfgFh6X+qdJJjA0s7VfyTaVHzZ7mAQryQXkIOxHhjcFfAAKnD3rl +N17yn1aZIWz4MWjAwh7R0lctOBMooQJIO7h38r7YvWgkrqm49EGRCZvTYCvPdGZR +nJRyfo914WTvk8I9391NVxdD6zeV+UE+McULFZlRcSXm8oNgdgspYxi0hTR3cgme +NJeWj/Ab+Xr1dzgqzsZ6XfC6t2BuTZMsgs+0KHf2X8rjvVyBo6vZ4ix7G5wEp+gj +f9FjoBoMPLSETnypFsDAvCvM1mHNKvsyUVQ1VemdAD18aoF8U74Ne8CC1c4dj6OQ +46nwVRl9slp3Sxd7XLz8cURep+kl1g2dcvGhtih+B1zenqFjpg3FUdu+KcHK+bD3 +kNsvw98moWX0rcpi1ndOthpE6+2z3w/9lMKL+mkOSTzwHyo+wRa8rlRo8dRahzN+ +rytes94auw3EfTMqmjf3yKCwcnc6DI+gw5CKbFAgOZaO42fntNF2LvZVQrsRznnA +2Yd6WCSOuhAJiQqcV+jCCLqA6EUDgwYkx948+N/XPisut+LpAOrbKZ2d9pEYoReF +l7PfJw8mxrJbD0DcveQ3cWZWtc9JHuOANzWV/OsSvMmzDSVJFIEDIAVuY5ooMvYr +Ycgu4Zt8SBvcW5NxJFVIRgqO9yZxGZ+TVkR6nKVTEN1gnZkvmeSDSpUQDSYmMR3J +uK2HfD5u+oFaqTBS9VSYTW2pWuPWRlWUenAG7kRLCv9sb4bIaKSFwoA7OwIDIodh +OtouKx9w0f1GomhAKgtctENTzHtM/LvmOnLYR7ESQi1zIaCspLxI0UzUwj62E41H +Oi0D7a2uxLIiFzTFEKT10duu2HXv9aKUMcxE7CStnhIHZdG+r6vCa8Gohj3XWajW +ooia9E7ny0/Kh3cKTeTu2r2Nx7aopQEL/+Dd1QHvnUAuKrjbKhgwRyn9dVjXbLz3 +tzFhsYz/pzA/oF+U0nzBjipwbTHx7yGjOgLk28kRTuEtS83vSMc6/K4Zr6aJEqaj +rsjNa8zR3zzvANPDHDvEeYljGYtkhs41eqmNhftSgr0yzgiGqfxfKHtZ5hbZLMum +wj2ZS9ABwJ03wSAuuTYQexWT9TMUGx6LcmTWkfyQCb6T+5xI6FT5b9q+4WPAqZC4 +PakrXGh7kmMibiro1gec3l7rS6LY/K/Qo5sdQU/i3zYGHSU99c88m/4wjdsn45mt +8W3ENKxF0YJZToICIldXWJto3mZsHfg/Mo3Stn/dWCW3QkDeASh9W5htH0Sk9nET +QTAWqZdwyd235R9gvFPQjE/chbq8KoILBb7cBb9Z4bgdeTMgNX5Y7SYhsFEXArXZ +MB6EsU9FZE8FYBXOmdmI+Al3viapc/D5KF2KEh/ZXmferkG+65/pBJxjdcBj52WN +pH1Id87tBn6oRS7UNeH2lSrRUf3obAJWFf482Bsfcspz+bk9/0/BmOU7gpNq7pZt +O3KRCrvjOagFuFigdbxUlEZ3VC/egeDHt0ZMiBhwmjeb9ensPbQit7/B7mqMVf0h +cpIpjvoGoLCSpFB+oaWG5uzP3Xyy4JL1jYa+TLAOdNcFdMoGAa9zjOtOJ+PJUZhg +UlridAFNUbzCFxUOPtWkbd3Wpy/FoSyDyDDsVvDOf54V9ep9BZ9OqwDaX15UiE/R +xPVZCrSTmZ4I3U51ybOJ52lXaF+Qs6hhXiBZrz2JjbEosQBc4XGcETzlELFgP36K +yhtMYs+Lnm05SYzL7cBXBZIMXWJJudTdx6pfWdOJ+U35m6ASoFK+XRyXQczSMiDm +g/fQ1Od3+nGqQigZpUNOzi3vxzLFvghivVSqqdIVyryNeYjEn3ZkbJ3rk1ff5s5j +uwIzp0e6/Wuk18ItStIRc+Wl5ToDTeOR/c7M1MUudWpaUKFVaDNsJDxx2kw6xFuz +tZZxlBX64zLB4Fzz+SXrSKNc9DrXTg9ESd7DR6cgtsUeIKKi3i5ArZse25OmQgm5 +BSewh1tZIGwOAbIOWlIZWUmeO9mQwbdYcTAp9EjRiyHIE/Y2pR93/zYFut/kn7l6 +ScKsEUbzPALFIapeb1KLYOIjYr5kZ3NxLpCWGYqvv/WutsZX8lfs5JvmScv7Ylif +cd9bk7qb13mavEIsVp2qmU9cW78tyKeJ5g2dxklA2dTnj3P0fAkBrdpHXSm/hwD6 +BLC91PtTzaChrmJkzxQ83lRq6DXx1QWsCq+eha5e8abYIWDBxmeS/O26WkFZ0THA ++kVNy15QJPKK5DU6N/tmmPqhpw71h6qJyePYZezCEsWEPORMzFCKV3mUK3BzXCGE +0BzHTk0l1xGYFhDMeBsBlwTvhWCn3zNhtdLdv05ginbIwC3BwGu/rz9T4sctgkOk +xbJdpH3L3jFsReo9Jn5Ibulrd9MN5/qnMiruo6yjy/CbnkLIpq4SMp92tjDGy1pJ +j2IeMPzAHDncnLpizeK+k32TT60fMDMyso6rWC6KR3Xjg9wXWDQyZWBGz9hTpnPz +WGS9djTcniQVVVCHLiRAY+IKizVCM1zkjfO+iHRf4BGyjfATC09BYtIe5nfw83bg +0T/97lMN6BDwISG7AAy+nrcfIsT5nKVCTDTOsKcgcgln5Hs5o4WXjPCLsPllZkBw +XlvPHH3SbVjEOnJ+Rd/+/l//lwMCOxAgkKdsWsfmSH4AdwS52974hCs7mT6u1OuW +J87ylLubIbHl8VOU2j8WlP2O4KYoXtM7seMRgBnpKcnOdc5KsxzsPtm9LLwkwqul +pWv9hOGy/aDboYrCgX8yBEtEMHUFDl0FPwM5M7xhDomF57H77nMLO7FKfsXu8P5p +i/0KbsPYNnfL2HiCkvzWTdar00yIP+jIoetzhgEQrr5/EakF+AOezbmTeIUr8+nE +RQMcD6sHnFt3CepDM5y1kEXIIJ162nARjVmqVo+cY3JdIB5mBzd9xhb+tNr4JRq1 +Sav6L8JE65naYxvTrQoVoOxT1kVi+FvoHJGs0UWchV3OEsRc5GvMXZR16cfN/QwK +eXM4GfSdWkND5cgjB4dNtSh9RnI5ppkffH1WpDvVJ+HrMCQ1H1WJv3ZuS9wIbtm0 +J87If7xMqnmlAa0zU/8isWxpklc5mYXBJ+1omZkMI3jmZJgOn4QW2hGONaPGdz8f +fgPh75t4TvYkB6G2M8959QO5Ruh35ghLTjYTG5fRLQEM2D24O8/Fp//FoenvaZe/ +s0doUjWMMPxdCV4pQLG/UuKk7fH3tFJOcB2DMiDiW/DfKoV55Xrb0+j9EFK37Zrw +UREHaWY/VWtoxsq2wyoyZkuQilJWA7xAIOaY5puznyNZaKnFrkqGZzla2iquc0Ql +dOweot8gelXoqJ1eYnzg7GIEi9OZJcDkWLB5X4aSLp/jp+KZIyHv2eFmf6q8vLQH +hIerp6nh9ff/tKs5O1rh6N5lCWamv44+Z5Vk8W5+Ss5giOlFc2nQtPMo/pcEwz3g +k+/DHSmxBstDLiviwVK6rtBj1RdkG6XpG8B7x55NyjRIO0CTpregcYuTnhEUaqkm +dSTfqeg3YjDOOFL0ejYJy6fwizkuAb87CN7pYwTu+kdzZD/ndVSCXhXIo8Uin3GK +AACLLBdthKBzx3He+etyi0kYVJ0VZZxkwaZEzh1RrcfMfRhBlEb3rmtt33ZgivPz +dPt+flOZj6V4r+yh+pQVjEtwfXvrZZWSCnxKwpXyryslnkA5HFs3s5N3u1tzW3ph +aKj1lkZJTd4nIE3gzux3097v5Oc2jw60nj7Ddjcp02soDx1IGBMueTFt9p0dPZJ3 +SM7G00pxzaA9jFAsuO+gL65f03DFbJoenXYmRO0VquBkX3kqQ4FtJTYvDH+ZujVK +yYtjzSOS7ym66EJwQhD1kj0fEM/h0kIQuaXZJwl06e9HAuEj6HmN7Mi/e8DqUAp6 +VN8x+92PmRc6Xz1bQT9Zom12NmW6bsNZdy3sR6fSH/2WJ6dNDOjVD+WpiL0omY+j +/Sv4u5xfseoqIn0P8N1cVDWmIstSVq9SMpIMXHsYeqnK3bDwN4EotZDzefAA1eVi +qC0LTR0ZTfZl+DpA3dprnY0LZfR3tNGllc7N/OB1rMDXsZGs+9XLz6Nt/+WV1YVv +Y5iac3EiiaeYwW/uguNibl1Z9IdFDG8bVDB55s2DXKiyQBToqNRarL4/rhbEEX+a +IRkEo3hzzPrBASZgZ+OAacXcsLw+NPSH6QrAQZ/DO4XBqSYcQbZiwEDf/ipLAhsP +R5MgUklgTkgci19gD7I/h/dOegp57Yzqh9LZcp/IG+Wzjqo8Zk88IKEqarN60M1L +5PHkwOsG8FDFzuj4mClyxlyIO00H+Bmd91Qih0tSL8AdBAJomlmyK5yKMDsxn5jj +gfBcaa0KcW2HRBrKU1Wk0QLj42TCZRjoy2IcT8oADCEhAEScc9nNDo15wPaeAu5f +BlOoewI5d96ZcwthiVEi9BH7Sw5khzftZlEoGpnochmXyoVs3GH0PkDqGnj2FglO +YBbCNTid0FetvzGOqKeGoZ2LdTnML3qtBsM2nEL+9sc5C8aJZy7QA4ywfou3T/zL +0+nWu/rrilskPU1/2RNHIrBxoXBvyaAxUXWphh4xRte+194NWjqtBR0/7Fw1lac/ +EbTnM75Wh41hFBAviLUaLO0W6bYNSdPdoKCfCTUjMqcrEHVll/XiHI5wEpX2La3L +jtI+IlNIGp35NDfyanfwy4Qm7bL1pxAvQAZxar4px0n5VL3NVtCmGA/JyTKygRHV +WhQlbwMQcvjf0OCHS6VQwpvsOdOgg8GLtFPLMGOgBr5gW6vpY5s7zCrkJOhbwA6t +3rDlR+ygV7Q3gqqc/L+p9cnCh7NyAJwsC1C8OMRKxYTdzQv/oMlkMMtbrhbkb7jJ +7vVaiHjjD0ve1NOa2ofTo2hfcrx17Notu8sjYGUcCerOMn8nSWODbIkMfWgZ+vfP +IbcVEnK7bAzfVhoK3ZHOtEbDE6JPCbY2p5AnCGgXbKNx5hvisCorKcEGG6LOIRGJ +/qGcZ374/YCAxHDA7mesq9tbMfo6JcOEhT7FRS4U1o3DVOiYbYOu76lThbjZQvT4 +qAkipqZvN5dl5oqp6oLtGnwuCidYp00rT9eye0tM8obKen/huKcJc8sBzLgJRWZ1 +0phw5Ow60RfxXLfORG7DHKskJfGWXqA6Pi8mSp6Fuy9+VxKRTdgynCXitcn74EV1 +7HfThEQaTLI+UA5I9jVM3NF/N+z8qKp1TUE6PrgzkVgUHZuR9syDdzyeEGn1ftCY +s+swUoaAsiMjts/fIkRGmHhD9B9l/vyXQcQ7Q7SBnmUGmPgu3qJ+sr59MO8v5pYp +U0UR3+s90M1KufzzRRQhVC30cdEsaykjcHA1a1MTe3VVgBiwi0Gx2ITEe6ZRnUhL +so7KImknQE98voLNtweoctgPepBGlRRD7THtbnnR746W3IUftLcFlQk0mzA3J4LF +HgR0bBkLNqPHfO2qCR5oyPAe2yZSXowjzo7rF9Nmrrh7242nZNNlbGt71S0ibWTZ +Z5THPTMZRcy42MSvY8XY7j6q0NR8uA4Gif4SwYlVzSNzbGhkc2EgKFNsaGRzYSkg +PHNsaGRzYUB0ZXN0dmVjdG9yPsLeOwYTbQwAAAAsBYJnkmA8AhkBIqEGTql6tbro +VIidNBuKEOLeZtYLQeCNBQAUY6K57CM5FDgAAAAAPt8Qacd9pIHgl1r7SlrohIu2 +A4i6DcvM1tdrwZRA6X0G382+OXZKkph4YpTuk0jSki5xkXo+bfFR3OSNG/KDOwbb +vLWUYvbeHYV693ne3HaQRYFyuNqDIAf3chDSgRRKy9myiMY/+YGDZnULo1/9RFnW +EDp7sXHtLBlozc+df5kPJ3lz0iRQVfZx1xOPE0rv9dnt9QE9im/XCaryJGf96/TH +acYDTXiECNZFhk7EF5W+rn3lN3+wlkGL55UTwusEzeEjM2Jef+JGLz+XVDfT5pQA +FrHV3oS6dFB1TNCf2rH3nz01ggqL4koI8KVP6A9NuDRYtl03Zmo9gZBcYRpkMom/ +jPzWua+qX1AUAKkdMkgOzvLwMk6ksQMzMWDATAoFzOCfGaZsGtbGKJHb5OAtXzMb +5esVQ5jRhxNT4K9nZEyaGweeoKiHu7RWLwbXb13/jeGuZJSgKppNlPphAvfnPH4q +NE3ggdvHjgbEL0yWnnVA+E1KgfmPMBlYJEo9xfrwozBRgM+S9dNQgVkRbdXsVRs+ +lb1diYRbhLUFukEe8he1j9G1+kLz/8a78pHLgcC9WBbj4hcWPdv3vp4NTsvPLftF +bjAT6weKIW8GHW7EgodHtnO+q4rf1GwRAw2eCtHV18oOTFu7F0vf6t+axsl8s2zx +u9jhICNUX59PXODxEM/mLObOJVGAASTVQs8mR8eeLOjX6sLNaVJiMwNO6qosxMX1 +ImtEnCEzfFTfnADPiEVbild1cSIpOTeA1fO6h2ysNTK1/wpDXhlKjlo/JjJT61ch +Ss8aegrAIG4R+Q4bMJOillKZ+CkmEy8LRuxKF2l6ts2U8LZGLRI/e643whYBKgw8 +EL73wbgp9LLq2syedRT0QKNCIFJwwiWTx9+w/bl5WoWwzmIvVtaI6zU6Uou3E+aV +AToj8YPEYJ9ynCJXpYzNI7YyRDEifRtEwLc7um2ejV/bMA9MW71e7oZWPE++/4TE +tfykCV5CyKcJAvW9M4zxvo/1dorW9O17e4tSlUe1D0QeQIePVR0qqIDPVB/eHxg7 +wedld4/Na6wk5uQoRPMMkBNVzBU4xTR/sxDSAKop881YBf9ZfqT7KAPULVVeLtf9 +kIa7g80K3Ti8aI0KuwVO5X+dZCNlCT9Uj75OAxI1LGzDkQaSn3pD5NkVikn9MvlK +CCAAi4gXzD9yfEbP9pwJ9wQgRKcdskZVaOActsFkRfdTmW07oBCvjCs56edfFv9t +3NsGaU8Zfvj7gUB0tlc+iXSFLlJJyJTtZ7AXUL4NDdnCzncOniskvYo1ZweKxKXW +4bM04NGmHG5mOOvFOY4W6/IvLq2yvBdBAP8yGJwc1t6VdSZeLYFIO/iQiPEL6A5P +ZdIejAqs394LSZQkSrc98Z7nyJm4OPr/0dE0St20yZaBoTPzanABZbH/czNkvrOp +BiKIU53SRc9+D1ZSUDqLjwa3mIrt3Xc3LD1o5XEqlyOISwX8NJzn5Se9fmVygHuA +s/pbXxCWTmWqaNZyJX6jW1/ok+KGwtb41RMj9/TA8PylFmbJtXkNeQAtqmM6MArQ +bDQ0PCfJ81t20GJxFZzuxrK2vID+6H7kel8Rjeth+vVH2oKt5uQ+8kpn8uvPohkV +k+6k4WhBogU1bSR0H+uezVxt02ActaZ+2h9JC1IceEtncbmMhQwJepl2OBTYyvoo +4u8TxrQmlHcxjDYjk+d1YmIY03rR2TH/SRvZaaYJ0p3DVQH/0nfAYDju4MAGVRR6 +N/xCA8L15u8XseHCXHxEFok7AM/+q7rNA26OTfXHDE8HQZeH/UhCcdVGvYT0kG42 +fxM84XtkAQEfbh5hXxzz+HUYh8UOLFGE/0zOZSB7yEpS8u3Xm7hUdmkP/2U/wFSb +/rHRUFJviIaVD5sZJ3Qoh5t4watZZfYGW9HSsZo8QcVpkJiFSCRGIL8zMXl8RPah +QzZclRkMJK40lYGNF8OZ1rgwGcXrCfirgN2ljGFFo7T8P0afJIt5EdXB0LQR33T4 +MFCz7gQ19s1Og4Ntcu+V2MoBX2C7Fj68nsHFsWLDdkUCnIqssV+wHzcIfnLeB7VT +4uaXJpoTZW742KF1H5s+cL//DSc6W4/N/r3mfvxFQsDWK6tAObIfuvYKUIHdhRmX +EmHw3BhVeNXaNF9ktLMOu6H+7x5CKclnjcOR0dY7A34+m+JQNdg6tJiOCAvWv+PR +p/ipWiptr2lJQWgBYuhTPf0lkB058xaiw4xUPsB+A3elj+2BlJtjKzPcGOSjOy2Z +Fv4hMSFWNXdQrrKMH7d8YpIt9h9BsfoabbLlkVq81ibW8dZu9GU2GiEHgtoYfmT5 +81Ed0C/X1n2UOzTs2O6YKoGWjZ31OUZzHxv5KCbXNK7cyjnbmjX4ddgCCdb7CvIZ +akaFVoMEBwrxx7L103/fnaVjUGDSoRNPwnxlwqyr++Oktg/P1f1c81R3ST54wLGZ +KXPrt3a0wZPR9jFkJv5lPIg6F5QDbYk5FXPiWpT/lJLm1ydze13TKvmEdvvEf72i +bOgOJMx2yP1r9Ro4cB8KS1b4b24z/QD6ANeJ6Tz2XAwQ9g+wS4PtXCZldmqC+A7Y +7z2lWVMhCS2TPEKWpYWZKfh3/kMmv87CwQ060+yo0uXimeEQAi5xwQxfHIpUC9Aa +GzzI0zg7mAxHUKbjYmRPp1Vt7822lsLjLTE8S+vWLW3t0dhw3jKZB7KE19MGM+ZL +rKS3wuxQ72X29UMkOYUs8CGBlm27v906ngRI1DVvYzLupmFRRqhfgPw9p03zotMX +JxJco+8EZlj0D9SW81rzkeypykGidHsDkh2U/QgozJfhFlh0E1EeXy/xOrWeu7Tf ++wsy4jc2n6djODvEyHzH5R9/IWxeMpDew0Jf4R2syUWfeGFVca0jm7VLJWOOwTp0 +2mFu/JBG7PUzdDwGX0yLoa5b3q5WbmI/mO9AgwRxS96wdSKh/l8WrSHUpFIMVcBL +hY1E5hT5FLjRHXQ09VVBYW+Rpx82uOIwPQTw0neMCeLhsu0oRgPJqfVbwcTlNvXf +JnfWXelle8dNzooFv3ZfbSCzkiDrcCUB7WL6cOAbfSnETLfV8EVN528xoWRy5k5J +lkaLP49SfmAhEjcvW1gAp1C/mW+kpq8YBr5mGluibL7bkq3VksIlB/3LHEtbqJij +dmoIL7bxZYJ8p/YjCc3hnkx+lowxfKovge6atrK+8aK3bj50r7cIiWlEBUc71RUY +cm5a4XGwFcLibIDccUI9cTYBbLZQE56AvAuEGAU5z3bcmKIfotFYLbjTm6hX+Zb6 +XFWEe9ZS3pnqVzLoTOXs8Gfxcn5IQ6+YTvhqCDYffphk+Mz+kX0zmWl01uhi+/1L +rdTyR2g0C1/hxdcaBR7ic2BN7IvRH2p8C3G7KsA+5SU56MnS2GctPmVckQnTBuco +/1nxQ7hZZQhQKO0hx5PzR8oUamR6cMgtA7eJAogZloKT55hOGkKXSMdL+iYGB4e7 +gbM9ul6bXw0sOZZWwIBWHJ+Mw69GIb2zKKJx83J0a+uTfHUfU8M97vgCDs2LRPF3 +AS9Ejbheakhk8aiOtp4wz9xzBZzXLYLNhbPXZli1TQX0xXbgVDajp20QseySPo3a +oq+EIka+0cLdyIEGTrb0MAdUevP+L81mCSgCUTDeTCe8S6Yr5M2k1qJEhBpJP6JW +08V918IFc0WpjoLEVuHF1kOjG23sG1RbPLMfTILqDi612Rt/2AoH4GybWhUaqD4/ +eBWYOw5AV84mw2gCxQfMTn+JVPcBtqkJ551Ty9x2O+wBWixsprVtpVW6RBISyY7f +OwZ6THKOuVehUUPNiOaJ34QOgsPsoxrX91TTB3U6MsIhAuJ7DwSalJbiodEz4AaV +3TtcMFfq0vL5WVEMb4Lse5o0ju5nTz5J1ABvEvIP3wQsljsWGL5IHMbnvMNIclme +6s41fACoFVEix77OGrCKpWQgaM1bTVEiO7+3Iiepp5ghaXMveX+5F5g2ginL1Jrg +w8dxXoqho9dBL8LjcSr8FLFLYHnl7KMSgCVUC4zHj4PWjyng6iZyi92JDH+aYv5q +UVbrrQRGOVKzqKHRzs3Nu+ApYFxwEebCd7U+RuYfAUAYofEo0dVONToen9l0ce5Q +dHUa0sIM634m4K7l0NpYpnXc4pj8P2/osGoK5ghnz6E10Xg+906qoquyIiKG64P+ +qrIi9nhTnyKYE0Us3betySvolpIwSt1ZlOMiBYe0E5gc3a7BaSRRA/rXOsFHqZmF +HZgjLUKrEdEywRvMHPExrGmYrH6pBJpHxZ/SE78PVm2xMY5BLKVKHGO0qfN0T+MT +algk5RLA9CavqFZjVnyKqXGOlHAPuDjQyi7AIzkiED+4zV3U8SblmbimyTTdBps/ +ajyJELVZON3b70wjnY76o+0DcTAJr3OEC3bYlnpH9nJeWBruHsML7BfeSk0II6O9 +VKEJYN29yMstVC4VmbBaVk6m1+zF1q4KR4tq6lIazwCbaXrzPyhtlOP/FoM5bULk +m3FtbI8b5JwpLgn/r286WCDh4ontnWGVhawOR2P4cS1N5NT0cfNaIKi2AXGXKPhM +avMKLjqKF70iX1EQAmeIYPB3fIx8WG7BxDqZopQEX86qaJ0FOk0jG1/dfz1DMT/W +mjEp36ac6vacuoPCK7ZSVTp3afZceKjacCgptY0OTuUmNcCvn2JDvE6Hj50RKZ/4 +XmDnWZA2NF//HicRRGGXIftahrFWLfBKtKs9Iu5yHaSfa8hoSzVx/jjFnJJfpl5a +oK2HDkbI/WyDXBkxH3+cn/ewxBTdw3zjCIWIj1JMlzvyaTeXyuHoDByeYn8Yvgg6 +EGh153yRefOgsWfoXEoj3g0KAf5Oiyq4jLcjsI/HXuUibBj0AhpxVLUb9BVAdXQo +6xwZjwONquOLlxMQjWOzN5bi6Neg4ozlY/0wzvtU4T6i0YojUeGiZN8t6/ziLYsh +WP5oaN4E5a0ZqbELdI4B+nnJ1SfGZjo6YPQb3+tiAtpa8S8NbX3kZFlZASPzWULL +zz9qFO0W9flQwmFBevoag9wkSsxKQ+N1K1b/lqkEXP9sBaA4a16PRIMma8Q+RvtD +2NBLywbot2iOdP0QP2//7l5ITVzfOAJCoUOXwEz9PcokZnqM6gvjjWYVCfN5vo4d +J7TbjTakSH23n9gr9wlBXDPvS+5Nvdczg29iYDY4nzLoKEo5Dy9vJkcOcNOn0Qj4 +wENt6oP4wKdV4zEzdh1lA6/NaHW2IjLs9b1RhKnwoOkj6A+arD9s5pmmXQZT9Ytx +DSYD95AQVmvKA4kRGtR70XApvPQ69LpjTbdKCeIqDajHs29BLKOjn+1l1pLHjOSX +4OTX7u6qks71A5Ma3Cc6zXl2jBKcHW5C4uUoyTODIhLR+1LD6rTpAg+FF30AUCAL +ZYnD6YvrCZ87V5KGA0dDks4MWBLH1RZEpuXnff3ef/aBS34u58BvizDj2eNpPA47 ++tlY3Ez1t4iwdg6EWJj8n13M45tYrLXdOaviEGQZzI6RTmuUcuiCdW1eKPjZwdMN +NUuXX6kToqF66VMmum/ZAVVrqsHh3mtwIraqmBxeiVeTMJplMUnvhbnZ8g0kvLec +9UwKLboh8tGnSRcW2uzeTBN3cV7tgJg+sj7pLEG6tQm4dv3qq2yMtQDqdAszEpH2 +8fr+Rt5wtCjkU3GhQaJYxMAO/ey0DGcF6vLhWnEzyj1A/sthQ+WGu5mz3dKw+kku +2WXvpVrFKVVSOM0kYyOKZp19co6p+onruMeuh9fxjEHgpRGvYNAurrbXyJDi6InA +Dmx2p3NdYdZt9hzsKu/ysKmDNcW6GHG/div1cCsCxeTiNl9+LxN9LoZ33nVCf9GM +OpKFrBWBSLikUZhlDnxTahowOmcYtNwWcamPpvy5an0JtTt2HyaXA5ZVVfv4kYLK +/5gw7unmOeBZf8omhLNtB2vcvhM7rUSQRSYfLMKaMVNHDr/kCeHxNuEOKh+4bMVp +tyT0oyvdvt8oA2aF6OvWh76hHQrsa9SAZ3amD3HPOQUcjZSMdKSxAkghykw1Gcpp +dJ0BZbznkfpreRZiqIi+1SLMdIonGr0I1NruAJD5IcSj8PRrd/uAeNZZc4IzLPIt +wLJ2KD7lnhp125LNNafoxYjIeHcsDVyG13iJ0gLYtkmwmz1lDCugiElcL1JbUMNe +gnfaC2yTZXh5jzl0FRoyRzGEZdgd8kjDSU+NHFMi0iVHm0RFOVUYW2bIYwxfBTB0 +TYDARLGex9zLxD/viiyxr+M9GpNqfh41ld8yj9TOzb89Se0geSeFumFwKM1azT37 +AClv0qAMm7lbu4Xc+chPk8jT5aZCP49neFf0u2tlGfNbiEoygXbTXl7avU4eBeRl +eDnkV2MTaXedvQa/B8K0w8e6y/BGNCgKhYFUZpQ5mGFppaOw4J2Y/k1Ka2tq4x1c +Q4YJOJVXu4iC9ObEKp/GKWlRlIQxdD4i7CWj+Ffp+yXWAVnbzfMA34nHtwqON6PX +01FM3XzBvRHO8IwjkrIIQLghZ3RDqAwkaHzhAtP6FbVM07fBjTMjydXLCuvtOvbf +99hIWU0K+2xl7mX7LSnHbo3bGJ9e+C1fGcn2sjHHCjRa62nqTOaeg825DHkZAO2i +6wRyGtvvm+Ykw+4+xTh5pJjlIWl+5HPC+Eo0fnf0KODlL/+/Xx13QKFl2Ha/1rvD +Y+JJyx4gJqOpvy5bH1QOQTqsa+Xs8FU3PxGixWK9bYc1oMkpw8UmscbNSdaGnsk0 +hY9JW7gcg4Rz4EgGHXsPJfn/fDOONkf1PABUzwYbFf3sTOmt4q3hdZW04yW/IkyP +cgycClLppUikc8VXjJ1KYFhiS/jHOM7tTMO60wChRC93gnBHWpqEDnfVj2BdA0Pj +2pnAtCACkhCd57DMK5TrpcJSHZC9PCVsWoLuAs3uQCFzsT7H16sr98D6kbtpqnI8 +r3zCsuT+Eu4dIzT/OWYM6z0VIhqHiA84O+ScQKCFcz4wO/zkfzqtSjrk2XHBaCSt +grAAuixYsmahMtiCG+VsNTvdAK65kxYIsB4QZoX6ejutlKEhlCNoFOz3S4CPvgZf +C2+ZLpVKEMmqe6EzevpmFd1CPXEriGnAO+TZ+frpX6KU0tx2tTiGBT32gMgyf+vI +eCKzOhbRmS62fPK5EsNfccritXWGTYJ8MqFErieMNuYEJmaOJu8k4ZV2Iri8Jd5q +NCTrCY2yModvwAaHn+L0aUAFD8B9Bc9ZeQ1fy2cc8Fhz90JxEYbxd+qI3xdk9oC8 +4uUSIq3WRdG+5JHiZ8Fo1Iv2ThVACNAEf3Oja6Y19F/YC6ZYCfH8kaG79HPYm1bh +MRjIEXwzZhd4/6UTKpD0m1y663MRQkeeIRIDoqDUikD4Xd52sdyPCqI0Y+6n8BLD +k7tpu7jlTdIG69Bp8uddYtlsV1ofjl9wwCdJbvpMmkFaxEcIQ9XSg85Z8rz4mt24 +pVnYVt6aXupk5e9PNQgnbqXXGQAODUl9pr5ngRZlUQzdu6bEGgDKuauW2V0LI5an +ZQVnse0HxKk6Y6vwDOmGShR5xDlR61OxD1Vt6tDfIZtr45GhycyJhVrGkhZfE4Iw +Bpx7TAIvIdGxcTh5r3A3FOYswvDAq3I3TYpepGhI6Qq5C0cCiIWTOfsyIn+pr9bw +x9GauaTcPuzqD0LUvzcr+paSqDRmUCWRQGPmXDyZCCZcwDHvklCxRaR9r9txCXFD +vvgsXBD0z5B9QUaCzIRLVjU3IxIqewVxHiXRrDkFRhZEzKYG6DLyDUEsYDmIJous +uhuGdgduU4PCy63KgcH0wvqRuKyCxOwsZn4qCV4iLv8grKZCF867wX7dGGJGS7ru +q2TXoBjAvufwIwHFNvVBnaHUJuazC2KuLbq/n/TjelQEzfUKa/mNbxSVIy5Av1Gg +cOfS3Ku0dGnpSUqQj1r8LXdWM4bJFjsg4Qyv7EokInm+/SM3iBjBaw5aVfaZxA4N +V6wb9LeKr/DMplpgCCbG8t0ZIbDay9ixrdtjmPxZtbTFneL11p3EksN5lj4j777C +OLyjCwyV6BCn1Y3R8wHOImjvoiSCy6Em/FS3C5qj+uS0aKEhZ3UeXNcklIln24v7 +qdGBlgKUcczzE01vvzmBj2YCxZLbR3brPZbxNmi5J+747CeuESxzcB4VYjYJkwP6 +LiPGOz1c708mmV6h+7fe/4Ls8Caua1+xWRWf33DLdvg8iRqLM7LSqN4iytGE9xsx +uh6bAyQZwA10KIdaMIKI/CeZX1v4HktAjfy0dO3+aMpgAdM25EW12u3HHJA73Wey +FQsJZ3jRqnCQYJehRuAI55vgGyb/GPKbvSnJZzToJPPZsHZ5n1dbqGT03+2KFZUv +f51kjKjNRypmIiutjIjGyn9SZmKoB1buIM5XVREw+fXnhO46oxIxFuU0eWZrchu1 +v7A42fj4jtyTaDCSg7Tj8n3Z3Qn85clb12BS5qmNUrjW9shQE3VnsR5ZMvroM675 +2AWLiGs8IzlNNigqoEXZPQhoiAgLQqXWI6gCD/ZetC7tHENGtuzzo2T788D70WFb +Ta6zh7fgZWs0ZVE3RPfutxPAP0qNnSVhedFEMsRrN6QxWJ2JkarMoSCctQ8eJfvp +anrM1vVfxGPnjwGD1UPWSWij4SePlntnwZoINIHzflDzFq1IdNSdfQjbSOKEWfpB +HnnS4AUZhwSfav8uRqT+67AVXCHzupFfbWrvJ+SbUUhvkQLrFLVFOtmJdZCAijFO +FzLMHk2KxX/byB0su2sMjDAro8psKLfehO44rPIFvhgbONCifeEyywFvgBgD2CG5 +ySSs70zKcCFNvCpQmWAMOPXa9wD7jwPpYTqOLXK94fiiU4UwOGd2DaExKzLt1HKO +CB4cPU8+WxcPrEYtxSOiAYljPlTeQ+61WCt9utU3OGNZeBNKpJuJxN+IQhKy30WO +LvptX/Quhk1d9Q6d7IRn/2BllMH6ya6MwVZGPwuNVk5gUIGyw+ibIBqZ2xXTesA7 +DvvdYgKnytKCIslX/WCMfeOstDFpDsEJ6ANJ+ek8o5gxpfQQqaPPNRqz/VeQ+H5o +W+YZ1fbmYCVmjQVzrx0e1qs13OznVkWkIKmzL/OdxdiC/zCtLjOJmuLW2zeTiAGC +ImgLDSwpjfRXD10HBX5NnxuYDcCawwlI/tWEBssll4dA54yL9I/3UrpAolc9GmEv +0T8az8TqC+K743f4qw0N0jw6Zp9Gg8yagrS+kDig9kzLm6rddKyt0c0Ec0hi7tZk +g0jplkjF+dbIDTq95yG8Hox0ysCO2g2evaf/7M+UTVcaaAVAuQMbPnzvsFnx4I61 +luIRXElnGhuw5j+TplDb8xrimWCr8IRe9gqWTCOPNsbUW994RcRJGoRIqYRtdh6E +oPut6NIlj+a4dU1zVaV9/9DdxfG04tj3j1IzoICmVTjlMmmVigk6tOI8EWwbyg7F +kqOKui9CH9+7omSwbtnvetUp2BoG+8RUM5UsZsijoQW2JOcsNR9J+iuIhfoZvM19 +SKKcpeR5y0VEiz6AaCSz68w+bp5mWorBqw9OQCaLkwJ8OqcerMj4ApitMhU6ATgs +uOQXvCWcNUSLmZNM4FqtQ1HNPDBxW1F5MVcdtBIGxlWstuu+ykesko6t0l1tDWaQ +iMarZslTxdSuDf+yzgyjWNVOaCrqIvIj9FLklIWPRG5t2UxrEJxHZOhox8Nml8D6 +o54o+lV+xNqPROstHpjCbx8WvOw9Hq8QV8mIiRVNoh0hpscA1k+yiwEwC5rBmTTA +4vAgBn/o1b/31aLOyJhGXbhY3y6gPubiemNSbluzPRN6d1jsx9xu22qTA5+xfOix +zJ7g1mbOJDvPByeaggYC8DLa1EZaIslhj1IfD+moWJ8HfQGBROEUmeSrNXcT6GIR +cOPI8dhFKBmkiIYC2Gf89V8DxFNopFTJ5BXioVzcJE+c+dnajtlPSr2H5t/l/1Mn +LOs5PRvp5YWc0bEENnpr+CyMcl6/7nGpr3NFoIoLb5g9vIA7cOTCSgBcnHr9m83F +s0/iNudNR1ci1rht1+HBwz+h5hVKCjnfq5fE2H30mPAK9ffhADrpzLmJmLPl9RDa +3+3nbP8uTmjpNofgKvpCol6U1bxroVq6wCWxYBjAgsNTw+cnEAkQ/94HoyYzLl9H +lK40aNITY64xtvzN4Ma9OcsEhZPKVwJv1vEaPyn2jdtRoVljFgFQDZMkkXNFJnkW +o6zpXhoGzfk82Yu2StEtMR7P2IyHsw9EI3gFk2raHdbbL6LCjfqEakKgfLP27t/u +6c+HLPGBcDsucJL/OzHj5PU3x0T/UYzQ7UqQ2DRokWCRGXGDl3ha0R3/atL3kskX +Yw/Rx5KeOyj02lBC/1MJ0WO8DxR+6TQopENtI7MIa0reTtDA82kl8rkTtHMYIhKJ +MY7wL3dTYkVmdMVAW+jAxebjyjgXdYp//k1SWG0vIZA2E+mat5YsMo46lLw3M60l +kcIThAkUrG4f9l4FOFPAq7cmno0shoJWxmMubes8fBTszsQKBmeSYDlpAAAEwB/O +DPn6lQoCiFh78/O3+fMdP6VLgyyqxIeKLmIVEhRqjEi0KZnBFpQLGeaeCllanNO3 +kEomrsaFX+dQncZPVIO/XLZ7E0qM7DhHKZGj/xc8OzEf0Jpm2Ylb5YnEdRo8cysV +lsY2VIRFO+Z1ewcMfxx2XeJPPgPDutwvvbsiGHKDsCp4a5QYZDFWdjuFnuCejIwn +xbNZrnx++5yQQQyYqDCCEXcUDJakLDypJStS8KMLaejMkagCT8FWJxwA5Oo70UAA +1NaymmeWaDkns+ysVTNxiGtahSK61OO38YeHvsWiwTa1BEZ5ASTHqpCwg0JwuMS1 +8NueEVQWRPZVz9oJvwQMnYXMYaxxgwQchTaNqyIVgCYi7NN5cwGFI1yUZYgev+ld +cbcB8+ly5mFwljeFCtu9lHt67pIWtWScb1UAlGvBMDGLacHMCsG08CJttyu/kude +MgswPAS3TdZ3poiK7OUlZvkzDVdR5EqDBtRqruqbdAQEjJyh6cdkYlOTJ8qyVoUj +WMdvjrWQSZqsumV8FRSuiewUOlhebvsq7Oxla/aV0TaJF2I1h9mU8gM4ZkUP2RhP +qhWaalWuWjgT6WV7evigu4seqGIPs7VXr3EzUqyfVySh2oVT7FO41cpR6nqLjZdL +BlVa+EByI9ygcnsIp/kKcHVLUlt/nKOWYZy4ZKEKQbUjSRsZVCa0c+q802VRZFxc +ezGV2lFthZws6ahXLWYEPjEG+DglzvpfI/Cw8NsSTWJJKLc7P3ychuxuOdkXptdn +tGpk1PKaOqc6ybFDhDnOWDxZjhNyHzYUGTwGKLJuI1EPveA5UGwKbypmjvbPF4BB +lRNAJLhCrjxGxJg+ILEPUNc3Wbyn3Zdqd7hCTLgWxHkKaVAFZ4Rttie7xUY1Zyd1 +InGLKtEEC3wnzAPEZrqRHYGXUCuexQuHN+vB2biFrYUNjNNz4ydMmwavhjsK3Jp0 +3JuBC7UzBnQaEplCseSW8bWyRZkBbNkaWOR6mfwfgbWc4Vy16wKqwnYaNVQxOGq3 +VIiOtOQ1o6WRcWaZ5Yai92sWRNyoWrGCKVU+yeFOGCsuNhE8jvZkc+e31jQmbpaU +s6CqlwSvQzw2ZrkEA/SvDCcUDbtNqbcyRuOzhWdiQxgGisd9ZseJqyy19WXBbExV +CuCIHREQHsW2mBJtMAeuXpwGsPdH7rTCb5BOOqB7jBUosBZYj2dIylMi6FXJLdBB +GEA4Vqw3XlcxGXAp5mGa2ZcJcyxX5qGprXujZiBTDCpKYNJCfQMo+qdExQA1IoZk +MiN/EfpRV9GuqfVak8lWRyFAglYp1bGYHXIrKrUsVlmoGOSYGLFLWUQtK+W3KRR5 +O4ZjbjRzaWNUX/NEXxqKyMOUp9lNWJSLn1aZEkpFP5l9RdJTyfZrTuYwdpYuBuhR +OmnPb/s9qgYYfTa6c8wsbJsyiNvIVfOaR8J/rPMOo4xFIECXYaMrYqdbNtFZUdx3 +fpGlcbghfaoohFHDX4QJaxA1qrdJluwKqbF1CIjJ2fos2/ukd8wmRoS36qez+XE4 +y2PHMDwS99wbUHenEdi8wYiGrfuI6LHCd1OMq2gzZrJEF6W+15SNHBJ4693HkjXf +pnDjkkCYYzrlKUPwge/C3jsGGG0MAAAALAWCZ5JgOQKbDCKhBk6perW66FSInTQb +ihDi3mbWC0HgjQUAFGOiuewjORQ4AAAAABcUEMk/6qEuwvf+FwjYLfbK+jIWZBYL +hoyq2oiJxvjIOzJ6ij6/KntUm8/WbYHGIx//RKHf5j8hjmHCtQcFnLZhjEgDnWhe +PDdt2G9idoTVIUkZL+egigx5JJvIh2w0zbFI8WUIkui0qcZQusHtc/o7dqoo4zu4 +hyJKsWJpOzr9+n6p5jK98k9nBzFUHQT9WJP1vNWonfbjhfo/HkQXgU0Qj7WUgThG +5GBCm2wMVZWUN9R3ynEc4t+L0VFyo8go9d+X+LbhFoHFIrcSU6ZFv2cERMVVU4sX +Cw80gwGZe1f+vl1ZeARj+9kQbcS8qbWTfb+cpwtlK5Xm8KZhsriWEMgAjEmuGPuU +vZqrmqb8aYxO4TlYfybUzLxYphqZPGvcXdP1N2c6LTF04vNFn5fpvr5IrM5I0x9k +XRhMJpewnyeFCQaJ3PXE8pOENfho7g+bqL7b8Rj6i8hIwLLJjqcy6gxfsJ2Xxa5w +zp5roDKQ8hL9kw2gKNbFry39Er9Kkn9qANEcc32qUfQFmI6fKHObKkCwiWi/pxWq +Fl2gBnRd5NXxw4V07ZLEeGqyD7tWzSdM3750dlBo370iqBmG6TWwDt7zofgjiqMP +i2CWHewa4yX/tdh8VErO5wRVP4Upk30Bkiy4XMO6vKePuWzQrDafGhcbtX8XgImA +Ik+BKSVkDz31/QwH5f69Hx6OU0y+FpwRpIyCqSSTQ31jkHpnERDllwfze74/ZE3N +mpVthHp0l4CSUrFgtW0KmvtERUyKoplhY0Gx5OZCwrorDxvv3nzsRUXjH2hWkg8b +jx2ShbJA4MFMA2RgT1XN65Iz1DSrbmhTL6l8Ilq+huIdgTSJ8AtvgN6arxemTlcP +K1YdX6CHCtH3BFKOPU2EWzqlr5daMU6pqXaq6N5+hrAHySe9/TR0A//bY/8v6+/x +M2+4N4DDqgiM3YBEWq2Oxvy9BnPvi5xj96bRhGT+Cc2A0NV2ZJMZMEBxwcwnVMVJ +DGeBrlldZ0lN70yuQQFl5SbWwb8iFNCMQpnrToYwAYVnFa8+n0pr9WWHwpChAF7+ +C8eY/5GXvPNdj1kuevr8ex8tTGpFQlDT/lUKB7xwK+kPUfAOhH2Eq+LHDWkVOBi7 +T1rUilzlYedbQPZ/cK37ddxwjRRiN7L9S+UM+pc67Un7dOexFF975mBk0Pjj2Xzw +kdiqDjHmUwcwYtaM1aO8XqVlEqvlok9s6z1tBnN4lTItJIhkZ7q6C5jNv0kUzWbi +TxTepSkGbaJy18z6KsFltOTx9t0qhMZYbhWkGYLEmzeBgyHtIcUaqRP++0JemkC1 +az9RjOVdde4tQkI36xt76WT+trI0Sw3VAPcRW4oSiOwgepbL/yVo1W1EFCuy1hs0 +rb7W3ReuTqRKbKpm8R38owb66X66Lzc+JtToI09g6gaRGg33li56gqo/XPedCm9l +1x+H5zummxpYvvwk7HLz7XZhtDeeyf0oNRcw3ypkWFheTF0Mkr2wnSvdFm82tFc6 +K4i81I7xTwCpemXmPKB+gKWKJ6OcOQMIj1u3X1RNuDIfppvpdKm7o6wFb7Z3gOs0 +cU95C0XyRkVdyAnyZ8XmcFfN2fFtPfGyZuO8MyURxlQwKJghbQ7nwyXsyHVw+6UR +NeBQxCZm1f0w2AG8AecJjGDUdcUPPNhisif6bhCYsM8Qa7Hp2bFhYzXmBV3Ffppw +j0y1lQSisEBGfculaaRImjhFGPKgMncoVcA1UpMSZ7gSqEYu3y32wzBGmHNVqvAt +BtLrlBKMoi8ENLhWABwXvwK3hweLwYWZzyBy1pJNZA0h4DVV8JM4hG0uTrWJ6GXP +q01RubhaAFMgwTdLEH+CXSRNbMTSrAatpkDkV531NAkx4Jd9+JHKfXkB4fa8qTt+ +2ooK4WTsdX42RIW9ZfykXtPcANyGVUOJk8WEutPhf/m65ttkhy6yBWq2ibe5fkVW ++OVHD3L2WpBNNB/dEm2MEtiCjKRDdA2f/9d+sNTm2wZRnuq4py/tIJJlhwan8DLb +hjBoUYMHJ4WghRu+EPTJh9M4ssnNPHSHJiqngX36v+AiIylIuZO2ARsP8tVLG34Q +J+RicR/J3WnBZUfTTZuksrgo7UddbouPLgeY/r57y8P0djaTuVY9p43R2o4oRHQc +R+gHSMJQohtaTeKTQmVq/vnUODPEOQa1PDXmW3MeJrFkVjKwOe0X67KrXOudbtK0 +mdvfgMjzCk5qcDtHPpEMZtNx3lDJWGfWOKuxoMb1CyoHVZK48FW94yFxItT7n/8C +iElUNls0wUXFL4KSzc1Op/E/YVzbwI7og6gVYQVkF/exfiMyZzMxWkwfyAsn+1PP +SHOovneovXZaF1Ii7SCyPjLyrfUXlZRmn3bjE12rF8apdrZ4nMISBdGPTjYeZPKv +smjw4hnrZbz5xtlxHAF6FZX/RTHkb67hsCgzYy6r8f5FPRjzdiDKa64CerM51/F7 +A1vVwKZ03KgV1AGGKR+64qam/jzOy1T1bPXsRFBw8m2ELlGwfZslZxs2yS+EPEEs +5D78ciY1D95ADfQdCeV56cvc6EfkTwIkYJQQ5nSlQSTIJ6J30VCVpr+pBx2BEidO +8iYXnU7HyudoFGhGFmlXpL1/94PcjklHPX0/WD5DJQ0pjEcG392EC34SYTPKMGpX +VikuytMn4EF8ypjkEDNOFo6pEeNGhC9NGJUYUIS//UOCiuu2Qv7VbDdtUtw69rQp +uDhYJQsly2t0UhUyK/ll+vRTMATLz0w2RQZ40iOREriTV36mMhDHT8FCdFTloOG/ +5J1WgNz1QaHcS+v0Dznc8rWvMV1A2HH86BltNiZPw7yt1Lapj03Js33aNGgoHfBc +902otEWduTHo2DzMG6bS0cNaqjyyj6nwSISuQbBPcKzz6HBreXPW+NDJsNux2jPO +Vb6lYYEKCravEYmjmT/sI/8UDybycjFfoE2BRGbbwLzcHSjan10XJOmZnROtEOXi +urpnsW/q+wnOpvgyngOMwA//gWeegHM4m+NDDR0sigHn3rlc3ooavBIH+EM5KDrc +2qttaaYfI7lGCRLRYhvYY8sGoLRT5q0d8YcESpRq/mFkRZi9VYtDkJ66Oe0HEora +7lZ2u3vDFTmMePqKRJWkJ21mF1leF86k/emjnMRQoqN6yuc4wo0cTUUTG5yS0U77 +3nLNhnYBdWbHVxwYlWWMHy8GisbrThVYLAFuOcJ5tRHhSUKCvz82bf7QP4LLONrG +e/T8WlUMzxSWIFsKeaFEqpXPzUmSZWmYL1cFi+3sNS2FTD4HcNicKX8GJCofxuRM +Vp/LYzqibJHHaRE9rFP1NFK9FHPkUOnNG27VY8gQ/h9Cce0GeEWTEzn3/NxNX6/F +kn3ANfcrC6z48Bopr9hT3o1Xb38kBzr0/2b1lsVWJ9tuvt1Gtpt/Spo1MER8ZZsU +ZlISQUdV8t8X8jibthWQBVJkYiDgg/9VSp45eeeoKRj0+H7y0LuPkkXOkwrOTvWW +Nz08ghRO2oHvQSHCa/yF4yeCYiIxnrDtnHFI6dM9ycPbcZY3WE5nfji7hpTO3vLp +VjiPN56+MkTxemqlsY2ca6mR2aCGW/zS8QewG1oP9Ngtk9LpPurLnGJhEkZp+53e +rISxsXBdYijIicI1EeDNgGYbgYFB0JhD2dk+DrIqFazyabmx1yIFy3nhWAz/GyWm +qwDh+9cVYf4tWhdUzyuNBpY0BKXIbfjsX+HWfiRimN96AFZQ3PB40jao5KLXtgAn +/ZNcL/wSMije3erKUrOiYe1TPfGJ0XWM1RT0rwl3rpvBKAskopB4lbd2M9ypOcnY +vXBOkSgZ3+SwkzoxiMFT8DGouGVjwUqeYiSSSdzK8XB6hoN7fMZ2571DGzDoYbjV +PpsuK+wpUFIbF6vhqMo4wAIFMs7caoqd26wtdN+FnXSfU3kO61wpoFWy373wWis0 +eJDfmRcgtHXvg4J+ughn/RxsP9zCvROaJhrPeWaCrVyE9i/PY/FtMcBxIwsh2tqv +dsxKhR+1BUpyW8fzVlC+Pc4I++MoPgsz3BnemI79AZVId9/CEsOwuf+ZQjcZbCMR +906mplo5hRLkK0v1RoXOkfpsvKiy/d+ZNqFoB2Bvs3h6frHaVoP7RIOU3gdTZp64 +glnwLLGr97beyy3bBfRIHKGdW16lES1W+Y29j/Pg+0PhQYer80Zb3We1gL8b95JN +g2ju7dz4hYwSwxTLKI/S9r22VqVEaYZLsvk43crUzGEochz671EcgrTOpxmtRm5U +LvV4qZIYltxOrsvBvSbWNnTa5wG8YVMGY1RzA9q1ImmC8r8iAlW1o8w+vUhfQIRW ++eUixhfcNxcE7511GN51GDdxxDKAdtMSQtZtj6HrheD9+3lDnl4vh0x3hF3pVTVF +BYM82YGKs9/IGIiy8PeWztmbV/ZN50dlnyVYXq3iwrdct0pBb/nzciq19mBQACou +RdA3brxoR//lLAq9U9DShG4UdMwXYYghO44hCP4GzQwjFV0gK3DT2lcVyCcTTwD1 +IQxiQedjbA6ttuaPWNW3GE5YjRd30NLXdNGSFmXKFoUpi02hxKZRcTcfCsQzkyAi +0TCp8krFAXdIi9dlAg5dpEaVCSUJY7Io31RZQ1uHtnq0cifLPGU4wCC98TjgehvU +PLxUvoU79aZxFm3/yY2tqKSjgPLnQDaLSsnGfdWibE4Dgvmuwzs5K175k5/NWEY9 +Tb7OkQQznF1Fwhc5aQOe6irVJ3zsRt5yHufn+05n987X9TcFRjz+fR6cnf1hf9SN +2Xdq0On5dMQnPegl+iGGay6bRQpsWuBvItDqA88LfuB7B9XbgSvkE7mO9aM148ed +LPeO0JqXtYbrChukuCbrP1Uo5d8I8v3WvzKOyI0r+UZlXjmbffu4J3cUhSzy+7NA +XLeaKUfbPY6urGc6FgGW3g35QzisMI0IKOSIiNDexSzYOBkkalaxie4WrWZFn2cS +otYtosNmg3HBN7+SQH93Al9m3mq9rC/twSYtnyRgXyyL6J8dpFCCOYpP9EulGvUS +EHTqTtN22GauBpmcJ03MgTEpnmu942AlgQVg01Mnjw28NAP6CM8Qi+JbC8gWmSik +/+oDKndF63dFM2S3F3YfnC0IRz/QtUKVQhhGs0DVO7w13zndRNSwmuJoI1LQYo2D +SdhUAYX0VHFUBtgptDC/u2UNJT0LdTfoezhXo6evoPoiSPmTckHdIP0jksjBTU8D +dcXv4C9OX9mB2fbYOh6W02gDiEwFWmKpn80XSGW0+nzQ5YP9w0Uqt6TQU1r7WIfn +8LvxmwNqECjVZZa4F5VQnWHEmVIq2Y+N2B2ccNfnAR540NIx95s0eP3JszK+d/oV +GaEfin+P+V+AxSnUzt0E0AyPF/StiO7SYRfeksOYvXT3Fgla+qSwHkYLgw7/4tIr +2GSes3d5PnJ+JD34U3VMTONFpeLNyVijeGAyyDh5YQJPB2HbCXdhHQWdOz+skhcn +bIUlbSGqOnJDRWB3y7PDB9a52kEQWUcRoDeOptxvwruI3IFdH6XNnBgu9KR09dGY +WRdTmnZRSkJRK3YDs0GyXKBz6MxSchSYhCAsy8/GNMtM0J5gGyum5IORUoyVLupz +kgooHjMjWDbIjkdeex9hIuixk4knU+RSdr9jxV+iBkxm8vpOvxh1MIaUYjy3f91m +Rf1b+B4vPZQq4YHp8ohauBKzhfKmcvPeSarpI1cxgyBZ2ckyf1n/RW2w71J8c/Tu +6UuGjwB+bWGMClgdm3cy62FssH4Va2upw4ZWqydwxST7Mm/G8J3YAFxtqJQ6d13x +VNspSUJ8bfi6jsLRdInjGu0qjSeuGWjH5pw3dzboQsbdwd0zaP7oJLNeEhE5uNNv +LsoYWhzcUnx/fZ7PgsrufiJNOcB6KNpmnoS4d9Jb6oYdPkwHQUkprGlAMySvzr/R +hdzYI6EErZEk5SS/cpGcO4sMAE8F9JKapiGE/H6sCsvwkWnIDs56dFp9cICDj9sS +5+MSUSsppAEnsBSslkq+jPpBJdFembHaklu2PrOKIM2+MN4I2nqancFEvuJDgjvU +aJu24h5VAwF6LEOFdhgYZfKHoXHyZzq/TS22k1fV83Vvi6Fi8JNv7l63WWRHKaPO +TmebUS2YICxYkex0FG3Nsz6Sgpk5tN1FOVoyyIdWL9Hpzamlr8/XAaPtGYsqz3HA +DD25BRxu9gCHXG//5dwlyRmRQPPXuM4tz7axaztyJAmDsKyBbFkO8y0pIlzE4TO3 +9n0sTHVE6rAWM2wV8cO7dxpwToqMbUHgHAPjKtYzOmCzy3B2C90xmwLnXeFqLtN1 +/40dWpRqRvGA0cnQNnL1lZ/+2E1PcIsbpeHT9FuIz4+dbNsL5iCqbnGLxl75n9oI +UyTM5YN22c14qDwKhQkzEn1AUhqHTBOPK7C7aHPQlXvcL9g6C89bvjEa+zWZvKTQ +fwoLHEhV0+ePMqruO1CBvjd40vi4qUb3S1J0OhfXQ8Jjtv44rFr9nYCVgIGVNHj8 +1hYHhjjhiNAz9u/ddU1AnALqZBiFAI6acdTUGmLQPDaQ+J4Et9D9lE8UCN5U700k +NdsnPxQDPPwaOpE7C3hM9hmILCypKMFbD1P3/pXKs8F3KztMstPzIYjOigY+6Pta +2AvwADuOTJSvVUBTBMDy+uB+LnpEVsoCzkeF9x9dCC5C20MQIG7+poc/b+/8tTSi +ZbQVRjncVEno6jY7eAqwlAiSGwKzoPzAu20Eq1R/Osf2MM5vm6uRHUyjY/rnxjDt +yk5XS+Xq/k4Bk5iTimZ3dyx4cKZKV3Adyp2SzUPbAchG57zSmKagZmqlbUowBm9u +W8CZf+qypQymn3jMcs/Yk2J5sPDc/Mw0vyx3YFpxq6QraPQS4cL4atvB4q88spBg +rO1FH3CtA/eWHkRNkfzj0Uivgwic4KgWqgVyO2qpceLnCemoG9nb63BYvzI7SRNj +W434sTFo7HDevlLSTkEWkT8GJAlv8UjF8TbEUmCNKqbLdQOTY/GInjDbHZ+rW9nE +ONDSo1h81NTYAaj4zQJgwqvdILs3AsGgotJQOy8SF3rUMpOw2bJxpkvONVenZW9z +b+rzni9l/FAEHqbKr25yJSejED0XLAfkAIrbz/PND7SSb/NmQ99l3ZMsDL9sGmOv +Gyah0hx6KQxWSs2F6Bsh65OyqkIUQ8jMON8pTgeznuTYTC8PGoRED70lOXTbkSGt +qJszbXYybzN5+Ao1YSz22/42D0vY2jCjxicKbxbET7x2Ac1+OCGObNVdRxjFWzMX +tPCTbcQHdqW+SHWKV4ynStor9nRTuU2vWqYDDrLApH1PpRpfMlsJt1LqyVRpLPnR +ETrNTrFXrNX/1xKvMGJSDuQ7qqXNHb3IDEPnI4SsveI+NXD7qr7f1k2WFpNAMz2p +cM9lpZ7b5U+JzwDHk6yhnBkNrD6PdPD15cFGv5jD9dJEaOIRa+O9PD3/GLg3k82S +cmSV46V1xPiS+w2xXnbfcePN7dxok7QZTLQXd9OPpzywpkZg+5kNePshBrao6tDU +IiWh4xLdAtWi3ZCB6IRvIVtOYbIhcBXTdnogY7C57VEkp85F4JpjuXboR892zXo1 +vg6gleyal85fheFu38tVK+r6agF8BaphFhN4NeW/2u1pOGEfG9E0YaU42bavBUox +8mj9f3IUO8PznTu0digEs5DdjslFVdusCaZ2Wjunz1nAu2Z3oLakWYc328MQPqvq +BZkIhbeNsMvpbzmlzVwSCnsCQ3+TiFS86jm7bRK2bboVyA2neqiVZVYGcrv/usws +taKhjVZ6SB9olM2Go+G3WoFUjO2y1RXloJtyxpTV9g3e1xN50e7Lx4TbrUTw0ApZ +MruoFkW2/sIPmQ7uSqAXS9806HQ4F4EPvDXalu+87M78VyUnK+IrOUItxcIZ7Ci7 +bPnMMXKolpZc0ShlcvdqDKbdIdvfr04QZYjbBFoU96uCUXF57RWtiZpqPM+bTw3J +isSnDclhZpBW63mY6k+tRgpFbX5WQzryO/qvtvcMC/1PEDjoYKxdqcknthreXfOK +J1D0Mv6klx5FVTlov4aUYHBaSFJEOZxE6Vx7EuE2B/9fcUmlSdWwWt3LgWPfW93s +XNmoruBp9LApGIzJuOV55E2IPlRLtFVFs66Myb4XcnVjd8YyvlblJZCNu7JwWL1e +lV4L/tBK9DpdNRzbRbZ21hnknu39Xr7dXXMlsHaPT27jGoJSVlMVB5FZpaylD5Ar +qDU6eo8d/4CnGGpfb7mFupNvfRVW6l53orPBoAYS9XyNublL9tnjn1GKgfQiufdj +Qo+YqiCAhYyzBP93JQHsSDi9QxRRhQFg4OwpwuQYHZMTbJCO/at1jiBIXVzwWmNg +CTbmdWUlYIH/0B7tM1YlquujerrE2HTLI2CRZp6gs3RAuAxDiK7v16Ug3pj+s+uY +PkuEetoQsbw/mKY7fDnv132cBkLMStK3mdyS9OxUyqg6pMFPpdfsUXn4NXJM8Psi +hEiw5w/w6ROQnP/Oget4Btp1Ga4mjtlg4AuFU92OSE9xoVcAjRl+X999+h+dBhlZ +kNhdVVTFf3lmLFgHGLRNsEQUFIdl5zG828BxqM+h7nu7UhZ3f9tPPxYU/yVZkUk8 +N3pJTeiduaee3f/rfQheHLeai4Dp2QFAbW5bbNTxnmKvD91Tz79zdMIYPtD0Ka0U +Wt3pWfyuqYbMRH5kSFDAt5exqZcyBVHp61pWKIpNlmM452LapD5Q1/1pTbgJlM// +40gFZWuc010Buk2TyE8/eMkP/M3fB7iWawhklk5JIWlt+ljWXoHRwVIc8gD/HX/b +sQ0YP9sSjRFwWup/yfHQJz2TE9mJLLLbNftFWiceW/I2YgHyPL5f8buJNzNQ9kXN +e+el/Iaqy+58zrrdS4bQVbaMHJC7k21nIdN6KAzQvtZzai2aSXSSWcjAnOjV6hN/ +E3Zb5zvzz95HUehKJ1Rs/UIwGeBqB4xJvHW1ZicHeQXu/RXbnnh29ynPTx9XDCjZ +RdcgT04TQ6sM9nSM57OKfMlIitEeCCmHBV0XY6b/rQhHQG4mn2PebbtulQnBhFIj +i63EgnW55Yr+pbKOx1I3K/x3TNDCu5H/EyoG288Y3adI9EktPmWhbOIM+9/7PTNx +lRo+SUryP4OYA4pSJLWsC7eaZ+hbYA0J51iKRV8y6FDEo4h7C6BJnTMHTDEaLFCB +R91j7uGHcB4dGmCWm6zHC+GKvL83saI+1LzHZaUXdGd37WXw7v7wL1/+LlS3CNOv +mDJlV2xlyTK4ZOQ/zh9rk18TPXbMTgRNWwmP86PAAk6LK2Bg4BoPd8vRoSavhFQB +riYxyw7cF2DoiDskT5wAz8crXuMiie0bEZMjg4KuLKSPDgcLREyk/h48PkwnxOUM +sdmv2lDem7hkg7si6WFaR+00PCN3O2ulBrOfmul9z5kDj7jFTmXpbED1xjQYvlXe +tBSfM74gIUr1wvCkkilprhr14kiniXVUg2FwvDdt9PXUtXBmF6Y0FstRbNKNPMkH +CxYHfjrOQCcNvroEvJl79mJshKC9nRdelM5M6pN8fCdjHNMHxWFfLfL5Je5ZPTc7 +CmkVx6bp2bh2AthdoQPObQs5GfHMNaXCXlAXJuxK2SP8GSHCveVI3sleWApRCRV4 +dAICSMOXbYVzc3bBv8VSXxgczU57f/IpTMD7+ktW/2KyfoLtLp5jF9H8fpuVXc1f +WTdcmSXMzBC/dsSlpnLhr5SsSCVbppkf7pn/FR74XpKFuUW9YtNqWv7fTAIYCjoT +ym14UklQU1ZeCgQF+mUj6YdgF0F4gS5/6FlshlFIOymsW7/FrrCcX4I0stcozj1X +ztSog2crFdqW2wOZf8SNgeFETySI0/9cnX23DKtGf5+1PhuktRvHOpObIwbmCmTz +z45d7VqWcVXmk88OpqZ7WjS9qTqfQ/78xxSzQ0GJrq+XKO/4cU9m+1zcI5esc0XG +qAXG53SN6EQ7P9i3Mpj+280c8A5lbnGNSNV2YJHC72RbcDqBzzv6Xn6AJdKlKvq4 +S4sK9/3iZ2TiVMc3eKSKEqwGKFdayDhATZUzqt/tamA4tq0arH29ROnkGytAy6E5 +la0rpsBO2rpE4WRsMXKanI2L2ulffrg8UF6U90NO4qPFsfsxhS1H+NljrAILCHHT +jH8ymn0nQ36xBmSGTA31uu36/fY6vM4WIrmOJrIg5lJvHvEKmayCXCGcJfCqPo73 +aA/YcFweWiuU8T49zKe0nbabo0cIvgcre37XgVPKWk/vtSqtJNe94OsZS0ss6vff +pdItALabBFQpXDdn0TTKLhQz0/D8byrKRKDU2K1akibr+uvOjK+0aOOyx+Ahd17m +3jnHUzgqZ/6srjg/vA8Ufuk0KKRDbSOzCGtK3k7QwPNpJfK5E7RzGCISiTGO8C93 +U2JFZnTFQFvowMXm48o4F3WKf/5NUlhtLyGQNhPpmreWLDKOOpS8NzOtJZHCE4QJ +FKxuH/ZeBThTwKu3Jp6NLIaCVsZjLm3rPHwU7A== +=W1ZV +-----END PGP PUBLIC KEY BLOCK-----`; + + export default () => describe('PQC', function () { it('ML-KEM + X25519 - Generate/encrypt/decrypt', async function () { const sessionKey = { data: new Uint8Array(16).fill(1), algorithm: 'aes128' }; @@ -1206,4 +2268,32 @@ jXoyrGhDDakI/1XygauU5DpNuhgApoA= expect(data).to.equal(plaintext); await expect(verified).to.eventually.be.true; }); + + // TODO key generation & signing test skipped for now as it's too slow + it.skip('SLH-DSA - generate/sign/verify', async function () { + const digest = new Uint8Array(32).fill(1); + const hashAlgo = openpgp.enums.hash.sha3_256; + + // TODO: key generation test skipped for now as it's too slow + const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.pqc_slhdsa_shake256s); + const signature = await sign(openpgp.enums.publicKey.pqc_slhdsa_shake128s, hashAlgo, publicParams, privateParams, null, digest); + const verified = await verify(openpgp.enums.publicKey.pqc_slhdsa_shake128s, hashAlgo, signature, publicParams, null, null, digest); + expect(verified).to.be.true; + + // test that unexpected hash algo is rejected both on signing and verification + const unexpectedHashAlgo = openpgp.enums.hash.sha256; + await expect(sign(openpgp.enums.publicKey.pqc_slhdsa_shake128s, unexpectedHashAlgo, publicParams, privateParams, null, digest)).to.be.rejectedWith(/Unexpected hash algorithm for PQC signature/); + await expect(verify(openpgp.enums.publicKey.pqc_slhdsa_shake128s, hashAlgo, signature, publicParams, null, null, digest)).to.be.rejectedWith(/Unexpected hash algorithm for PQC signature/); + }); + + it('SLH-DSA - Test vector', async function () { + const privateKey = await openpgp.readKey({ armoredKey: slhdsaPrivateKey }); + const publicKey = await openpgp.readKey({ armoredKey: slhdsaPublicKey }); + + // `getSigningKey()` internally verifies the ML-DSA binding sigs + const signingKey1 = await privateKey.getSigningKey(); + const signingKey2 = await publicKey.getSigningKey(); + + expect(signingKey1.getKeyID().equals(signingKey2.getKeyID())).to.be.true; + }); });