mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-10 21:31:53 +01:00
[tls] Add a PEMTrustManager to deal with different PEM files (e.g. self-signed or global CA certificates) (#2622)
* Added a PEMTrustManager to deal with different PEM files (e.g. self-signed or global CA certificates) Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
This commit is contained in:
parent
68fb13f610
commit
9609ffb9b6
@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.io.net.http;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509ExtendedTrustManager;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link PEMTrustManager} is a {@link X509ExtendedTrustManager} implementation which loads a certificate in
|
||||
* PEM format and validates it against the servers certificate.
|
||||
*
|
||||
* @author Christoph Weitkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class PEMTrustManager extends X509ExtendedTrustManager {
|
||||
|
||||
public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
|
||||
public static final String END_CERT = "-----END CERTIFICATE-----";
|
||||
|
||||
private final X509Certificate trustedCert;
|
||||
|
||||
/**
|
||||
* Creates a {@link PEMTrustManager} instance by passing the PEM certificate as {@link String}.
|
||||
* The PEM format typically starts with <code>"-----BEGIN CERTIFICATE-----"</code> and ends with
|
||||
* <code>"-----END CERTIFICATE-----"</code>. The base 64 encoded certificate information are placed in between.
|
||||
*
|
||||
* @param pemCert the PEM certificate
|
||||
* @throws CertificateException
|
||||
*/
|
||||
public PEMTrustManager(String pemCert) throws CertificateException {
|
||||
if (!pemCert.isBlank() && pemCert.startsWith(BEGIN_CERT)) {
|
||||
try (InputStream certInputStream = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8))) {
|
||||
trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(certInputStream);
|
||||
} catch (IOException e) {
|
||||
throw new CertificateException(e);
|
||||
}
|
||||
}
|
||||
throw new CertificateParsingException("Certificate is either empty or cannot be parsed correctly");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link PEMTrustManager} instance by reading the PEM certificate from the given file.
|
||||
* This is useful if you have a private CA certificate stored in a file. Be aware that the certificate is read once
|
||||
* at the start of the system. There is no automatic refresh e.g. if the certificate will expire.
|
||||
*
|
||||
* @param path path to the PEM file
|
||||
* @return a {@link PEMTrustManager} instance
|
||||
* @throws FileNotFoundException
|
||||
* @throws CertificateInstantiationException
|
||||
*/
|
||||
public static PEMTrustManager getInstanceFromFile(String path) throws FileNotFoundException, CertificateException {
|
||||
String pemCert = readPEMCertificateStringFromFile(path);
|
||||
if (pemCert != null) {
|
||||
return new PEMTrustManager(pemCert);
|
||||
}
|
||||
throw new CertificateInstantiationException(String.format("Unable to read certificate from file: %s", path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link PEMTrustManager} instance by downloading the PEM certificate from the given server.
|
||||
* This is useful if you have to deal with self-signed certificates which may differ on each server. This method
|
||||
* pins the certificate on first connection with the server ("trust on first use") by using a trust all connection
|
||||
* and retrieves the servers certificate chain. Be aware that the certificate is downloaded once at the start of the
|
||||
* system. There is no automatic refresh e.g. if the certificate will expire.
|
||||
*
|
||||
* @param url url of the server
|
||||
* @return a {@link PEMTrustManager} instance
|
||||
* @throws MalformedURLException
|
||||
* @throws CertificateInstantiationException
|
||||
*/
|
||||
public static PEMTrustManager getInstanceFromServer(String url) throws MalformedURLException, CertificateException {
|
||||
return getInstanceFromServer(new URL(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link PEMTrustManager} instance by downloading the PEM certificate from the given server.
|
||||
* This is useful if you have to deal with self-signed certificates which may differ on each server. This method
|
||||
* pins the certificate on first connection with the server ("trust on first use") by using a trust all connection
|
||||
* and retrieves the servers certificate chain. Be aware that the certificate is downloaded once at the start of the
|
||||
* system. There is no automatic refresh e.g. if the certificate will expire.
|
||||
*
|
||||
* @param url url of the server
|
||||
* @return a {@link PEMTrustManager} instance
|
||||
* @throws CertificateInstantiationException
|
||||
*/
|
||||
public static PEMTrustManager getInstanceFromServer(URL url) throws CertificateException {
|
||||
String pemCert = getPEMCertificateFromServer(url);
|
||||
if (pemCert != null) {
|
||||
return new PEMTrustManager(pemCert);
|
||||
}
|
||||
throw new CertificateInstantiationException(String.format("Unable to load certificate from server: %s", url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
validatePEMCertificate(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate @Nullable [] chain, @Nullable String authType)
|
||||
throws CertificateException {
|
||||
validatePEMCertificate(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
X509Certificate[] certs = { trustedCert };
|
||||
return certs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate @Nullable [] chain, @Nullable String authType,
|
||||
@Nullable Socket socket) throws CertificateException {
|
||||
validatePEMCertificate(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate @Nullable [] chain, @Nullable String authType,
|
||||
@Nullable SSLEngine engine) throws CertificateException {
|
||||
validatePEMCertificate(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate @Nullable [] chain, @Nullable String authType,
|
||||
@Nullable Socket socket) throws CertificateException {
|
||||
validatePEMCertificate(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate @Nullable [] chain, @Nullable String authType,
|
||||
@Nullable SSLEngine engine) throws CertificateException {
|
||||
validatePEMCertificate(chain);
|
||||
}
|
||||
|
||||
private static @Nullable String getPEMCertificateFromServer(URL url) throws CertificateException {
|
||||
HttpsURLConnection connection = null;
|
||||
try {
|
||||
TrustManager[] trustManagers = { TrustAllTrustManager.getInstance() };
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(null, trustManagers, new SecureRandom());
|
||||
|
||||
connection = (HttpsURLConnection) url.openConnection();
|
||||
connection.setSSLSocketFactory(sslContext.getSocketFactory());
|
||||
connection.connect();
|
||||
|
||||
Certificate[] certs = connection.getServerCertificates();
|
||||
|
||||
byte[] bytes = ((X509Certificate) certs[0]).getEncoded();
|
||||
if (bytes.length != 0) {
|
||||
return BEGIN_CERT + Base64.getEncoder().encodeToString(bytes) + END_CERT;
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException | IOException e) {
|
||||
LoggerFactory.getLogger(PEMTrustManager.class).error("An unexpected exception occurred: ", e);
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable String readPEMCertificateStringFromFile(String path) throws FileNotFoundException {
|
||||
File certFile = new File(path);
|
||||
if (certFile.exists()) {
|
||||
try {
|
||||
return new String(Files.readAllBytes(certFile.toPath()), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
LoggerFactory.getLogger(PEMTrustManager.class).error("An unexpected IOException occurred: ", e);
|
||||
}
|
||||
} else {
|
||||
throw new FileNotFoundException(String.format("File %s does not exist", path));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void validatePEMCertificate(X509Certificate @Nullable [] chain) throws CertificateException {
|
||||
if (chain == null || !trustedCert.equals(chain[0])) {
|
||||
throw new CertificateException();
|
||||
}
|
||||
}
|
||||
|
||||
public static class CertificateInstantiationException extends CertificateException {
|
||||
|
||||
private static final long serialVersionUID = -5861764697217665026L;
|
||||
|
||||
public CertificateInstantiationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,6 @@ import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@ -185,7 +184,7 @@ public class ExtensibleTrustManagerImpl extends X509ExtendedTrustManager impleme
|
||||
private Collection<List<?>> getSubjectAlternatives(X509Certificate[] chain) throws CertificateParsingException {
|
||||
Collection<List<?>> subjectAlternativeNames = chain[0].getSubjectAlternativeNames();
|
||||
|
||||
return (subjectAlternativeNames != null) ? subjectAlternativeNames : Collections.emptyList();
|
||||
return (subjectAlternativeNames != null) ? subjectAlternativeNames : List.of();
|
||||
}
|
||||
|
||||
private String getCommonName(X509Certificate x509Certificate) {
|
||||
|
Loading…
Reference in New Issue
Block a user