diff --git a/dslink-core/.gitignore b/dslink-core/.gitignore new file mode 100644 index 00000000..ae3c1726 --- /dev/null +++ b/dslink-core/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/dslink-v2-websocket/src/main/java/org/iot/dsa/dslink/websocket/WsBinaryTransport.java b/dslink-v2-websocket/src/main/java/org/iot/dsa/dslink/websocket/WsBinaryTransport.java index 07623754..3b1f1e2b 100644 --- a/dslink-v2-websocket/src/main/java/org/iot/dsa/dslink/websocket/WsBinaryTransport.java +++ b/dslink-v2-websocket/src/main/java/org/iot/dsa/dslink/websocket/WsBinaryTransport.java @@ -1,5 +1,6 @@ package org.iot.dsa.dslink.websocket; +import com.acuity.iot.dsa.dslink.sys.cert.SysCertManager; import com.acuity.iot.dsa.dslink.transport.BufferedBinaryTransport; import com.acuity.iot.dsa.dslink.transport.DSTransport; import java.io.IOException; @@ -7,6 +8,9 @@ import java.nio.ByteBuffer; import javax.websocket.*; import org.glassfish.tyrus.client.ClientManager; +import org.glassfish.tyrus.client.ClientProperties; +import org.glassfish.tyrus.client.SslContextConfigurator; +import org.glassfish.tyrus.client.SslEngineConfigurator; import org.iot.dsa.util.DSException; /** @@ -97,7 +101,13 @@ public DSTransport open() { } client.setDefaultMaxBinaryMessageBufferSize(64 * 1024); client.setDefaultMaxTextMessageBufferSize(64 * 1024); - client.connectToServer(this, new URI(getConnectionUrl())); + URI connUri = new URI(getConnectionUrl()); + if ("wss".equalsIgnoreCase(connUri.getScheme())) { + SslEngineConfigurator sslEngineConfigurator = new SslEngineConfigurator(new SslContextConfigurator()); + sslEngineConfigurator.setHostnameVerifier(SysCertManager.getInstance().getHostnameVerifier()); + client.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator); + } + client.connectToServer(this, connUri); debug(debug() ? "Transport open" : null); } catch (Exception x) { DSException.throwRuntime(x); diff --git a/dslink-v2-websocket/src/main/java/org/iot/dsa/dslink/websocket/WsTextTransport.java b/dslink-v2-websocket/src/main/java/org/iot/dsa/dslink/websocket/WsTextTransport.java index 506309b5..f3d016c2 100644 --- a/dslink-v2-websocket/src/main/java/org/iot/dsa/dslink/websocket/WsTextTransport.java +++ b/dslink-v2-websocket/src/main/java/org/iot/dsa/dslink/websocket/WsTextTransport.java @@ -2,6 +2,7 @@ import com.acuity.iot.dsa.dslink.io.DSCharBuffer; import com.acuity.iot.dsa.dslink.io.DSIoException; +import com.acuity.iot.dsa.dslink.sys.cert.SysCertManager; import com.acuity.iot.dsa.dslink.transport.DSTextTransport; import com.acuity.iot.dsa.dslink.transport.DSTransport; import java.io.IOException; @@ -18,6 +19,9 @@ import javax.websocket.RemoteEndpoint; import javax.websocket.Session; import org.glassfish.tyrus.client.ClientManager; +import org.glassfish.tyrus.client.ClientProperties; +import org.glassfish.tyrus.client.SslContextConfigurator; +import org.glassfish.tyrus.client.SslEngineConfigurator; import org.iot.dsa.util.DSException; /** @@ -149,7 +153,13 @@ public DSTransport open() { } client.setDefaultMaxBinaryMessageBufferSize(64 * 1024); client.setDefaultMaxTextMessageBufferSize(64 * 1024); - client.connectToServer(this, new URI(getConnectionUrl())); + URI connUri = new URI(getConnectionUrl()); + if ("wss".equalsIgnoreCase(connUri.getScheme())) { + SslEngineConfigurator sslEngineConfigurator = new SslEngineConfigurator(new SslContextConfigurator()); + sslEngineConfigurator.setHostnameVerifier(SysCertManager.getInstance().getHostnameVerifier()); + client.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator); + } + client.connectToServer(this, connUri); } catch (Exception x) { DSException.throwRuntime(x); } diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/backup/SysBackupService.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/backup/SysBackupService.java index 8789af35..df41eb68 100644 --- a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/backup/SysBackupService.java +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/backup/SysBackupService.java @@ -25,6 +25,10 @@ import org.iot.dsa.node.action.DSAction; import org.iot.dsa.time.DSTime; +/** + * @author Daniel Shapiro + * @author Aaron Hansen + */ public class SysBackupService extends DSNode implements Runnable { static final String ENABLED = "Enabled"; diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/AnonymousTrustFactory.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/AnonymousTrustFactory.java index 8072668a..152c90d0 100644 --- a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/AnonymousTrustFactory.java +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/AnonymousTrustFactory.java @@ -1,12 +1,19 @@ package com.acuity.iot.dsa.dslink.sys.cert; import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.Provider; import java.security.Security; import java.security.cert.CertificateException; +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import javax.net.ssl.*; /** @@ -14,6 +21,7 @@ * falls back to the default Java trust manager. * * @author Aaron Hansen + * @author Daniel Shapiro */ public class AnonymousTrustFactory extends TrustManagerFactorySpi { @@ -114,8 +122,13 @@ public void checkClientTrusted(X509Certificate[] chain, String authType) return; } if (defaultX509Mgr != null) { - defaultX509Mgr.checkClientTrusted(chain, authType); + try { + defaultX509Mgr.checkClientTrusted(chain, authType); + return; + } catch (CertificateException e) { + } } + checkLocally(chain, authType); } @Override @@ -125,7 +138,43 @@ public void checkServerTrusted(X509Certificate[] chain, String authType) return; } if (defaultX509Mgr != null) { - defaultX509Mgr.checkServerTrusted(chain, authType); + try { + defaultX509Mgr.checkServerTrusted(chain, authType); + return; + } catch (CertificateException e) { + } + } + checkLocally(chain, authType); + } + + private void checkLocally(X509Certificate[] chain, String authType) throws CertificateException { + Set chainAsSet = new HashSet(); + Collections.addAll(chainAsSet, chain); + X509Certificate anchorCert; + try { + if (CertificateVerifier.isSelfSigned(chain[0])) { + anchorCert = chain[0]; + } else { + PKIXCertPathBuilderResult result = CertificateVerifier.verifyCertificate(chain[0], chainAsSet); + TrustAnchor anchor = result.getTrustAnchor(); + anchorCert = anchor.getTrustedCert(); + } + + if (anchorCert == null) { + throw new CertificateException(); + } + + if (!certManager.isInTrustStore(anchorCert)) { + certManager.addToQuarantine(anchorCert); + throw new CertificateException(); + } + + } catch (CertificateVerificationException e1) { + throw new CertificateException(); + } catch (NoSuchAlgorithmException e) { + throw new CertificateException(); + } catch (NoSuchProviderException e) { + throw new CertificateException(); } } diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertCollection.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertCollection.java new file mode 100644 index 00000000..70f8c51a --- /dev/null +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertCollection.java @@ -0,0 +1,46 @@ +package com.acuity.iot.dsa.dslink.sys.cert; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Base64.Encoder; +import org.iot.dsa.node.DSIObject; +import org.iot.dsa.node.DSNode; +import org.iot.dsa.time.DSTime; + +/** + * @author Daniel Shapiro + */ +public class CertCollection extends DSNode { + + public void addCertificate(X509Certificate cert) throws CertificateEncodingException { + String name = certToName(cert); + addCertificate(name, encodeCertificate(cert)); + } + + public void addCertificate(String name, String cert) { + put(name, new CertNode().updateValue(cert)); + } + + public boolean containsCertificate(X509Certificate cert) { + DSIObject obj = get(certToName(cert)); + String certStr; + try { + certStr = encodeCertificate(cert); + } catch (CertificateEncodingException e) { + warn(e); + return false; + } + return obj != null && obj instanceof CertNode && certStr.equals(((CertNode) obj).toElement().toString()); + } + + public static String certToName(X509Certificate cert) { + return DSTime.encodeForFiles(DSTime.getCalendar(System.currentTimeMillis()), new StringBuilder(cert.getIssuerX500Principal().getName())).toString(); + } + + public static String encodeCertificate(X509Certificate cert) throws CertificateEncodingException { + Encoder encoder = Base64.getEncoder(); + return encoder.encodeToString(cert.getEncoded()); + } + +} diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertNode.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertNode.java new file mode 100644 index 00000000..23a10ead --- /dev/null +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertNode.java @@ -0,0 +1,70 @@ +package com.acuity.iot.dsa.dslink.sys.cert; + +import org.iot.dsa.node.DSInfo; +import org.iot.dsa.node.DSString; +import org.iot.dsa.node.DSValueNode; +import org.iot.dsa.node.action.ActionInvocation; +import org.iot.dsa.node.action.ActionResult; +import org.iot.dsa.node.action.DSAction; + +/** + * @author Daniel Shapiro + */ +public class CertNode extends DSValueNode { + + private static final String VALUE = "value"; + private static final String ALLOW = "Allow"; + private static final String REMOVE = "Remove"; + + private DSInfo value = getInfo(VALUE); + private DSInfo allow = getInfo(ALLOW); + private DSInfo remove = getInfo(REMOVE); + + private SysCertManager certManager; + + @Override + protected void declareDefaults() { + super.declareDefaults(); + declareDefault(VALUE, DSString.valueOf("")).setHidden(true).setReadOnly(true); + declareDefault(ALLOW, DSAction.DEFAULT); + declareDefault(REMOVE, DSAction.DEFAULT); + } + + public CertNode updateValue(String newVal) { + put(VALUE, newVal); + return this; + } + + @Override + public DSInfo getValueChild() { + return value; + } + + @Override + public ActionResult onInvoke(DSInfo action, ActionInvocation invocation) { + if (action == remove) { + remove(); + } else if (action == allow) { + allow(); + } else { + super.onInvoke(action, invocation); + } + return null; + } + + private void remove() { + getParent().remove(getInfo()); + } + + private void allow() { + getCertManager().allow(getInfo()); + } + + public SysCertManager getCertManager() { + if (certManager == null) { + certManager = (SysCertManager) getAncestor(SysCertManager.class); + } + return certManager; + } + +} diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertificateVerificationException.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertificateVerificationException.java new file mode 100644 index 00000000..8c51f3c0 --- /dev/null +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertificateVerificationException.java @@ -0,0 +1,19 @@ +package com.acuity.iot.dsa.dslink.sys.cert; + +/** + * This class wraps an exception that could be thrown during + * the certificate verification process. + * + * @author Svetlin Nakov + */ +public class CertificateVerificationException extends Exception { + private static final long serialVersionUID = 1L; + + public CertificateVerificationException(String message, Throwable cause) { + super(message, cause); + } + + public CertificateVerificationException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertificateVerifier.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertificateVerifier.java new file mode 100644 index 00000000..17785013 --- /dev/null +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertificateVerifier.java @@ -0,0 +1,161 @@ +package com.acuity.iot.dsa.dslink.sys.cert; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderException; +import java.security.cert.CertStore; +import java.security.cert.CertificateException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.TrustAnchor; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; +import java.util.HashSet; +import java.util.Set; + +/** + * Class for building a certification chain for given certificate and verifying + * it. Relies on a set of root CA certificates and intermediate certificates + * that will be used for building the certification chain. The verification + * process assumes that all self-signed certificates in the set are trusted + * root CA certificates and all other certificates in the set are intermediate + * certificates. + * + * @author Svetlin Nakov + */ +public class CertificateVerifier { + + /** + * Attempts to build a certification chain for given certificate and to verify + * it. Relies on a set of root CA certificates and intermediate certificates + * that will be used for building the certification chain. The verification + * process assumes that all self-signed certificates in the set are trusted + * root CA certificates and all other certificates in the set are intermediate + * certificates. + * + * @param cert - certificate for validation + * @param additionalCerts - set of trusted root CA certificates that will be + * used as "trust anchors" and intermediate CA certificates that will be + * used as part of the certification chain. All self-signed certificates + * are considered to be trusted root CA certificates. All the rest are + * considered to be intermediate CA certificates. + * @return the certification chain (if verification is successful) + * @throws CertificateVerificationException - if the certification is not + * successful (e.g. certification path cannot be built or some + * certificate in the chain is expired or CRL checks are failed) + */ + public static PKIXCertPathBuilderResult verifyCertificate(X509Certificate cert, + Set additionalCerts) + throws CertificateVerificationException { + try { + // Check for self-signed certificate + if (isSelfSigned(cert)) { + throw new CertificateVerificationException( + "The certificate is self-signed."); + } + + // Prepare a set of trusted root CA certificates + // and a set of intermediate certificates + Set trustedRootCerts = new HashSet(); + Set intermediateCerts = new HashSet(); + for (X509Certificate additionalCert : additionalCerts) { + if (isSelfSigned(additionalCert)) { + trustedRootCerts.add(additionalCert); + } else { + intermediateCerts.add(additionalCert); + } + } + + // Attempt to build the certification chain and verify it + PKIXCertPathBuilderResult verifiedCertChain = + verifyCertificate(cert, trustedRootCerts, intermediateCerts); + + // Check whether the certificate is revoked by the CRL + // given in its CRL distribution point extension +// CRLVerifier.verifyCertificateCRLs(cert); + + // The chain is built and verified. Return it as a result + return verifiedCertChain; + } catch (CertPathBuilderException certPathEx) { + throw new CertificateVerificationException( + "Error building certification path: " + + cert.getSubjectX500Principal(), certPathEx); + } catch (CertificateVerificationException cvex) { + throw cvex; + } catch (Exception ex) { + throw new CertificateVerificationException( + "Error verifying the certificate: " + + cert.getSubjectX500Principal(), ex); + } + } + + /** + * Checks whether given X.509 certificate is self-signed. + */ + public static boolean isSelfSigned(X509Certificate cert) + throws CertificateException, NoSuchAlgorithmException, + NoSuchProviderException { + try { + // Try to verify certificate signature with its own public key + PublicKey key = cert.getPublicKey(); + cert.verify(key); + return true; + } catch (SignatureException sigEx) { + // Invalid signature --> not self-signed + return false; + } catch (InvalidKeyException keyEx) { + // Invalid key --> not self-signed + return false; + } + } + + /** + * Attempts to build a certification chain for given certificate and to verify + * it. Relies on a set of root CA certificates (trust anchors) and a set of + * intermediate certificates (to be used as part of the chain). + * @param cert - certificate for validation + * @param trustedRootCerts - set of trusted root CA certificates + * @param intermediateCerts - set of intermediate certificates + * @return the certification chain (if verification is successful) + * @throws GeneralSecurityException - if the verification is not successful + * (e.g. certification path cannot be built or some certificate in the + * chain is expired) + */ + private static PKIXCertPathBuilderResult verifyCertificate(X509Certificate cert, Set trustedRootCerts, + Set intermediateCerts) throws GeneralSecurityException { + + // Create the selector that specifies the starting certificate + X509CertSelector selector = new X509CertSelector(); + selector.setCertificate(cert); + + // Create the trust anchors (set of root CA certificates) + Set trustAnchors = new HashSet(); + for (X509Certificate trustedRootCert : trustedRootCerts) { + trustAnchors.add(new TrustAnchor(trustedRootCert, null)); + } + + // Configure the PKIX certificate builder algorithm parameters + PKIXBuilderParameters pkixParams = + new PKIXBuilderParameters(trustAnchors, selector); + + // Disable CRL checks (this is done manually as additional step) + pkixParams.setRevocationEnabled(true); + + // Specify a list of intermediate certificates + CertStore intermediateCertStore = CertStore.getInstance("Collection", + new CollectionCertStoreParameters(intermediateCerts), "BC"); + pkixParams.addCertStore(intermediateCertStore); + + // Build and verify the certification chain + CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", "BC"); + PKIXCertPathBuilderResult result = + (PKIXCertPathBuilderResult) builder.build(pkixParams); + return result; + } + +} \ No newline at end of file diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/HostnameWhitelist.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/HostnameWhitelist.java new file mode 100644 index 00000000..5cad988f --- /dev/null +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/HostnameWhitelist.java @@ -0,0 +1,91 @@ +package com.acuity.iot.dsa.dslink.sys.cert; + +import org.iot.dsa.node.DSBool; +import org.iot.dsa.node.DSIValue; +import org.iot.dsa.node.DSInfo; +import org.iot.dsa.node.DSJavaEnum; +import org.iot.dsa.node.DSMap; +import org.iot.dsa.node.DSNode; +import org.iot.dsa.node.DSValueType; +import org.iot.dsa.node.action.ActionInvocation; +import org.iot.dsa.node.action.ActionResult; +import org.iot.dsa.node.action.DSAbstractAction; + +/** + * @author Daniel Shapiro + */ +public class HostnameWhitelist extends DSNode { + + public static enum WhitelistValue { + ALLOWED, FORBIDDEN; + } + + public static enum WhitelistOption { + ALLOWED, FORBIDDEN, REMOVE; + } + + private static final String ENABLED = "Enabled"; + private static final String ADD_HOSTNAME = "Add Hostname"; + + private DSInfo enabled = getInfo(ENABLED); + + @Override + protected void declareDefaults() { + super.declareDefaults(); + declareDefault(ENABLED, DSBool.FALSE); + declareDefault(ADD_HOSTNAME, getAddHostnameAction()); + } + + public boolean isEnabled() { + return enabled.getElement().toBoolean(); + } + + private DSAbstractAction getAddHostnameAction() { + DSAbstractAction act = new DSAbstractAction() { + + @Override + public void prepareParameter(DSInfo info, DSMap parameter) { + } + + @Override + public ActionResult invoke(DSInfo info, ActionInvocation invocation) { + ((HostnameWhitelist) info.getParent()).addHostname(invocation.getParameters()); + return null; + } + }; + act.addParameter("Hostname", DSValueType.STRING, null); + act.addParameter("Status", DSJavaEnum.valueOf(WhitelistValue.ALLOWED), "Whether this hostname should be whitelisted or blacklisted"); + return act; + } + + private void addHostname(DSMap parameters) { + String hostname = parameters.getString("Hostname"); + String statusStr = parameters.getString("Status"); + WhitelistOption option = WhitelistOption.valueOf(statusStr); + put(hostname, DSJavaEnum.valueOf(option)); + } + + public WhitelistValue checkHostname(String hostname) { + DSIValue value = getValue(hostname); + String str = null; + if (value != null) { + str = value.toElement().toString(); + } + try { + return WhitelistValue.valueOf(str); + } catch (Exception e) { + return null; + } + } + + protected void onChildChanged(DSInfo info) { + if (info.isValue()) { + String val = info.getValue().toElement().toString(); + if (WhitelistOption.REMOVE.name().equals(val)) { + remove(info); + } + } + } + + +} diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/KeyToolUtil.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/KeyToolUtil.java new file mode 100644 index 00000000..4b8bf6f2 --- /dev/null +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/KeyToolUtil.java @@ -0,0 +1,132 @@ +package com.acuity.iot.dsa.dslink.sys.cert; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.iot.dsa.logging.DSLogger; +import org.iot.dsa.time.DSTime; + + +/** + * @author Daniel Shapiro + */ +public class KeyToolUtil extends DSLogger { + + private static KeyToolUtil inst = new KeyToolUtil(); + private KeyToolUtil() { + + } + + private String executeCommand(String[] cmd) { + try { + ProcessBuilder builder = new ProcessBuilder(); + Process process = builder.command(cmd).start(); + process.waitFor(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line = null; + while ( (line = reader.readLine()) != null) { + sb.append(line); + sb.append(System.getProperty("line.separator")); + } + return sb.toString(); + } catch (Exception e) { + error("", e); + return ""; + } + } + + public static void generateSelfSigned(String keystore, String password) { + String[] cmd = new String[]{ + "keytool", + "-genkey", + "-keystore", keystore, + "-storepass", password, + "-keypass", password, + "-alias", "dsa", + "-keyalg", "RSA", + "-validity", "18000", + "-dname", "\"CN=dslink-java-v2, O=DSA, C=US\"" + }; + inst.executeCommand(cmd); + } + + public static String generateCSR(String keystore, String password) throws IOException { + String filename = "dsa.csr"; + String[] cmd = new String[]{ + "keytool", + "-certreq", + "-keystore", keystore, + "-storepass", password, + "-alias", "dsa", + "-keyalg", "RSA", + "-validity", "18000", + "-dname", "\"CN=dslink-java-v2, O=DSA, C=US\"", + "-file", filename + }; + inst.executeCommand(cmd); + return new String(Files.readAllBytes(Paths.get(filename))); + } + + public static void importCACert(String keystore, String certStr, String alias, String password) throws IOException { + String filename = DSTime.encodeForFiles(DSTime.getCalendar(System.currentTimeMillis()), new StringBuilder("tempCACert")).toString(); + Files.write(Paths.get(filename), certStr.getBytes()); + String[] cmd = new String[]{ + "keytool", + "-import", + "-trustcacerts", + "-keystore", keystore, + "-storepass", password, + "-alias", alias, + "-file", filename + }; + inst.executeCommand(cmd); + + new File(filename).delete(); + } + + public static void importPrimaryCert(String keystore, String certStr, String password) throws IOException { + String filename = DSTime.encodeForFiles(DSTime.getCalendar(System.currentTimeMillis()), new StringBuilder("tempCert")).toString(); + Files.write(Paths.get(filename), certStr.getBytes()); + String[] cmd = new String[]{ + "keytool", + "-import", + "-trustcacerts", + "-keystore", keystore, + "-storepass", password, + "-alias", "dsa", + "-file", filename + }; + inst.executeCommand(cmd); + + new File(filename).delete(); + } + + public static String getEntry(String keystore, String password) { + String[] cmd = new String[]{ + "keytool", + "-list", + "-v", + "-keystore", keystore, + "-storepass", password, + "-alias", "dsa", + }; + return inst.executeCommand(cmd); + } + + public static void deleteEntry(String keystore, String password) { + String[] cmd = new String[]{ + "keytool", + "-delete", + "-keystore", keystore, + "-storepass", password, + "-alias", "dsa", + }; + inst.executeCommand(cmd); + } + +} diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/SysCertManager.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/SysCertManager.java index 657cea24..bc10f1f6 100644 --- a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/SysCertManager.java +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/SysCertManager.java @@ -1,11 +1,26 @@ package com.acuity.iot.dsa.dslink.sys.cert; import java.io.File; +import java.io.IOException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSession; import org.iot.dsa.node.DSBool; import org.iot.dsa.node.DSInfo; +import org.iot.dsa.node.DSMap; import org.iot.dsa.node.DSNode; import org.iot.dsa.node.DSString; +import org.iot.dsa.node.DSValueType; +import org.iot.dsa.node.action.ActionInvocation; +import org.iot.dsa.node.action.ActionResult; +import org.iot.dsa.node.action.ActionSpec.ResultType; +import org.iot.dsa.node.action.DSAbstractAction; +import org.iot.dsa.node.action.DSActionValues; import org.iot.dsa.security.DSPasswordAes128; +import org.iot.dsa.util.DSException; +import com.acuity.iot.dsa.dslink.sys.cert.HostnameWhitelist.WhitelistValue; /** * Certificate management for the whole process. This is basically a stub for future @@ -13,27 +28,78 @@ * as accepts self signed (anonymous) certs from the broker. * * @author Aaron Hansen + * @author Daniel Shapiro */ public class SysCertManager extends DSNode { // Constants // --------- - private static final String ALLOW_CLIENTS = "Allow_Anonymous_Clients"; - private static final String ALLOW_SERVERS = "Allow_Anonymous_Servers"; - private static final String CERTFILE = "Cert_File"; - private static final String CERTFILE_PASS = "Cert_File_Pass"; - private static final String CERTFILE_TYPE = "Cert_File_Type"; + private static final String ALLOW_CLIENTS = "Allow Anonymous Clients"; + private static final String ALLOW_SERVERS = "Allow Anonymous Servers"; + private static final String VERIFY_HOSTNAMES = "Enable Hostname-Certificate Verification"; + private static final String HOSTNAME_WHITELIST = "Hostname Whitelist"; + private static final String CERTFILE = "Cert File"; + private static final String CERTFILE_PASS = "Cert File Pass"; + private static final String CERTFILE_TYPE = "Cert File Type"; + private static final String LOCAL_TRUSTSTORE = "Local Truststore"; + private static final String QUARANTINE = "Quarantine"; + private static final String GENERATE_CSR = "Generate Certificate Signing Request"; + private static final String IMPORT_CA_CERT = "Import CA Certificate"; + private static final String IMPORT_PRIMARY_CERT = "Import Primary Certificate"; + private static final String GENERATE_SELF_SIGNED = "Generate Self-Signed Certificate"; + private static final String DELETE_KS_ENTRY = "Delete Keystore Entry"; + private static final String GET_KS_ENTRY = "Get Keystore Entry"; // Fields // ------ private DSInfo allowClients = getInfo(ALLOW_CLIENTS); private DSInfo allowServers = getInfo(ALLOW_SERVERS); - private DSInfo keystore = getInfo(CERTFILE); + private DSInfo verifyHostnames = getInfo(VERIFY_HOSTNAMES); + private DSInfo keystorePath = getInfo(CERTFILE); private DSInfo keystorePass = getInfo(CERTFILE_PASS); private DSInfo keystoreType = getInfo(CERTFILE_TYPE); + private CertCollection localTruststore; + private CertCollection quarantine; + private HostnameWhitelist whitelist; + private static SysCertManager inst; + + private static HostnameVerifier oldHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + private HostnameVerifier hostnameVerifier = new SysHostnameVerifier(); + + public SysCertManager() { + } + + public static SysCertManager getInstance() { + return inst; + } + + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + private CertCollection getLocalTruststore() { + if (localTruststore == null) { + localTruststore = (CertCollection) getInfo(LOCAL_TRUSTSTORE).getObject(); + } + return localTruststore; + } + + private CertCollection getQuarantine() { + if (quarantine == null) { + quarantine = (CertCollection) getInfo(QUARANTINE).getObject(); + } + return quarantine; + } + + private HostnameWhitelist getHostnameWhitelist() { + if (whitelist == null) { + whitelist = (HostnameWhitelist) getInfo(HOSTNAME_WHITELIST).getObject(); + } + return whitelist; + } + // Methods // ------- @@ -50,50 +116,192 @@ public boolean allowAnonymousClients() { public boolean allowAnonymousServers() { return allowServers.getElement().toBoolean(); } + + public boolean hostnameVerificationEnabled() { + return verifyHostnames.getElement().toBoolean(); + } @Override public void declareDefaults() { declareDefault(ALLOW_CLIENTS, DSBool.FALSE); declareDefault(ALLOW_SERVERS, DSBool.TRUE); + declareDefault(VERIFY_HOSTNAMES, DSBool.TRUE); + declareDefault(HOSTNAME_WHITELIST, new HostnameWhitelist()); declareDefault(CERTFILE, DSString.valueOf("dslink.jks")); declareDefault(CERTFILE_TYPE, DSString.valueOf("JKS")); declareDefault(CERTFILE_PASS, DSPasswordAes128.valueOf("dsarocks")); + declareDefault(LOCAL_TRUSTSTORE, new CertCollection()); + declareDefault(QUARANTINE, new CertCollection()).setTransient(true); + declareDefault(GENERATE_CSR, getGenerateCSRAction()); + declareDefault(IMPORT_CA_CERT, getImportCACertAction()); + declareDefault(IMPORT_PRIMARY_CERT, getImportPrimaryCertAction()); + declareDefault(GENERATE_SELF_SIGNED, getGenerateSelfSignedAction()); + declareDefault(GET_KS_ENTRY, getGetKSEntryAction()); + declareDefault(DELETE_KS_ENTRY, getDeleteKSEntryAction()); + } + + private DSAbstractAction getGenerateCSRAction() { + DSAbstractAction act = new DSAbstractAction() { + + @Override + public void prepareParameter(DSInfo info, DSMap parameter) { + } + + @Override + public ActionResult invoke(DSInfo info, ActionInvocation invocation) { + String csr = ((SysCertManager) info.getParent()).generateCSR(); + return csr != null ? new DSActionValues(info.getAction()).addResult(DSString.valueOf(csr)) : null; + } + }; + act.setResultType(ResultType.VALUES); + act.addValueResult("CSR", DSValueType.STRING).setEditor("textarea"); + return act; + } + + private String generateCSR() { + try { + return KeyToolUtil.generateCSR(getKeystorePath(), getCertFilePass()); + } catch (IOException e) { + DSException.throwRuntime(e); + return null; + } + } + + private DSAbstractAction getImportCACertAction() { + DSAbstractAction act = new DSAbstractAction() { + + @Override + public void prepareParameter(DSInfo info, DSMap parameter) { + } + + @Override + public ActionResult invoke(DSInfo info, ActionInvocation invocation) { + DSMap parameters = invocation.getParameters(); + ((SysCertManager) info.getParent()).importCACert(parameters); + return null; + } + }; + act.addParameter("Alias", DSValueType.STRING, null); + act.addParameter("Certificate", DSValueType.STRING, null).setEditor("textarea"); + return act; + } + + private void importCACert(DSMap parameters) { + String alias = parameters.getString("Alias"); + String certStr = parameters.getString("Certificate"); + try { + KeyToolUtil.importCACert(getKeystorePath(), certStr, alias, getCertFilePass()); + } catch (IOException e) { + DSException.throwRuntime(e); + } + } + + private DSAbstractAction getImportPrimaryCertAction() { + DSAbstractAction act = new DSAbstractAction() { + + @Override + public void prepareParameter(DSInfo info, DSMap parameter) { + } + + @Override + public ActionResult invoke(DSInfo info, ActionInvocation invocation) { + DSMap parameters = invocation.getParameters(); + ((SysCertManager) info.getParent()).importPrimaryCert(parameters); + return null; + } + }; + act.addParameter("Certificate", DSValueType.STRING, null).setEditor("textarea"); + return act; + } + + private void importPrimaryCert(DSMap parameters) { + String certStr = parameters.getString("Certificate"); + try { + KeyToolUtil.importPrimaryCert(getKeystorePath(), certStr, getCertFilePass()); + } catch (IOException e) { + DSException.throwRuntime(e); + } + } + + private DSAbstractAction getGenerateSelfSignedAction() { + DSAbstractAction act = new DSAbstractAction() { + + @Override + public void prepareParameter(DSInfo info, DSMap parameter) { + } + + @Override + public ActionResult invoke(DSInfo info, ActionInvocation invocation) { + ((SysCertManager) info.getParent()).keytoolGenkey(); + return null; + } + }; + return act; + } + + private DSAbstractAction getGetKSEntryAction() { + DSAbstractAction act = new DSAbstractAction() { + + @Override + public void prepareParameter(DSInfo info, DSMap parameter) { + } + + @Override + public ActionResult invoke(DSInfo info, ActionInvocation invocation) { + String result = ((SysCertManager) info.getParent()).getKSEntry(); + return new DSActionValues(info.getAction()).addResult(DSString.valueOf(result)); + } + }; + act.setResultType(ResultType.VALUES); + act.addValueResult("Entry", DSValueType.STRING).setEditor("textarea"); + return act; + } + + private String getKSEntry() { + return KeyToolUtil.getEntry(getKeystorePath(), getCertFilePass()); + } + + private DSAbstractAction getDeleteKSEntryAction() { + DSAbstractAction act = new DSAbstractAction() { + + @Override + public void prepareParameter(DSInfo info, DSMap parameter) { + } + + @Override + public ActionResult invoke(DSInfo info, ActionInvocation invocation) { + ((SysCertManager) info.getParent()).deleteKSEntry(); + return null; + } + }; + return act; + } + + private void deleteKSEntry() { + KeyToolUtil.deleteEntry(getKeystorePath(), getCertFilePass()); } private String getCertFilePass() { DSPasswordAes128 pass = (DSPasswordAes128) keystorePass.getObject(); return pass.decode(); } + + private String getKeystorePath() { + return keystorePath.getElement().toString(); + } /** * Executes the java keytool to generate a new self signed cert. */ private void keytoolGenkey() { - try { - String pass = getCertFilePass(); - String[] cmd = new String[]{ - "keytool", - "-genkey", - "-keystore", keystore.getElement().toString(), - "-storepass", pass, - "-keypass", pass, - "-alias", "dsa", - "-keyalg", "RSA", - "-validity", "18000", - "-dname", "\"CN=dslink-java-v2, O=DSA, C=US\"" - }; - ProcessBuilder builder = new ProcessBuilder(); - Process process = builder.command(cmd).start(); - process.waitFor(); - } catch (Exception x) { - error(getPath(), x); - } + KeyToolUtil.generateSelfSigned(getKeystorePath(), getCertFilePass()); } @Override public void onStarted() { + inst = this; AnonymousTrustFactory.init(this); - String keystore = this.keystore.getElement().toString(); + String keystore = this.keystorePath.getElement().toString(); File f = new File(keystore); if (!f.exists()) { keytoolGenkey(); @@ -106,6 +314,49 @@ public void onStarted() { } catch (Exception x) { error(getParent(), x); } + HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier); + } + + public boolean isInTrustStore(X509Certificate cert) { + return getLocalTruststore().containsCertificate(cert); + } + + public void addToQuarantine(X509Certificate cert) { + try { + getQuarantine().addCertificate(cert); + } catch (CertificateEncodingException e) { + error("", e); + } + } + + public void allow(DSInfo certInfo) { + String name = certInfo.getName(); + CertNode certNode = (CertNode) certInfo.getNode(); + String certStr = certNode.toElement().toString(); + getQuarantine().remove(certInfo); + getLocalTruststore().addCertificate(name, certStr); + } + + private class SysHostnameVerifier implements HostnameVerifier { + @Override + public boolean verify(String hostname, SSLSession session) { + if (getHostnameWhitelist().isEnabled()) { + WhitelistValue wlval = getHostnameWhitelist().checkHostname(hostname); + if (wlval != null) { + switch (wlval) { + case ALLOWED: + return true; + case FORBIDDEN: + return false; + } + } + } + if (hostnameVerificationEnabled()) { + return oldHostnameVerifier.verify(hostname, session); + } else { + return true; + } + } } } diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/LoggerNode.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/LoggerNode.java index 65e78274..20058abc 100644 --- a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/LoggerNode.java +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/LoggerNode.java @@ -8,6 +8,9 @@ import org.iot.dsa.node.action.ActionResult; import org.iot.dsa.node.action.DSAbstractAction; +/** + * @author Daniel Shapiro + */ public class LoggerNode extends StreamableLogNode { private DSInfo levelInfo = getInfo("Log Level"); diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/StreamableLogNode.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/StreamableLogNode.java index ebfcece1..1717d7c0 100644 --- a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/StreamableLogNode.java +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/StreamableLogNode.java @@ -20,6 +20,9 @@ import org.iot.dsa.node.action.ActionTable; import org.iot.dsa.node.action.DSAbstractAction; +/** + * @author Daniel Shapiro + */ public abstract class StreamableLogNode extends DSNode { private static DSList levelRange; diff --git a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/SysLogService.java b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/SysLogService.java index c8524cd3..f0b0c70b 100644 --- a/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/SysLogService.java +++ b/dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/logging/SysLogService.java @@ -16,6 +16,9 @@ import org.iot.dsa.node.action.ActionResult; import org.iot.dsa.node.action.DSAbstractAction; +/** + * @author Daniel Shapiro + */ public class SysLogService extends StreamableLogNode { private DSInfo levelInfo = getInfo("Default Log Level");