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