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

Allow adding/changing Jetty server certificates via REST API #2905

Closed
wants to merge 2 commits into from
Closed
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
22 changes: 22 additions & 0 deletions bom/compile/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<properties>
<californium.version>2.7.4</californium.version>
<jetty.version>9.4.50.v20221201</jetty.version>
<bouncycastle.version>1.70</bouncycastle.version>
<pax.web.version>8.0.15</pax.web.version>
<swagger.version>2.1.9</swagger.version>
</properties>
Expand Down Expand Up @@ -190,6 +191,26 @@
<scope>compile</scope>
</dependency>

<!-- bouncycastle -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
<scope>compile</scope>
</dependency>

<!-- JmDNS -->
<dependency>
<groupId>org.jmdns</groupId>
Expand Down Expand Up @@ -436,6 +457,7 @@
<version>5.12.1</version>
<scope>compile</scope>
</dependency>

</dependencies>

</project>
2 changes: 1 addition & 1 deletion bundles/org.openhab.core.io.jetty.certificate/bnd.bnd
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Bundle-SymbolicName: ${project.artifactId}
Bundle-Activator: org.openhab.core.io.jetty.certificate.internal.CertificateGenerator
Bundle-Activator: org.openhab.core.io.jetty.certificate.CertificateGenerator
Import-Package: !org.bouncycastle.*,*
8 changes: 0 additions & 8 deletions bundles/org.openhab.core.io.jetty.certificate/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,6 @@

<name>openHAB Core :: Bundles :: SSL Certificate Generator</name>

<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.52</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.jetty.certificate.internal;
package org.openhab.core.io.jetty.certificate;

import java.io.ByteArrayInputStream;
import java.io.File;
Expand All @@ -20,38 +20,20 @@
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECField;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EllipticCurve;
import java.security.spec.InvalidKeySpecException;
import java.util.Date;
import java.util.Random;

