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