]> WPIA git - gigi.git/blob - src/club/wpia/gigi/dbObjects/Certificate.java
upd: store different types of revocation
[gigi.git] / src / club / wpia / gigi / dbObjects / Certificate.java
1 package club.wpia.gigi.dbObjects;
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.InputStream;
8 import java.security.GeneralSecurityException;
9 import java.security.cert.CertificateFactory;
10 import java.security.cert.X509Certificate;
11 import java.sql.Date;
12 import java.util.Arrays;
13 import java.util.Collections;
14 import java.util.HashMap;
15 import java.util.LinkedList;
16 import java.util.List;
17 import java.util.Map.Entry;
18
19 import club.wpia.gigi.GigiApiException;
20 import club.wpia.gigi.database.DBEnum;
21 import club.wpia.gigi.database.GigiPreparedStatement;
22 import club.wpia.gigi.database.GigiResultSet;
23 import club.wpia.gigi.output.template.Outputable;
24 import club.wpia.gigi.output.template.TranslateCommand;
25 import club.wpia.gigi.pages.account.certs.CertificateRequest;
26 import club.wpia.gigi.util.KeyStorage;
27
28 public class Certificate implements IdCachable {
29
30     public enum RevocationType implements DBEnum {
31         USER("user"), SUPPORT("support"), PING_TIMEOUT("ping_timeout");
32
33         private final String dbName;
34
35         private RevocationType(String dbName) {
36             this.dbName = dbName;
37         }
38
39         @Override
40         public String getDBName() {
41             return dbName;
42         }
43     }
44
45     public enum SANType implements DBEnum {
46         EMAIL("email"), DNS("DNS");
47
48         private final String opensslName;
49
50         private SANType(String opensslName) {
51             this.opensslName = opensslName;
52         }
53
54         public String getOpensslName() {
55             return opensslName;
56         }
57
58         @Override
59         public String getDBName() {
60             return opensslName;
61         }
62     }
63
64     public static class SubjectAlternateName implements Comparable<SubjectAlternateName> {
65
66         private SANType type;
67
68         private String name;
69
70         public SubjectAlternateName(SANType type, String name) {
71             this.type = type;
72             this.name = name;
73         }
74
75         public String getName() {
76             return name;
77         }
78
79         public SANType getType() {
80             return type;
81         }
82
83         @Override
84         public int compareTo(SubjectAlternateName o) {
85             int i = type.compareTo(o.type);
86             if (i != 0) {
87                 return i;
88             }
89             return name.compareTo(o.name);
90         }
91
92         @Override
93         public int hashCode() {
94             final int prime = 31;
95             int result = 1;
96             result = prime * result + ((name == null) ? 0 : name.hashCode());
97             result = prime * result + ((type == null) ? 0 : type.hashCode());
98             return result;
99         }
100
101         @Override
102         public boolean equals(Object obj) {
103             if (this == obj) {
104                 return true;
105             }
106             if (obj == null) {
107                 return false;
108             }
109             if (getClass() != obj.getClass()) {
110                 return false;
111             }
112             SubjectAlternateName other = (SubjectAlternateName) obj;
113             if (name == null) {
114                 if (other.name != null) {
115                     return false;
116                 }
117             } else if ( !name.equals(other.name)) {
118                 return false;
119             }
120             if (type != other.type) {
121                 return false;
122             }
123             return true;
124         }
125
126     }
127
128     public enum CSRType {
129         CSR, SPKAC;
130     }
131
132     private int id;
133
134     private CertificateOwner owner;
135
136     private String serial;
137
138     private Digest md;
139
140     private String csrName;
141
142     private String crtName;
143
144     private String csr = null;
145
146     private CSRType csrType;
147
148     private List<SubjectAlternateName> sans;
149
150     private CertificateProfile profile;
151
152     private HashMap<String, String> dn;
153
154     private String dnString;
155
156     private CACertificate ca;
157
158     /**
159      * Creates a new Certificate. WARNING: this is an internal API. Creating
160      * certificates for users must be done using the {@link CertificateRequest}
161      * -API.
162      * 
163      * @param owner
164      *            the owner for whom the certificate should be created.
165      * @param actor
166      *            the acting user that creates the certificate
167      * @param dn
168      *            the distinguished name of the subject of this certificate (as
169      *            Map using OpenSSL-Style keys)
170      * @param md
171      *            the {@link Digest} to sign the certificate with
172      * @param csr
173      *            the CSR/SPKAC-Request containing the public key in question
174      * @param csrType
175      *            the type of the csr parameter
176      * @param profile
177      *            the profile under which this certificate is to be issued
178      * @param sans
179      *            additional subject alternative names
180      * @throws GigiApiException
181      *             in case the request is malformed or internal errors occur
182      * @throws IOException
183      *             when the request cannot be written.
184      */
185     public Certificate(CertificateOwner owner, User actor, HashMap<String, String> dn, Digest md, String csr, CSRType csrType, CertificateProfile profile, SubjectAlternateName... sans) throws GigiApiException, IOException {
186         if ( !profile.canBeIssuedBy(owner, actor)) {
187             throw new GigiApiException("You are not allowed to issue these certificates.");
188         }
189         this.owner = owner;
190         this.dn = dn;
191         if (dn.size() == 0) {
192             throw new GigiApiException("DN must not be empty.");
193         }
194         dnString = stringifyDN(dn);
195         this.md = md;
196         this.csr = csr;
197         this.csrType = csrType;
198         this.profile = profile;
199         this.sans = Arrays.asList(sans);
200         synchronized (Certificate.class) {
201
202             try (GigiPreparedStatement inserter = new GigiPreparedStatement("INSERT INTO certs SET md=?::`mdType`, csr_type=?::`csrType`, crt_name='', memid=?, profile=?")) {
203                 inserter.setString(1, md.toString().toLowerCase());
204                 inserter.setString(2, this.csrType.toString());
205                 inserter.setInt(3, owner.getId());
206                 inserter.setInt(4, profile.getId());
207                 inserter.execute();
208                 id = inserter.lastInsertId();
209             }
210
211             try (GigiPreparedStatement san = new GigiPreparedStatement("INSERT INTO `subjectAlternativeNames` SET `certId`=?, contents=?, type=?::`SANType`")) {
212                 for (SubjectAlternateName subjectAlternateName : sans) {
213                     san.setInt(1, id);
214                     san.setString(2, subjectAlternateName.getName());
215                     san.setString(3, subjectAlternateName.getType().getOpensslName());
216                     san.execute();
217                 }
218             }
219
220             try (GigiPreparedStatement insertAVA = new GigiPreparedStatement("INSERT INTO `certAvas` SET `certId`=?, name=?, value=?")) {
221                 insertAVA.setInt(1, id);
222                 for (Entry<String, String> e : this.dn.entrySet()) {
223                     insertAVA.setString(2, e.getKey());
224                     insertAVA.setString(3, e.getValue());
225                     insertAVA.execute();
226                 }
227             }
228             File csrFile = KeyStorage.locateCsr(id);
229             csrName = csrFile.getPath();
230             try (FileOutputStream fos = new FileOutputStream(csrFile)) {
231                 fos.write(this.csr.getBytes("UTF-8"));
232             }
233             try (GigiPreparedStatement updater = new GigiPreparedStatement("UPDATE `certs` SET `csr_name`=? WHERE id=?")) {
234                 updater.setString(1, csrName);
235                 updater.setInt(2, id);
236                 updater.execute();
237             }
238
239             cache.put(this);
240         }
241     }
242
243     private Certificate(GigiResultSet rs) {
244         this.id = rs.getInt("id");
245         dnString = rs.getString("subject");
246         md = Digest.valueOf(rs.getString("md").toUpperCase());
247         csrName = rs.getString("csr_name");
248         crtName = rs.getString("crt_name");
249         owner = CertificateOwner.getById(rs.getInt("memid"));
250         profile = CertificateProfile.getById(rs.getInt("profile"));
251         this.serial = rs.getString("serial");
252
253         try (GigiPreparedStatement ps2 = new GigiPreparedStatement("SELECT `contents`, `type` FROM `subjectAlternativeNames` WHERE `certId`=?")) {
254             ps2.setInt(1, id);
255             GigiResultSet rs2 = ps2.executeQuery();
256             sans = new LinkedList<>();
257             while (rs2.next()) {
258                 sans.add(new SubjectAlternateName(SANType.valueOf(rs2.getString("type").toUpperCase()), rs2.getString("contents")));
259             }
260         }
261     }
262
263     public enum CertificateStatus {
264         /**
265          * This certificate is not in the database, has no id and only exists as
266          * this java object.
267          */
268         DRAFT("draft"),
269         /**
270          * The certificate has been signed. It is stored in the database.
271          * {@link Certificate#cert()} is valid.
272          */
273         ISSUED("issued"),
274
275         /**
276          * The certificate has been revoked.
277          */
278         REVOKED("revoked"),
279
280         /**
281          * If this certificate cannot be updated because an error happened in
282          * the signer.
283          */
284         ERROR("error");
285
286         private final Outputable name;
287
288         private CertificateStatus(String codename) {
289             this.name = new TranslateCommand(codename);
290
291         }
292
293         public Outputable getName() {
294             return name;
295         }
296
297     }
298
299     public synchronized CertificateStatus getStatus() {
300         try (GigiPreparedStatement searcher = new GigiPreparedStatement("SELECT crt_name, created, revoked, serial, caid FROM certs WHERE id=?")) {
301             searcher.setInt(1, id);
302             GigiResultSet rs = searcher.executeQuery();
303             if ( !rs.next()) {
304                 throw new IllegalStateException("Certificate not in Database");
305             }
306
307             crtName = rs.getString(1);
308             serial = rs.getString(4);
309             if (rs.getTimestamp(2) == null) {
310                 return CertificateStatus.DRAFT;
311             }
312             ca = CACertificate.getById(rs.getInt("caid"));
313             if (rs.getTimestamp(2) != null && rs.getTimestamp(3) == null) {
314                 return CertificateStatus.ISSUED;
315             }
316             return CertificateStatus.REVOKED;
317         }
318     }
319
320     /**
321      * @param start
322      *            the date from which on the certificate should be valid. (or
323      *            null if it should be valid instantly)
324      * @param period
325      *            the period for which the date should be valid. (a
326      *            <code>yyyy-mm-dd</code> or a "2y" (2 calendar years), "6m" (6
327      *            months)
328      * @return A job which can be used to monitor the progress of this task.
329      * @throws IOException
330      *             for problems with writing the CSR/SPKAC
331      * @throws GigiApiException
332      *             if the period is bogus
333      */
334     public Job issue(Date start, String period, User actor) throws IOException, GigiApiException {
335         if (getStatus() != CertificateStatus.DRAFT) {
336             throw new IllegalStateException();
337         }
338
339         return Job.sign(this, start, period);
340
341     }
342
343     public Job revoke(RevocationType type) {
344         if (getStatus() != CertificateStatus.ISSUED) {
345             throw new IllegalStateException();
346         }
347         return Job.revoke(this, type);
348
349     }
350
351     public CACertificate getParent() {
352         CertificateStatus status = getStatus();
353         if (status != CertificateStatus.REVOKED && status != CertificateStatus.ISSUED) {
354             throw new IllegalStateException(status + " is not wanted here.");
355         }
356         return ca;
357     }
358
359     public X509Certificate cert() throws IOException, GeneralSecurityException {
360         CertificateStatus status = getStatus();
361         if (status != CertificateStatus.REVOKED && status != CertificateStatus.ISSUED) {
362             throw new IllegalStateException(status + " is not wanted here.");
363         }
364         InputStream is = null;
365         X509Certificate crt = null;
366         try {
367             is = new FileInputStream(crtName);
368             CertificateFactory cf = CertificateFactory.getInstance("X.509");
369             crt = (X509Certificate) cf.generateCertificate(is);
370         } finally {
371             if (is != null) {
372                 is.close();
373             }
374         }
375         return crt;
376     }
377
378     public Certificate renew() {
379         return null;
380     }
381
382     public int getId() {
383         return id;
384     }
385
386     public String getSerial() {
387         getStatus();
388         // poll changes
389         return serial;
390     }
391
392     public String getDistinguishedName() {
393         return dnString;
394     }
395
396     public Digest getMessageDigest() {
397         return md;
398     }
399
400     public CertificateOwner getOwner() {
401         return owner;
402     }
403
404     public List<SubjectAlternateName> getSANs() {
405         return Collections.unmodifiableList(sans);
406     }
407
408     public CertificateProfile getProfile() {
409         return profile;
410     }
411
412     private static final String CONCAT = "string_agg(concat('/', `name`, '=', REPLACE(REPLACE(value, '\\\\', '\\\\\\\\'), '/', '\\\\/')), '')";
413
414     public synchronized static Certificate getBySerial(String serial) {
415         if (serial == null || "".equals(serial)) {
416             return null;
417         }
418         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT certs.id, " + CONCAT + " as `subject`, `md`, `csr_name`, `crt_name`,`memid`, `profile`, `certs`.`serial` FROM `certs` LEFT JOIN `certAvas` ON `certAvas`.`certId`=`certs`.`id` WHERE `serial`=? GROUP BY `certs`.`id`")) {
419             ps.setString(1, serial);
420             GigiResultSet rs = ps.executeQuery();
421             if ( !rs.next()) {
422                 return null;
423             }
424             int id = rs.getInt(1);
425             Certificate c1 = cache.get(id);
426             if (c1 != null) {
427                 return c1;
428             }
429             Certificate certificate = new Certificate(rs);
430             cache.put(certificate);
431             return certificate;
432         }
433     }
434
435     private static ObjectCache<Certificate> cache = new ObjectCache<>();
436
437     public synchronized static Certificate getById(int id) {
438         Certificate cacheRes = cache.get(id);
439         if (cacheRes != null) {
440             return cacheRes;
441         }
442
443         try {
444             try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT certs.id, " + CONCAT + " as subject, md, csr_name, crt_name,memid, profile, certs.serial FROM `certs` LEFT JOIN `certAvas` ON `certAvas`.`certId`=certs.id WHERE certs.id=? GROUP BY certs.id")) {
445                 ps.setInt(1, id);
446                 GigiResultSet rs = ps.executeQuery();
447                 if ( !rs.next()) {
448                     return null;
449                 }
450
451                 Certificate c = new Certificate(rs);
452                 cache.put(c);
453                 return c;
454             }
455         } catch (IllegalArgumentException e) {
456
457         }
458         return null;
459     }
460
461     public static String escapeAVA(String value) {
462
463         return value.replace("\\", "\\\\").replace("/", "\\/");
464     }
465
466     public static String stringifyDN(HashMap<String, String> contents) {
467         StringBuffer res = new StringBuffer();
468         for (Entry<String, String> i : contents.entrySet()) {
469             res.append("/" + i.getKey() + "=");
470             res.append(escapeAVA(i.getValue()));
471         }
472         return res.toString();
473     }
474
475     public static HashMap<String, String> buildDN(String... contents) {
476         HashMap<String, String> res = new HashMap<>();
477         for (int i = 0; i + 1 < contents.length; i += 2) {
478             res.put(contents[i], contents[i + 1]);
479         }
480         return res;
481     }
482
483     public java.util.Date getRevocationDate() {
484         if (getStatus() == CertificateStatus.REVOKED) {
485             try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT revoked FROM certs WHERE id=?")) {
486                 prep.setInt(1, getId());
487                 GigiResultSet res = prep.executeQuery();
488                 if (res.next()) {
489                     return new java.util.Date(res.getTimestamp("revoked").getTime());
490                 }
491             }
492         }
493         return null;
494     }
495
496     public void setLoginEnabled(boolean activate) {
497         if (activate) {
498             if ( !isLoginEnabled()) {
499                 try (GigiPreparedStatement prep = new GigiPreparedStatement("INSERT INTO `logincerts` SET `id`=?")) {
500                     prep.setInt(1, id);
501                     prep.execute();
502                 }
503             }
504         } else {
505             try (GigiPreparedStatement prep = new GigiPreparedStatement("DELETE FROM `logincerts` WHERE `id`=?")) {
506                 prep.setInt(1, id);
507                 prep.execute();
508             }
509         }
510     }
511
512     public boolean isLoginEnabled() {
513         try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT 1 FROM `logincerts` WHERE `id`=?")) {
514             prep.setInt(1, id);
515             GigiResultSet res = prep.executeQuery();
516             return res.next();
517         }
518     }
519
520     public static Certificate[] findBySerialPattern(String serial) {
521         try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT `id` FROM `certs` WHERE `serial` LIKE ? GROUP BY `id`  LIMIT 100", true)) {
522             prep.setString(1, serial);
523             return fetchCertsToArray(prep);
524         }
525     }
526
527     public static Certificate[] findBySANPattern(String request, SANType type) {
528         try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT `certId` FROM `subjectAlternativeNames` WHERE `contents` LIKE ? and `type`=?::`SANType` GROUP BY `certId` LIMIT 100", true)) {
529             prep.setString(1, request);
530             prep.setEnum(2, type);
531             return fetchCertsToArray(prep);
532         }
533     }
534
535     private static Certificate[] fetchCertsToArray(GigiPreparedStatement prep) {
536         GigiResultSet res = prep.executeQuery();
537         res.last();
538         Certificate[] certs = new Certificate[res.getRow()];
539         res.beforeFirst();
540         for (int i = 0; res.next(); i++) {
541             certs[i] = Certificate.getById(res.getInt(1));
542         }
543         return certs;
544     }
545 }