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