Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add implementations for all hashes supported by Fabric v2.5 #580

Merged
merged 1 commit into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
)
14 changes: 8 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -67,27 +67,29 @@ 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=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ interface Builder {

/**
* Specify the hashing implementation used to generate digests of messages sent to the Fabric network.
*
* <p>Standard hash implementation are provided in {@link Hash}. The default value is {@link Hash#SHA256}.</p>
* @param hash A hashing function.
* @return The builder instance, allowing multiple configuration options to be chained.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte[], byte[]> hash = Hash::sha256;
private Function<byte[], byte[]> hash = Hash.SHA256;
private final DefaultCallOptions.Builder optionsBuilder = DefaultCallOptions.newBuiler();

@Override
Expand Down
44 changes: 41 additions & 3 deletions java/src/main/java/org/hyperledger/fabric/client/Hash.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte[], byte[]> {
/**
* 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<byte[], byte[]> implementation;

Hash(final Function<byte[], byte[]> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
46 changes: 36 additions & 10 deletions java/src/test/java/org/hyperledger/fabric/client/HashTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,21 +20,15 @@
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;

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))
Expand All @@ -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
Expand All @@ -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);
}
}
19 changes: 10 additions & 9 deletions node/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
module.exports = {
"roots": [
"<rootDir>/src"
roots: [
'<rootDir>/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,
}
4 changes: 2 additions & 2 deletions node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions node/src/hash/hashes.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
29 changes: 26 additions & 3 deletions node/src/hash/hashes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Loading