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