]> WPIA git - gigi.git/commitdiff
add: implement OCSP serving
authorFelix Dörre <felix@dogcraft.de>
Fri, 11 Dec 2015 11:23:56 +0000 (12:23 +0100)
committerFelix Dörre <felix@dogcraft.de>
Mon, 26 Jun 2017 15:51:21 +0000 (17:51 +0200)
Change-Id: I2a8aa170d79b9e77dbb951b01ed1c4b5186f3440

src/club/wpia/gigi/Launcher.java
src/club/wpia/gigi/crypto/OCSPRequest.java [new file with mode: 0644]
src/club/wpia/gigi/crypto/OCSPResponse.java [new file with mode: 0644]
src/club/wpia/gigi/ocsp/OCSPIssuer.java [new file with mode: 0644]
src/club/wpia/gigi/ocsp/OCSPIssuerId.java [new file with mode: 0644]
src/club/wpia/gigi/ocsp/OCSPIssuerManager.java [new file with mode: 0644]
src/club/wpia/gigi/ocsp/OCSPResponder.java [new file with mode: 0644]
src/club/wpia/gigi/util/ServerConstants.java
util-testing/club/wpia/gigi/util/SimpleSigner.java

index b4ae8e2ae007168cdea9affa4c3fd5e4e0a538a4..fdcc739b7074112541dd161acf232b9331de39cf 100644 (file)
@@ -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 (file)
index 0000000..180297e
--- /dev/null
@@ -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<CertId> certIds;
+
+    private final List<Extension> 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<CertId> certIds) {
+        this.certIds = certIds;
+        this.extensions = Collections.<Extension>emptyList();
+    }
+
+    OCSPRequest(List<CertId> certIds, List<Extension> 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<Extension> exts = new LinkedList<>();
+        LinkedList<CertId> 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<CertId> 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 (file)
index 0000000..6fb48ee
--- /dev/null
@@ -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 (file)
index 0000000..29fb527
--- /dev/null
@@ -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 (file)
index 0000000..63c9f90
--- /dev/null
@@ -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 (file)
index 0000000..8529686
--- /dev/null
@@ -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<String, KeyPair> openRequests = new HashMap<>();
+
+    private Map<AlgorithmId, Map<OCSPIssuerId, OCSPIssuer>> 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<String, OCSPIssuer> toServe, Map<AlgorithmId, Map<OCSPIssuerId, OCSPIssuer>> map) {
+        OCSPResponder.log.info("Indexing OCSP issuers for " + md);
+        HashMap<OCSPIssuerId, OCSPIssuer> 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<String, OCSPIssuer> 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<String, OCSPIssuer> 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<String, OCSPIssuer> 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<AlgorithmId, Map<OCSPIssuerId, OCSPIssuer>> 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<OCSPIssuerId, OCSPIssuer> 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 (file)
index 0000000..27e6869
--- /dev/null
@@ -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<CertId> 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<OCSPIssuerId, OCSPIssuer> 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;
+    }
+
+}
index 3fd2e70e7f57a72aefff38847c811f84ab0cf7da..4e2d9c928fd82d9f173a486b93773dac5b720e7e 100644 (file)
@@ -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;
 
index 5a2a5fa1f80e77f147464fc63970743404190a46..76edd6ed4c31f16dc3ca2da1752b701312c9e6dc 100644 (file)
@@ -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[] {