]> WPIA git - gigi.git/blob - src/org/cacert/gigi/pages/account/certs/CertificateRequest.java
add: cert-rules closer to reality
[gigi.git] / src / org / cacert / gigi / pages / account / certs / CertificateRequest.java
1 package org.cacert.gigi.pages.account.certs;
2
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.security.GeneralSecurityException;
6 import java.security.PublicKey;
7 import java.security.interfaces.DSAPublicKey;
8 import java.security.interfaces.ECPublicKey;
9 import java.security.interfaces.RSAPublicKey;
10 import java.util.Arrays;
11 import java.util.Base64;
12 import java.util.HashMap;
13 import java.util.HashSet;
14 import java.util.LinkedHashSet;
15 import java.util.Set;
16 import java.util.TreeSet;
17
18 import javax.servlet.http.HttpServletRequest;
19
20 import org.cacert.gigi.GigiApiException;
21 import org.cacert.gigi.crypto.SPKAC;
22 import org.cacert.gigi.dbObjects.Certificate;
23 import org.cacert.gigi.dbObjects.Certificate.CSRType;
24 import org.cacert.gigi.dbObjects.Certificate.SANType;
25 import org.cacert.gigi.dbObjects.Certificate.SubjectAlternateName;
26 import org.cacert.gigi.dbObjects.CertificateOwner;
27 import org.cacert.gigi.dbObjects.CertificateProfile;
28 import org.cacert.gigi.dbObjects.CertificateProfile.PropertyTemplate;
29 import org.cacert.gigi.dbObjects.Digest;
30 import org.cacert.gigi.dbObjects.Organisation;
31 import org.cacert.gigi.dbObjects.User;
32 import org.cacert.gigi.output.template.Scope;
33 import org.cacert.gigi.output.template.SprintfCommand;
34 import org.cacert.gigi.util.PEM;
35
36 import sun.security.pkcs.PKCS9Attribute;
37 import sun.security.pkcs10.PKCS10;
38 import sun.security.pkcs10.PKCS10Attribute;
39 import sun.security.pkcs10.PKCS10Attributes;
40 import sun.security.util.DerInputStream;
41 import sun.security.util.DerValue;
42 import sun.security.util.ObjectIdentifier;
43 import sun.security.x509.AVA;
44 import sun.security.x509.AlgorithmId;
45 import sun.security.x509.CertificateExtensions;
46 import sun.security.x509.DNSName;
47 import sun.security.x509.ExtendedKeyUsageExtension;
48 import sun.security.x509.Extension;
49 import sun.security.x509.GeneralName;
50 import sun.security.x509.GeneralNameInterface;
51 import sun.security.x509.GeneralNames;
52 import sun.security.x509.PKIXExtensions;
53 import sun.security.x509.RDN;
54 import sun.security.x509.RFC822Name;
55 import sun.security.x509.SubjectAlternativeNameExtension;
56 import sun.security.x509.X500Name;
57
58 public class CertificateRequest {
59
60     public static final String DEFAULT_CN = "CAcert WoT User";
61
62     public static final ObjectIdentifier OID_KEY_USAGE_SSL_SERVER = ObjectIdentifier.newInternal(new int[] {
63             1, 3, 6, 1, 5, 5, 7, 3, 1
64     });
65
66     public static final ObjectIdentifier OID_KEY_USAGE_SSL_CLIENT = ObjectIdentifier.newInternal(new int[] {
67             1, 3, 6, 1, 5, 5, 7, 3, 2
68     });
69
70     public static final ObjectIdentifier OID_KEY_USAGE_CODESIGN = ObjectIdentifier.newInternal(new int[] {
71             1, 3, 6, 1, 5, 5, 7, 3, 3
72     });
73
74     public static final ObjectIdentifier OID_KEY_USAGE_EMAIL_PROTECTION = ObjectIdentifier.newInternal(new int[] {
75             1, 3, 6, 1, 5, 5, 7, 3, 4
76     });
77
78     public static final ObjectIdentifier OID_KEY_USAGE_TIMESTAMP = ObjectIdentifier.newInternal(new int[] {
79             1, 3, 6, 1, 5, 5, 7, 3, 8
80     });
81
82     public static final ObjectIdentifier OID_KEY_USAGE_OCSP = ObjectIdentifier.newInternal(new int[] {
83             1, 3, 6, 1, 5, 5, 7, 3, 9
84     });
85
86     private CSRType csrType;
87
88     private final PublicKey pk;
89
90     private String csr;
91
92     public String name = DEFAULT_CN;
93
94     private Set<SubjectAlternateName> SANs;
95
96     private Digest selectedDigest = Digest.getDefault();
97
98     private CertificateProfile profile = CertificateProfile.getById(1);
99
100     private String ou = "";
101
102     private Organisation org = null;
103
104     private User u;
105
106     private String pDNS, pMail;
107
108     public CertificateRequest(User issuer, String csr) throws IOException, GeneralSecurityException, GigiApiException {
109         u = issuer;
110         byte[] data = PEM.decode("(NEW )?CERTIFICATE REQUEST", csr);
111         PKCS10 parsed = new PKCS10(data);
112         PKCS10Attributes atts = parsed.getAttributes();
113
114         TreeSet<SubjectAlternateName> SANs = new TreeSet<>();
115         for (RDN r : parsed.getSubjectName().rdns()) {
116             for (AVA a : r.avas()) {
117                 if (a.getObjectIdentifier().equals((Object) PKCS9Attribute.EMAIL_ADDRESS_OID)) {
118                     SANs.add(new SubjectAlternateName(SANType.EMAIL, a.getValueString()));
119                 } else if (a.getObjectIdentifier().equals((Object) X500Name.commonName_oid)) {
120                     String value = a.getValueString();
121                     if (value.contains(".") && !value.contains(" ")) {
122                         SANs.add(new SubjectAlternateName(SANType.DNS, value));
123                     } else {
124                         name = value;
125                     }
126                 } else if (a.getObjectIdentifier().equals((Object) PKIXExtensions.SubjectAlternativeName_Id)) {
127                     // TODO? parse invalid SANs
128                 }
129             }
130         }
131
132         for (PKCS10Attribute b : atts.getAttributes()) {
133
134             if ( !b.getAttributeId().equals((Object) PKCS9Attribute.EXTENSION_REQUEST_OID)) {
135                 // unknown attrib
136                 continue;
137             }
138
139             for (Extension c : ((CertificateExtensions) b.getAttributeValue()).getAllExtensions()) {
140                 if (c instanceof SubjectAlternativeNameExtension) {
141
142                     SubjectAlternativeNameExtension san = (SubjectAlternativeNameExtension) c;
143                     GeneralNames obj = san.get(SubjectAlternativeNameExtension.SUBJECT_NAME);
144                     for (int i = 0; i < obj.size(); i++) {
145                         GeneralName generalName = obj.get(i);
146                         GeneralNameInterface peeled = generalName.getName();
147                         if (peeled instanceof DNSName) {
148                             SANs.add(new SubjectAlternateName(SANType.DNS, ((DNSName) peeled).getName()));
149                         } else if (peeled instanceof RFC822Name) {
150                             SANs.add(new SubjectAlternateName(SANType.EMAIL, ((RFC822Name) peeled).getName()));
151                         }
152                     }
153                 } else if (c instanceof ExtendedKeyUsageExtension) {
154                     ExtendedKeyUsageExtension ekue = (ExtendedKeyUsageExtension) c;
155                     for (String s : ekue.getExtendedKeyUsage()) {
156                         if (s.equals(OID_KEY_USAGE_SSL_SERVER.toString())) {
157                             // server
158                             profile = CertificateProfile.getByName("server");
159                         } else if (s.equals(OID_KEY_USAGE_SSL_CLIENT.toString())) {
160                             // client
161                             profile = CertificateProfile.getByName("client");
162                         } else if (s.equals(OID_KEY_USAGE_CODESIGN.toString())) {
163                             // code sign
164                         } else if (s.equals(OID_KEY_USAGE_EMAIL_PROTECTION.toString())) {
165                             // emailProtection
166                             profile = CertificateProfile.getByName("mail");
167                         } else if (s.equals(OID_KEY_USAGE_TIMESTAMP.toString())) {
168                             // timestamp
169                         } else if (s.equals(OID_KEY_USAGE_OCSP.toString())) {
170                             // OCSP
171                         }
172                     }
173                 } else {
174                     // Unknown requested extension
175                 }
176             }
177
178         }
179         this.SANs = SANs;
180         pk = parsed.getSubjectPublicKeyInfo();
181         String sign = getSignatureAlgorithm(data);
182         guessDigest(sign);
183
184         this.csr = csr;
185         this.csrType = CSRType.CSR;
186     }
187
188     public CertificateRequest(User issuer, String spkac, String spkacChallenge) throws IOException, GigiApiException, GeneralSecurityException {
189         u = issuer;
190         String cleanedSPKAC = spkac.replaceAll("[\r\n]", "");
191         byte[] data = Base64.getDecoder().decode(cleanedSPKAC);
192         SPKAC parsed = new SPKAC(data);
193         if ( !parsed.getChallenge().equals(spkacChallenge)) {
194             throw new GigiApiException("Challenge mismatch");
195         }
196         pk = parsed.getPubkey();
197         String sign = getSignatureAlgorithm(data);
198         guessDigest(sign);
199         this.SANs = new HashSet<>();
200         this.csr = "SPKAC=" + cleanedSPKAC;
201         this.csrType = CSRType.SPKAC;
202
203     }
204
205     private static String getSignatureAlgorithm(byte[] data) throws IOException {
206         DerInputStream in = new DerInputStream(data);
207         DerValue[] seq = in.getSequence(3);
208         return AlgorithmId.parse(seq[1]).getName();
209     }
210
211     private void guessDigest(String sign) {
212         if (sign.toLowerCase().startsWith("sha512")) {
213             selectedDigest = Digest.SHA512;
214         } else if (sign.toLowerCase().startsWith("sha384")) {
215             selectedDigest = Digest.SHA384;
216         }
217     }
218
219     public void checkKeyStrength(PrintWriter out) {
220         out.println("Type: " + pk.getAlgorithm() + "<br/>");
221         if (pk instanceof RSAPublicKey) {
222             out.println("Exponent: " + ((RSAPublicKey) pk).getPublicExponent() + "<br/>");
223             out.println("Length: " + ((RSAPublicKey) pk).getModulus().bitLength());
224         } else if (pk instanceof DSAPublicKey) {
225             DSAPublicKey dpk = (DSAPublicKey) pk;
226             out.println("Length: " + dpk.getY().bitLength() + "<br/>");
227             out.println(dpk.getParams());
228         } else if (pk instanceof ECPublicKey) {
229             ECPublicKey epk = (ECPublicKey) pk;
230             out.println("Length-x: " + epk.getW().getAffineX().bitLength() + "<br/>");
231             out.println("Length-y: " + epk.getW().getAffineY().bitLength() + "<br/>");
232             out.println(epk.getParams().getCurve());
233         }
234     }
235
236     private Set<SubjectAlternateName> parseSANBox(String SANs) {
237         String[] SANparts = SANs.split("[\r\n]+|, *");
238         Set<SubjectAlternateName> parsedNames = new LinkedHashSet<>();
239         for (String SANline : SANparts) {
240             String[] parts = SANline.split(":", 2);
241             if (parts.length == 1) {
242                 if (parts[0].trim().equals("")) {
243                     continue;
244                 }
245                 if (parts[0].contains("@")) {
246                     parsedNames.add(new SubjectAlternateName(SANType.EMAIL, parts[0]));
247                 } else {
248                     parsedNames.add(new SubjectAlternateName(SANType.DNS, parts[0]));
249                 }
250                 continue;
251             }
252             try {
253                 SANType t = Certificate.SANType.valueOf(parts[0].toUpperCase());
254                 if (t == null) {
255                     continue;
256                 }
257                 parsedNames.add(new SubjectAlternateName(t, parts[1]));
258             } catch (IllegalArgumentException e) {
259                 // invalid enum type
260                 continue;
261             }
262         }
263         return parsedNames;
264     }
265
266     public Set<SubjectAlternateName> getSANs() {
267         return SANs;
268     }
269
270     public String getName() {
271         return name;
272     }
273
274     public Organisation getOrg() {
275         return org;
276     }
277
278     public String getOu() {
279         return ou;
280     }
281
282     public Digest getSelectedDigest() {
283         return selectedDigest;
284     }
285
286     public CertificateProfile getProfile() {
287         return profile;
288     }
289
290     public synchronized boolean update(String nameIn, String hashAlg, String profileStr, String newOrgStr, String ou, String SANsStr, PrintWriter out, HttpServletRequest req) throws GigiApiException {
291         GigiApiException error = new GigiApiException();
292         this.name = nameIn;
293         if (hashAlg != null) {
294             selectedDigest = Digest.valueOf(hashAlg);
295         }
296         this.profile = CertificateProfile.getByName(profileStr);
297         if (newOrgStr != null) {
298             Organisation neworg = Organisation.getById(Integer.parseInt(newOrgStr));
299             if (neworg == null || u.getOrganisations().contains(neworg)) {
300                 PropertyTemplate orga = profile.getTemplates().get("orga");
301                 if (orga != null) {
302                     org = neworg;
303                 } else {
304                     org = null;
305                     error.mergeInto(new GigiApiException("No organisations for this certificate profile."));
306                 }
307             } else {
308                 error.mergeInto(new GigiApiException("Selected organisation is not part of your account."));
309             }
310         }
311
312         this.ou = ou;
313
314         if ( !this.profile.canBeIssuedBy(u)) {
315             this.profile = CertificateProfile.getById(1);
316             error.mergeInto(new GigiApiException("Certificate Profile is invalid."));
317             throw error;
318         }
319
320         CertificateOwner owner = org != null ? org : u;
321
322         verifySANs(error, profile, parseSANBox(SANsStr), owner);
323
324         if ( !error.isEmpty()) {
325             throw error;
326         }
327         return true;
328     }
329
330     private void verifySANs(GigiApiException error, CertificateProfile p, Set<SubjectAlternateName> sANs2, CertificateOwner owner) {
331         Set<SubjectAlternateName> filteredSANs = new LinkedHashSet<>();
332         PropertyTemplate domainTemp = p.getTemplates().get("domain");
333         PropertyTemplate emailTemp = p.getTemplates().get("email");
334         pDNS = null;
335         pMail = null;
336         for (SubjectAlternateName san : sANs2) {
337             if (san.getType() == SANType.DNS) {
338                 if (domainTemp != null && owner.isValidDomain(san.getName())) {
339                     if (pDNS != null && !domainTemp.isMultiple()) {
340                         // remove
341                     } else {
342                         if (pDNS == null) {
343                             pDNS = san.getName();
344                         }
345                         filteredSANs.add(san);
346                         continue;
347                     }
348                 }
349             } else if (san.getType() == SANType.EMAIL) {
350                 if (emailTemp != null && owner.isValidEmail(san.getName())) {
351                     if (pMail != null && !emailTemp.isMultiple()) {
352                         // remove
353                     } else {
354                         if (pMail == null) {
355                             pMail = san.getName();
356                         }
357                         filteredSANs.add(san);
358                         continue;
359                     }
360                 }
361             }
362             HashMap<String, Object> vars = new HashMap<>();
363             vars.put("SAN", san.getType().toString().toLowerCase() + ":" + san.getName());
364             error.mergeInto(new GigiApiException(new Scope(new SprintfCommand(//
365                     "The requested Subject alternate name \"{0}\" has been removed.", Arrays.asList("${SAN}")), vars)));
366         }
367         SANs = filteredSANs;
368     }
369
370     // domain email name name=WoTUser orga
371     public synchronized Certificate draft() throws GigiApiException {
372
373         GigiApiException error = new GigiApiException();
374
375         HashMap<String, String> subject = new HashMap<>();
376         PropertyTemplate domainTemp = profile.getTemplates().get("domain");
377         PropertyTemplate emailTemp = profile.getTemplates().get("email");
378         PropertyTemplate nameTemp = profile.getTemplates().get("name");
379         PropertyTemplate wotUserTemp = profile.getTemplates().get("name=WoTUser");
380
381         // Ok, let's determine the CN
382         // the CN is
383         // 1. the user's "real name", iff the real name is to be included i.e.
384         // not empty (name), or to be forced to WOTUser
385
386         // 2. the user's "primary domain", iff "1." doesn't match and there is a
387         // primary domain. (domainTemp != null)
388
389         String verifiedCN = null;
390         if (org == null) {
391             verifiedCN = verifyName(error, nameTemp, wotUserTemp, verifiedCN);
392         } else {
393             if ( !name.equals("")) {
394                 verifiedCN = name;
395             }
396         }
397         if (pDNS == null && domainTemp != null && domainTemp.isRequired()) {
398             error.mergeInto(new GigiApiException("Server Certificates require a DNS name."));
399         } else if (domainTemp != null && verifiedCN == null) {
400             // user may add domains
401             verifiedCN = pDNS;
402         }
403         if (verifiedCN != null) {
404             subject.put("CN", verifiedCN);
405         }
406
407         if (pMail != null) {
408             if (emailTemp != null) {
409                 subject.put("EMAIL", pMail);
410             } else {
411                 // verify SANs should prevent this
412                 pMail = null;
413                 error.mergeInto(new GigiApiException("You may not include an email in this certificate."));
414             }
415         } else {
416             if (emailTemp != null && emailTemp.isRequired()) {
417                 error.mergeInto(new GigiApiException("You need to include an email in this certificate."));
418             }
419         }
420
421         if (org != null) {
422             subject.put("O", org.getName());
423             subject.put("C", org.getState());
424             subject.put("ST", org.getProvince());
425             subject.put("L", org.getCity());
426             if (ou != null) {
427                 subject.put("OU", ou);
428             }
429         }
430         System.out.println(subject);
431         if ( !error.isEmpty()) {
432             throw error;
433         }
434         return new Certificate(u, subject, selectedDigest.toString(), //
435                 this.csr, this.csrType, profile, SANs.toArray(new SubjectAlternateName[SANs.size()]));
436     }
437
438     private String verifyName(GigiApiException error, PropertyTemplate nameTemp, PropertyTemplate wotUserTemp, String verifiedCN) {
439         // real names,
440         // possible configurations: name {y,null,?}, name=WoTUser {y,null}
441         // semantics:
442         // y * -> real
443         // null y -> default
444         // null null -> null
445         // ? y -> real, default
446         // ? null -> real, null
447         boolean realIsOK = false;
448         boolean nullIsOK = false;
449         boolean defaultIsOK = false;
450         if (wotUserTemp != null && ( !wotUserTemp.isRequired() || wotUserTemp.isMultiple())) {
451             error.mergeInto(new GigiApiException("Internal configuration error detected."));
452         }
453         if (nameTemp != null && nameTemp.isRequired() && !nameTemp.isMultiple()) {
454             realIsOK = true;
455         } else if (nameTemp == null) {
456             defaultIsOK = wotUserTemp != null;
457             nullIsOK = !defaultIsOK;
458         } else if (nameTemp != null && !nameTemp.isRequired() && !nameTemp.isMultiple()) {
459             realIsOK = true;
460             defaultIsOK = wotUserTemp != null;
461             nullIsOK = !defaultIsOK;
462         } else {
463             error.mergeInto(new GigiApiException("Internal configuration error detected."));
464         }
465         if (u.isValidName(name)) {
466             if (realIsOK) {
467                 verifiedCN = name;
468             } else {
469                 error.mergeInto(new GigiApiException("Your real name is not allowed in this certificate."));
470                 if (defaultIsOK) {
471                     name = DEFAULT_CN;
472                 } else if (nullIsOK) {
473                     name = "";
474                 }
475             }
476         } else if (name.equals(DEFAULT_CN)) {
477             if (defaultIsOK) {
478                 verifiedCN = name;
479             } else {
480                 error.mergeInto(new GigiApiException("The default name is not allowed in this certificate."));
481                 if (nullIsOK) {
482                     name = "";
483                 } else if (realIsOK) {
484                     name = u.getName().toString();
485                 }
486             }
487         } else if (name.equals("")) {
488             if (nullIsOK) {
489                 verifiedCN = name;
490             } else {
491                 error.mergeInto(new GigiApiException("A name is required in this certificate."));
492                 if (defaultIsOK) {
493                     name = DEFAULT_CN;
494                 } else if (realIsOK) {
495                     name = u.getName().toString();
496                 }
497             }
498         } else {
499             error.mergeInto(new GigiApiException("The name you entered was invalid."));
500
501         }
502         if (wotUserTemp != null) {
503             if ( !wotUserTemp.isRequired() || wotUserTemp.isMultiple()) {
504                 error.mergeInto(new GigiApiException("Internal configuration error detected."));
505             }
506             if ( !name.equals(DEFAULT_CN)) {
507                 name = DEFAULT_CN;
508                 error.mergeInto(new GigiApiException("You may not change the name for this certificate type."));
509             } else {
510                 verifiedCN = DEFAULT_CN;
511             }
512
513         } else {
514             if (nameTemp != null) {
515                 if (name.equals("")) {
516                     if (nameTemp.isRequired()) {
517                         // nothing, but required
518                         name = DEFAULT_CN;
519                         error.mergeInto(new GigiApiException("No name entered, but one was required."));
520                     } else {
521                         // nothing and not required
522
523                     }
524                 } else if (u.isValidName(name)) {
525                     verifiedCN = name;
526                 } else {
527                     if (nameTemp.isRequired()) {
528                         error.mergeInto(new GigiApiException("The name entered, does not match the details in your account. You cannot issue certificates with this name. Enter a name that matches the one that has been assured in your account, because a name is required for this certificate type."));
529                     } else if (name.equals(DEFAULT_CN)) {
530                         verifiedCN = DEFAULT_CN;
531                     } else {
532                         name = DEFAULT_CN;
533                         error.mergeInto(new GigiApiException("The name entered, does not match the details in your account. You cannot issue certificates with this name. Enter a name that matches the one that has been assured in your account or keep the default name."));
534                     }
535                 }
536             } else {
537                 if ( !name.equals("")) {
538                     name = "";
539                     error.mergeInto(new GigiApiException("No real name is included in this certificate. The real name, you entered will be ignored."));
540                 }
541             }
542         }
543         return verifiedCN;
544     }
545 }