import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
Expand All @@ -66,35 +48,35 @@
*/
public class CertificateGenerator implements BundleActivator {

private static final String JETTY_KEYSTORE_PATH_PROPERTY = "jetty.keystore.path";
private static final String KEYSTORE_PASSWORD = "openhab";
private static final String KEYSTORE_ENTRY_ALIAS = "mykey";
private static final String KEYSTORE_JKS_TYPE = "JKS";
private static final String CURVE_NAME = "prime256v1";
public static final String JETTY_KEYSTORE_PATH_PROPERTY = "jetty.keystore.path";
public static final String KEYSTORE_PASSWORD = "openhab";
public static final String KEYSTORE_ENTRY_ALIAS = "mykey";
public static final String KEYSTORE_JKS_TYPE = "JKS";
private static final String KEY_PAIR_GENERATOR_TYPE = "EC";
private static final String KEY_FACTORY_TYPE = "EC";
private static final String CONTENT_SIGNER_ALGORITHM = "SHA256withECDSA";
private static final String CERTIFICATE_X509_TYPE = "X.509";
private static final String X500_NAME = "CN=openhab.org, OU=None, O=None, L=None, C=None";
public static final String X500_NAME = "CN=openhab.org, OU=None, O=None, L=None, C=None";

private Logger logger;

private File keystoreFile;
private static final Logger LOGGER = LoggerFactory.getLogger(CertificateGenerator.class);

@Override
public void start(BundleContext context) throws Exception {
logger = LoggerFactory.getLogger(CertificateGenerator.class);

try {
KeyStore keystore = ensureKeystore();
String keystorePath = System.getProperty(JETTY_KEYSTORE_PATH_PROPERTY);
File keystoreFile = new File(keystorePath);
KeyStore keystore = ensureKeystore(keystoreFile);

if (!isCertificateInKeystore(keystore)) {
logger.debug("{} alias not found. Generating a new certificate.", KEYSTORE_ENTRY_ALIAS);
LOGGER.debug("{} alias not found. Generating a new certificate.", KEYSTORE_ENTRY_ALIAS);
generateCertificate(keystore);
LOGGER.debug("Save the keystore into {}.", keystoreFile.getAbsolutePath());
keystore.store(new FileOutputStream(keystoreFile), KEYSTORE_PASSWORD.toCharArray());
} else {
logger.debug("{} alias found. Do nothing.", KEYSTORE_ENTRY_ALIAS);
LOGGER.debug("{} alias found. Do nothing.", KEYSTORE_ENTRY_ALIAS);
}
} catch (CertificateException | KeyStoreException e) {
logger.error("Failed to generate a new SSL Certificate.", e);
LOGGER.error("Failed to generate a new SSL Certificate.", e);
}
}

Expand All @@ -108,13 +90,11 @@ public void stop(BundleContext context) throws Exception {
*
* @throws KeyStoreException if the creation of the keystore fails or if it is not readable.
*/
private KeyStore ensureKeystore() throws KeyStoreException {
String keystorePath = System.getProperty(JETTY_KEYSTORE_PATH_PROPERTY);
keystoreFile = new File(keystorePath);
private KeyStore ensureKeystore(File keystoreFile) throws KeyStoreException {
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_JKS_TYPE);
if (!keystoreFile.exists()) {
try {
logger.debug("No keystore found. Creation of {}", keystoreFile.getAbsolutePath());
LOGGER.debug("No keystore found. Creation of {}", keystoreFile.getAbsolutePath());
boolean newFileCreated = keystoreFile.createNewFile();
if (newFileCreated) {
keyStore.load(null, null);
Expand All @@ -126,7 +106,7 @@ private KeyStore ensureKeystore() throws KeyStoreException {
}
} else {
try (InputStream keystoreStream = new FileInputStream(keystoreFile)) {
logger.debug("Keystore found. Trying to load {}", keystoreFile.getAbsolutePath());
LOGGER.debug("Keystore found. Trying to load {}", keystoreFile.getAbsolutePath());
keyStore.load(keystoreStream, KEYSTORE_PASSWORD.toCharArray());
} catch (NoSuchAlgorithmException | CertificateException | IOException e) {
throw new KeyStoreException("Failed to load the keystore " + keystoreFile.getAbsolutePath(), e);
Expand All @@ -139,7 +119,7 @@ private KeyStore ensureKeystore() throws KeyStoreException {
/**
* Check if the keystore contains a certificate with the KEYSTORE_ENTRY_ALIAS.
*
* @param keystore
* @param keystore the keystore to check
* @return true if the alias is already present in the keystore, else false.
* @throws KeyStoreException If the keystore cannot be read.
*/
Expand All @@ -150,62 +130,36 @@ private boolean isCertificateInKeystore(KeyStore keystore) throws KeyStoreExcept
/**
* Generate a new certificate and store it in the given keystore.
*
* @param keystore
* @param keystore the keystore to add this certificate to
* @throws CertificateException if the certificate generation has failed.
* @throws KeyStoreException If save of the keystore has failed.
*/
private void generateCertificate(KeyStore keystore) throws CertificateException, KeyStoreException {
public static void generateCertificate(KeyStore keystore) throws CertificateException, KeyStoreException {
try {
long startTime = System.currentTimeMillis();
org.bouncycastle.jce.spec.ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec(CURVE_NAME);
ECField field = new ECFieldFp(ecSpec.getCurve().getField().getCharacteristic());
EllipticCurve curve = new EllipticCurve(field, ecSpec.getCurve().getA().toBigInteger(),
ecSpec.getCurve().getB().toBigInteger());
ECPoint pointG = new ECPoint(ecSpec.getG().getXCoord().toBigInteger(),
ecSpec.getG().getYCoord().toBigInteger());
ECParameterSpec spec = new ECParameterSpec(curve, pointG, ecSpec.getN(), ecSpec.getH().intValue());
KeyPairGenerator g = KeyPairGenerator.getInstance(KEY_PAIR_GENERATOR_TYPE);
g.initialize(spec, new SecureRandom());
KeyPair keysPair = g.generateKeyPair();

ECPrivateKeySpec ecPrivSpec = new ECPrivateKeySpec(((ECPrivateKey) keysPair.getPrivate()).getS(), spec);
ECPublicKeySpec ecPublicSpec = new ECPublicKeySpec(((ECPublicKey) keysPair.getPublic()).getW(), spec);
KeyFactory kf = KeyFactory.getInstance(KEY_FACTORY_TYPE);
PrivateKey privateKey = kf.generatePrivate(ecPrivSpec);
PublicKey publicKey = kf.generatePublic(ecPublicSpec);

logger.debug("Keys generated in {} ms.", (System.currentTimeMillis() - startTime));
KeyPair keyPair = KeyPairGenerator.getInstance(KEY_PAIR_GENERATOR_TYPE).generateKeyPair();
LOGGER.debug("Keys generated in {} ms.", (System.currentTimeMillis() - startTime));

X500Name issuerDN = new X500Name(X500_NAME);
Integer randomNumber = new Random().nextInt();
BigInteger serialNumber = BigInteger.valueOf(randomNumber >= 0 ? randomNumber : randomNumber * -1);
SubjectPublicKeyInfo pubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
Date notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30);
Date notAfter = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10));
X500Name subjectDN = new X500Name(X500_NAME);
byte[] publickeyb = publicKey.getEncoded();
ASN1Sequence sequence = (ASN1Sequence) ASN1Primitive.fromByteArray(publickeyb);
SubjectPublicKeyInfo subPubKeyInfo = new SubjectPublicKeyInfo(sequence);
X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder(issuerDN, serialNumber, notBefore,
notAfter, subjectDN, subPubKeyInfo);

X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(issuerDN,
new BigInteger(10, new SecureRandom()), notBefore, notAfter, issuerDN, pubKeyInfo);
ContentSigner contentSigner = new JcaContentSignerBuilder(CONTENT_SIGNER_ALGORITHM)
.build(keysPair.getPrivate());
X509CertificateHolder certificateHolder = v3CertGen.build(contentSigner);
.build(keyPair.getPrivate());
X509CertificateHolder certificateHolder = certificateBuilder.build(contentSigner);

Certificate certificate = java.security.cert.CertificateFactory.getInstance(CERTIFICATE_X509_TYPE)
.generateCertificate(new ByteArrayInputStream(
ByteBuffer.wrap(certificateHolder.toASN1Structure().getEncoded()).array()));

logger.debug("Total certificate generation time: {} ms.", (System.currentTimeMillis() - startTime));
LOGGER.debug("Total certificate generation time: {} ms.", (System.currentTimeMillis() - startTime));

keystore.setKeyEntry(KEYSTORE_ENTRY_ALIAS, privateKey, KEYSTORE_PASSWORD.toCharArray(),
keystore.setKeyEntry(KEYSTORE_ENTRY_ALIAS, keyPair.getPrivate(), KEYSTORE_PASSWORD.toCharArray(),
new java.security.cert.Certificate[] { certificate });

logger.debug("Save the keystore into {}.", keystoreFile.getAbsolutePath());

keystore.store(new FileOutputStream(keystoreFile), KEYSTORE_PASSWORD.toCharArray());
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException | OperatorCreationException
| InvalidAlgorithmParameterException e) {
} catch (NoSuchAlgorithmException | IOException | OperatorCreationException e) {
throw new CertificateException("Failed to generate the new certificate.", e);
}
}
Expand Down
10 changes: 10 additions & 0 deletions bundles/org.openhab.core.io.net/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
<artifactId>org.openhab.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.jetty.certificate</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

</project>
10 changes: 10 additions & 0 deletions bundles/org.openhab.core.io.rest.core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@
<artifactId>org.openhab.core.semantics</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.jetty.certificate</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
Expand Down
Loading