21 changed files with 883 additions and 239 deletions
@ -0,0 +1,191 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed 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 org.thingsboard.server.common.transport.config.ssl; |
|||
|
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
|
|||
import javax.net.ssl.KeyManagerFactory; |
|||
import javax.net.ssl.TrustManagerFactory; |
|||
import java.io.IOException; |
|||
import java.security.GeneralSecurityException; |
|||
import java.security.KeyStore; |
|||
import java.security.KeyStore.PrivateKeyEntry; |
|||
import java.security.KeyStoreException; |
|||
import java.security.NoSuchAlgorithmException; |
|||
import java.security.PrivateKey; |
|||
import java.security.PublicKey; |
|||
import java.security.UnrecoverableEntryException; |
|||
import java.security.UnrecoverableKeyException; |
|||
import java.security.cert.Certificate; |
|||
import java.security.cert.X509Certificate; |
|||
import java.util.Collections; |
|||
import java.util.Enumeration; |
|||
import java.util.HashSet; |
|||
import java.util.Set; |
|||
|
|||
public abstract class AbstractSslCredentials implements SslCredentials { |
|||
|
|||
private char[] keyPasswordArray; |
|||
|
|||
private KeyStore keyStore; |
|||
|
|||
private PrivateKey privateKey; |
|||
|
|||
private PublicKey publicKey; |
|||
|
|||
private X509Certificate[] chain; |
|||
|
|||
private X509Certificate[] trusts; |
|||
|
|||
@Override |
|||
public void init(boolean trustsOnly) throws IOException, GeneralSecurityException { |
|||
String keyPassword = getKeyPassword(); |
|||
if (StringUtils.isEmpty(keyPassword)) { |
|||
this.keyPasswordArray = new char[0]; |
|||
} else { |
|||
this.keyPasswordArray = keyPassword.toCharArray(); |
|||
} |
|||
this.keyStore = this.loadKeyStore(trustsOnly, this.keyPasswordArray); |
|||
Set<X509Certificate> trustedCerts = getTrustedCerts(this.keyStore); |
|||
this.trusts = trustedCerts.toArray(new X509Certificate[0]); |
|||
if (!trustsOnly) { |
|||
PrivateKeyEntry privateKeyEntry = null; |
|||
String keyAlias = this.getKeyAlias(); |
|||
if (!StringUtils.isEmpty(keyAlias)) { |
|||
privateKeyEntry = tryGetPrivateKeyEntry(this.keyStore, keyAlias, this.keyPasswordArray); |
|||
} else { |
|||
for (Enumeration<String> e = this.keyStore.aliases(); e.hasMoreElements(); ) { |
|||
String alias = e.nextElement(); |
|||
privateKeyEntry = tryGetPrivateKeyEntry(this.keyStore, alias, this.keyPasswordArray); |
|||
if (privateKeyEntry != null) { |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
if (privateKeyEntry == null) { |
|||
throw new IllegalArgumentException("Failed to get private key from the keystore or pem files. " + |
|||
"Please check if the private key exists in the keystore or pem files and if the provided private key password is valid."); |
|||
} |
|||
this.chain = asX509Certificates(privateKeyEntry.getCertificateChain()); |
|||
this.privateKey = privateKeyEntry.getPrivateKey(); |
|||
if (this.chain.length > 0) { |
|||
this.publicKey = this.chain[0].getPublicKey(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public PrivateKey getPrivateKey() { |
|||
return this.privateKey; |
|||
} |
|||
|
|||
@Override |
|||
public PublicKey getPublicKey() { |
|||
return this.publicKey; |
|||
} |
|||
|
|||
@Override |
|||
public X509Certificate[] getCertificateChain() { |
|||
return this.chain; |
|||
} |
|||
|
|||
@Override |
|||
public X509Certificate[] getTrustedCertificates() { |
|||
return this.trusts; |
|||
} |
|||
|
|||
@Override |
|||
public TrustManagerFactory createTrustManagerFactory() throws NoSuchAlgorithmException, KeyStoreException { |
|||
TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); |
|||
tmFactory.init(this.keyStore); |
|||
return tmFactory; |
|||
} |
|||
|
|||
@Override |
|||
public KeyManagerFactory createKeyManagerFactory() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException { |
|||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); |
|||
kmf.init(this.keyStore, this.keyPasswordArray); |
|||
return kmf; |
|||
} |
|||
|
|||
protected abstract boolean canUse(); |
|||
|
|||
protected abstract String getKeyPassword(); |
|||
|
|||
protected abstract String getKeyAlias(); |
|||
|
|||
protected abstract KeyStore loadKeyStore(boolean isPrivateKeyRequired, char[] keyPasswordArray) throws IOException, GeneralSecurityException; |
|||
|
|||
private static X509Certificate[] asX509Certificates(Certificate[] certificates) { |
|||
if (null == certificates || 0 == certificates.length) { |
|||
throw new IllegalArgumentException("certificates missing!"); |
|||
} |
|||
X509Certificate[] x509Certificates = new X509Certificate[certificates.length]; |
|||
for (int index = 0; certificates.length > index; ++index) { |
|||
if (null == certificates[index]) { |
|||
throw new IllegalArgumentException("[" + index + "] is null!"); |
|||
} |
|||
try { |
|||
x509Certificates[index] = (X509Certificate) certificates[index]; |
|||
} catch (ClassCastException e) { |
|||
throw new IllegalArgumentException("[" + index + "] is not a x509 certificate! Instead it's a " |
|||
+ certificates[index].getClass().getName()); |
|||
} |
|||
} |
|||
return x509Certificates; |
|||
} |
|||
|
|||
private static PrivateKeyEntry tryGetPrivateKeyEntry(KeyStore keyStore, String alias, char[] pwd) { |
|||
PrivateKeyEntry entry = null; |
|||
try { |
|||
if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) { |
|||
try { |
|||
entry = (KeyStore.PrivateKeyEntry) keyStore |
|||
.getEntry(alias, new KeyStore.PasswordProtection(pwd)); |
|||
} catch (UnsupportedOperationException e) { |
|||
PrivateKey key = (PrivateKey) keyStore.getKey(alias, pwd); |
|||
Certificate[] certs = keyStore.getCertificateChain(alias); |
|||
entry = new KeyStore.PrivateKeyEntry(key, certs); |
|||
} |
|||
} |
|||
} catch (KeyStoreException | UnrecoverableEntryException | NoSuchAlgorithmException ignored) {} |
|||
return entry; |
|||
} |
|||
|
|||
private static Set<X509Certificate> getTrustedCerts(KeyStore ks) { |
|||
Set<X509Certificate> set = new HashSet<>(); |
|||
try { |
|||
for (Enumeration<String> e = ks.aliases(); e.hasMoreElements(); ) { |
|||
String alias = e.nextElement(); |
|||
if (ks.isCertificateEntry(alias)) { |
|||
Certificate cert = ks.getCertificate(alias); |
|||
if (cert instanceof X509Certificate) { |
|||
set.add((X509Certificate)cert); |
|||
} |
|||
} else if (ks.isKeyEntry(alias)) { |
|||
Certificate[] certs = ks.getCertificateChain(alias); |
|||
if ((certs != null) && (certs.length > 0) && |
|||
(certs[0] instanceof X509Certificate)) { |
|||
set.add((X509Certificate)certs[0]); |
|||
} |
|||
} |
|||
} |
|||
} catch (KeyStoreException ignored) {} |
|||
return Collections.unmodifiableSet(set); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed 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 org.thingsboard.server.common.transport.config.ssl; |
|||
|
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.thingsboard.server.common.data.ResourceUtils; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
|
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.security.GeneralSecurityException; |
|||
import java.security.KeyStore; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = false) |
|||
public class KeystoreSslCredentials extends AbstractSslCredentials { |
|||
|
|||
private String type; |
|||
private String storeFile; |
|||
private String storePassword; |
|||
private String keyPassword; |
|||
private String keyAlias; |
|||
|
|||
@Override |
|||
protected boolean canUse() { |
|||
return ResourceUtils.resourceExists(this, this.storeFile); |
|||
} |
|||
|
|||
@Override |
|||
protected KeyStore loadKeyStore(boolean trustsOnly, char[] keyPasswordArray) throws IOException, GeneralSecurityException { |
|||
String keyStoreType = StringUtils.isEmpty(this.type) ? KeyStore.getDefaultType() : this.type; |
|||
KeyStore keyStore = KeyStore.getInstance(keyStoreType); |
|||
try (InputStream tsFileInputStream = ResourceUtils.getInputStream(this, this.storeFile)) { |
|||
keyStore.load(tsFileInputStream, StringUtils.isEmpty(this.storePassword) ? new char[0] : this.storePassword.toCharArray()); |
|||
} |
|||
return keyStore; |
|||
} |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed 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 org.thingsboard.server.common.transport.config.ssl; |
|||
|
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
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.PEMDecryptorProvider; |
|||
import org.bouncycastle.openssl.PEMEncryptedKeyPair; |
|||
import org.bouncycastle.openssl.PEMKeyPair; |
|||
import org.bouncycastle.openssl.PEMParser; |
|||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; |
|||
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; |
|||
import org.thingsboard.server.common.data.ResourceUtils; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.io.InputStreamReader; |
|||
import java.security.GeneralSecurityException; |
|||
import java.security.KeyStore; |
|||
import java.security.PrivateKey; |
|||
import java.security.Security; |
|||
import java.security.cert.CertPath; |
|||
import java.security.cert.Certificate; |
|||
import java.security.cert.CertificateFactory; |
|||
import java.security.cert.X509Certificate; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = false) |
|||
public class PemSslCredentials extends AbstractSslCredentials { |
|||
|
|||
private String certFile; |
|||
private String keyFile; |
|||
private String keyPassword; |
|||
private final String keyAlias = "serveralias"; |
|||
|
|||
@Override |
|||
protected boolean canUse() { |
|||
return ResourceUtils.resourceExists(this, this.certFile); |
|||
} |
|||
|
|||
@Override |
|||
protected KeyStore loadKeyStore(boolean trustsOnly, char[] keyPasswordArray) throws IOException, GeneralSecurityException { |
|||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { |
|||
Security.addProvider(new BouncyCastleProvider()); |
|||
} |
|||
List<X509Certificate> certificates = new ArrayList<>(); |
|||
PrivateKey privateKey = null; |
|||
JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter(); |
|||
JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter(); |
|||
try (InputStream inStream = ResourceUtils.getInputStream(this, this.certFile)) { |
|||
try (PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) { |
|||
Object object; |
|||
while((object = pemParser.readObject()) != null) { |
|||
if (object instanceof X509CertificateHolder) { |
|||
X509Certificate x509Cert = certConverter.getCertificate((X509CertificateHolder) object); |
|||
certificates.add(x509Cert); |
|||
} else if (object instanceof PEMEncryptedKeyPair) { |
|||
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPasswordArray); |
|||
privateKey = keyConverter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate(); |
|||
} else if (object instanceof PEMKeyPair) { |
|||
privateKey = keyConverter.getKeyPair((PEMKeyPair) object).getPrivate(); |
|||
} else if (object instanceof PrivateKeyInfo) { |
|||
privateKey = keyConverter.getPrivateKey((PrivateKeyInfo) object); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (privateKey == null && !StringUtils.isEmpty(this.keyFile)) { |
|||
if (ResourceUtils.resourceExists(this, this.keyFile)) { |
|||
try (InputStream inStream = ResourceUtils.getInputStream(this, this.keyFile)) { |
|||
try (PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) { |
|||
Object object; |
|||
while ((object = pemParser.readObject()) != null) { |
|||
if (object instanceof PEMEncryptedKeyPair) { |
|||
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPasswordArray); |
|||
privateKey = keyConverter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate(); |
|||
break; |
|||
} else if (object instanceof PEMKeyPair) { |
|||
privateKey = keyConverter.getKeyPair((PEMKeyPair) object).getPrivate(); |
|||
break; |
|||
} else if (object instanceof PrivateKeyInfo) { |
|||
privateKey = keyConverter.getPrivateKey((PrivateKeyInfo) object); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (certificates.isEmpty()) { |
|||
throw new IllegalArgumentException("No certificates found in certFile: " + this.certFile); |
|||
} |
|||
if (privateKey == null && !trustsOnly) { |
|||
throw new IllegalArgumentException("Unable to load private key neither from certFile: " + this.certFile + " nor from keyFile: " + this.keyFile); |
|||
} |
|||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
|||
keyStore.load(null); |
|||
List<Certificate> unique = certificates.stream().distinct().collect(Collectors.toList()); |
|||
for (int i = 0; i < unique.size(); i++) { |
|||
keyStore.setCertificateEntry("root-" + i, unique.get(i)); |
|||
} |
|||
if (privateKey != null) { |
|||
CertificateFactory factory = CertificateFactory.getInstance("X.509"); |
|||
CertPath certPath = factory.generateCertPath(certificates); |
|||
List<? extends Certificate> path = certPath.getCertificates(); |
|||
Certificate[] x509Certificates = path.toArray(new Certificate[0]); |
|||
keyStore.setKeyEntry(this.keyAlias, privateKey, keyPasswordArray, x509Certificates); |
|||
} |
|||
return keyStore; |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed 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 org.thingsboard.server.common.transport.config.ssl; |
|||
|
|||
import javax.net.ssl.KeyManagerFactory; |
|||
import javax.net.ssl.TrustManagerFactory; |
|||
import java.io.IOException; |
|||
import java.security.GeneralSecurityException; |
|||
import java.security.KeyStoreException; |
|||
import java.security.NoSuchAlgorithmException; |
|||
import java.security.PrivateKey; |
|||
import java.security.PublicKey; |
|||
import java.security.UnrecoverableKeyException; |
|||
import java.security.cert.X509Certificate; |
|||
|
|||
public interface SslCredentials { |
|||
|
|||
void init(boolean trustsOnly) throws IOException, GeneralSecurityException; |
|||
|
|||
PrivateKey getPrivateKey(); |
|||
|
|||
PublicKey getPublicKey(); |
|||
|
|||
X509Certificate[] getCertificateChain(); |
|||
|
|||
X509Certificate[] getTrustedCertificates(); |
|||
|
|||
TrustManagerFactory createTrustManagerFactory() throws NoSuchAlgorithmException, KeyStoreException; |
|||
|
|||
KeyManagerFactory createKeyManagerFactory() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException; |
|||
|
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed 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 org.thingsboard.server.common.transport.config.ssl; |
|||
|
|||
import lombok.Data; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
|
|||
@Slf4j |
|||
@Data |
|||
public class SslCredentialsConfig { |
|||
|
|||
private boolean enabled = true; |
|||
private SslCredentialsType type; |
|||
private PemSslCredentials pem; |
|||
private KeystoreSslCredentials keystore; |
|||
|
|||
private SslCredentials credentials; |
|||
|
|||
private final String name; |
|||
private final boolean trustsOnly; |
|||
|
|||
public SslCredentialsConfig(String name, boolean trustsOnly) { |
|||
this.name = name; |
|||
this.trustsOnly = trustsOnly; |
|||
} |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
if (this.enabled) { |
|||
log.info("{}: Initializing SSL credentials.", name); |
|||
if (SslCredentialsType.PEM.equals(type) && pem.canUse()) { |
|||
this.credentials = this.pem; |
|||
} else if (keystore.canUse()) { |
|||
if (SslCredentialsType.PEM.equals(type)) { |
|||
log.warn("{}: Specified PEM configuration is not valid. Using SSL keystore configuration as fallback.", name); |
|||
} |
|||
this.credentials = this.keystore; |
|||
} else { |
|||
throw new RuntimeException(name + ": Invalid SSL credentials configuration. None of the PEM or KEYSTORE configurations can be used!"); |
|||
} |
|||
try { |
|||
this.credentials.init(this.trustsOnly); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException(name + ": Failed to init SSL credentials configuration.", e); |
|||
} |
|||
} else { |
|||
log.info("{}: Skipping initialization of disabled SSL credentials.", name); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed 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 org.thingsboard.server.common.transport.config.ssl; |
|||
|
|||
public enum SslCredentialsType { |
|||
PEM, |
|||
KEYSTORE |
|||
} |
|||
Loading…
Reference in new issue