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;
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);
}
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;
+ }
}
}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+
+}
import java.util.Map;
import java.util.Properties;
+import club.wpia.gigi.ocsp.OCSPResponder;
+
public class ServerConstants {
public enum Host {
* 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;
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;
try (Reader reader = new InputStreamReader(new FileInputStream("config/gigi.properties"), "UTF-8")) {
p.load(reader);
}
+ ServerConstants.init(p);
DatabaseConnection.init(p);
runSigner();
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);
}
+ 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[] {