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