Skip to content

Commit 6d077e6

Browse files
committed
check CRLs when verifying a cert; add action that generates a CSR
1 parent 9b4ea1d commit 6d077e6

File tree

5 files changed

+293
-4
lines changed

5 files changed

+293
-4
lines changed

dslink-v2/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ artifacts {
44
}
55

66
dependencies {
7+
compile 'org.bouncycastle:bcprov-jdk15on:+'
8+
compile 'org.bouncycastle:bcpkix-jdk15on:+'
79
testImplementation 'junit:junit:[4.12,)'
810
}
911

dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/AnonymousTrustFactory.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.acuity.iot.dsa.dslink.sys.cert;
22

33
import java.security.KeyStore;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.NoSuchProviderException;
46
import java.security.Provider;
57
import java.security.Security;
68
import java.security.cert.CertificateException;
@@ -147,10 +149,15 @@ public void checkServerTrusted(X509Certificate[] chain, String authType)
147149
private void checkLocally(X509Certificate[] chain, String authType) throws CertificateException {
148150
Set<X509Certificate> chainAsSet = new HashSet<X509Certificate>();
149151
Collections.addAll(chainAsSet, chain);
152+
X509Certificate anchorCert;
150153
try {
151-
PKIXCertPathBuilderResult result = CertificateVerifier.verifyCertificate(chain[0], chainAsSet);
152-
TrustAnchor anchor = result.getTrustAnchor();
153-
X509Certificate anchorCert = anchor.getTrustedCert();
154+
if (CertificateVerifier.isSelfSigned(chain[0])) {
155+
anchorCert = chain[0];
156+
} else {
157+
PKIXCertPathBuilderResult result = CertificateVerifier.verifyCertificate(chain[0], chainAsSet);
158+
TrustAnchor anchor = result.getTrustAnchor();
159+
anchorCert = anchor.getTrustedCert();
160+
}
154161

155162
if (anchorCert == null) {
156163
throw new CertificateException();
@@ -163,6 +170,10 @@ private void checkLocally(X509Certificate[] chain, String authType) throws Certi
163170

164171
} catch (CertificateVerificationException e1) {
165172
throw new CertificateException();
173+
} catch (NoSuchAlgorithmException e) {
174+
throw new CertificateException();
175+
} catch (NoSuchProviderException e) {
176+
throw new CertificateException();
166177
}
167178
}
168179

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package com.acuity.iot.dsa.dslink.sys.cert;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.net.MalformedURLException;
7+
import java.net.URL;
8+
import java.security.cert.CRLException;
9+
import java.security.cert.CertificateException;
10+
import java.security.cert.CertificateFactory;
11+
import java.security.cert.CertificateParsingException;
12+
import java.security.cert.X509CRL;
13+
import java.security.cert.X509Certificate;
14+
import java.util.ArrayList;
15+
import java.util.Hashtable;
16+
import java.util.List;
17+
18+
import javax.naming.Context;
19+
import javax.naming.NamingException;
20+
import javax.naming.directory.Attribute;
21+
import javax.naming.directory.Attributes;
22+
import javax.naming.directory.DirContext;
23+
import javax.naming.directory.InitialDirContext;
24+
25+
import org.bouncycastle.asn1.ASN1InputStream;
26+
import org.bouncycastle.asn1.ASN1Primitive;
27+
import org.bouncycastle.asn1.DERIA5String;
28+
import org.bouncycastle.asn1.DEROctetString;
29+
import org.bouncycastle.asn1.x509.CRLDistPoint;
30+
import org.bouncycastle.asn1.x509.DistributionPoint;
31+
import org.bouncycastle.asn1.x509.DistributionPointName;
32+
import org.bouncycastle.asn1.x509.GeneralName;
33+
import org.bouncycastle.asn1.x509.GeneralNames;
34+
import org.bouncycastle.asn1.x509.X509Extensions;
35+
36+
/**
37+
* Class that verifies CRLs for given X509 certificate. Extracts the CRL
38+
* distribution points from the certificate (if available) and checks the
39+
* certificate revocation status against the CRLs coming from the
40+
* distribution points. Supports HTTP, HTTPS, FTP and LDAP based URLs.
41+
*
42+
* @author Svetlin Nakov
43+
*/
44+
public class CRLVerifier {
45+
46+
/**
47+
* Extracts the CRL distribution points from the certificate (if available)
48+
* and checks the certificate revocation status against the CRLs coming from
49+
* the distribution points. Supports HTTP, HTTPS, FTP and LDAP based URLs.
50+
*
51+
* @param cert the certificate to be checked for revocation
52+
* @throws CertificateVerificationException if the certificate is revoked
53+
*/
54+
public static void verifyCertificateCRLs(X509Certificate cert)
55+
throws CertificateVerificationException {
56+
try {
57+
List<String> crlDistPoints = getCrlDistributionPoints(cert);
58+
for (String crlDP : crlDistPoints) {
59+
X509CRL crl = downloadCRL(crlDP);
60+
if (crl.isRevoked(cert)) {
61+
throw new CertificateVerificationException(
62+
"The certificate is revoked by CRL: " + crlDP);
63+
}
64+
}
65+
} catch (Exception ex) {
66+
if (ex instanceof CertificateVerificationException) {
67+
throw (CertificateVerificationException) ex;
68+
} else {
69+
throw new CertificateVerificationException(
70+
"Can not verify CRL for certificate: " +
71+
cert.getSubjectX500Principal());
72+
}
73+
}
74+
}
75+
76+
/**
77+
* Downloads CRL from given URL. Supports http, https, ftp and ldap based URLs.
78+
*/
79+
private static X509CRL downloadCRL(String crlURL) throws IOException,
80+
CertificateException, CRLException,
81+
CertificateVerificationException, NamingException {
82+
if (crlURL.startsWith("http://") || crlURL.startsWith("https://")
83+
|| crlURL.startsWith("ftp://")) {
84+
X509CRL crl = downloadCRLFromWeb(crlURL);
85+
return crl;
86+
} else if (crlURL.startsWith("ldap://")) {
87+
X509CRL crl = downloadCRLFromLDAP(crlURL);
88+
return crl;
89+
} else {
90+
throw new CertificateVerificationException(
91+
"Can not download CRL from certificate " +
92+
"distribution point: " + crlURL);
93+
}
94+
}
95+
96+
/**
97+
* Downloads a CRL from given LDAP url, e.g.
98+
* ldap://ldap.infonotary.com/dc=identity-ca,dc=infonotary,dc=com
99+
*/
100+
private static X509CRL downloadCRLFromLDAP(String ldapURL)
101+
throws CertificateException, NamingException, CRLException,
102+
CertificateVerificationException {
103+
Hashtable<String , String> env = new Hashtable<String , String>();
104+
env.put(Context.INITIAL_CONTEXT_FACTORY,
105+
"com.sun.jndi.ldap.LdapCtxFactory");
106+
env.put(Context.PROVIDER_URL, ldapURL);
107+
108+
DirContext ctx = new InitialDirContext(env);
109+
Attributes avals = ctx.getAttributes("");
110+
Attribute aval = avals.get("certificateRevocationList;binary");
111+
byte[] val = (byte[])aval.get();
112+
if ((val == null) || (val.length == 0)) {
113+
throw new CertificateVerificationException(
114+
"Can not download CRL from: " + ldapURL);
115+
} else {
116+
InputStream inStream = new ByteArrayInputStream(val);
117+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
118+
X509CRL crl = (X509CRL)cf.generateCRL(inStream);
119+
return crl;
120+
}
121+
}
122+
123+
/**
124+
* Downloads a CRL from given HTTP/HTTPS/FTP URL, e.g.
125+
* http://crl.infonotary.com/crl/identity-ca.crl
126+
*/
127+
private static X509CRL downloadCRLFromWeb(String crlURL)
128+
throws MalformedURLException, IOException, CertificateException,
129+
CRLException {
130+
URL url = new URL(crlURL);
131+
InputStream crlStream = url.openStream();
132+
try {
133+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
134+
X509CRL crl = (X509CRL) cf.generateCRL(crlStream);
135+
return crl;
136+
} finally {
137+
crlStream.close();
138+
}
139+
}
140+
141+
/**
142+
* Extracts all CRL distribution point URLs from the "CRL Distribution Point"
143+
* extension in a X.509 certificate. If CRL distribution point extension is
144+
* unavailable, returns an empty list.
145+
*/
146+
public static List<String> getCrlDistributionPoints(
147+
X509Certificate cert) throws CertificateParsingException, IOException {
148+
byte[] crldpExt = cert.getExtensionValue(
149+
X509Extensions.CRLDistributionPoints.getId());
150+
if (crldpExt == null) {
151+
List<String> emptyList = new ArrayList<String>();
152+
return emptyList;
153+
}
154+
ASN1InputStream oAsnInStream = new ASN1InputStream(
155+
new ByteArrayInputStream(crldpExt));
156+
ASN1Primitive derObjCrlDP = oAsnInStream.readObject();
157+
DEROctetString dosCrlDP = (DEROctetString) derObjCrlDP;
158+
byte[] crldpExtOctets = dosCrlDP.getOctets();
159+
ASN1InputStream oAsnInStream2 = new ASN1InputStream(
160+
new ByteArrayInputStream(crldpExtOctets));
161+
ASN1Primitive derObj2 = oAsnInStream2.readObject();
162+
CRLDistPoint distPoint = CRLDistPoint.getInstance(derObj2);
163+
164+
oAsnInStream.close();
165+
oAsnInStream2.close();
166+
167+
List<String> crlUrls = new ArrayList<String>();
168+
for (DistributionPoint dp : distPoint.getDistributionPoints()) {
169+
DistributionPointName dpn = dp.getDistributionPoint();
170+
// Look for URIs in fullName
171+
if (dpn != null) {
172+
if (dpn.getType() == DistributionPointName.FULL_NAME) {
173+
GeneralName[] genNames = GeneralNames.getInstance(
174+
dpn.getName()).getNames();
175+
// Look for an URI
176+
for (int j = 0; j < genNames.length; j++) {
177+
if (genNames[j].getTagNo() == GeneralName.uniformResourceIdentifier) {
178+
String url = DERIA5String.getInstance(
179+
genNames[j].getName()).getString();
180+
crlUrls.add(url);
181+
}
182+
}
183+
}
184+
}
185+
}
186+
return crlUrls;
187+
}
188+
189+
}

dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/CertificateVerifier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public static PKIXCertPathBuilderResult verifyCertificate(X509Certificate cert,
7777

7878
// Check whether the certificate is revoked by the CRL
7979
// given in its CRL distribution point extension
80-
// CRLVerifier.verifyCertificateCRLs(cert); //TODO
80+
CRLVerifier.verifyCertificateCRLs(cert);
8181

8282
// The chain is built and verified. Return it as a result
8383
return verifiedCertChain;

dslink-v2/src/main/java/com/acuity/iot/dsa/dslink/sys/cert/SysCertManager.java

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
package com.acuity.iot.dsa.dslink.sys.cert;
22

33
import java.io.File;
4+
import java.io.IOException;
5+
import java.io.StringWriter;
6+
import java.security.KeyPair;
7+
import java.security.KeyPairGenerator;
8+
import java.security.NoSuchAlgorithmException;
9+
import java.security.SecureRandom;
410
import java.security.cert.CertificateEncodingException;
511
import java.security.cert.X509Certificate;
12+
import java.time.format.ResolverStyle;
13+
import java.util.Iterator;
14+
import javax.security.auth.x500.X500Principal;
15+
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
16+
import org.bouncycastle.operator.ContentSigner;
17+
import org.bouncycastle.operator.OperatorCreationException;
18+
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
19+
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
20+
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
21+
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
622
import org.iot.dsa.node.DSBool;
23+
import org.iot.dsa.node.DSIValue;
724
import org.iot.dsa.node.DSInfo;
25+
import org.iot.dsa.node.DSMap;
826
import org.iot.dsa.node.DSNode;
927
import org.iot.dsa.node.DSString;
28+
import org.iot.dsa.node.DSValueType;
29+
import org.iot.dsa.node.action.ActionInvocation;
30+
import org.iot.dsa.node.action.ActionResult;
31+
import org.iot.dsa.node.action.ActionSpec;
32+
import org.iot.dsa.node.action.ActionSpec.ResultType;
33+
import org.iot.dsa.node.action.ActionValues;
34+
import org.iot.dsa.node.action.DSAbstractAction;
35+
import org.iot.dsa.node.action.DSAction;
36+
import org.iot.dsa.node.action.DSActionValues;
1037
import org.iot.dsa.security.DSPasswordAes128;
38+
import org.iot.dsa.util.DSException;
1139

1240
/**
1341
* Certificate management for the whole process. This is basically a stub for future
@@ -28,6 +56,7 @@ public class SysCertManager extends DSNode {
2856
private static final String CERTFILE_TYPE = "Cert_File_Type";
2957
private static final String LOCAL_TRUSTSTORE = "Local_Truststore";
3058
private static final String QUARANTINE = "Quarantine";
59+
private static final String GENERATE_CSR = "Generate_Certificate_Signing_Request";
3160

3261
// Fields
3362
// ------
@@ -37,6 +66,7 @@ public class SysCertManager extends DSNode {
3766
private DSInfo keystore = getInfo(CERTFILE);
3867
private DSInfo keystorePass = getInfo(CERTFILE_PASS);
3968
private DSInfo keystoreType = getInfo(CERTFILE_TYPE);
69+
private DSInfo generateCSR = getInfo(GENERATE_CSR);
4070
private CertCollection localTruststore;
4171
private CertCollection quarantine;
4272

@@ -80,6 +110,24 @@ public void declareDefaults() {
80110
declareDefault(CERTFILE_PASS, DSPasswordAes128.valueOf("dsarocks"));
81111
declareDefault(LOCAL_TRUSTSTORE, new CertCollection());
82112
declareDefault(QUARANTINE, new CertCollection()).setTransient(true);
113+
declareDefault(GENERATE_CSR, getGenerateCSRAction());
114+
}
115+
116+
private DSAbstractAction getGenerateCSRAction() {
117+
DSAbstractAction act = new DSAbstractAction() {
118+
119+
@Override
120+
public void prepareParameter(DSInfo info, DSMap parameter) {
121+
}
122+
123+
@Override
124+
public ActionResult invoke(DSInfo info, ActionInvocation invocation) {
125+
return ((SysCertManager) info.getParent()).generateCSR(info);
126+
}
127+
};
128+
act.setResultType(ResultType.VALUES);
129+
act.addValueResult("CSR", DSValueType.STRING);
130+
return act;
83131
}
84132

85133
private String getCertFilePass() {
@@ -150,5 +198,44 @@ public void allow(DSInfo certInfo) {
150198
getLocalTruststore().addCertificate(name, certStr);
151199
}
152200

201+
private ActionResult generateCSR(DSInfo actionInfo) {
202+
KeyPairGenerator keyGen;
203+
try {
204+
keyGen = KeyPairGenerator.getInstance("RSA");
205+
} catch (NoSuchAlgorithmException e) {
206+
DSException.throwRuntime(e);
207+
return null;
208+
}
209+
keyGen.initialize(2048, new SecureRandom());
210+
KeyPair pair = keyGen.generateKeyPair();
211+
PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder(
212+
new X500Principal("CN=dslink-java-v2"), pair.getPublic());
213+
JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder("SHA256withRSA");
214+
ContentSigner signer;
215+
try {
216+
signer = csBuilder.build(pair.getPrivate());
217+
} catch (OperatorCreationException e) {
218+
DSException.throwRuntime(e);
219+
return null;
220+
}
221+
PKCS10CertificationRequest csr = p10Builder.build(signer);
222+
StringWriter str = new StringWriter();
223+
JcaPEMWriter pemWriter = new JcaPEMWriter(str);
224+
try {
225+
pemWriter.writeObject(csr);
226+
} catch (IOException e) {
227+
DSException.throwRuntime(e);
228+
return null;
229+
} finally {
230+
try {
231+
pemWriter.close();
232+
str.close();
233+
} catch (IOException e) {
234+
DSException.throwRuntime(e);
235+
return null;
236+
}
237+
}
238+
return new DSActionValues(actionInfo.getAction()).addResult(DSString.valueOf(str));
239+
}
153240

154241
}

0 commit comments

Comments
 (0)