]> WPIA git - gigi.git/blob - src/org/cacert/gigi/dbObjects/Certificate.java
Update Certificate-DN-API (for escape-safe-strings)
[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.DatabaseConnection;
21 import org.cacert.gigi.database.GigiPreparedStatement;
22 import org.cacert.gigi.database.GigiResultSet;
23 import org.cacert.gigi.util.Job;
24 import org.cacert.gigi.util.KeyStorage;
25 import org.cacert.gigi.util.Notary;
26
27 public class Certificate {
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 User owner;
114
115     private String serial;
116
117     private String 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     public Certificate(User owner, HashMap<String, String> dn, String md, String csr, CSRType csrType, CertificateProfile profile, SubjectAlternateName... sans) throws GigiApiException {
136         if ( !owner.canIssue(profile)) {
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     }
151
152     private Certificate(String serial) {
153         //
154         String concat = "group_concat(concat('/', `name`, '=', REPLACE(REPLACE(value, '\\\\', '\\\\\\\\'), '/', '\\\\/')))";
155         GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("SELECT certs.id, " + concat + " as subject, md, csr_name, crt_name,memid, profile FROM `certs` LEFT JOIN certAvas ON certAvas.certid=certs.id WHERE serial=? GROUP BY certs.id");
156         ps.setString(1, serial);
157         GigiResultSet rs = ps.executeQuery();
158         if ( !rs.next()) {
159             throw new IllegalArgumentException("Invalid mid " + serial);
160         }
161         this.id = rs.getInt(1);
162         dnString = rs.getString(2);
163         md = rs.getString(3);
164         csrName = rs.getString(4);
165         crtName = rs.getString(5);
166         owner = User.getById(rs.getInt(6));
167         profile = CertificateProfile.getById(rs.getInt(7));
168         this.serial = serial;
169
170         GigiPreparedStatement ps2 = DatabaseConnection.getInstance().prepare("SELECT contents, type FROM `subjectAlternativeNames` WHERE certId=?");
171         ps2.setInt(1, id);
172         GigiResultSet rs2 = ps2.executeQuery();
173         sans = new LinkedList<>();
174         while (rs2.next()) {
175             sans.add(new SubjectAlternateName(SANType.valueOf(rs2.getString("type").toUpperCase()), rs2.getString("contents")));
176         }
177         rs2.close();
178
179         rs.close();
180     }
181
182     public enum CertificateStatus {
183         /**
184          * This certificate is not in the database, has no id and only exists as
185          * this java object.
186          */
187         DRAFT(),
188         /**
189          * The certificate has been signed. It is stored in the database.
190          * {@link Certificate#cert()} is valid.
191          */
192         ISSUED(),
193
194         /**
195          * The certificate has been revoked.
196          */
197         REVOKED(),
198
199         /**
200          * If this certificate cannot be updated because an error happened in
201          * the signer.
202          */
203         ERROR();
204
205         private CertificateStatus() {}
206
207     }
208
209     public CertificateStatus getStatus() {
210         if (id == 0) {
211             return CertificateStatus.DRAFT;
212         }
213         GigiPreparedStatement searcher = DatabaseConnection.getInstance().prepare("SELECT crt_name, created, revoked, serial FROM certs WHERE id=?");
214         searcher.setInt(1, id);
215         GigiResultSet rs = searcher.executeQuery();
216         if ( !rs.next()) {
217             throw new IllegalStateException("Certificate not in Database");
218         }
219
220         crtName = rs.getString(1);
221         serial = rs.getString(4);
222         if (rs.getTime(2) == null) {
223             return CertificateStatus.DRAFT;
224         }
225         if (rs.getTime(2) != null && rs.getTime(3) == null) {
226             return CertificateStatus.ISSUED;
227         }
228         return CertificateStatus.REVOKED;
229     }
230
231     /**
232      * @param start
233      *            the date from which on the certificate should be valid. (or
234      *            null if it should be valid instantly)
235      * @param period
236      *            the period for which the date should be valid. (a
237      *            <code>yyyy-mm-dd</code> or a "2y" (2 calendar years), "6m" (6
238      *            months)
239      * @return A job which can be used to monitor the progress of this task.
240      * @throws IOException
241      *             for problems with writing the CSR/SPKAC
242      * @throws GigiApiException
243      *             if the period is bogus
244      */
245     public Job issue(Date start, String period) throws IOException, GigiApiException {
246         if (getStatus() != CertificateStatus.DRAFT) {
247             throw new IllegalStateException();
248         }
249         Notary.writeUserAgreement(owner, "CCA", "issue certificate", "", true, 0);
250
251         GigiPreparedStatement inserter = DatabaseConnection.getInstance().prepare("INSERT INTO certs SET md=?, csr_type=?, crt_name='', memid=?, profile=?");
252         inserter.setString(1, md);
253         inserter.setString(2, csrType.toString());
254         inserter.setInt(3, owner.getId());
255         inserter.setInt(4, profile.getId());
256         inserter.execute();
257         id = inserter.lastInsertId();
258
259         GigiPreparedStatement san = DatabaseConnection.getInstance().prepare("INSERT INTO subjectAlternativeNames SET certId=?, contents=?, type=?");
260         for (SubjectAlternateName subjectAlternateName : sans) {
261             san.setInt(1, id);
262             san.setString(2, subjectAlternateName.getName());
263             san.setString(3, subjectAlternateName.getType().getOpensslName());
264             san.execute();
265         }
266
267         GigiPreparedStatement insertAVA = DatabaseConnection.getInstance().prepare("INSERT certAvas SET certid=?, name=?, value=?");
268         insertAVA.setInt(1, id);
269         for (Entry<String, String> e : dn.entrySet()) {
270             insertAVA.setString(2, e.getKey());
271             insertAVA.setString(3, e.getValue());
272             insertAVA.execute();
273         }
274         File csrFile = KeyStorage.locateCsr(id);
275         csrName = csrFile.getPath();
276         FileOutputStream fos = new FileOutputStream(csrFile);
277         fos.write(csr.getBytes());
278         fos.close();
279
280         GigiPreparedStatement updater = DatabaseConnection.getInstance().prepare("UPDATE certs SET csr_name=? WHERE id=?");
281         updater.setString(1, csrName);
282         updater.setInt(2, id);
283         updater.execute();
284         return Job.sign(this, start, period);
285
286     }
287
288     public Job revoke() {
289         if (getStatus() != CertificateStatus.ISSUED) {
290             throw new IllegalStateException();
291         }
292         return Job.revoke(this);
293
294     }
295
296     public X509Certificate cert() throws IOException, GeneralSecurityException {
297         CertificateStatus status = getStatus();
298         if (status != CertificateStatus.ISSUED) {
299             throw new IllegalStateException(status + " is not wanted here.");
300         }
301         InputStream is = null;
302         X509Certificate crt = null;
303         try {
304             is = new FileInputStream(crtName);
305             CertificateFactory cf = CertificateFactory.getInstance("X.509");
306             crt = (X509Certificate) cf.generateCertificate(is);
307         } finally {
308             if (is != null) {
309                 is.close();
310             }
311         }
312         return crt;
313     }
314
315     public Certificate renew() {
316         return null;
317     }
318
319     public int getId() {
320         return id;
321     }
322
323     public String getSerial() {
324         getStatus();
325         // poll changes
326         return serial;
327     }
328
329     public String getDistinguishedName() {
330         return dnString;
331     }
332
333     public String getMessageDigest() {
334         return md;
335     }
336
337     public User getOwner() {
338         return owner;
339     }
340
341     public List<SubjectAlternateName> getSANs() {
342         return Collections.unmodifiableList(sans);
343     }
344
345     public CertificateProfile getProfile() {
346         return profile;
347     }
348
349     public static Certificate getBySerial(String serial) {
350         // TODO caching?
351         try {
352             return new Certificate(serial);
353         } catch (IllegalArgumentException e) {
354
355         }
356         return null;
357     }
358
359     public static String escapeAVA(String value) {
360
361         return value.replace("\\", "\\\\").replace("/", "\\/");
362     }
363
364     public static String stringifyDN(HashMap<String, String> contents) {
365         StringBuffer res = new StringBuffer();
366         for (Entry<String, String> i : contents.entrySet()) {
367             res.append("/" + i.getKey() + "=");
368             res.append(escapeAVA(i.getValue()));
369         }
370         return res.toString();
371     }
372
373     public static HashMap<String, String> buildDN(String... contents) {
374         HashMap<String, String> res = new HashMap<>();
375         for (int i = 0; i + 1 < contents.length; i += 2) {
376             res.put(contents[i], contents[i + 1]);
377         }
378         return res;
379     }
380 }