Skip to content

Commit

Permalink
Ed25519 support for all gateways
Browse files Browse the repository at this point in the history
Warning1: This branch must be run with
the fabric pull-request branch on
hyperledger/fabric#3343

Warning2: The java gateway does not support ed25519
TLS certificates. This will lead to either warning
users that java-gateway cannot communicate to ed25519
peers/orderers with TLS certificates OR the pull request
on hyperledger/fabric#3343
fabric should disable ed25519 TLS certificates.

Signed-off-by: Johann Westphall <johannwestphall@gmail.com>
  • Loading branch information
johannww committed Nov 25, 2022
1 parent 214d05b commit 81f5ee4
Show file tree
Hide file tree
Showing 22 changed files with 268 additions and 32 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ PEER_IMAGE_PULL ?= hyperledger-fabric.jfrog.io/fabric-peer:amd64-2.5-stable

# PEER_IMAGE_TAG is what to tag the pulled peer image as, it will also be used in docker-compose to reference the image
# In fabric-gateway main branch this version tag should correspond to the version in the fabric main branch
PEER_IMAGE_TAG ?= 2.5
PEER_IMAGE_TAG ?= 3.0

# TWO_DIGIT_VERSION specifies which chaincode images to pull, they will be tagged to be consistent with PEER_IMAGE_TAG
# In fabric-gateway main branch it should typically be the latest released chaincode version available in dockerhub.
Expand Down Expand Up @@ -148,8 +148,8 @@ all: test

.PHONEY: pull-latest-peer
pull-latest-peer:
docker pull $(PEER_IMAGE_PULL)
docker tag $(PEER_IMAGE_PULL) hyperledger/fabric-peer:$(PEER_IMAGE_TAG)
#docker pull $(PEER_IMAGE_PULL)
#docker tag $(PEER_IMAGE_PULL) hyperledger/fabric-peer:$(PEER_IMAGE_TAG)
# also need to retag the following images for the chaincode builder
for IMAGE in baseos ccenv javaenv nodeenv; do \
docker pull hyperledger/fabric-$${IMAGE}:$(TWO_DIGIT_VERSION); \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.grpc.Channel;
import org.hyperledger.fabric.client.identity.Identity;
import org.hyperledger.fabric.client.identity.Signer;
import org.hyperledger.fabric.client.identity.X509Identity;
import org.hyperledger.fabric.protos.common.ChannelHeader;
import org.hyperledger.fabric.protos.common.Envelope;
import org.hyperledger.fabric.protos.common.Header;
Expand Down Expand Up @@ -128,9 +129,19 @@ public GatewayImpl connect() {

private final GatewayClient client;
private final SigningIdentity signingIdentity;
private static final String ED25519_ID = "1.3.101.112";

private GatewayImpl(final Builder builder) {
signingIdentity = new SigningIdentity(builder.identity, builder.hash, builder.signer);
if (builder.identity instanceof X509Identity && ((X509Identity) builder.identity)
.getCertificate()
.getPublicKey()
.getAlgorithm()
.equals(ED25519_ID)) {
signingIdentity = new SigningIdentity(builder.identity, Hash::none, builder.signer);
System.out.println("Forcing \"hash::None\" because private key is Ed25519");
} else {
signingIdentity = new SigningIdentity(builder.identity, builder.hash, builder.signer);
}
client = new GatewayClient(builder.grpcChannel, builder.optionsBuilder.build());
}

Expand Down
9 changes: 9 additions & 0 deletions java/src/main/java/org/hyperledger/fabric/client/Hash.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,13 @@ public static byte[] sha256(final byte[] message) {
throw new RuntimeException(e);
}
}

