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