]> WPIA git - gigi.git/blob - src/club/wpia/gigi/ocsp/OCSPIssuerManager.java
fix: resource leaks in OCSPIssuerManager
[gigi.git] / src / club / wpia / gigi / ocsp / OCSPIssuerManager.java
1 package club.wpia.gigi.ocsp;
2
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.FileOutputStream;
6 import java.io.IOException;
7 import java.io.OutputStreamWriter;
8 import java.io.Writer;
9 import java.security.GeneralSecurityException;
10 import java.security.InvalidKeyException;
11 import java.security.KeyPair;
12 import java.security.KeyPairGenerator;
13 import java.security.KeyStore;
14 import java.security.MessageDigest;
15 import java.security.NoSuchAlgorithmException;
16 import java.security.PrivateKey;
17 import java.security.PublicKey;
18 import java.security.SecureRandom;
19 import java.security.Signature;
20 import java.security.SignatureException;
21 import java.security.cert.Certificate;
22 import java.security.cert.CertificateFactory;
23 import java.security.cert.X509Certificate;
24 import java.util.Collections;
25 import java.util.Date;
26 import java.util.HashMap;
27 import java.util.Map;
28
29 import club.wpia.gigi.util.PEM;
30 import sun.security.pkcs10.PKCS10;
31 import sun.security.pkcs10.PKCS10Attributes;
32 import sun.security.x509.AlgorithmId;
33 import sun.security.x509.X500Name;
34
35 /**
36  * Manages the set of {@link OCSPIssuer}s by updating their OCSP certificates
37  * and renewing the issuers. The Thread executing all the management work has to
38  * be started manually. The {@link #get(AlgorithmId)} method provides a
39  * requested set of issuers.
40  */
41 public class OCSPIssuerManager implements Runnable {
42
43     private final Map<String, KeyPair> openRequests = new HashMap<>();
44
45     private Map<AlgorithmId, Map<OCSPIssuerId, OCSPIssuer>> map = new HashMap<>();
46
47     private long nextHousekeeping;
48
49     private boolean isGood(PrivateKey k, Certificate target, Certificate parent) throws GeneralSecurityException {
50         X509Certificate ocsp = (X509Certificate) target;
51         if ( !ocsp.getExtendedKeyUsage().contains("1.3.6.1.5.5.7.3.9")) {
52             OCSPResponder.log.severe("OCSP cert does not have correct EKU set.");
53             return false;
54         }
55         target.verify(parent.getPublicKey());
56         if ( !matches(k, target.getPublicKey())) {
57             OCSPResponder.log.severe("Public key contained in cert does not match.");
58             return false;
59         }
60         return true;
61     }
62
63     private boolean matches(PrivateKey k, PublicKey pk) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
64         SecureRandom ref = new SecureRandom();
65         Signature s = Signature.getInstance("SHA512WithRSA");
66         s.initSign(k);
67         byte[] data = new byte[20];
68         ref.nextBytes(data);
69         s.update(data);
70         byte[] signature = s.sign();
71         s.initVerify(pk);
72         s.update(data);
73         boolean verify = s.verify(signature);
74         return verify;
75     }
76
77     private void index(AlgorithmId aid, MessageDigest md, Map<String, OCSPIssuer> toServe, Map<AlgorithmId, Map<OCSPIssuerId, OCSPIssuer>> map) {
78         OCSPResponder.log.fine("Indexing OCSP issuers for " + md);
79         HashMap<OCSPIssuerId, OCSPIssuer> issuers = new HashMap<>();
80         for (OCSPIssuer i : toServe.values()) {
81             issuers.put(new OCSPIssuerId(aid, md, i.getTarget()), i);
82         }
83         map.put(aid, Collections.unmodifiableMap(issuers));
84     }
85
86     /**
87      * Scans for CAs to issue OCSP responses for in the directory f.
88      * 
89      * @param f
90      *            The directory to scan recursively.
91      * @param keys
92      *            a keystore with all private keys for all OCSP certificates
93      *            (will be used and updated with new ocsp certs)
94      * @param toServe
95      *            A map with {@link OCSPIssuer}s to be populated with all
96      *            scanned CAs
97      */
98     private void scanAndUpdateCAs(File f, KeyStore keys, Map<String, OCSPIssuer> toServe) {
99         if (f.isDirectory()) {
100             File[] list = f.listFiles();
101             if (list == null) {
102                 return;
103             }
104
105             for (File file : list) {
106                 scanAndUpdateCAs(file, keys, toServe);
107             }
108
109             return;
110         }
111         if ( !f.getName().equals("ca.crt")) {
112             return;
113         }
114         try {
115             String keyName = f.getParentFile().getName();
116             updateCA(f, keyName, keys, toServe);
117         } catch (GeneralSecurityException e) {
118             e.printStackTrace();
119         } catch (IOException e) {
120             e.printStackTrace();
121         }
122
123     }
124
125     private void updateCA(File f, String keyName, KeyStore keys, Map<String, OCSPIssuer> toServe) throws GeneralSecurityException, IOException {
126         X509Certificate parent = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new FileInputStream(f));
127
128         Certificate[] storedCertificateChain = keys.getCertificateChain(keyName);
129
130         if (storedCertificateChain == null) {
131             OCSPResponder.log.info("Keystore entry for OCSP certificate for CA " + keyName + " was not found");
132         } else {
133             if ( !storedCertificateChain[1].equals(parent)) {
134                 OCSPResponder.log.severe("unexpeced CA certificate in keystore entry for OCSP certificate.");
135                 return;
136             }
137             PrivateKey key = (PrivateKey) keys.getKey(keyName, "pass".toCharArray());
138             isGood(key, storedCertificateChain[0], storedCertificateChain[1]);
139             toServe.put(keyName, new OCSPIssuer((X509Certificate) storedCertificateChain[1], (X509Certificate) storedCertificateChain[0], key));
140         }
141         boolean hasKeyRequest = openRequests.containsKey(keyName);
142         File ocspCsr = new File(f.getParentFile(), "ocsp.csr");
143         File ocspCrt = new File(f.getParentFile(), "ocsp.crt");
144         if (hasKeyRequest) {
145             KeyPair r = openRequests.get(keyName);
146             if (ocspCrt.exists()) {
147                 X509Certificate x = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new FileInputStream(ocspCrt));
148                 // attempt to load ocspCrt, try if it matches with the
149                 // given key.
150                 // if it does match: load crt with key into primary
151                 // keystore entry. Update Issuer
152                 if (r.getPublic().equals(x.getPublicKey()) && isGood(r.getPrivate(), x, parent)) {
153                     OCSPResponder.log.info("Loading OCSP Certificate");
154                     keys.setKeyEntry(keyName, r.getPrivate(), "pass".toCharArray(), new Certificate[] {
155                             x, parent
156                     });
157                     openRequests.remove(keyName);
158                     ocspCsr.delete();
159                     ocspCrt.delete();
160                     toServe.put(keyName, new OCSPIssuer(parent, x, r.getPrivate()));
161                 } else {
162                     // if it does not match: check CSR
163                     OCSPResponder.log.severe("OCSP certificate does not fit.");
164                 }
165             } else {
166                 // Seems the signer should work now.. Let's wait for
167                 // now.
168             }
169         } else {
170             if (keys.containsAlias(keyName)) {
171                 X509Certificate c = (X509Certificate) keys.getCertificate(keyName);
172                 Date expiery = c.getNotAfter();
173                 Date now = new Date();
174                 long deltas = expiery.getTime() - now.getTime();
175                 deltas /= 1000;
176                 deltas /= 60 * 60 * 24;
177                 OCSPResponder.log.fine("Remaining days for OCSP certificate: " + deltas);
178                 if (deltas > 30 * 3) {
179                     return;
180                 }
181             }
182             OCSPResponder.log.info("Requesting OCSP certificate");
183             // request a new OCSP certificate with a new RSA-key.
184             requestNewOCSPCert(keyName, ocspCsr, ocspCrt);
185
186             // assuming this cert will be ready in 30 seconds
187             nextHousekeeping = Math.min(nextHousekeeping, System.currentTimeMillis() + 30 * 1000);
188         }
189     }
190
191     private void requestNewOCSPCert(String keyName, File ocspCsr, File ocspCrt) throws IOException, GeneralSecurityException {
192         KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
193         kpg.initialize(4096);
194         KeyPair kp = kpg.generateKeyPair();
195         openRequests.put(keyName, kp);
196         PKCS10 p10 = new PKCS10(kp.getPublic(), new PKCS10Attributes());
197         Signature s = Signature.getInstance("SHA512WithRSA");
198         s.initSign(kp.getPrivate());
199         p10.encodeAndSign(new X500Name("CN=OSCP Responder"), s);
200         ocspCsr.delete();
201         ocspCrt.delete();
202         String csr = PEM.encode("CERTIFICATE REQUEST", p10.getEncoded());
203         try (FileOutputStream fos = new FileOutputStream(ocspCsr); Writer w = new OutputStreamWriter(fos, "UTF-8")) {
204             w.write(csr);
205         }
206     }
207
208     @Override
209     public void run() {
210         File ks = new File("ocsp.pkcs12");
211         File f = new File("ocsp");
212         if ( !ks.exists() || !ks.isFile() || !f.exists() || !f.isDirectory()) {
213             OCSPResponder.log.info("OCSP issuing is not configured");
214             return;
215         }
216         while (true) {
217             try {
218                 // Hourly is enough
219                 nextHousekeeping = System.currentTimeMillis() + 60 * 60 * 1000;
220                 KeyStore keys;
221
222                 try {
223                     keys = KeyStore.getInstance("PKCS12");
224                     if (ks.exists()) {
225                         if (ks.length() == 0) {
226                             keys.load(null);
227                         } else {
228                             try (FileInputStream ks_file = new FileInputStream(ks)) {
229                                 keys.load(ks_file, "pass".toCharArray());
230                             }
231                         }
232                     } else {
233                         // assuming ocsp is disabled
234                         return;
235                     }
236                 } catch (GeneralSecurityException e) {
237                     throw new Error(e);
238                 } catch (IOException e) {
239                     throw new Error(e);
240                 }
241                 Map<String, OCSPIssuer> toServe = new HashMap<>();
242
243                 scanAndUpdateCAs(f, keys, toServe);
244                 try (FileOutputStream ks_file = new FileOutputStream(ks)) {
245                     keys.store(ks_file, "pass".toCharArray());
246                 } catch (GeneralSecurityException e) {
247                     throw new Error(e);
248                 } catch (IOException e) {
249                     throw new Error(e);
250                 }
251
252                 try {
253                     Map<AlgorithmId, Map<OCSPIssuerId, OCSPIssuer>> map = new HashMap<>();
254                     index(AlgorithmId.get("SHA-1"), MessageDigest.getInstance("SHA-1"), toServe, map);
255                     index(AlgorithmId.get("SHA-256"), MessageDigest.getInstance("SHA-256"), toServe, map);
256                     synchronized (this) {
257                         this.map = Collections.unmodifiableMap(map);
258                     }
259                 } catch (NoSuchAlgorithmException e) {
260                     e.printStackTrace();
261                 }
262             } catch (Throwable t) {
263                 t.printStackTrace();
264             }
265             try {
266                 long dt = Math.max(3000, nextHousekeeping - System.currentTimeMillis());
267                 Thread.sleep(dt);
268             } catch (InterruptedException e) {
269                 e.printStackTrace();
270             }
271         }
272     }
273
274     public synchronized Map<OCSPIssuerId, OCSPIssuer> get(AlgorithmId alg) {
275         return map.get(alg);
276     }
277 }