From 1d4b38bd5da9636f4ba80244d92c89b4b5cbdf88 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Felix=20D=C3=B6rre?= Date: Fri, 11 Dec 2015 12:23:56 +0100 Subject: [PATCH] add: implement OCSP serving Change-Id: I2a8aa170d79b9e77dbb951b01ed1c4b5186f3440 --- src/club/wpia/gigi/Launcher.java | 12 +- src/club/wpia/gigi/crypto/OCSPRequest.java | 159 +++++++++++ src/club/wpia/gigi/crypto/OCSPResponse.java | 243 ++++++++++++++++ src/club/wpia/gigi/ocsp/OCSPIssuer.java | 113 ++++++++ src/club/wpia/gigi/ocsp/OCSPIssuerId.java | 99 +++++++ .../wpia/gigi/ocsp/OCSPIssuerManager.java | 270 ++++++++++++++++++ src/club/wpia/gigi/ocsp/OCSPResponder.java | 133 +++++++++ src/club/wpia/gigi/util/ServerConstants.java | 8 +- .../club/wpia/gigi/util/SimpleSigner.java | 29 +- 9 files changed, 1059 insertions(+), 7 deletions(-) create mode 100644 src/club/wpia/gigi/crypto/OCSPRequest.java create mode 100644 src/club/wpia/gigi/crypto/OCSPResponse.java create mode 100644 src/club/wpia/gigi/ocsp/OCSPIssuer.java create mode 100644 src/club/wpia/gigi/ocsp/OCSPIssuerId.java create mode 100644 src/club/wpia/gigi/ocsp/OCSPIssuerManager.java create mode 100644 src/club/wpia/gigi/ocsp/OCSPResponder.java diff --git a/src/club/wpia/gigi/Launcher.java b/src/club/wpia/gigi/Launcher.java index b4ae8e2a..fdcc739b 100644 --- a/src/club/wpia/gigi/Launcher.java +++ b/src/club/wpia/gigi/Launcher.java @@ -58,6 +58,7 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import club.wpia.gigi.api.GigiAPI; import club.wpia.gigi.email.EmailProvider; import club.wpia.gigi.natives.SetUID; +import club.wpia.gigi.ocsp.OCSPResponder; import club.wpia.gigi.util.CipherInfo; import club.wpia.gigi.util.PEM; import club.wpia.gigi.util.ServerConstants; @@ -306,7 +307,7 @@ public class Launcher { private void initHandlers() throws GeneralSecurityException, IOException { HandlerList hl = new HandlerList(); hl.setHandlers(new Handler[] { - ContextLauncher.generateStaticContext(), ContextLauncher.generateGigiContexts(conf.getMainProps(), conf.getTrustStore()), ContextLauncher.generateAPIContext() + ContextLauncher.generateStaticContext(), ContextLauncher.generateGigiContexts(conf.getMainProps(), conf.getTrustStore()), ContextLauncher.generateAPIContext(), ContextLauncher.generateOCSPContext() }); s.setHandler(hl); } @@ -395,6 +396,15 @@ public class Launcher { return sch; } + protected static Handler generateOCSPContext() { + ServletContextHandler sch = new ServletContextHandler(); + + sch.addVirtualHosts(new String[] { + ServerConstants.getHostName(Host.OCSP_RESPONDER) + }); + sch.addServlet(new ServletHolder(new OCSPResponder()), "/*"); + return sch; + } } } diff --git a/src/club/wpia/gigi/crypto/OCSPRequest.java b/src/club/wpia/gigi/crypto/OCSPRequest.java new file mode 100644 index 00000000..180297e3 --- /dev/null +++ b/src/club/wpia/gigi/crypto/OCSPRequest.java @@ -0,0 +1,159 @@ +package club.wpia.gigi.crypto; + +import java.io.IOException; +import java.security.cert.Extension; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import sun.security.provider.certpath.CertId; +import sun.security.util.DerInputStream; +import sun.security.util.DerOutputStream; +import sun.security.util.DerValue; +import sun.security.util.ObjectIdentifier; + +/** + * Adapted from {@link sun.security.provider.certpath.OCSPRequest} + */ +public class OCSPRequest { + + static final ObjectIdentifier NONCE_EXTENSION_OID = ObjectIdentifier.newInternal(new int[] { + 1, 3, 6, 1, 5, 5, 7, 48, 1, 2 + }); + + // List of request CertIds + private final List certIds; + + private final List extensions; + + private byte[] nonce; + + private Extension nonceExt; + + /* + * Constructs an OCSPRequest. This constructor is used to construct an + * unsigned OCSP Request for a single user cert. + */ + OCSPRequest(CertId certId) { + this(Collections.singletonList(certId)); + } + + OCSPRequest(List certIds) { + this.certIds = certIds; + this.extensions = Collections.emptyList(); + } + + OCSPRequest(List certIds, List extensions) { + this.certIds = certIds; + this.extensions = extensions; + } + + /** + * Creates a new OCSPRequest from its binary data. + * + * @param in + * the binary form of the OCSP request. + * @throws IOException + * if the input is malformed + */ + public OCSPRequest(byte[] in) throws IOException { + DerInputStream dis = new DerInputStream(in); + DerInputStream req = dis.getDerValue().getData(); + DerInputStream tbsreq = req.getDerValue().getData(); + // req.getDerValue()optional signature + + LinkedList exts = new LinkedList<>(); + LinkedList cis = new LinkedList<>(); + // handles the content of structure + // @formatter:off + //TBSRequest ::= SEQUENCE { + // version [0] EXPLICIT Version DEFAULT v1, + // requestorName [1] EXPLICIT GeneralName OPTIONAL, + // requestList SEQUENCE OF Request, + // requestExtensions [2] EXPLICIT Extensions OPTIONAL } + // @formatter:on + while (tbsreq.available() > 0) { + // Handle content + if (tbsreq.peekByte() == DerValue.tag_Sequence) { + for (DerValue certId : tbsreq.getSequence(1)) { + CertId ci = new CertId(certId.getData().getDerValue().getData()); + cis.add(ci); + } + // Handle extensions + } else if (tbsreq.peekByte() == DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 2)) { + DerValue[] seq = tbsreq.getDerValue().getData().getSequence(5); + for (DerValue derValue : seq) { + sun.security.x509.Extension e = new sun.security.x509.Extension(derValue); + if (e.getExtensionId().equals((Object) NONCE_EXTENSION_OID)) { + nonce = e.getValue(); + nonceExt = e; + } else if (e.isCritical()) { + throw new IOException("Unknown critical extension"); + } + + exts.add(e); + } + // Skip any other element + } else { + tbsreq.getDerValue(); + } + } + + if (exts.isEmpty()) { + extensions = null; + } else { + extensions = Collections.unmodifiableList(exts); + } + certIds = Collections.unmodifiableList(cis); + } + + byte[] encodeBytes() throws IOException { + + // encode tbsRequest + DerOutputStream tmp = new DerOutputStream(); + DerOutputStream requestsOut = new DerOutputStream(); + for (CertId certId : certIds) { + DerOutputStream certIdOut = new DerOutputStream(); + certId.encode(certIdOut); + requestsOut.write(DerValue.tag_Sequence, certIdOut); + } + + tmp.write(DerValue.tag_Sequence, requestsOut); + if ( !extensions.isEmpty()) { + DerOutputStream extOut = new DerOutputStream(); + for (Extension ext : extensions) { + ext.encode(extOut); + if (ext.getId().equals(NONCE_EXTENSION_OID.toString())) { + nonce = ext.getValue(); + nonceExt = ext; + } + } + DerOutputStream extsOut = new DerOutputStream(); + extsOut.write(DerValue.tag_Sequence, extOut); + tmp.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 2), extsOut); + } + + DerOutputStream tbsRequest = new DerOutputStream(); + tbsRequest.write(DerValue.tag_Sequence, tmp); + + // OCSPRequest without the signature + DerOutputStream ocspRequest = new DerOutputStream(); + ocspRequest.write(DerValue.tag_Sequence, tbsRequest); + + byte[] bytes = ocspRequest.toByteArray(); + + return bytes; + } + + public List getCertIds() { + return certIds; + } + + byte[] getNonce() { + return nonce; + } + + public Extension getNonceExt() { + return nonceExt; + } +} diff --git a/src/club/wpia/gigi/crypto/OCSPResponse.java b/src/club/wpia/gigi/crypto/OCSPResponse.java new file mode 100644 index 00000000..6fb48eef --- /dev/null +++ b/src/club/wpia/gigi/crypto/OCSPResponse.java @@ -0,0 +1,243 @@ +package club.wpia.gigi.crypto; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Signature; +import java.security.cert.CRLReason; +import java.security.cert.Extension; +import java.security.cert.X509Certificate; +import java.util.Date; + +import javax.security.auth.x500.X500Principal; + +import sun.security.provider.certpath.CertId; +import sun.security.util.DerOutputStream; +import sun.security.util.DerValue; +import sun.security.util.ObjectIdentifier; +import sun.security.x509.AlgorithmId; + +public class OCSPResponse { + + private Extension nonceExt; + + public static class SingleResponse { + + private final CertId target; + + private final Date thisUpdate; + + private final Date nextUpdate; + + private final Date revoked; + + private final CRLReason res; + + private final boolean unknown; + + public SingleResponse(CertId target, Date thisUpdate, Date nextUpdate) { + this(target, thisUpdate, nextUpdate, null); + } + + public SingleResponse(CertId target, Date thisUpdate, Date nextUpdate, Date revoked) { + this(target, thisUpdate, nextUpdate, revoked, null); + } + + public SingleResponse(CertId target, Date thisUpdate, Date nextUpdate, Date revoked, CRLReason res) { + this.target = target; + this.thisUpdate = thisUpdate; + this.nextUpdate = nextUpdate; + this.revoked = revoked; + this.res = res; + unknown = false; + } + + public SingleResponse(CertId target, Date thisUpdate, Date nextUpdate, boolean unkown) { + this.target = target; + this.thisUpdate = thisUpdate; + this.nextUpdate = nextUpdate; + this.revoked = null; + this.res = null; + this.unknown = unkown; + } + + private DerValue produceSingleResponse() throws IOException { + try (DerOutputStream r = new DerOutputStream()) { + try (DerOutputStream target = new DerOutputStream()) { + this.target.encode(target); + if (revoked == null && !unknown) { + target.putTag(DerValue.TAG_CONTEXT, false, (byte) 0); + target.write(0); + } else if (revoked == null && unknown) { + target.putTag(DerValue.TAG_CONTEXT, false, (byte) 2); + target.write(0); + } else { + try (DerOutputStream gt = new DerOutputStream()) { + gt.putGeneralizedTime(revoked); + // revocationReason [0] EXPLICIT CRLReason OPTIONAL + if (res != null) { + try (DerOutputStream crlr = new DerOutputStream()) { + crlr.putEnumerated(res.ordinal()); + gt.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 0), crlr); + } + } + + target.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 1), gt); + } + + } + target.putGeneralizedTime(thisUpdate); + try (DerOutputStream gt = new DerOutputStream()) { + gt.putGeneralizedTime(nextUpdate); + target.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 0), gt); + } + + r.write(DerValue.tag_Sequence, target); + } + return new DerValue(r.toByteArray()); + } + } + } + + private final SingleResponse[] res; + + private X509Certificate[] signers; + + private final X500Principal dn; + + private final byte[] keyHash; + + public OCSPResponse(X500Principal dn, SingleResponse[] res) { + this.dn = dn; + keyHash = null; + this.res = res; + } + + public OCSPResponse(byte[] keyHash, SingleResponse[] res) { + dn = null; + this.keyHash = keyHash; + this.res = res; + } + + private OCSPResponse() { + dn = null; + res = null; + keyHash = null; + } + + public void setSigners(X509Certificate[] signers) { + this.signers = signers; + } + + /** + * Produce possibly signed binary data for this OCSPResponse + * + * @param s + * the signature to sign the data with. Always required for + * publicly visible instance. + * @return the binary representation + * @throws IOException + * if IO fails. + * @throws GeneralSecurityException + * if signing fails. + */ + public byte[] produceResponce(Signature s) throws IOException, GeneralSecurityException { + try (DerOutputStream dos2 = new DerOutputStream()) { + try (DerOutputStream dos = new DerOutputStream()) { + if (res != null) { + dos.putEnumerated(0); // successful + ObjectIdentifier ocspBasic = new ObjectIdentifier(new int[] { + 1, 3, 6, 1, 5, 5, 7, 48, 1, 1 + }); + try (DerOutputStream tagS = new DerOutputStream()) { + try (DerOutputStream responseBytes = new DerOutputStream()) { + responseBytes.putOID(ocspBasic); + responseBytes.putOctetString(produceBasicOCSPResponse(s)); + tagS.write(DerValue.tag_Sequence, responseBytes); + } + dos.write((byte) 0xA0, tagS); + } + } else { + dos.putEnumerated(1); // malformed request + } + + dos2.write(DerValue.tag_Sequence, dos); + } + return dos2.toByteArray(); + } + + } + + private byte[] produceBasicOCSPResponse(Signature s) throws IOException, GeneralSecurityException { + + try (DerOutputStream o = new DerOutputStream()) { + try (DerOutputStream basicReponse = new DerOutputStream()) { + produceResponseData(basicReponse); + byte[] toSign = basicReponse.toByteArray(); + + AlgorithmId.get(s.getAlgorithm()).encode(basicReponse); + s.update(toSign); + basicReponse.putBitString(s.sign()); + + if (signers != null) { + try (DerOutputStream certSeq = new DerOutputStream()) { + try (DerOutputStream certs = new DerOutputStream()) { + for (X509Certificate signer : signers) { + certs.write(signer.getEncoded()); + } + certSeq.write(DerValue.tag_Sequence, certs); + } + basicReponse.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 0), certSeq); + } + } + + o.write(DerValue.tag_Sequence, basicReponse.toByteArray()); + } + return o.toByteArray(); + } + + } + + private void produceResponseData(DerOutputStream basicReponse) throws IOException { + try (DerOutputStream tbsResp = new DerOutputStream()) { + produceResponderId(tbsResp); + tbsResp.putGeneralizedTime(new Date(System.currentTimeMillis())); + DerValue[] tgt = new DerValue[res.length]; + int i = 0; + for (SingleResponse c : res) { + tgt[i++] = c.produceSingleResponse(); + } + tbsResp.putSequence(tgt); + + if (nonceExt != null) { + try (DerOutputStream extsSeq = new DerOutputStream()) { + try (DerOutputStream extsOut = new DerOutputStream()) { + nonceExt.encode(extsOut); + extsSeq.write(DerValue.tag_Sequence, extsOut); + tbsResp.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 1), extsSeq); + } + } + } + basicReponse.write(DerValue.tag_Sequence, tbsResp.toByteArray()); + } + } + + private void produceResponderId(DerOutputStream tbsResp) throws IOException { + if (dn != null) { + tbsResp.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 1), dn.getEncoded()); + } else { + try (DerOutputStream dos = new DerOutputStream()) { + dos.putOctetString(keyHash); + tbsResp.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 2), dos); + } + // by hash + } + } + + public void updateNonce(OCSPRequest or) { + nonceExt = or.getNonceExt(); + } + + public static byte[] invalid() throws IOException, GeneralSecurityException { + return new OCSPResponse().produceResponce(null); + } +} diff --git a/src/club/wpia/gigi/ocsp/OCSPIssuer.java b/src/club/wpia/gigi/ocsp/OCSPIssuer.java new file mode 100644 index 00000000..29fb527c --- /dev/null +++ b/src/club/wpia/gigi/ocsp/OCSPIssuer.java @@ -0,0 +1,113 @@ +package club.wpia.gigi.ocsp; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.cert.CRLReason; +import java.security.cert.X509Certificate; +import java.util.Date; + +import club.wpia.gigi.crypto.OCSPRequest; +import club.wpia.gigi.crypto.OCSPResponse; +import club.wpia.gigi.crypto.OCSPResponse.SingleResponse; +import club.wpia.gigi.dbObjects.CACertificate; +import club.wpia.gigi.dbObjects.Certificate; +import sun.security.provider.certpath.CertId; + +/** + * An instance that creates OCSP responses. + */ +public class OCSPIssuer { + + /** + * The CA certificate to issue OCSP responses for. + */ + private final X509Certificate target; + + /** + * The OCSP certificate for which we have the private key. + */ + private final X509Certificate cert; + + /** + * The OCSP certificate's private key to sign the responses with. + */ + private final PrivateKey key; + + private final byte[] subjectKeyIdentifier; + + public OCSPIssuer(X509Certificate target, X509Certificate x, PrivateKey key) throws IOException, GeneralSecurityException { + this.target = target; + this.cert = x; + this.key = key; + this.subjectKeyIdentifier = OCSPResponder.calcKeyHash(cert, MessageDigest.getInstance("SHA-1")); + } + + public X509Certificate getTarget() { + return target; + } + + public byte[] getKeyId() { + return subjectKeyIdentifier; + } + + private SingleResponse respond(CertId id, Certificate cert) { + if (cert != null) { + Date dt = cert.getRevocationDate(); + if (dt != null) { + return new OCSPResponse.SingleResponse(id, new Date(System.currentTimeMillis() - 10000), new Date(System.currentTimeMillis() + 10000), dt, CRLReason.UNSPECIFIED); + } else { + return new OCSPResponse.SingleResponse(id, new Date(System.currentTimeMillis() - 10000), new Date(System.currentTimeMillis() + 10000)); + } + } else { + return new OCSPResponse.SingleResponse(id, new Date(System.currentTimeMillis() - 10000), new Date(System.currentTimeMillis() + 10000), true); + } + } + + /** + * Responds with the status of one certificate. + * + * @param req + * the {@link OCSPRequest} to take the nonce from. + * @param id + * The certificate for which to look up revocation information. + * @return the signed {@link OCSPResponse} in binary data. + * @throws GeneralSecurityException + * if signing fails + * @throws IOException + * if encoding fails + */ + public byte[] respondBytes(OCSPRequest req, CertId id) throws GeneralSecurityException, IOException { + Certificate tcert = Certificate.getBySerial(id.getSerialNumber().toString(16).toLowerCase()); + if (tcert == null) { + return OCSPResponse.invalid(); + } + CACertificate cc = tcert.getParent(); + if ( !cc.getCertificate().getSubjectDN().equals(getTarget().getSubjectDN())) { + tcert = null; + OCSPResponder.log.warning("OCSP request with different Issuer: Based on serial: " + cc.getCertificate().getSubjectDN() + " but based on request: " + getTarget().getSubjectDN()); + return OCSPResponse.invalid(); + } + + SingleResponse[] responses = new OCSPResponse.SingleResponse[1]; + responses[0] = respond(id, tcert); + + OCSPResponse ocspResponse = new OCSPResponse(getKeyId(), responses); + if (cert != getTarget()) { + ocspResponse.setSigners(new X509Certificate[] { + cert + }); + } else { + ocspResponse.setSigners(new X509Certificate[] { + // getCert() + }); + + } + ocspResponse.updateNonce(req); + Signature s = Signature.getInstance("SHA512WithRSA"); + s.initSign(key); + return ocspResponse.produceResponce(s); + } +} diff --git a/src/club/wpia/gigi/ocsp/OCSPIssuerId.java b/src/club/wpia/gigi/ocsp/OCSPIssuerId.java new file mode 100644 index 00000000..63c9f90d --- /dev/null +++ b/src/club/wpia/gigi/ocsp/OCSPIssuerId.java @@ -0,0 +1,99 @@ +package club.wpia.gigi.ocsp; + +import java.security.MessageDigest; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.security.auth.x500.X500Principal; + +import sun.security.provider.certpath.CertId; +import sun.security.x509.AlgorithmId; + +/** + * Idenfies an {@link OCSPIssuer} by remembering its public key hash and its + * name hash together with the used hash algorithm. A {@link OCSPIssuer} can be + * identified by several {@link OCSPIssuerId}s when they use different hash + * algorithms. + */ +public class OCSPIssuerId { + + private final byte[] keyHash; + + private final byte[] nameHash; + + private final AlgorithmId alg; + + /** + * Creates a new OCSPIssuerId for a given {@link OCSPIssuer}. The hash + * algorithm has to be specified twice, once for description purposes as + * {@link AlgorithmId} and once instantiated as {@link MessageDigest}. + * + * @param alg + * the description of the hash algorithm + * @param md + * the instantiated hash algorithm + * @param iss + * the issuer to hash. + */ + public OCSPIssuerId(AlgorithmId alg, MessageDigest md, X509Certificate target) { + X500Principal dn = target.getSubjectX500Principal(); + this.keyHash = OCSPResponder.calcKeyHash(target, md); + this.nameHash = md.digest(dn.getEncoded()); + this.alg = alg; + } + + /** + * Creates a new OCSPIssuerId from the {@link CertId} inside an OCSP + * request. + * + * @param id + * the {@link CertId} + */ + public OCSPIssuerId(CertId id) { + keyHash = id.getIssuerKeyHash(); + nameHash = id.getIssuerNameHash(); + alg = id.getHashAlgorithm(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((alg == null) ? 0 : alg.hashCode()); + result = prime * result + Arrays.hashCode(keyHash); + result = prime * result + Arrays.hashCode(nameHash); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + OCSPIssuerId other = (OCSPIssuerId) obj; + if (alg == null) { + if (other.alg != null) { + return false; + } + } else if ( !alg.equals(other.alg)) { + return false; + } + if ( !Arrays.equals(keyHash, other.keyHash)) { + return false; + } + if ( !Arrays.equals(nameHash, other.nameHash)) { + return false; + } + return true; + } + + public AlgorithmId getAlg() { + return alg; + } +} diff --git a/src/club/wpia/gigi/ocsp/OCSPIssuerManager.java b/src/club/wpia/gigi/ocsp/OCSPIssuerManager.java new file mode 100644 index 00000000..85296864 --- /dev/null +++ b/src/club/wpia/gigi/ocsp/OCSPIssuerManager.java @@ -0,0 +1,270 @@ +package club.wpia.gigi.ocsp; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import club.wpia.gigi.util.PEM; +import sun.security.pkcs10.PKCS10; +import sun.security.pkcs10.PKCS10Attributes; +import sun.security.x509.AlgorithmId; +import sun.security.x509.X500Name; + +/** + * Manages the set of {@link OCSPIssuer}s by updating their OCSP certificates + * and renewing the issuers. The Thread executing all the management work has to + * be started manually. The {@link #get(AlgorithmId)} method provides a + * requested set of issuers. + */ +public class OCSPIssuerManager implements Runnable { + + private final Map openRequests = new HashMap<>(); + + private Map> map = new HashMap<>(); + + private long nextHousekeeping; + + private boolean isGood(PrivateKey k, Certificate target, Certificate parent) throws GeneralSecurityException { + X509Certificate ocsp = (X509Certificate) target; + if ( !ocsp.getExtendedKeyUsage().contains("1.3.6.1.5.5.7.3.9")) { + OCSPResponder.log.severe("OCSP cert does not have correct EKU set."); + return false; + } + target.verify(parent.getPublicKey()); + if ( !matches(k, target.getPublicKey())) { + OCSPResponder.log.severe("Public key contained in cert does not match."); + return false; + } + return true; + } + + private boolean matches(PrivateKey k, PublicKey pk) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + SecureRandom ref = new SecureRandom(); + Signature s = Signature.getInstance("SHA512WithRSA"); + s.initSign(k); + byte[] data = new byte[20]; + ref.nextBytes(data); + s.update(data); + byte[] signature = s.sign(); + s.initVerify(pk); + s.update(data); + boolean verify = s.verify(signature); + return verify; + } + + private void index(AlgorithmId aid, MessageDigest md, Map toServe, Map> map) { + OCSPResponder.log.info("Indexing OCSP issuers for " + md); + HashMap issuers = new HashMap<>(); + for (OCSPIssuer i : toServe.values()) { + issuers.put(new OCSPIssuerId(aid, md, i.getTarget()), i); + } + map.put(aid, Collections.unmodifiableMap(issuers)); + } + + /** + * Scans for CAs to issue OCSP responses for in the directory f. + * + * @param f + * The directory to scan recursively. + * @param keys + * a keystore with all private keys for all OCSP certificates + * (will be used and updated with new ocsp certs) + * @param toServe + * A map with {@link OCSPIssuer}s to be populated with all + * scanned CAs + */ + private void scanAndUpdateCAs(File f, KeyStore keys, Map toServe) { + if (f.isDirectory()) { + for (File f1 : f.listFiles()) { + scanAndUpdateCAs(f1, keys, toServe); + } + return; + } + if ( !f.getName().equals("ca.crt")) { + return; + } + try { + String keyName = f.getParentFile().getName(); + OCSPResponder.log.info("CA: " + keyName); + updateCA(f, keyName, keys, toServe); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + private void updateCA(File f, String keyName, KeyStore keys, Map toServe) throws GeneralSecurityException, IOException { + X509Certificate parent = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new FileInputStream(f)); + + Certificate[] storedCertificateChain = keys.getCertificateChain(keyName); + + if (storedCertificateChain == null) { + OCSPResponder.log.info("Keystore entry for OCSP certificate for CA " + keyName + " was not found"); + } else { + if ( !storedCertificateChain[1].equals(parent)) { + OCSPResponder.log.severe("unexpeced CA certificate in keystore entry for OCSP certificate."); + return; + } + PrivateKey key = (PrivateKey) keys.getKey(keyName, "pass".toCharArray()); + isGood(key, storedCertificateChain[0], storedCertificateChain[1]); + toServe.put(keyName, new OCSPIssuer((X509Certificate) storedCertificateChain[1], (X509Certificate) storedCertificateChain[0], key)); + } + boolean hasKeyRequest = openRequests.containsKey(keyName); + File ocspCsr = new File(f.getParentFile(), "ocsp.csr"); + File ocspCrt = new File(f.getParentFile(), "ocsp.crt"); + if (hasKeyRequest) { + KeyPair r = openRequests.get(keyName); + if (ocspCrt.exists()) { + X509Certificate x = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new FileInputStream(ocspCrt)); + // attempt to load ocspCrt, try if it matches with the + // given key. + // if it does match: load crt with key into primary + // keystore entry. Update Issuer + if (r.getPublic().equals(x.getPublicKey()) && isGood(r.getPrivate(), x, parent)) { + OCSPResponder.log.info("Loading OCSP Certificate"); + keys.setKeyEntry(keyName, r.getPrivate(), "pass".toCharArray(), new Certificate[] { + x, parent + }); + openRequests.remove(keyName); + ocspCsr.delete(); + ocspCrt.delete(); + toServe.put(keyName, new OCSPIssuer(parent, x, r.getPrivate())); + } else { + // if it does not match: check CSR + OCSPResponder.log.severe("OCSP certificate does not fit."); + } + } else { + // Seems the signer should work now.. Let's wait for + // now. + } + } else { + if (keys.containsAlias(keyName)) { + X509Certificate c = (X509Certificate) keys.getCertificate(keyName); + Date expiery = c.getNotAfter(); + Date now = new Date(); + long deltas = expiery.getTime() - now.getTime(); + deltas /= 1000; + deltas /= 60 * 60 * 24; + OCSPResponder.log.info("Remaining days for OCSP certificate: " + deltas); + if (deltas > 30 * 3) { + return; + } + } + OCSPResponder.log.info("Requesting OCSP certificate"); + // request a new OCSP certificate with a new RSA-key. + requestNewOCSPCert(keyName, ocspCsr, ocspCrt); + + // assuming this cert will be ready in 30 seconds + nextHousekeeping = Math.min(nextHousekeeping, System.currentTimeMillis() + 30 * 1000); + } + } + + private void requestNewOCSPCert(String keyName, File ocspCsr, File ocspCrt) throws IOException, GeneralSecurityException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(4096); + KeyPair kp = kpg.generateKeyPair(); + openRequests.put(keyName, kp); + PKCS10 p10 = new PKCS10(kp.getPublic(), new PKCS10Attributes()); + Signature s = Signature.getInstance("SHA512WithRSA"); + s.initSign(kp.getPrivate()); + p10.encodeAndSign(new X500Name("CN=OSCP Responder"), s); + ocspCsr.delete(); + ocspCrt.delete(); + String csr = PEM.encode("CERTIFICATE REQUEST", p10.getEncoded()); + try (Writer w = new OutputStreamWriter(new FileOutputStream(ocspCsr), "UTF-8")) { + w.write(csr); + } + } + + @Override + public void run() { + File ks = new File("ocsp.pkcs12"); + File f = new File("ocsp"); + if ( !ks.exists() || !ks.isFile() || !f.exists() || !f.isDirectory()) { + OCSPResponder.log.info("OCSP issuing is not configured"); + return; + } + while (true) { + try { + // Hourly is enough + nextHousekeeping = System.currentTimeMillis() + 60 * 60 * 1000; + KeyStore keys; + + try { + keys = KeyStore.getInstance("PKCS12"); + if (ks.exists()) { + if (ks.length() == 0) { + keys.load(null); + } else { + keys.load(new FileInputStream(ks), "pass".toCharArray()); + } + } else { + // assuming ocsp is disabled + return; + } + } catch (GeneralSecurityException e) { + throw new Error(e); + } catch (IOException e) { + throw new Error(e); + } + Map toServe = new HashMap<>(); + + scanAndUpdateCAs(f, keys, toServe); + try { + keys.store(new FileOutputStream(ks), "pass".toCharArray()); + } catch (GeneralSecurityException e) { + throw new Error(e); + } catch (IOException e) { + throw new Error(e); + } + + try { + Map> map = new HashMap<>(); + index(AlgorithmId.get("SHA-1"), MessageDigest.getInstance("SHA-1"), toServe, map); + index(AlgorithmId.get("SHA-256"), MessageDigest.getInstance("SHA-256"), toServe, map); + synchronized (this) { + this.map = Collections.unmodifiableMap(map); + } + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } catch (Throwable t) { + t.printStackTrace(); + } + try { + long dt = Math.max(3000, nextHousekeeping - System.currentTimeMillis()); + Thread.sleep(dt); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + public synchronized Map get(AlgorithmId alg) { + return map.get(alg); + } +} diff --git a/src/club/wpia/gigi/ocsp/OCSPResponder.java b/src/club/wpia/gigi/ocsp/OCSPResponder.java new file mode 100644 index 00000000..27e68694 --- /dev/null +++ b/src/club/wpia/gigi/ocsp/OCSPResponder.java @@ -0,0 +1,133 @@ +package club.wpia.gigi.ocsp; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpMethod; + +import club.wpia.gigi.crypto.OCSPRequest; +import club.wpia.gigi.crypto.OCSPResponse; +import club.wpia.gigi.database.DatabaseConnection; +import club.wpia.gigi.database.DatabaseConnection.Link; +import sun.security.provider.certpath.CertId; +import sun.security.util.DerInputStream; +import sun.security.util.DerValue; + +/** + * This is the entry point for OCSP Issuing + */ +public class OCSPResponder extends HttpServlet { + + static final Logger log = Logger.getLogger(OCSPResponder.class.getName()); + + private static final long serialVersionUID = 1L; + + private final OCSPIssuerManager mgm = new OCSPIssuerManager(); + + public OCSPResponder() {} + + public static byte[] calcKeyHash(X509Certificate x, MessageDigest md) { + try { + DerInputStream dis = new DerInputStream(x.getPublicKey().getEncoded()); + DerValue[] seq = dis.getSequence(2); + byte[] bitString = seq[1].getBitString(); + return md.digest(bitString); + } catch (IOException e) { + throw new Error(e); + } + } + + @Override + public void init() throws ServletException { + super.init(); + new Thread(mgm).start(); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try (Link l = DatabaseConnection.newLink(true)) { + byte[] bytes; + if (req.getMethod().equals(HttpMethod.POST.toString())) { + bytes = getBytes(req); + if (bytes == null) { + resp.sendError(500); + resp.getWriter().println("OCSP request too large"); + return; + } + } else { + bytes = Base64.getDecoder().decode(req.getPathInfo().substring(1)); + } + OCSPRequest or = new OCSPRequest(bytes); + byte[] res = respond(or); + resp.setContentType("application/ocsp-response"); + resp.getOutputStream().write(res); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + + private byte[] respond(OCSPRequest or) throws GeneralSecurityException, IOException { + List ids = or.getCertIds(); + if (ids.size() != 1) { + // We don't implement multi-requests as: + // a) we don't know of applications using them + // b) this will introduce additional complexity + // c) there is at least one corner-case that needs to be thought of: + // an OCSP request might contain requests for certs from different + // issuers, what issuer's ocsp cert should sign the response? + return OCSPResponse.invalid(); + } + CertId id = ids.get(0); + OCSPIssuerId iid = new OCSPIssuerId(id); + Map m0; + m0 = mgm.get(iid.getAlg()); + if (m0 == null) { + log.warning("Algorithm " + iid.getAlg() + " not indexed."); + return OCSPResponse.invalid(); + } + OCSPIssuer iss = m0.get(iid); + + if (iss == null) { + log.warning("CertID not handled:\n" +// + Base64.getEncoder().encodeToString(id.getIssuerNameHash()) + "\n"// + + Base64.getEncoder().encodeToString(id.getIssuerKeyHash()) + "\n" // + + id.getHashAlgorithm() + "\n"// + + id.getSerialNumber().toString(16)); + + return OCSPResponse.invalid(); + } + return iss.respondBytes(or, id); + } + + private byte[] getBytes(HttpServletRequest req) throws IOException { + InputStream i = req.getInputStream(); + ByteArrayOutputStream o = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int len; + while ((len = i.read(buf)) > 0) { + o.write(buf, 0, len); + if (o.size() > 64 * 1024) { + // for now have 64k as maximum + return null; + } + } + byte[] dat = o.toByteArray(); + return dat; + } + +} diff --git a/src/club/wpia/gigi/util/ServerConstants.java b/src/club/wpia/gigi/util/ServerConstants.java index 3fd2e70e..4e2d9c92 100644 --- a/src/club/wpia/gigi/util/ServerConstants.java +++ b/src/club/wpia/gigi/util/ServerConstants.java @@ -5,6 +5,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Properties; +import club.wpia.gigi.ocsp.OCSPResponder; + public class ServerConstants { public enum Host { @@ -35,7 +37,11 @@ public class ServerConstants { * Hosts the certificate repository for the certificates generated * during NRE. Also not served by Gigi. */ - CRT_REPO("g2.crt"); + CRT_REPO("g2.crt"), + /** + * Hosts the {@link OCSPResponder}. + */ + OCSP_RESPONDER("g2.ocsp"); private final String value; diff --git a/util-testing/club/wpia/gigi/util/SimpleSigner.java b/util-testing/club/wpia/gigi/util/SimpleSigner.java index 5a2a5fa1..76edd6ed 100644 --- a/util-testing/club/wpia/gigi/util/SimpleSigner.java +++ b/util-testing/club/wpia/gigi/util/SimpleSigner.java @@ -41,17 +41,16 @@ import javax.security.auth.x500.X500Principal; import club.wpia.gigi.crypto.SPKAC; import club.wpia.gigi.database.DatabaseConnection; +import club.wpia.gigi.database.DatabaseConnection.Link; import club.wpia.gigi.database.GigiPreparedStatement; import club.wpia.gigi.database.GigiResultSet; -import club.wpia.gigi.database.DatabaseConnection.Link; -import club.wpia.gigi.dbObjects.CertificateProfile; -import club.wpia.gigi.dbObjects.Digest; import club.wpia.gigi.dbObjects.Certificate.CSRType; import club.wpia.gigi.dbObjects.Certificate.SANType; import club.wpia.gigi.dbObjects.Certificate.SubjectAlternateName; +import club.wpia.gigi.dbObjects.CertificateProfile; +import club.wpia.gigi.dbObjects.Digest; import club.wpia.gigi.output.DateSelector; -import club.wpia.gigi.util.KeyStorage; -import club.wpia.gigi.util.PEM; +import club.wpia.gigi.util.ServerConstants.Host; import sun.security.pkcs10.PKCS10; import sun.security.util.DerOutputStream; import sun.security.util.DerValue; @@ -96,6 +95,7 @@ public class SimpleSigner { try (Reader reader = new InputStreamReader(new FileInputStream("config/gigi.properties"), "UTF-8")) { p.load(reader); } + ServerConstants.init(p); DatabaseConnection.init(p); runSigner(); @@ -474,6 +474,9 @@ public class SimpleSigner { addExtension(extensions, new ObjectIdentifier(new int[] { 2, 5, 29, 37 }), generateEKU(eku)); + addExtension(extensions, new ObjectIdentifier(new int[] { + 1, 3, 6, 1, 5, 5, 7, 1, 1 + }), generateAIA()); } DerOutputStream extensionsSeq = new DerOutputStream(); extensionsSeq.write(DerValue.tag_Sequence, extensions); @@ -503,6 +506,22 @@ public class SimpleSigner { } + private static byte[] generateAIA() throws IOException { + try (DerOutputStream dos = new DerOutputStream()) { + try (DerOutputStream seq = new DerOutputStream()) { + seq.putOID(new ObjectIdentifier(new int[] { + 1, 3, 6, 1, 5, 5, 7, 48, 2 + })); + seq.write((byte) 0x86, ("http://" + ServerConstants.getHostName(Host.OCSP_RESPONDER)).getBytes("UTF-8")); + dos.write(DerValue.tag_Sequence, seq); + } + byte[] data = dos.toByteArray(); + dos.reset(); + dos.write(DerValue.tag_Sequence, data); + return dos.toByteArray(); + } + } + private static byte[] generateKU() throws IOException { try (DerOutputStream dos = new DerOutputStream()) { dos.putBitString(new byte[] { -- 2.39.2