/**
* SHA-256 hash the supplied message to create a digest for signing.
* @param message Message to be hashed.
* @return Message digest.
*/
public static byte[] none(final byte[] message) {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2020 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/

package org.hyperledger.fabric.client.identity;

import java.security.GeneralSecurityException;
import java.security.Provider;
import java.security.Signature;

import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

final class ED25519Signer implements Signer {

private static final Provider PROVIDER = new BouncyCastleProvider();
private static final String ALGORITHM_NAME = "Ed25519";

private final EdDSAPrivateKey privateKey;

ED25519Signer(final EdDSAPrivateKey privateKey) {
this.privateKey = privateKey;
}

@Override
public byte[] sign(final byte[] message) throws GeneralSecurityException {
byte[] rawSignature = generateSignature(message);
return rawSignature;
}

private byte[] generateSignature(final byte[] message) throws GeneralSecurityException {
Signature signer = Signature.getInstance(ALGORITHM_NAME, PROVIDER);
signer.initSign(privateKey);
signer.update(message);
return signer.sign();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
import java.io.UncheckedIOException;
import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMException;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
Expand All @@ -33,6 +35,8 @@
* Utility methods for creating and manipulating identity information.
*/
public final class Identities {
private static final Provider BC_PROVIDER = new BouncyCastleProvider();

/**
* Read a PEM format X.509 certificate.
* @param pem PEM data.
Expand Down Expand Up @@ -109,7 +113,7 @@ public static PrivateKey readPrivateKey(final Reader pemReader) throws IOExcepti
try {
Object pemObject = readPemObject(pemReader);
PrivateKeyInfo privateKeyInfo = asPrivateKeyInfo(pemObject);
return new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo);
return new JcaPEMKeyConverter().setProvider(BC_PROVIDER).getPrivateKey(privateKeyInfo);
} catch (PEMException e) {
throw new InvalidKeyException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import java.security.PrivateKey;
import java.security.interfaces.ECPrivateKey;

import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey;

/**
* Factory methods to create standard signing implementations.
*/
Expand All @@ -22,6 +24,8 @@ public final class Signers {
public static Signer newPrivateKeySigner(final PrivateKey privateKey) {
if (privateKey instanceof ECPrivateKey) {
return new ECPrivateKeySigner((ECPrivateKey) privateKey);
} else if (privateKey instanceof EdDSAPrivateKey) {
return new ED25519Signer((EdDSAPrivateKey) privateKey);
} else {
throw new IllegalArgumentException("Unsupported private key type: " + privateKey.getClass().getTypeName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@

package org.hyperledger.fabric.client.identity;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.hyperledger.fabric.client.TestUtils;
import org.junit.jupiter.api.Test;

Expand All @@ -23,9 +32,10 @@

public class IdentitiesTest {
private static final TestUtils testUtils = TestUtils.getInstance();
private static final Provider BC_PROVIDER = new BouncyCastleProvider();

private final X509Credentials credentials = new X509Credentials();
private final String x509CertificatePem = "-----BEGIN CERTIFICATE-----\n" +
private final static String x509CertificatePem = "-----BEGIN CERTIFICATE-----\n" +
"MIICGDCCAb+gAwIBAgIQHWBLQRSL/SxAckSUBCAceDAKBggqhkjOPQQDAjBzMQsw\n" +
"CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\n" +
"YW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\n" +
Expand All @@ -39,12 +49,29 @@ public class IdentitiesTest {
"O6R7m7rxRD/A8hmEVcogX6x1kt7NvWH0OfgCIHpKlOFXN50hrMirci4scErbc/ra\n" +
"G8OCh+bs1rqfv9cM\n" +
"-----END CERTIFICATE-----";
private final String pkcs8PrivateKeyPem = "-----BEGIN PRIVATE KEY-----\n" +
private final static String pkcs8PrivateKeyPem = "-----BEGIN PRIVATE KEY-----\n" +
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8yzkTu0ilOAwJZgj\n" +
"fU/MO5V532NgyJEB7QW6KKsrTwGhRANCAAS/gIwgOSgVqobQsSPl7Wsd0dLl91pR\n" +
"wujlA8wQ+n3GUX1yhZ8BnyRXDMOHD4bh0OdkM1isyhkrVcKkYxyxew1y\n" +
"-----END PRIVATE KEY-----";

private final String x509EdCertificatePem = "-----BEGIN CERTIFICATE-----\n" +
"MIIB/DCCAaKgAwIBAgIRAPEpXrM4I5IE+DoWemB4QgowCgYIKoZIzj0EAwIwczEL\n" +
"MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\n" +
"cmFuY2lzY28xGTAXBgNVBAoTEG9yZzMuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\n" +
"Lm9yZzMuZXhhbXBsZS5jb20wHhcNMjIxMTI0MTU0NzAwWhcNMzIxMTIxMTU0NzAw\n" +
"WjBsMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\n" +
"U2FuIEZyYW5jaXNjbzEPMA0GA1UECxMGY2xpZW50MR8wHQYDVQQDDBZVc2VyMUBv\n" +
"cmczLmV4YW1wbGUuY29tMCowBQYDK2VwAyEA6fsMyYSIlNnurJIKiXzcmBWYmVha\n" +
"KeFS0aiD1tgdzIyjTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsG\n" +
"A1UdIwQkMCKAILdru8n5fu441eogpM1QkCnGx6a5AAbCN6g3F+QupVvCMAoGCCqG\n" +
"SM49BAMCA0gAMEUCIQCj0ha3aAtb7C3PGrgZT0zVIIiwL0x5I1MlDGhvoYrGJAIg\n" +
"aKwqPJ6h5O8ZWr/DQbRfTFE6yDFTJyWfayJLIXzHiRw=\n" +
"-----END CERTIFICATE-----\n";
private final String pkcs8EdPrivateKeyPem = "-----BEGIN PRIVATE KEY-----\n" +
"MC4CAQAwBQYDK2VwBCIEIIy/o6SuOCDSwfL4H3e6M9JxQSffp64BvnG1xb9Vgjyq\n" +
"-----END PRIVATE KEY-----";

@Test
void certificate_read_error_throws_IOException() {
String failMessage = "read failure";
Expand Down Expand Up @@ -106,11 +133,20 @@ void read_and_write_X509_certificate_PEM() throws CertificateException {
}

@Test
void read_and_write_PKCS8_private_key_PEM() throws InvalidKeyException {
void read_and_write_PKCS8_private_key_PEM() throws InvalidKeyException, IOException, NoSuchAlgorithmException, SignatureException, CertificateException {
PrivateKey privateKey = Identities.readPrivateKey(pkcs8PrivateKeyPem);
String result = Identities.toPemString(privateKey);

assertThat(result).isEqualToIgnoringNewLines(pkcs8PrivateKeyPem);
PrivateKey fromIdentitiesPem = Identities.readPrivateKey(result);
Signature signer = Signature.getInstance("NONEwithECDSA", BC_PROVIDER);
signer.initSign(fromIdentitiesPem);
signer.update("message".getBytes());
byte[] sig = signer.sign();

signer.initVerify(Identities.readX509Certificate(x509CertificatePem));
signer.update("message".getBytes());

assert(signer.verify(sig));
}

@Test
Expand All @@ -130,4 +166,20 @@ void write_and_read_private_key() throws InvalidKeyException {

assertThat(actual).isEqualTo(expected);
}
}

@Test
void read_and_write_X509_ed25519_certificate_PEM() throws CertificateException {
Certificate certificate = Identities.readX509Certificate(x509EdCertificatePem);
String result = Identities.toPemString(certificate);

assertThat(result).isEqualToIgnoringNewLines(x509EdCertificatePem);
}

@Test
void read_and_write_PKCS8_ed2519_private_key_PEM() throws InvalidKeyException {
PrivateKey privateKey = Identities.readPrivateKey(pkcs8EdPrivateKeyPem);
String result = Identities.toPemString(privateKey);

assertThat(result).isEqualToIgnoringNewLines(pkcs8EdPrivateKeyPem);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
Expand All @@ -29,7 +32,7 @@ public final class SignerTest {
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 {
private static void assertValidECSignature(X509Certificate certificate, final byte[] signature) throws GeneralSecurityException {
Signature verifier = Signature.getInstance("SHA256withECDSA", PROVIDER);
verifier.initVerify(certificate);
verifier.update(MESSAGE);
Expand All @@ -38,6 +41,17 @@ private static void assertValidSignature(X509Certificate certificate, final byte
.isTrue();
}

private static void assertValidEd25519Signature(X509Certificate certificate, final byte[] signature) throws GeneralSecurityException {
Signature verifier = Signature.getInstance("Ed25519", PROVIDER);
KeyFactory ed25519Fact = KeyFactory.getInstance("Ed25519", PROVIDER);
PublicKey pub = ed25519Fact.generatePublic(new X509EncodedKeySpec(certificate.getPublicKey().getEncoded()));
verifier.initVerify(pub);
verifier.update(MESSAGE);
assertThat(verifier.verify(signature))
.withFailMessage("invalid signature: %s", Arrays.toString(signature))
.isTrue();
}

@Test
void new_signer_from_unsupported_private_key_type_throws_IllegalArgumentException() throws NoSuchAlgorithmException {
KeyPairGenerator generator = KeyPairGenerator.getInstance("DSA", new BouncyCastleProvider());
Expand All @@ -53,7 +67,7 @@ void sign_with_P256_key() throws GeneralSecurityException {
Signer signer = Signers.newPrivateKeySigner(CREDENTIALS.getPrivateKey());
byte[] signature = signer.sign(DIGEST);

assertValidSignature(CREDENTIALS.getCertificate(), signature);
assertValidECSignature(CREDENTIALS.getCertificate(), signature);
}

@Test
Expand All @@ -66,10 +80,19 @@ void sign_null_digest_throws_NullPointerException() {

@Test
void sign_with_P384_key() throws GeneralSecurityException {
X509Credentials credentials = new X509Credentials("P-384");
X509Credentials credentials = new X509Credentials("EC", "P-384");
Signer signer = Signers.newPrivateKeySigner(credentials.getPrivateKey());
byte[] signature = signer.sign(DIGEST);

assertValidSignature(credentials.getCertificate(), signature);
assertValidECSignature(credentials.getCertificate(), signature);
}

@Test
void sign_with_ED25519_key() throws GeneralSecurityException {
X509Credentials credentials = new X509Credentials("Ed25519", "");
Signer signer = Signers.newPrivateKeySigner(credentials.getPrivateKey());
byte[] signature = signer.sign(MESSAGE);

assertValidEd25519Signature(credentials.getCertificate(), signature);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcECContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

public final class X509Credentials {
private static final Provider BC_PROVIDER = new BouncyCastleProvider();
Expand All @@ -46,20 +47,22 @@ public final class X509Credentials {
* Create credentials using a P-256 curve.
*/
public X509Credentials() {
this("P-256");
this("EC", "P-256");
}

public X509Credentials(String curveName) {
KeyPair keyPair = generateKeyPair(curveName);
public X509Credentials(String keyAlgorithm, String curveName) {
KeyPair keyPair = generateKeyPair(keyAlgorithm, curveName);
certificate = generateCertificate(keyPair);
privateKey = keyPair.getPrivate();
}

private KeyPair generateKeyPair(String curveName) {
private KeyPair generateKeyPair(String keyAlgorithm, String curveName) {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", BC_PROVIDER);
AlgorithmParameterSpec curveParam = new ECGenParameterSpec(curveName);
generator.initialize(curveParam);
KeyPairGenerator generator = KeyPairGenerator.getInstance(keyAlgorithm, BC_PROVIDER);
if (keyAlgorithm == "EC" && curveName.length() > 0) {
AlgorithmParameterSpec curveParam = new ECGenParameterSpec(curveName);
generator.initialize(curveParam);
}
return generator.generateKeyPair();
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
Expand All @@ -84,8 +87,15 @@ private X509Certificate generateCertificate(KeyPair keyPair) {
AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);

try {
ContentSigner contentSigner = new BcECContentSignerBuilder(sigAlgId, digAlgId)
.build(PrivateKeyFactory.createKey(keyPair.getPrivate().getEncoded()));
ContentSigner contentSigner = null;
if (keyPair.getPrivate().getAlgorithm() == "EC")
contentSigner = new BcECContentSignerBuilder(sigAlgId, digAlgId)
.build(PrivateKeyFactory.createKey(keyPair.getPrivate().getEncoded()));
else if (keyPair.getPrivate().getAlgorithm() == "Ed25519"){
contentSigner = new JcaContentSignerBuilder("Ed25519")
.setProvider(BC_PROVIDER)
.build(keyPair.getPrivate());
}
X509CertificateHolder holder = builder.build(contentSigner);
return new JcaX509CertificateConverter().getCertificate(holder);
} catch (IOException e) {
Expand Down

0 comments on commit 81f5ee4

Please sign in to comment.