Skip to content

Commit

Permalink
[FAB-6887] Support mutual TLS for peer/orderer
Browse files Browse the repository at this point in the history
Adds support for configuring client certificates
when connecting via TLS to a peer or orderer.

Change-Id: Id9200822f266d79672294180bca34c9b724c9f0d
Signed-off-by: Gari Singh <gari.r.singh@gmail.com>
  • Loading branch information
mastersingh24 committed Nov 8, 2017
1 parent 0b3e5aa commit f678c01
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 57 deletions.
101 changes: 64 additions & 37 deletions src/main/java/org/hyperledger/fabric/sdk/Endpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -47,8 +48,12 @@
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.hyperledger.fabric.sdk.security.CryptoPrimitives;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;

import org.hyperledger.fabric.sdk.exception.CryptoException;
import org.hyperledger.fabric.sdk.security.CryptoPrimitives;
import static org.hyperledger.fabric.sdk.helper.Utils.parseGrpcUrl;

class Endpoint {
Expand All @@ -62,22 +67,27 @@ class Endpoint {
private static final Map<String, String> CN_CACHE = Collections.synchronizedMap(new HashMap<>());

Endpoint(String url, Properties properties) {

logger.trace(String.format("Creating endpoint for url %s", url));
this.url = url;

String cn = null;
String sslp = null;
String nt = null;
byte[] pemBytes = null;

X509Certificate[] clientCert = new X509Certificate[] {};
PrivateKey clientKey = null;
Properties purl = parseGrpcUrl(url);
String protocol = purl.getProperty("protocol");
this.addr = purl.getProperty("host");
this.port = Integer.parseInt(purl.getProperty("port"));

if (properties != null) {
if ("grpcs".equals(protocol)) {
CryptoPrimitives cp;
try {
cp = new CryptoPrimitives();
} catch (Exception e) {
throw new RuntimeException(e);
}
if (properties.containsKey("pemFile") && properties.containsKey("pemBytes")) {
throw new RuntimeException("Properties \"pemBytes\" and \"pemFile\" can not be both set.");
}
Expand All @@ -94,25 +104,54 @@ class Endpoint {
if (null != pemBytes) {
try {
cn = properties.getProperty("hostnameOverride");

if (cn == null && "true".equals(properties.getProperty("trustServerCertificate"))) {
final String cnKey = new String(pemBytes, StandardCharsets.UTF_8);

cn = CN_CACHE.get(cnKey);
if (cn == null) {
CryptoPrimitives cp = new CryptoPrimitives();

X500Name x500name = new JcaX509CertificateHolder((X509Certificate) cp.bytesToCertificate(pemBytes)).getSubject();
X500Name x500name = new JcaX509CertificateHolder(
(X509Certificate) cp.bytesToCertificate(pemBytes)).getSubject();
RDN rdn = x500name.getRDNs(BCStyle.CN)[0];
cn = IETFUtils.valueToString(rdn.getFirst().getValue());
CN_CACHE.put(cnKey, cn);
}

}
} catch (Exception e) {
/// Mostly a development env. just log it.
logger.error("Error getting Subject CN from certificate. Try setting it specifically with hostnameOverride property. " + e.getMessage());

logger.error(
"Error getting Subject CN from certificate. Try setting it specifically with hostnameOverride property. "
+ e.getMessage());
}
}
// check for mutual TLS - both clientKey and clientCert must be present
byte[] ckb = null, ccb = null;
if (properties.containsKey("clientKeyFile") && properties.containsKey("clientKeyBytes")) {
throw new RuntimeException("Properties \"clientKeyFile\" and \"clientKeyBytes\" must cannot both be set");
} else if (properties.containsKey("clientCertFile") && properties.containsKey("clientCertBytes")) {
throw new RuntimeException("Properties \"clientCertFile\" and \"clientCertBytes\" must cannot both be set");
} else if (properties.containsKey("clientKeyFile") || properties.containsKey("clientCertFile")) {
if ((properties.getProperty("clientKeyFile") != null) && (properties.getProperty("clientCertFile") != null)) {
try {
ckb = Files.readAllBytes(Paths.get(properties.getProperty("clientKeyFile")));
ccb = Files.readAllBytes(Paths.get(properties.getProperty("clientCertFile")));
} catch (IOException e) {
throw new RuntimeException("Failed to parse TLS client key and/or cert", e);
}
} else {
throw new RuntimeException("Properties \"clientKeyFile\" and \"clientCertFile\" must both be set or both be null");
}
} else if (properties.containsKey("clientKeyBytes") || properties.containsKey("clientCertBytes")) {
ckb = (byte[]) properties.get("clientKeyBytes");
ccb = (byte[]) properties.get("clientCertBytes");
if ((ckb == null) || (ccb == null)) {
throw new RuntimeException("Properties \"clientKeyBytes\" and \"clientCertBytes\" must both be set or both be null");
}
}
if ((ckb != null) && (ccb != null)) {
try {
clientKey = cp.bytesToPrivateKey(ckb);
clientCert = new X509Certificate[] {(X509Certificate) cp.bytesToCertificate(ccb)};
} catch (CryptoException e) {
throw new RuntimeException("Failed to parse TLS client key and/or certificate", e);
}
}

Expand All @@ -132,13 +171,11 @@ class Endpoint {
throw new RuntimeException("Property of negotiationType has to be either TLS or plainText");
}
}

}

try {
if (protocol.equalsIgnoreCase("grpc")) {
this.channelBuilder = NettyChannelBuilder.forAddress(addr, port)
.usePlaintext(true);
this.channelBuilder = NettyChannelBuilder.forAddress(addr, port).usePlaintext(true);
addNettyBuilderProps(channelBuilder, properties);
} else if (protocol.equalsIgnoreCase("grpcs")) {
if (pemBytes == null) {
Expand All @@ -152,12 +189,9 @@ class Endpoint {
NegotiationType ntype = nt.equals("TLS") ? NegotiationType.TLS : NegotiationType.PLAINTEXT;

InputStream myInputStream = new ByteArrayInputStream(pemBytes);
SslContext sslContext = GrpcSslContexts.forClient()
.trustManager(myInputStream)
.sslProvider(sslprovider)
.build();
this.channelBuilder = NettyChannelBuilder.forAddress(addr, port)
.sslContext(sslContext)
SslContext sslContext = GrpcSslContexts.forClient().trustManager(myInputStream)
.sslProvider(sslprovider).keyManager(clientKey, clientCert).build();
this.channelBuilder = NettyChannelBuilder.forAddress(addr, port).sslContext(sslContext)
.negotiationType(ntype);
if (cn != null) {
channelBuilder.overrideAuthority(cn);
Expand All @@ -177,24 +211,16 @@ class Endpoint {
logger.error(e);
throw new RuntimeException(e);
}

}

private static final Pattern METHOD_PATTERN = Pattern.compile("grpc\\.NettyChannelBuilderOption\\.([^.]*)$");
private static final Map<Class<?>, Class<?>> WRAPPERS_TO_PRIM
= new ImmutableMap.Builder<Class<?>, Class<?>>()
.put(Boolean.class, boolean.class)
.put(Byte.class, byte.class)
.put(Character.class, char.class)
.put(Double.class, double.class)
.put(Float.class, float.class)
.put(Integer.class, int.class)
.put(Long.class, long.class)
.put(Short.class, short.class)
.put(Void.class, void.class)
.build();

private void addNettyBuilderProps(NettyChannelBuilder channelBuilder, Properties props) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
private static final Map<Class<?>, Class<?>> WRAPPERS_TO_PRIM = new ImmutableMap.Builder<Class<?>, Class<?>>()
.put(Boolean.class, boolean.class).put(Byte.class, byte.class).put(Character.class, char.class)
.put(Double.class, double.class).put(Float.class, float.class).put(Integer.class, int.class)
.put(Long.class, long.class).put(Short.class, short.class).put(Void.class, void.class).build();

private void addNettyBuilderProps(NettyChannelBuilder channelBuilder, Properties props)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

if (props == null) {
return;
Expand Down Expand Up @@ -267,7 +293,8 @@ private void addNettyBuilderProps(NettyChannelBuilder channelBuilder, Properties
sep = ", ";

}
logger.trace(String.format("Endpoint with url: %s set managed channel builder method %s (%s) ", url, method, sb.toString()));
logger.trace(String.format("Endpoint with url: %s set managed channel builder method %s (%s) ", url,
method, sb.toString()));

}

Expand Down
52 changes: 32 additions & 20 deletions src/main/java/org/hyperledger/fabric/sdk/HFClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,8 @@ public class HFClient {

private CryptoSuite cryptoSuite;


static {


if (null == System.getProperty("org.hyperledger.fabric.sdk.logGRPC")) {
// Turn this off by default!
Logger.getLogger("io.netty").setLevel(Level.OFF);
Expand Down Expand Up @@ -96,19 +94,18 @@ public void setCryptoSuite(CryptoSuite cryptoSuite) throws CryptoException, Inva
throw new InvalidArgumentException("CryptoSuite may only be set once.");

}
// if (cryptoSuiteFactory == null) {
// cryptoSuiteFactory = cryptoSuite.getCryptoSuiteFactory();
// } else {
// if (cryptoSuiteFactory != cryptoSuite.getCryptoSuiteFactory()) {
// throw new InvalidArgumentException("CryptoSuite is not derivied from cryptosuite factory");
// }
// }
// if (cryptoSuiteFactory == null) {
// cryptoSuiteFactory = cryptoSuite.getCryptoSuiteFactory();
// } else {
// if (cryptoSuiteFactory != cryptoSuite.getCryptoSuiteFactory()) {
// throw new InvalidArgumentException("CryptoSuite is not derivied from cryptosuite factory");
// }
// }

this.cryptoSuite = cryptoSuite;

}


/**
* createNewInstance create a new instance of the HFClient
*
Expand Down Expand Up @@ -160,7 +157,8 @@ public Channel newChannel(String name) throws InvalidArgumentException {
* @throws InvalidArgumentException
*/

public Channel newChannel(String name, Orderer orderer, ChannelConfiguration channelConfiguration, byte[]... channelConfigurationSignatures) throws TransactionException, InvalidArgumentException {
public Channel newChannel(String name, Orderer orderer, ChannelConfiguration channelConfiguration,
byte[]... channelConfigurationSignatures) throws TransactionException, InvalidArgumentException {

clientCheck();
if (Utils.isNullOrEmpty(name)) {
Expand All @@ -175,7 +173,8 @@ public Channel newChannel(String name, Orderer orderer, ChannelConfiguration cha

logger.trace("Creating channel :" + name);

Channel newChannel = Channel.createNewInstance(name, this, orderer, channelConfiguration, channelConfigurationSignatures);
Channel newChannel = Channel.createNewInstance(name, this, orderer, channelConfiguration,
channelConfigurationSignatures);

channels.put(name, newChannel);
return newChannel;
Expand Down Expand Up @@ -213,7 +212,8 @@ public Channel deSerializeChannel(File file) throws IOException, ClassNotFoundEx
* @throws InvalidArgumentException
*/

public Channel deSerializeChannel(byte[] channelBytes) throws IOException, ClassNotFoundException, InvalidArgumentException {
public Channel deSerializeChannel(byte[] channelBytes)
throws IOException, ClassNotFoundException, InvalidArgumentException {

Channel channel;
ObjectInputStream in = null;
Expand Down Expand Up @@ -260,6 +260,10 @@ public Channel deSerializeChannel(byte[] channelBytes) throws IOException, Class
* useful in development to get past default server hostname verification during
* TLS handshake, when the server host name does not match the certificate.
* </li>
* <li>clientKeyFile - File location for private key pem for mutual TLS</li>
* <li>clientCertFile - File location for x509 pem certificate for mutual TLS</li>
* <li>clientKeyBytes - Private key pem bytes for mutual TLS</li>
* <li>clientCertBytes - x509 pem certificate bytes for mutual TLS</li>
* <li>hostnameOverride - Specify the certificates CN -- for development only.
* <li>sslProvider - Specify the SSL provider, openSSL or JDK.</li>
* <li>negotiationType - Specify the type of negotiation, TLS or plainText.</li>
Expand Down Expand Up @@ -365,7 +369,8 @@ public void setUserContext(User userContext) throws InvalidArgumentException {
userContextCheck(userContext);
this.userContext = userContext;

logger.debug(format("Setting user context to MSPID: %s user: %s", userContext.getMspId(), userContext.getName()));
logger.debug(
format("Setting user context to MSPID: %s user: %s", userContext.getMspId(), userContext.getName()));

}

Expand All @@ -384,6 +389,8 @@ public void setUserContext(User userContext) throws InvalidArgumentException {
* useful in development to get past default server hostname verification during
* TLS handshake, when the server host name does not match the certificate.
* </li>
* <li>clientKeyFile - File location for PKCS8-encoded private key pem for mutual TLS</li>
* <li>clientCertFile - File location for x509 pem certificate for mutual TLS</li>
* <li>hostnameOverride - Specify the certificates CN -- for development only.
* <li>sslProvider - Specify the SSL provider, openSSL or JDK.</li>
* <li>negotiationType - Specify the type of negotiation, TLS or plainText.</li>
Expand Down Expand Up @@ -449,6 +456,10 @@ public Orderer newOrderer(String name, String grpcURL) throws InvalidArgumentExc
* useful in development to get past default server hostname verification during
* TLS handshake, when the server host name does not match the certificate.
* </li>
* <li>clientKeyFile - File location for private key pem for mutual TLS</li>
* <li>clientCertFile - File location for x509 pem certificate for mutual TLS</li>
* <li>clientKeyBytes - Private key pem bytes for mutual TLS</li>
* <li>clientCertBytes - x509 pem certificate bytes for mutual TLS</li>
* <li>sslProvider - Specify the SSL provider, openSSL or JDK.</li>
* <li>negotiationType - Specify the type of negotiation, TLS or plainText.</li>
* <li>hostnameOverride - Specify the certificates CN -- for development only.
Expand Down Expand Up @@ -500,7 +511,7 @@ public Set<String> queryChannels(Peer peer) throws InvalidArgumentException, Pro

return systemChannel.queryChannels(peer);
} catch (InvalidArgumentException e) {
throw e; //dont log
throw e; //dont log
} catch (ProposalException e) {
logger.error(format("queryChannels for peer %s failed." + e.getMessage(), peer.getName()), e);
throw e;
Expand Down Expand Up @@ -549,7 +560,8 @@ public List<ChaincodeInfo> queryInstalledChaincodes(Peer peer) throws InvalidArg
* @throws InvalidArgumentException
*/

public byte[] getChannelConfigurationSignature(ChannelConfiguration channelConfiguration, User signer) throws InvalidArgumentException {
public byte[] getChannelConfigurationSignature(ChannelConfiguration channelConfiguration, User signer)
throws InvalidArgumentException {

clientCheck();

Expand All @@ -567,7 +579,8 @@ public byte[] getChannelConfigurationSignature(ChannelConfiguration channelConfi
* @throws InvalidArgumentException
*/

public byte[] getUpdateChannelConfigurationSignature(UpdateChannelConfiguration updateChannelConfiguration, User signer) throws InvalidArgumentException {
public byte[] getUpdateChannelConfigurationSignature(UpdateChannelConfiguration updateChannelConfiguration,
User signer) throws InvalidArgumentException {

clientCheck();

Expand All @@ -586,8 +599,8 @@ public byte[] getUpdateChannelConfigurationSignature(UpdateChannelConfiguration
* @throws ProposalException
*/

public Collection<ProposalResponse> sendInstallProposal(InstallProposalRequest installProposalRequest, Collection<Peer> peers)
throws ProposalException, InvalidArgumentException {
public Collection<ProposalResponse> sendInstallProposal(InstallProposalRequest installProposalRequest,
Collection<Peer> peers) throws ProposalException, InvalidArgumentException {

clientCheck();

Expand All @@ -604,7 +617,6 @@ private void clientCheck() throws InvalidArgumentException {
throw new InvalidArgumentException("No cryptoSuite has been set.");
}


userContextCheck(userContext);

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.SHA3Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
Expand Down Expand Up @@ -258,6 +261,27 @@ private X509Certificate getX509Certificate(byte[] pemCertificate) throws CryptoE

}

/**
* Return PrivateKey from pem bytes.
*
* @param pemKey pem-encoded private key
* @return
*/
public PrivateKey bytesToPrivateKey(byte[] pemKey) throws CryptoException {
PrivateKey pk = null;
CryptoException ce = null;

try {
PEMParser pem = new PEMParser(new StringReader(new String(pemKey)));
PEMKeyPair kp = (PEMKeyPair) pem.readObject();
pk = new JcaPEMKeyConverter().getPrivateKey(kp.getPrivateKeyInfo());
} catch (Exception e) {
throw new CryptoException("Failed to convert private key bytes", e);
}

return pk;
}

@Override
public boolean verify(byte[] pemCertificate, String signatureAlgorithm, byte[] signature, byte[] plainText) throws CryptoException {
boolean isVerified = false;
Expand Down
Loading

0 comments on commit f678c01

Please sign in to comment.