From 53e1ec023f4ebf45911c3a078e8dfe3f715d02bf Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 25 Sep 2023 05:17:21 -0400 Subject: [PATCH] Add SHA-3 signature support (#1680) To support parsing, signing and verifying SHA3 signatures over messages and keys. --- src/crypto/hash/index.js | 15 ++++++++++++++- src/enums.js | 4 +++- src/packet/signature.js | 2 ++ test/crypto/hash/sha.js | 4 ++++ test/general/openpgp.js | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/crypto/hash/index.js b/src/crypto/hash/index.js index bdf6d60b..617b59a9 100644 --- a/src/crypto/hash/index.js +++ b/src/crypto/hash/index.js @@ -8,6 +8,7 @@ import { sha1 } from '@openpgp/noble-hashes/sha1'; import { sha224, sha256 } from '@openpgp/noble-hashes/sha256'; import { sha384, sha512 } from '@openpgp/noble-hashes/sha512'; +import { sha3_256, sha3_512 } from '@openpgp/noble-hashes/sha3'; import { ripemd160 } from '@openpgp/noble-hashes/ripemd160'; import * as stream from '@openpgp/web-stream-tools'; import md5 from './md5'; @@ -55,7 +56,9 @@ const hashFunctions = { sha256: nodeHash('sha256') || nobleHash(sha256, 'SHA-256'), sha384: nodeHash('sha384') || nobleHash(sha384, 'SHA-384'), sha512: nodeHash('sha512') || nobleHash(sha512, 'SHA-512'), - ripemd: nodeHash('ripemd160') || nobleHash(ripemd160) + ripemd: nodeHash('ripemd160') || nobleHash(ripemd160), + sha3_256: nodeHash('sha3-256') || nobleHash(sha3_256), + sha3_512: nodeHash('sha3-512') || nobleHash(sha3_512) }; export default { @@ -68,6 +71,8 @@ export default { sha384: hashFunctions.sha384, sha512: hashFunctions.sha512, ripemd: hashFunctions.ripemd, + sha3_256: hashFunctions.sha3_256, + sha3_512: hashFunctions.sha3_512, /** * Create a hash on the specified data using the specified algorithm @@ -91,6 +96,10 @@ export default { return this.sha512(data); case enums.hash.sha224: return this.sha224(data); + case enums.hash.sha3_256: + return this.sha3_256(data); + case enums.hash.sha3_512: + return this.sha3_512(data); default: throw new Error('Invalid hash function.'); } @@ -116,6 +125,10 @@ export default { return 64; case enums.hash.sha224: return 28; + case enums.hash.sha3_256: + return 32; + case enums.hash.sha3_512: + return 64; default: throw new Error('Invalid hash algorithm.'); } diff --git a/src/enums.js b/src/enums.js index 210c380b..5dbbffcc 100644 --- a/src/enums.js +++ b/src/enums.js @@ -175,7 +175,9 @@ export default { sha256: 8, sha384: 9, sha512: 10, - sha224: 11 + sha224: 11, + sha3_256: 12, + sha3_512: 14 }, /** A list of hash names as accepted by webCrypto functions. diff --git a/src/packet/signature.js b/src/packet/signature.js index 6e0c0dc1..6ab5a6a4 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -833,6 +833,8 @@ export function saltLengthForHash(hashAlgorithm) { case enums.hash.sha384: return 24; case enums.hash.sha512: return 32; case enums.hash.sha224: return 16; + case enums.hash.sha3_256: return 16; + case enums.hash.sha3_512: return 32; default: throw new Error('Unsupported hash function for V6 signatures'); } } diff --git a/test/crypto/hash/sha.js b/test/crypto/hash/sha.js index dc5284f0..5297aca5 100644 --- a/test/crypto/hash/sha.js +++ b/test/crypto/hash/sha.js @@ -14,4 +14,8 @@ export default () => it('SHA* with test vectors from NIST FIPS 180-2', async fun expect(util.uint8ArrayToHex(await hash.sha384(util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha384("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b')).to.equal('3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b'); expect(util.uint8ArrayToHex(await hash.sha512(util.stringToUint8Array('abc')), 'hash.sha512("abc") = ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f')).to.equal('ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f'); expect(util.uint8ArrayToHex(await hash.sha512(util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha512("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445')).to.equal('204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445'); + expect(util.uint8ArrayToHex(await hash.sha3_256(util.stringToUint8Array('abc')), 'hash.sha3_256("abc") = 3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532')).to.equal('3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532'); + expect(util.uint8ArrayToHex(await hash.sha3_256(util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha3_256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376')).to.equal('41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376'); + expect(util.uint8ArrayToHex(await hash.sha3_512(util.stringToUint8Array('abc')), 'hash.sha3_512("abc") = b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0')).to.equal('b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0'); + expect(util.uint8ArrayToHex(await hash.sha3_512(util.stringToUint8Array('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')), 'hash.sha3_512("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 04a371e84ecfb5b8b77cb48610fca8182dd457ce6f326a0fd3d7ec2f1e91636dee691fbe0c985302ba1b0d8dc78c086346b533b49c030d99a27daf1139d6e75e')).to.equal('04a371e84ecfb5b8b77cb48610fca8182dd457ce6f326a0fd3d7ec2f1e91636dee691fbe0c985302ba1b0d8dc78c086346b533b49c030d99a27daf1139d6e75e'); }); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 41533bdf..948e8862 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -297,6 +297,25 @@ DECl1Qu4QyeXin29uEXWiekMpNlZVsEuc8icCw6ABhIZ =/7PI -----END PGP PRIVATE KEY BLOCK-----`; +const priv_key_sha3 = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUsGZN8edBsAAAAgdUMlFMFCVKNo7sdUd6FVBos6NNjpUpSdrodk6BfPb/kA+3bu +A2+WY2LwyxlX5o07WR2VSn+wuegC3v28yO0tClHCtwYfGw4AAABIBYJk3x50BAsJ +CAcHFQ4MCgkICwIWAAIXgAKbAwIeCSIhBpbSe0QWuaCNSSLaePhXEP3BxQ2VHX3W +pW1U6svHvCUiBScJAgcCAAAAACMZIP8aHixoyC9wS3q/TNV/IfOQa81f+U5Ucz6H +4I+c5bWRYUzH/piBB4n5FoYlld+/SViCQIBCQ+fynLmaj5wlf22+mISTt/9je1Zf +YWlJ+WSJyi5gY5EH9DubfuIU3VaqCM0aQmVybmFkZXR0ZSA8YkBleGFtcGxlLm9y +Zz7CugYTGw4AAABLBYJk3x50BAsJCAcHFQ4MCgkICwIWAAIXgAIZAQKbAwIeCSIh +BpbSe0QWuaCNSSLaePhXEP3BxQ2VHX3WpW1U6svHvCUiBScJAgcCAAAAAMMGIJEi +9+yqkFKsNwX1H5II0riPudFpwBx2ypVjNk4aNb7Exl56Aac4tXEhz4fH41q0dAzF +ww2erZaiUqmohQ4AFSw1jN/WOiDfb1DkjT/HJ8vXMGpwWdgFPoqsWzTNhd5VCcdL +BmTfHnQZAAAAIAGMcsqVCXLclRhVamWciSxmnYF1FFs80W7dNUH07HUOAHh/S601 +If+/eZKDIj3jq7oOe2PzHSYEK+mpQD1hBpF2wpsGGBsOAAAALAWCZN8edAKbDCIh +BpbSe0QWuaCNSSLaePhXEP3BxQ2VHX3WpW1U6svHvCUiAAAAANj3IBknZTPsMpWA +we0Jl5gw/Dj4lWAGoJfWfk+6s3Q86Hag3Hu8VBsapzmul+vzy0KJa+ZRcZz2n8aj +0vTl4sOZ0EcCdFDfkh/tR//gKkT6BiSBG86WoFq3f6U/RC+z0Ym7Dw== +-----END PGP PRIVATE KEY BLOCK-----`; + const passphrase = 'hello world'; const plaintext = input.createSomeMessage(); const password1 = 'I am a password'; @@ -1821,6 +1840,23 @@ aOU= })).to.be.rejectedWith(/No signing keys provided/); }); + it('Signing with key which uses sha3 should generate a valid sha3 signature', async function() { + const privKey = await openpgp.readKey({ armoredKey: priv_key_sha3 }); + const pubKey = privKey.toPublic(); + const text = 'Hello, world.'; + const message = await openpgp.createCleartextMessage({ text }); + + const cleartextMessage = await openpgp.sign({ message, signingKeys: privKey, format: 'armored' }); + const parsedArmored = await openpgp.readCleartextMessage({ cleartextMessage }); + expect(parsedArmored.signature.packets.filterByTag(openpgp.enums.packet.signature)).to.have.length(1); + expect( + parsedArmored.signature.packets.filterByTag(openpgp.enums.packet.signature)[0].hashAlgorithm + ).to.equal(openpgp.enums.hash.sha3_512); + + const verified = await openpgp.verify({ message: parsedArmored, verificationKeys: pubKey, expectSigned: true }); + expect(verified.data).to.equal(text); + }); + it('should output cleartext message of expected format', async function() { const text = 'test'; const message = await openpgp.createCleartextMessage({ text });