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