From e56891e52c838d37837418499ac5e1d5d0a399c4 Mon Sep 17 00:00:00 2001 From: "Mark S. Lewis" Date: Sun, 23 Apr 2023 09:37:58 +0100 Subject: [PATCH] Add implementations for all hashes supported by Fabric v2.5 Signed-off-by: Mark S. Lewis --- go.mod | 7 +-- go.sum | 14 +++--- .../hyperledger/fabric/client/Gateway.java | 2 + .../fabric/client/GatewayImpl.java | 2 +- .../org/hyperledger/fabric/client/Hash.java | 44 ++++++++++++++-- .../fabric/client/TransactionContext.java | 2 +- .../hyperledger/fabric/client/HashTest.java | 46 +++++++++++++---- .../fabric/client/identity/SignerTest.java | 22 ++++---- node/jest.config.js | 19 +++---- node/package.json | 4 +- node/src/hash/hashes.test.ts | 38 ++++++++++++++ node/src/hash/hashes.ts | 29 +++++++++-- node/src/identity/hsmsigner.ts | 6 +-- node/src/identity/signers.test.ts | 4 +- node/src/identity/signers.ts | 8 +-- pkg/hash/hash.go | 37 +++++++++++++- pkg/hash/hash_test.go | 50 ++++++++++++------- pkg/identity/hsmsign.go | 2 +- 18 files changed, 258 insertions(+), 78 deletions(-) create mode 100644 node/src/hash/hashes.test.ts diff --git a/go.mod b/go.mod index fe4a2ffe4..ab8311cc2 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/miekg/pkcs11 v1.1.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.1 + golang.org/x/crypto v0.8.0 google.golang.org/grpc v1.53.0 google.golang.org/protobuf v1.28.1 ) @@ -23,9 +24,9 @@ require ( github.com/hashicorp/go-memdb v1.3.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2d2ef3fde..ead6f5e79 100644 --- a/go.sum +++ b/go.sum @@ -67,13 +67,15 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -81,13 +83,13 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/java/src/main/java/org/hyperledger/fabric/client/Gateway.java b/java/src/main/java/org/hyperledger/fabric/client/Gateway.java index 5714e7666..c52112203 100644 --- a/java/src/main/java/org/hyperledger/fabric/client/Gateway.java +++ b/java/src/main/java/org/hyperledger/fabric/client/Gateway.java @@ -229,6 +229,8 @@ interface Builder { /** * Specify the hashing implementation used to generate digests of messages sent to the Fabric network. + * + *

Standard hash implementation are provided in {@link Hash}. The default value is {@link Hash#SHA256}.

* @param hash A hashing function. * @return The builder instance, allowing multiple configuration options to be chained. */ diff --git a/java/src/main/java/org/hyperledger/fabric/client/GatewayImpl.java b/java/src/main/java/org/hyperledger/fabric/client/GatewayImpl.java index 1e1f2751b..c9459501b 100644 --- a/java/src/main/java/org/hyperledger/fabric/client/GatewayImpl.java +++ b/java/src/main/java/org/hyperledger/fabric/client/GatewayImpl.java @@ -33,7 +33,7 @@ public static final class Builder implements Gateway.Builder { private Channel grpcChannel; private Identity identity; private Signer signer = UNDEFINED_SIGNER; // No signer implementation is required if only offline signing is used - private Function hash = Hash::sha256; + private Function hash = Hash.SHA256; private final DefaultCallOptions.Builder optionsBuilder = DefaultCallOptions.newBuiler(); @Override diff --git a/java/src/main/java/org/hyperledger/fabric/client/Hash.java b/java/src/main/java/org/hyperledger/fabric/client/Hash.java index 5562a77a0..793baec49 100644 --- a/java/src/main/java/org/hyperledger/fabric/client/Hash.java +++ b/java/src/main/java/org/hyperledger/fabric/client/Hash.java @@ -8,21 +8,59 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.function.Function; /** * Hash function implementations used to generate a digest of a supplied message. */ -public final class Hash { - private Hash() { } +public enum Hash implements Function { + /** + * Returns the input message unchanged. This can be used if the signing implementation requires the full message + * bytes, not just a pre-generated digest, such as Ed25519. + */ + NONE(Function.identity()), + + /** SHA-256 hash. */ + SHA256(message -> digest("SHA-256", message)), + + /** SHA-384 hash. */ + SHA384(message -> digest("SHA-384", message)), + + /** SHA3-256 hash. */ + SHA3_256(message -> digest("SHA3-256", message)), + + /** SHA3-384 hash. */ + SHA3_384(message -> digest("SHA3-384", message)); /** * SHA-256 hash the supplied message to create a digest for signing. + * @deprecated Replaced by {@link #SHA256} * @param message Message to be hashed. * @return Message digest. */ + @Deprecated public static byte[] sha256(final byte[] message) { + return SHA256.apply(message); + } + + private final Function implementation; + + Hash(final Function implementation) { + this.implementation = implementation; + } + + /** + * Hash the supplied message to create a digest for signing. + * @param message Message to be hashed. + * @return Message digest. + */ + public byte[] apply(final byte[] message) { + return implementation.apply(message); + } + + private static byte[] digest(final String algorithm, final byte[] message) { try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); + MessageDigest digest = MessageDigest.getInstance(algorithm); return digest.digest(message); } catch (NoSuchAlgorithmException e) { // Should never happen with standard algorithm diff --git a/java/src/main/java/org/hyperledger/fabric/client/TransactionContext.java b/java/src/main/java/org/hyperledger/fabric/client/TransactionContext.java index e7e9c9c32..8c48b90ef 100644 --- a/java/src/main/java/org/hyperledger/fabric/client/TransactionContext.java +++ b/java/src/main/java/org/hyperledger/fabric/client/TransactionContext.java @@ -38,7 +38,7 @@ private static byte[] newNonce() { private String newTransactionId() { byte[] saltedCreator = GatewayUtils.concat(nonce, signingIdentity.getCreator()); - byte[] rawTransactionId = Hash.sha256(saltedCreator); + byte[] rawTransactionId = Hash.SHA256.apply(saltedCreator); byte[] hexTransactionId = Hex.encode(rawTransactionId); return new String(hexTransactionId, StandardCharsets.UTF_8); } diff --git a/java/src/test/java/org/hyperledger/fabric/client/HashTest.java b/java/src/test/java/org/hyperledger/fabric/client/HashTest.java index 39cf951c7..44bb4d11d 100644 --- a/java/src/test/java/org/hyperledger/fabric/client/HashTest.java +++ b/java/src/test/java/org/hyperledger/fabric/client/HashTest.java @@ -6,31 +6,57 @@ package org.hyperledger.fabric.client; -import java.nio.charset.StandardCharsets; - +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.nio.charset.StandardCharsets; +import java.security.Security; import static org.assertj.core.api.Assertions.assertThat; public final class HashTest { - @Test - void identical_messages_have_identical_hash() { + @BeforeAll + static void beforeAll() { + // Required from some Java 1.8 distributions that don't include SHA3 message digests + Security.addProvider(new BouncyCastleProvider()); + } + + @AfterAll + static void afterAll() { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + + @ParameterizedTest + @EnumSource(Hash.class) + void identical_messages_have_identical_hash(Hash hash) { byte[] message = "MESSAGE".getBytes(StandardCharsets.UTF_8); - byte[] hash1 = Hash.sha256(message); - byte[] hash2 = Hash.sha256(message); + byte[] hash1 = hash.apply(message); + byte[] hash2 = hash.apply(message); assertThat(hash1).isEqualTo(hash2); } - @Test - void different_messages_have_different_hash() { + @ParameterizedTest + @EnumSource(Hash.class) + void different_messages_have_different_hash(Hash hash) { byte[] foo = "foo".getBytes(StandardCharsets.UTF_8); byte[] bar = "bar".getBytes(StandardCharsets.UTF_8); - byte[] fooHash = Hash.sha256(foo); - byte[] barHash = Hash.sha256(bar); + byte[] fooHash = hash.apply(foo); + byte[] barHash = hash.apply(bar); assertThat(fooHash).isNotEqualTo(barHash); } + + @Test + void NONE_returns_input() { + byte[] message = "MESSAGE".getBytes(StandardCharsets.UTF_8); + byte[] result = Hash.NONE.apply(message); + assertThat(result).isEqualTo(message); + } } diff --git a/java/src/test/java/org/hyperledger/fabric/client/identity/SignerTest.java b/java/src/test/java/org/hyperledger/fabric/client/identity/SignerTest.java index 3338a5b26..fd38daed4 100644 --- a/java/src/test/java/org/hyperledger/fabric/client/identity/SignerTest.java +++ b/java/src/test/java/org/hyperledger/fabric/client/identity/SignerTest.java @@ -6,6 +6,10 @@ package org.hyperledger.fabric.client.identity; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.hyperledger.fabric.client.Hash; +import org.junit.jupiter.api.Test; + import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyPair; @@ -16,10 +20,6 @@ import java.security.cert.X509Certificate; import java.util.Arrays; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.hyperledger.fabric.client.Hash; -import org.junit.jupiter.api.Test; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -27,10 +27,8 @@ public final class SignerTest { private static final X509Credentials CREDENTIALS = new X509Credentials(); private static final Provider PROVIDER = new BouncyCastleProvider(); private static final byte[] MESSAGE = "MESSAGE".getBytes(StandardCharsets.UTF_8); - private static final byte[] DIGEST = Hash.sha256(MESSAGE); - private static void assertValidSignature(X509Certificate certificate, final byte[] signature) throws GeneralSecurityException { - Signature verifier = Signature.getInstance("SHA256withECDSA", PROVIDER); + private static void assertValidSignature(Signature verifier, X509Certificate certificate, final byte[] signature) throws GeneralSecurityException { verifier.initVerify(certificate); verifier.update(MESSAGE); assertThat(verifier.verify(signature)) @@ -51,9 +49,10 @@ void new_signer_from_unsupported_private_key_type_throws_IllegalArgumentExceptio @Test void sign_with_P256_key() throws GeneralSecurityException { Signer signer = Signers.newPrivateKeySigner(CREDENTIALS.getPrivateKey()); - byte[] signature = signer.sign(DIGEST); + byte[] signature = signer.sign(Hash.SHA256.apply(MESSAGE)); - assertValidSignature(CREDENTIALS.getCertificate(), signature); + Signature verifier = Signature.getInstance("SHA256withECDSA", PROVIDER); + assertValidSignature(verifier, CREDENTIALS.getCertificate(), signature); } @Test @@ -68,8 +67,9 @@ void sign_null_digest_throws_NullPointerException() { void sign_with_P384_key() throws GeneralSecurityException { X509Credentials credentials = new X509Credentials("P-384"); Signer signer = Signers.newPrivateKeySigner(credentials.getPrivateKey()); - byte[] signature = signer.sign(DIGEST); + byte[] signature = signer.sign(Hash.SHA384.apply(MESSAGE)); - assertValidSignature(credentials.getCertificate(), signature); + Signature verifier = Signature.getInstance("SHA384withECDSA", PROVIDER); + assertValidSignature(verifier, credentials.getCertificate(), signature); } } diff --git a/node/jest.config.js b/node/jest.config.js index 6d0887c04..aeb34b530 100644 --- a/node/jest.config.js +++ b/node/jest.config.js @@ -1,17 +1,18 @@ module.exports = { - "roots": [ - "/src" + roots: [ + '/src' ], - 'preset': 'ts-jest', - 'testEnvironment': 'node', - 'collectCoverage': true, - 'collectCoverageFrom': [ + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: [ '**/*.[jt]s?(x)', '!**/*.d.ts', ], - 'coverageProvider': 'v8', - 'testMatch': [ + coverageProvider: 'v8', + testMatch: [ '**/?(*.)+(spec|test).[jt]s?(x)' ], - 'maxWorkers': 1, // Workaround for Jest BigInt serialization bug: https://github.com/facebook/jest/issues/11617 + verbose: true, + workerThreads: true, } diff --git a/node/package.json b/node/package.json index 6b5acb839..5adb2b278 100644 --- a/node/package.json +++ b/node/package.json @@ -32,7 +32,7 @@ "dependencies": { "@grpc/grpc-js": "^1.8.7", "@hyperledger/fabric-protos": "~0.2.0", - "@noble/curves": "^0.7.3", + "@noble/curves": "^1.0.0", "asn1.js": "^5.4.1", "google-protobuf": "^3.21.2" }, @@ -50,7 +50,7 @@ "eslint": "^8.1.0", "eslint-plugin-jest": "^27.1.3", "eslint-plugin-tsdoc": "^0.2.14", - "jest": "^29.2.1", + "jest": "^29.5.0", "npm-run-all": "^4.1.5", "ts-jest": "^29.0.3", "typedoc": "^0.23.2", diff --git a/node/src/hash/hashes.test.ts b/node/src/hash/hashes.test.ts new file mode 100644 index 000000000..0639dd072 --- /dev/null +++ b/node/src/hash/hashes.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2023 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as hashes from './hashes'; + +describe('hashes', () => { + Object.entries(hashes).forEach(([name, hash]) => { + describe(`${name}`, () => { + it('Hashes of identical data are identical', () => { + const message = Buffer.from('foobar'); + + const hash1 = hash(message); + const hash2 = hash(message); + + expect(hash1).toEqual(hash2); + }); + + it('Hashes of different data are not identical', () => { + const foo = Buffer.from('foo'); + const bar = Buffer.from('bar'); + + const fooHash = hash(foo); + const barHash = hash(bar); + + expect(fooHash).not.toEqual(barHash); + }); + }); + }); + + it('none returns input', () => { + const message = Buffer.from('foobar'); + const hash = hashes.none(message); + expect(hash).toEqual(message); + }); +}); diff --git a/node/src/hash/hashes.ts b/node/src/hash/hashes.ts index 9911b541f..a3ad003b8 100644 --- a/node/src/hash/hashes.ts +++ b/node/src/hash/hashes.ts @@ -7,9 +7,32 @@ import { createHash } from 'crypto'; import { Hash } from './hash'; +/** + * Returns the input message unchanged. This can be used if the signing implementation requires the full message bytes, + * not just a pre-generated digest, such as Ed25519. + */ +export const none: Hash = (message) => message; + /** * SHA256 hash the supplied message bytes to create a digest for signing. */ -export const sha256: Hash = (message) => { - return createHash('sha256').update(message).digest(); -}; +export const sha256: Hash = (message) => digest('sha256', message); + +/** + * SHA384 hash the supplied message bytes to create a digest for signing. + */ +export const sha384: Hash = (message) => digest('sha384', message); + +/** + * SHA3-256 hash the supplied message bytes to create a digest for signing. + */ +export const sha3_256: Hash = (message) => digest('sha3-256', message); + +/** + * SHA3-384 hash the supplied message bytes to create a digest for signing. + */ +export const sha3_384: Hash = (message) => digest('sha3-384', message); + +function digest(algorithm: string, message: Uint8Array): Uint8Array { + return createHash(algorithm).update(message).digest(); +} diff --git a/node/src/identity/hsmsigner.ts b/node/src/identity/hsmsigner.ts index 55ebce85d..f1b81a083 100644 --- a/node/src/identity/hsmsigner.ts +++ b/node/src/identity/hsmsigner.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { P256 } from '@noble/curves/p256'; +import { p256 } from '@noble/curves/p256'; import * as pkcs11js from 'pkcs11js'; import { Signer } from './signer'; @@ -94,13 +94,13 @@ export class HSMSignerFactoryImpl implements HSMSignerFactory { throw err; } - const compactSignatureLength = P256.CURVE.nByteLength * 2; + const compactSignatureLength = p256.CURVE.nByteLength * 2; return { signer: (digest) => { pkcs11.C_SignInit(session, { mechanism: pkcs11js.CKM_ECDSA }, privateKeyHandle); const compactSignature = pkcs11.C_Sign(session, Buffer.from(digest), Buffer.alloc(compactSignatureLength)); - const signature = P256.Signature.fromCompact(compactSignature).normalizeS().toDERRawBytes(); + const signature = p256.Signature.fromCompact(compactSignature).normalizeS().toDERRawBytes(); return Promise.resolve(signature); }, close: () => pkcs11.C_CloseSession(session), diff --git a/node/src/identity/signers.test.ts b/node/src/identity/signers.test.ts index ec694aab8..6e89772bf 100644 --- a/node/src/identity/signers.test.ts +++ b/node/src/identity/signers.test.ts @@ -39,9 +39,9 @@ describe('signers', () => { const message = Buffer.from('conga'); const signer = newPrivateKeySigner(privateKey); - const digest = createHash('sha256').update(message).digest(); + const digest = createHash('sha384').update(message).digest(); const signature = await signer(digest); - const valid = verify('sha256', message, publicKey, signature); + const valid = verify('sha384', message, publicKey, signature); expect(valid).toBeTruthy(); }); diff --git a/node/src/identity/signers.ts b/node/src/identity/signers.ts index 48efa0398..d1f332e92 100644 --- a/node/src/identity/signers.ts +++ b/node/src/identity/signers.ts @@ -5,16 +5,16 @@ */ import { CurveFn } from '@noble/curves/abstract/weierstrass'; -import { P256 } from '@noble/curves/p256'; -import { P384 } from '@noble/curves/p384'; +import { p256 } from '@noble/curves/p256'; +import { p384 } from '@noble/curves/p384'; import { KeyObject } from 'crypto'; import { ecPrivateKeyAsRaw } from './asn1'; import { HSMSignerFactory, HSMSignerFactoryImpl as HSMSignerFactoryImplType } from './hsmsigner'; import { Signer } from './signer'; const namedCurves: Record = { - '1.2.840.10045.3.1.7': P256, - '1.3.132.0.34': P384, + '1.2.840.10045.3.1.7': p256, + '1.3.132.0.34': p384, }; /** diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go index 6b70668c9..78201b109 100644 --- a/pkg/hash/hash.go +++ b/pkg/hash/hash.go @@ -9,13 +9,46 @@ package hash import ( "crypto/sha256" + "crypto/sha512" + gohash "hash" + + "golang.org/x/crypto/sha3" ) // Hash function generates a digest for the supplied message. type Hash = func(message []byte) []byte +// NONE returns the input message unchanged. This can be used if the signing implementation requires the full message +// bytes, not just a pre-generated digest, such as Ed25519. +func NONE(message []byte) []byte { + return message +} + // SHA256 hash the supplied message bytes to create a digest for signing. func SHA256(message []byte) []byte { - digest := sha256.Sum256(message) - return digest[:] + return digest(sha256.New(), message) +} + +// SHA384 hash the supplied message bytes to create a digest for signing. +func SHA384(message []byte) []byte { + return digest(sha512.New384(), message) +} + +// SHA3_256 hash the supplied message bytes to create a digest for signing. +// +//lint:ignore ST1003 This naming is consistent with Go crypto package hash function constants. +func SHA3_256(message []byte) []byte { + return digest(sha3.New256(), message) +} + +// SHA3_384 hash the supplied message bytes to create a digest for signing. +// +//lint:ignore ST1003 This naming is consistent with Go crypto package hash function constants. +func SHA3_384(message []byte) []byte { + return digest(sha3.New384(), message) +} + +func digest(hasher gohash.Hash, message []byte) []byte { + hasher.Write(message) + return hasher.Sum(nil) } diff --git a/pkg/hash/hash_test.go b/pkg/hash/hash_test.go index 2dde84399..f346e9005 100644 --- a/pkg/hash/hash_test.go +++ b/pkg/hash/hash_test.go @@ -13,24 +13,40 @@ import ( ) func TestHash(t *testing.T) { - t.Run("SHA256", func(t *testing.T) { - t.Run("Hashes of identical data are identical", func(t *testing.T) { - message := []byte("foobar") - - hash1 := SHA256(message) - hash2 := SHA256(message) - - require.EqualValues(t, hash1, hash2) + for testName, testCase := range map[string]struct { + hash Hash + }{ + "NONE": {hash: NONE}, + "SHA256": {hash: SHA256}, + "SHA384": {hash: SHA384}, + "SHA3_256": {hash: SHA3_256}, + "SHA3_384": {hash: SHA3_384}, + } { + t.Run(testName, func(t *testing.T) { + t.Run("Hashes of identical data are identical", func(t *testing.T) { + message := []byte("foobar") + + hash1 := testCase.hash(message) + hash2 := testCase.hash(message) + + require.EqualValues(t, hash1, hash2) + }) + + t.Run("Hashes of different data are not identical", func(t *testing.T) { + foo := []byte("foo") + bar := []byte("bar") + + fooHash := testCase.hash(foo) + barHash := testCase.hash(bar) + + require.NotEqualValues(t, fooHash, barHash) + }) }) + } - t.Run("Hashes of different data are not identical", func(t *testing.T) { - foo := []byte("foo") - bar := []byte("bar") - - fooHash := SHA256(foo) - barHash := SHA256(bar) - - require.NotEqualValues(t, fooHash, barHash) - }) + t.Run("NONE returns input", func(t *testing.T) { + message := []byte("foobar") + result := NONE(message) + require.EqualValues(t, message, result) }) } diff --git a/pkg/identity/hsmsign.go b/pkg/identity/hsmsign.go index c84b2fefd..3891743d5 100644 --- a/pkg/identity/hsmsign.go +++ b/pkg/identity/hsmsign.go @@ -178,7 +178,7 @@ func (signer *hsmSigner) Sign(digest []byte) ([]byte, error) { r, s := unmarshalConcatSignature(signature) // Only Elliptic of 256 byte keys are supported - s = canonicalECDSASignatureSValue(s, elliptic.P256().Params().Params().N) + s = canonicalECDSASignatureSValue(s, elliptic.P256().Params().N) return asn1ECDSASignature(r, s) }