Allow to load PrivateKey via OpenSSL Engine (#8200)

Motivation:

OpenSSL itself has an abstraction which allows you to customize some things. For example it is possible to load the PrivateKey from the engine. We should support this.

Modifications:

Add two new static methods to OpenSslX509KeyManagerFactory which allow to create an OpenSslX509KeyManagerFactory that loads its PrivateKey via the OpenSSL Engine directly.

Result:

More flexible usage of OpenSSL possible
This commit is contained in:
Norman Maurer 2018-08-18 07:20:44 +02:00 committed by GitHub
parent bbe2e4d224
commit df00539fa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 485 additions and 91 deletions

View File

@ -0,0 +1,117 @@
/*
* Copyright 2018 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.ssl;
import io.netty.internal.tcnative.SSL;
import io.netty.util.AbstractReferenceCounted;
import io.netty.util.IllegalReferenceCountException;
import io.netty.util.ResourceLeakDetector;
import io.netty.util.ResourceLeakDetectorFactory;
import io.netty.util.ResourceLeakTracker;
final class DefaultOpenSslKeyMaterial extends AbstractReferenceCounted implements OpenSslKeyMaterial {
private static final ResourceLeakDetector<DefaultOpenSslKeyMaterial> leakDetector =
ResourceLeakDetectorFactory.instance().newResourceLeakDetector(DefaultOpenSslKeyMaterial.class);
private final ResourceLeakTracker<DefaultOpenSslKeyMaterial> leak;
private long chain;
private long privateKey;
DefaultOpenSslKeyMaterial(long chain, long privateKey) {
this.chain = chain;
this.privateKey = privateKey;
leak = leakDetector.track(this);
}
@Override
public long certificateChainAddress() {
if (refCnt() <= 0) {
throw new IllegalReferenceCountException();
}
return chain;
}
@Override
public long privateKeyAddress() {
if (refCnt() <= 0) {
throw new IllegalReferenceCountException();
}
return privateKey;
}
@Override
protected void deallocate() {
SSL.freeX509Chain(chain);
chain = 0;
SSL.freePrivateKey(privateKey);
privateKey = 0;
if (leak != null) {
boolean closed = leak.close(this);
assert closed;
}
}
@Override
public DefaultOpenSslKeyMaterial retain() {
if (leak != null) {
leak.record();
}
super.retain();
return this;
}
@Override
public DefaultOpenSslKeyMaterial retain(int increment) {
if (leak != null) {
leak.record();
}
super.retain(increment);
return this;
}
@Override
public DefaultOpenSslKeyMaterial touch() {
if (leak != null) {
leak.record();
}
super.touch();
return this;
}
@Override
public DefaultOpenSslKeyMaterial touch(Object hint) {
if (leak != null) {
leak.record(hint);
}
return this;
}
@Override
public boolean release() {
if (leak != null) {
leak.record();
}
return super.release();
}
@Override
public boolean release(int decrement) {
if (leak != null) {
leak.record();
}
return super.release(decrement);
}
}

View File

@ -15,110 +15,38 @@
*/
package io.netty.handler.ssl;
import io.netty.internal.tcnative.SSL;
import io.netty.util.AbstractReferenceCounted;
import io.netty.util.IllegalReferenceCountException;
import io.netty.util.ResourceLeakDetector;
import io.netty.util.ResourceLeakDetectorFactory;
import io.netty.util.ResourceLeakTracker;
import io.netty.util.ReferenceCounted;
/**
* Holds references to the native key-material that is used by OpenSSL.
*/
final class OpenSslKeyMaterial extends AbstractReferenceCounted {
private static final ResourceLeakDetector<OpenSslKeyMaterial> leakDetector =
ResourceLeakDetectorFactory.instance().newResourceLeakDetector(OpenSslKeyMaterial.class);
private final ResourceLeakTracker<OpenSslKeyMaterial> leak;
private long chain;
private long privateKey;
OpenSslKeyMaterial(long chain, long privateKey) {
this.chain = chain;
this.privateKey = privateKey;
leak = leakDetector.track(this);
}
interface OpenSslKeyMaterial extends ReferenceCounted {
/**
* Returns the pointer to the {@code STACK_OF(X509)} which holds the certificate chain.
*/
public long certificateChainAddress() {
if (refCnt() <= 0) {
throw new IllegalReferenceCountException();
}
return chain;
}
long certificateChainAddress();
/**
* Returns the pointer to the {@code EVP_PKEY}.
*/
public long privateKeyAddress() {
if (refCnt() <= 0) {
throw new IllegalReferenceCountException();
}
return privateKey;
}
long privateKeyAddress();
@Override
protected void deallocate() {
SSL.freeX509Chain(chain);
chain = 0;
SSL.freePrivateKey(privateKey);
privateKey = 0;
if (leak != null) {
boolean closed = leak.close(this);
assert closed;
}
}
OpenSslKeyMaterial retain();
@Override
public OpenSslKeyMaterial retain() {
if (leak != null) {
leak.record();
}
super.retain();
return this;
}
OpenSslKeyMaterial retain(int increment);
@Override
public OpenSslKeyMaterial retain(int increment) {
if (leak != null) {
leak.record();
}
super.retain(increment);
return this;
}
OpenSslKeyMaterial touch();
@Override
public OpenSslKeyMaterial touch() {
if (leak != null) {
leak.record();
}
super.touch();
return this;
}
OpenSslKeyMaterial touch(Object hint);
@Override
public OpenSslKeyMaterial touch(Object hint) {
if (leak != null) {
leak.record(hint);
}
return this;
}
boolean release();
@Override
public boolean release() {
if (leak != null) {
leak.record();
}
return super.release();
}
@Override
public boolean release(int decrement) {
if (leak != null) {
leak.record();
}
return super.release(decrement);
}
boolean release(int decrement);
}

View File

@ -62,10 +62,16 @@ class OpenSslKeyMaterialProvider {
long pkey = 0;
try {
chainBio = toBIO(allocator, encoded.retain());
pkeyBio = toBIO(allocator, key);
chain = SSL.parseX509Chain(chainBio);
pkey = key == null ? 0 : SSL.parsePrivateKey(pkeyBio, password);
OpenSslKeyMaterial keyMaterial = new OpenSslKeyMaterial(chain, pkey);
OpenSslKeyMaterial keyMaterial;
if (key instanceof OpenSslPrivateKey) {
keyMaterial = ((OpenSslPrivateKey) key).toKeyMaterial(chain);
} else {
pkeyBio = toBIO(allocator, key);
pkey = key == null ? 0 : SSL.parsePrivateKey(pkeyBio, password);
keyMaterial = new DefaultOpenSslKeyMaterial(chain, pkey);
}
// See the chain and pkey to 0 so we will not release it as the ownership was
// transferred to OpenSslKeyMaterial.

View File

@ -0,0 +1,190 @@
/*
* Copyright 2018 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.ssl;
import io.netty.internal.tcnative.SSL;
import io.netty.util.AbstractReferenceCounted;
import io.netty.util.IllegalReferenceCountException;
import javax.security.auth.Destroyable;
import java.security.PrivateKey;
final class OpenSslPrivateKey extends AbstractReferenceCounted implements PrivateKey {
private long privateKeyAddress;
OpenSslPrivateKey(long privateKeyAddress) {
this.privateKeyAddress = privateKeyAddress;
}
@Override
public String getAlgorithm() {
return "unkown";
}
@Override
public String getFormat() {
// As we do not support encoding we should return null as stated in the javadocs of PrivateKey.
return null;
}
@Override
public byte[] getEncoded() {
return null;
}
/**
* Returns the pointer to the {@code EVP_PKEY}.
*/
long privateKeyAddress() {
if (refCnt() <= 0) {
throw new IllegalReferenceCountException();
}
return privateKeyAddress;
}
@Override
protected void deallocate() {
SSL.freePrivateKey(privateKeyAddress);
privateKeyAddress = 0;
}
@Override
public OpenSslPrivateKey retain() {
super.retain();
return this;
}
@Override
public OpenSslPrivateKey retain(int increment) {
super.retain(increment);
return this;
}
@Override
public OpenSslPrivateKey touch() {
super.touch();
return this;
}
@Override
public OpenSslPrivateKey touch(Object hint) {
return this;
}
/**
* NOTE: This is a JDK8 interface/method. Due to backwards compatibility
* reasons it's not possible to slap the {@code @Override} annotation onto
* this method.
*
* @see Destroyable#destroy()
*/
public void destroy() {
release(refCnt());
}
/**
* NOTE: This is a JDK8 interface/method. Due to backwards compatibility
* reasons it's not possible to slap the {@code @Override} annotation onto
* this method.
*
* @see Destroyable#isDestroyed()
*/
public boolean isDestroyed() {
return refCnt() == 0;
}
/**
* Convert to a {@link OpenSslKeyMaterial}. Reference count of both is shared.
*/
OpenSslKeyMaterial toKeyMaterial(long certificateChain) {
return new OpenSslPrivateKeyMaterial(certificateChain);
}
private final class OpenSslPrivateKeyMaterial implements OpenSslKeyMaterial {
private long certificateChain;
OpenSslPrivateKeyMaterial(long certificateChain) {
this.certificateChain = certificateChain;
}
@Override
public long certificateChainAddress() {
if (refCnt() <= 0) {
throw new IllegalReferenceCountException();
}
return certificateChain;
}
@Override
public long privateKeyAddress() {
return OpenSslPrivateKey.this.privateKeyAddress();
}
@Override
public OpenSslKeyMaterial retain() {
OpenSslPrivateKey.this.retain();
return this;
}
@Override
public OpenSslKeyMaterial retain(int increment) {
OpenSslPrivateKey.this.retain(increment);
return this;
}
@Override
public OpenSslKeyMaterial touch() {
OpenSslPrivateKey.this.touch();
return this;
}
@Override
public OpenSslKeyMaterial touch(Object hint) {
OpenSslPrivateKey.this.touch(hint);
return this;
}
@Override
public boolean release() {
if (OpenSslPrivateKey.this.release()) {
releaseChain();
return true;
}
return false;
}
@Override
public boolean release(int decrement) {
if (OpenSslPrivateKey.this.release(decrement)) {
releaseChain();
return true;
}
return false;
}
private void releaseChain() {
SSL.freeX509Chain(certificateChain);
certificateChain = 0;
}
@Override
public int refCnt() {
return OpenSslPrivateKey.this.refCnt();
}
}
}

View File

@ -17,6 +17,7 @@ package io.netty.handler.ssl;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.internal.tcnative.SSL;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.internal.ObjectUtil;
@ -25,13 +26,25 @@ import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyManagerFactorySpi;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.X509KeyManager;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.KeyStoreSpi;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
@ -108,9 +121,8 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory {
}
kmf.init(keyStore, chars);
providerFactory = new ProviderFactory(
ReferenceCountedOpenSslContext.chooseX509KeyManager(kmf.getKeyManagers()),
password(chars), Collections.list(keyStore.aliases()));
providerFactory = new ProviderFactory(ReferenceCountedOpenSslContext.chooseX509KeyManager(
kmf.getKeyManagers()), password(chars), Collections.list(keyStore.aliases()));
}
private static String password(char[] password) {
@ -218,4 +230,144 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory {
}
}
}
/**
* Create a new initialized {@link OpenSslX509KeyManagerFactory} which loads its {@link PrivateKey} directly from
* an {@code OpenSSL engine} via the
* <a href="https://www.openssl.org/docs/man1.1.0/crypto/ENGINE_load_private_key.html">ENGINE_load_private_key</a>
* function.
*/
public static OpenSslX509KeyManagerFactory newEngineBased(File certificateChain, String password)
throws CertificateException, IOException,
KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
return newEngineBased(SslContext.toX509Certificates(certificateChain), password);
}
/**
* Create a new initialized {@link OpenSslX509KeyManagerFactory} which loads its {@link PrivateKey} directly from
* an {@code OpenSSL engine} via the
* <a href="https://www.openssl.org/docs/man1.1.0/crypto/ENGINE_load_private_key.html">ENGINE_load_private_key</a>
* function.
*/
public static OpenSslX509KeyManagerFactory newEngineBased(X509Certificate[] certificateChain, String password)
throws CertificateException, IOException,
KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
KeyStore store = new OpenSslEngineKeyStore(certificateChain.clone());
store.load(null, null);
OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory();
factory.init(store, password == null ? null : password.toCharArray());
return factory;
}
private static final class OpenSslEngineKeyStore extends KeyStore {
private OpenSslEngineKeyStore(final X509Certificate[] certificateChain) {
super(new KeyStoreSpi() {
private final Date creationDate = new Date();
@Override
public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException {
if (engineContainsAlias(alias)) {
try {
return new OpenSslPrivateKey(SSL.loadPrivateKeyFromEngine(
alias, password == null ? null : new String(password)));
} catch (Exception e) {
UnrecoverableKeyException keyException =
new UnrecoverableKeyException("Unable to load key from engine");
keyException.initCause(e);
throw keyException;
}
}
return null;
}
@Override
public Certificate[] engineGetCertificateChain(String alias) {
return engineContainsAlias(alias)? certificateChain.clone() : null;
}
@Override
public Certificate engineGetCertificate(String alias) {
return engineContainsAlias(alias)? certificateChain[0] : null;
}
@Override
public Date engineGetCreationDate(String alias) {
return engineContainsAlias(alias)? creationDate : null;
}
@Override
public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
throws KeyStoreException {
throw new KeyStoreException("Not supported");
}
@Override
public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException {
throw new KeyStoreException("Not supported");
}
@Override
public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException {
throw new KeyStoreException("Not supported");
}
@Override
public void engineDeleteEntry(String alias) throws KeyStoreException {
throw new KeyStoreException("Not supported");
}
@Override
public Enumeration<String> engineAliases() {
return Collections.enumeration(Collections.singleton(SslContext.ALIAS));
}
@Override
public boolean engineContainsAlias(String alias) {
return SslContext.ALIAS.equals(alias);
}
@Override
public int engineSize() {
return 1;
}
@Override
public boolean engineIsKeyEntry(String alias) {
return engineContainsAlias(alias);
}
@Override
public boolean engineIsCertificateEntry(String alias) {
return engineContainsAlias(alias);
}
@Override
public String engineGetCertificateAlias(Certificate cert) {
if (cert instanceof X509Certificate) {
for (X509Certificate x509Certificate : certificateChain) {
if (x509Certificate.equals(cert)) {
return SslContext.ALIAS;
}
}
}
return null;
}
@Override
public void engineStore(OutputStream stream, char[] password) {
throw new UnsupportedOperationException();
}
@Override
public void engineLoad(InputStream stream, char[] password) {
if (stream != null && password != null) {
throw new UnsupportedOperationException();
}
}
}, null, "native");
OpenSsl.ensureAvailability();
}
}
}

View File

@ -53,7 +53,6 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
@ -85,6 +84,8 @@ import java.util.List;
* </pre>
*/
public abstract class SslContext {
static final String ALIAS = "key";
static final CertificateFactory X509_CERT_FACTORY;
static {
try {
@ -1000,7 +1001,7 @@ public abstract class SslContext {
CertificateException, IOException {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setKeyEntry("key", key, keyPasswordChars, certChain);
ks.setKeyEntry(ALIAS, key, keyPasswordChars, certChain);
return ks;
}

View File

@ -221,7 +221,7 @@
<!-- Fedora-"like" systems. This is currently only used for the netty-tcnative dependency -->
<os.detection.classifierWithLikes>fedora</os.detection.classifierWithLikes>
<tcnative.artifactId>netty-tcnative</tcnative.artifactId>
<tcnative.version>2.0.12.Final</tcnative.version>
<tcnative.version>2.0.13.Final</tcnative.version>
<tcnative.classifier>${os.detected.classifier}</tcnative.classifier>
<conscrypt.groupId>org.conscrypt</conscrypt.groupId>
<conscrypt.artifactId>conscrypt-openjdk-uber</conscrypt.artifactId>