1 package org.cacert.gigi.pages.account.certs;
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;
16 import java.util.TreeSet;
18 import javax.servlet.http.HttpServletRequest;
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.Digest;
28 import org.cacert.gigi.dbObjects.Organisation;
29 import org.cacert.gigi.dbObjects.User;
30 import org.cacert.gigi.output.template.Scope;
31 import org.cacert.gigi.output.template.SprintfCommand;
32 import org.cacert.gigi.util.PEM;
34 import sun.security.pkcs.PKCS9Attribute;
35 import sun.security.pkcs10.PKCS10;
36 import sun.security.pkcs10.PKCS10Attribute;
37 import sun.security.pkcs10.PKCS10Attributes;
38 import sun.security.util.DerInputStream;
39 import sun.security.util.DerValue;
40 import sun.security.util.ObjectIdentifier;
41 import sun.security.x509.AVA;
42 import sun.security.x509.AlgorithmId;
43 import sun.security.x509.CertificateExtensions;
44 import sun.security.x509.DNSName;
45 import sun.security.x509.ExtendedKeyUsageExtension;
46 import sun.security.x509.Extension;
47 import sun.security.x509.GeneralName;
48 import sun.security.x509.GeneralNameInterface;
49 import sun.security.x509.GeneralNames;
50 import sun.security.x509.PKIXExtensions;
51 import sun.security.x509.RDN;
52 import sun.security.x509.RFC822Name;
53 import sun.security.x509.SubjectAlternativeNameExtension;
54 import sun.security.x509.X500Name;
56 public class CertificateRequest {
58 public static final String DEFAULT_CN = "CAcert WoT User";
60 public static final ObjectIdentifier OID_KEY_USAGE_SSL_SERVER = ObjectIdentifier.newInternal(new int[] {
61 1, 3, 6, 1, 5, 5, 7, 3, 1
64 public static final ObjectIdentifier OID_KEY_USAGE_SSL_CLIENT = ObjectIdentifier.newInternal(new int[] {
65 1, 3, 6, 1, 5, 5, 7, 3, 2
68 public static final ObjectIdentifier OID_KEY_USAGE_CODESIGN = ObjectIdentifier.newInternal(new int[] {
69 1, 3, 6, 1, 5, 5, 7, 3, 3
72 public static final ObjectIdentifier OID_KEY_USAGE_EMAIL_PROTECTION = ObjectIdentifier.newInternal(new int[] {
73 1, 3, 6, 1, 5, 5, 7, 3, 4
76 public static final ObjectIdentifier OID_KEY_USAGE_TIMESTAMP = ObjectIdentifier.newInternal(new int[] {
77 1, 3, 6, 1, 5, 5, 7, 3, 8
80 public static final ObjectIdentifier OID_KEY_USAGE_OCSP = ObjectIdentifier.newInternal(new int[] {
81 1, 3, 6, 1, 5, 5, 7, 3, 9
84 private CSRType csrType;
86 private final PublicKey pk;
90 public String CN = DEFAULT_CN;
92 private Set<SubjectAlternateName> SANs;
94 private Digest selectedDigest = Digest.getDefault();
96 private CertificateProfile profile = CertificateProfile.getById(1);
98 private String ou = "";
100 private Organisation org = null;
104 private String pDNS, pMail;
106 public CertificateRequest(User issuer, String csr) throws IOException, GeneralSecurityException, GigiApiException {
108 byte[] data = PEM.decode("(NEW )?CERTIFICATE REQUEST", csr);
109 PKCS10 parsed = new PKCS10(data);
110 PKCS10Attributes atts = parsed.getAttributes();
112 TreeSet<SubjectAlternateName> SANs = new TreeSet<>();
113 for (RDN r : parsed.getSubjectName().rdns()) {
114 for (AVA a : r.avas()) {
115 if (a.getObjectIdentifier().equals((Object) PKCS9Attribute.EMAIL_ADDRESS_OID)) {
116 SANs.add(new SubjectAlternateName(SANType.EMAIL, a.getValueString()));
117 } else if (a.getObjectIdentifier().equals((Object) X500Name.commonName_oid)) {
118 String value = a.getValueString();
119 if (value.contains(".") && !value.contains(" ")) {
120 SANs.add(new SubjectAlternateName(SANType.DNS, value));
124 } else if (a.getObjectIdentifier().equals((Object) PKIXExtensions.SubjectAlternativeName_Id)) {
125 // TODO? parse invalid SANs
130 for (PKCS10Attribute b : atts.getAttributes()) {
132 if ( !b.getAttributeId().equals((Object) PKCS9Attribute.EXTENSION_REQUEST_OID)) {
137 for (Extension c : ((CertificateExtensions) b.getAttributeValue()).getAllExtensions()) {
138 if (c instanceof SubjectAlternativeNameExtension) {
140 SubjectAlternativeNameExtension san = (SubjectAlternativeNameExtension) c;
141 GeneralNames obj = san.get(SubjectAlternativeNameExtension.SUBJECT_NAME);
142 for (int i = 0; i < obj.size(); i++) {
143 GeneralName generalName = obj.get(i);
144 GeneralNameInterface peeled = generalName.getName();
145 if (peeled instanceof DNSName) {
146 SANs.add(new SubjectAlternateName(SANType.DNS, ((DNSName) peeled).getName()));
147 } else if (peeled instanceof RFC822Name) {
148 SANs.add(new SubjectAlternateName(SANType.EMAIL, ((RFC822Name) peeled).getName()));
151 } else if (c instanceof ExtendedKeyUsageExtension) {
152 ExtendedKeyUsageExtension ekue = (ExtendedKeyUsageExtension) c;
153 for (String s : ekue.getExtendedKeyUsage()) {
154 if (s.equals(OID_KEY_USAGE_SSL_SERVER.toString())) {
156 profile = CertificateProfile.getByName("server");
157 } else if (s.equals(OID_KEY_USAGE_SSL_CLIENT.toString())) {
159 profile = CertificateProfile.getByName("client");
160 } else if (s.equals(OID_KEY_USAGE_CODESIGN.toString())) {
162 } else if (s.equals(OID_KEY_USAGE_EMAIL_PROTECTION.toString())) {
164 profile = CertificateProfile.getByName("mail");
165 } else if (s.equals(OID_KEY_USAGE_TIMESTAMP.toString())) {
167 } else if (s.equals(OID_KEY_USAGE_OCSP.toString())) {
172 // Unknown requested extension
178 pk = parsed.getSubjectPublicKeyInfo();
179 String sign = getSignatureAlgorithm(data);
183 this.csrType = CSRType.CSR;
186 public CertificateRequest(User issuer, String spkac, String spkacChallenge) throws IOException, GigiApiException, GeneralSecurityException {
188 String cleanedSPKAC = spkac.replaceAll("[\r\n]", "");
189 byte[] data = Base64.getDecoder().decode(cleanedSPKAC);
190 SPKAC parsed = new SPKAC(data);
191 if ( !parsed.getChallenge().equals(spkacChallenge)) {
192 throw new GigiApiException("Challenge mismatch");
194 pk = parsed.getPubkey();
195 String sign = getSignatureAlgorithm(data);
197 this.SANs = new HashSet<>();
198 this.csr = "SPKAC=" + cleanedSPKAC;
199 this.csrType = CSRType.SPKAC;
203 private static String getSignatureAlgorithm(byte[] data) throws IOException {
204 DerInputStream in = new DerInputStream(data);
205 DerValue[] seq = in.getSequence(3);
206 return AlgorithmId.parse(seq[1]).getName();
209 private void guessDigest(String sign) {
210 if (sign.toLowerCase().startsWith("sha512")) {
211 selectedDigest = Digest.SHA512;
212 } else if (sign.toLowerCase().startsWith("sha384")) {
213 selectedDigest = Digest.SHA384;
217 public void checkKeyStrength(PrintWriter out) {
218 out.println("Type: " + pk.getAlgorithm() + "<br/>");
219 if (pk instanceof RSAPublicKey) {
220 out.println("Exponent: " + ((RSAPublicKey) pk).getPublicExponent() + "<br/>");
221 out.println("Length: " + ((RSAPublicKey) pk).getModulus().bitLength());
222 } else if (pk instanceof DSAPublicKey) {
223 DSAPublicKey dpk = (DSAPublicKey) pk;
224 out.println("Length: " + dpk.getY().bitLength() + "<br/>");
225 out.println(dpk.getParams());
226 } else if (pk instanceof ECPublicKey) {
227 ECPublicKey epk = (ECPublicKey) pk;
228 out.println("Length-x: " + epk.getW().getAffineX().bitLength() + "<br/>");
229 out.println("Length-y: " + epk.getW().getAffineY().bitLength() + "<br/>");
230 out.println(epk.getParams().getCurve());
234 private TreeSet<SubjectAlternateName> parseSANBox(String SANs) {
235 String[] SANparts = SANs.split("[\r\n]+|, *");
236 TreeSet<SubjectAlternateName> parsedNames = new TreeSet<>();
237 for (String SANline : SANparts) {
238 String[] parts = SANline.split(":", 2);
239 if (parts.length == 1) {
240 if (parts[0].trim().equals("")) {
243 if (parts[0].contains("@")) {
244 parsedNames.add(new SubjectAlternateName(SANType.EMAIL, parts[0]));
246 parsedNames.add(new SubjectAlternateName(SANType.DNS, parts[0]));
251 SANType t = Certificate.SANType.valueOf(parts[0].toUpperCase());
255 parsedNames.add(new SubjectAlternateName(t, parts[1]));
256 } catch (IllegalArgumentException e) {
264 public Set<SubjectAlternateName> getSANs() {
268 public String getCN() {
272 public Organisation getOrg() {
276 public String getOu() {
280 public Digest getSelectedDigest() {
281 return selectedDigest;
284 public CertificateProfile getProfile() {
288 public boolean update(String CNin, String hashAlg, String profileStr, String newOrgStr, String ou, String SANsStr, PrintWriter out, HttpServletRequest req) throws GigiApiException {
289 GigiApiException error = new GigiApiException();
291 if (hashAlg != null) {
292 selectedDigest = Digest.valueOf(hashAlg);
294 this.profile = CertificateProfile.getByName(profileStr);
295 if (newOrgStr != null) {
296 Organisation neworg = Organisation.getById(Integer.parseInt(newOrgStr));
297 if (neworg == null || u.getOrganisations().contains(neworg)) {
300 error.mergeInto(new GigiApiException("Selected Organisation is not part of your account."));
305 boolean server = profile.getKeyName().equals("server");
306 SANs = verifySANs(error, server, parseSANBox(SANsStr));
308 if ( !error.isEmpty()) {
314 private Set<SubjectAlternateName> verifySANs(GigiApiException error, boolean server, Set<SubjectAlternateName> sANs2) {
315 Set<SubjectAlternateName> filteredSANs = new LinkedHashSet<>();
316 for (SubjectAlternateName san : sANs2) {
317 if (san.getType() == SANType.DNS) {
318 if (u.isValidDomain(san.getName()) && server) {
320 pDNS = san.getName();
322 filteredSANs.add(san);
325 } else if (san.getType() == SANType.EMAIL) {
326 if (u.isValidEmail(san.getName()) && !server) {
328 pMail = san.getName();
330 filteredSANs.add(san);
334 HashMap<String, Object> vars = new HashMap<>();
335 vars.put("SAN", san.getType().toString().toLowerCase() + ":" + san.getName());
336 error.mergeInto(new GigiApiException(new Scope(new SprintfCommand(//
337 "The requested Subject alternate name \"%s\" has been removed.", Arrays.asList("$SAN")), vars)));
342 public Certificate draft() throws GigiApiException {
344 GigiApiException error = new GigiApiException();
345 if ( !u.canIssue(this.profile)) {
346 this.profile = CertificateProfile.getById(1);
347 error.mergeInto(new GigiApiException("Certificate Profile is invalid."));
351 boolean server = profile.getKeyName().equals("server");
352 if ( !u.isValidName(CN) && !server && !CN.equals(DEFAULT_CN)) {
353 this.CN = DEFAULT_CN;
354 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."));
357 HashMap<String, String> subject = new HashMap<>();
358 if (server && pDNS != null) {
359 subject.put("CN", pDNS);
361 error.mergeInto(new GigiApiException("No email is included in this certificate."));
365 error.mergeInto(new GigiApiException("No real name is included in this certificate. The real name, you entered will be ignored."));
369 subject.put("CN", CN);
371 subject.put("EMAIL", pMail);
374 this.SANs = verifySANs(error, server, SANs);
376 subject.put("O", org.getName());
377 subject.put("C", org.getState());
378 subject.put("ST", org.getProvince());
379 subject.put("L", org.getCity());
380 subject.put("OU", ou);
383 if ( !error.isEmpty()) {
386 return new Certificate(u, subject, selectedDigest.toString(), //
387 this.csr, this.csrType, profile, SANs.toArray(new SubjectAlternateName[SANs.size()]));