1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package io.earcam.utilitarian.security;
20
21 import java.io.StringWriter;
22 import java.io.Writer;
23 import java.math.BigInteger;
24 import java.security.KeyPair;
25 import java.security.cert.X509Certificate;
26 import java.time.LocalDate;
27 import java.time.ZoneId;
28 import java.util.Date;
29 import java.util.Objects;
30 import java.util.concurrent.TimeUnit;
31
32 import javax.annotation.ParametersAreNonnullByDefault;
33
34 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
35 import org.bouncycastle.asn1.x500.X500Name;
36 import org.bouncycastle.asn1.x509.BasicConstraints;
37 import org.bouncycastle.cert.CertIOException;
38 import org.bouncycastle.cert.X509CertificateHolder;
39 import org.bouncycastle.cert.X509v3CertificateBuilder;
40 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
41 import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
42 import org.bouncycastle.jce.X509KeyUsage;
43 import org.bouncycastle.jce.provider.BouncyCastleProvider;
44 import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
45 import org.bouncycastle.operator.ContentSigner;
46 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
47
48 import io.earcam.unexceptional.Closing;
49 import io.earcam.unexceptional.Exceptional;
50
51 @ParametersAreNonnullByDefault
52 public class Certificates {
53
54 private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();
55 static final String DN_LOCALHOST = "DN=localhost, L=London, C=GB";
56
57 public static class CertificateBuilder {
58
59 @SuppressWarnings("squid:S1313")
60 private static final String EXTENSION_KEY_USAGE = "2.5.29.15";
61 @SuppressWarnings("squid:S1313")
62 static final String EXTENSION_MAY_ACT_AS_CA = "2.5.29.19";
63 private String issuerName = "acme";
64 private String subjectName;
65 private BigInteger serial = BigInteger.ONE;
66 private boolean canSignOtherCertificates = false;
67 private LocalDate validFrom = LocalDate.now(ZoneId.systemDefault());
68 private long duration = 365;
69 private TimeUnit unit = TimeUnit.DAYS;
70 private String signatureAlgorithm = "SHA256withRSA";
71 private KeyPair keyPair;
72
73
74 CertificateBuilder()
75 {}
76
77
78 public CertificateBuilder issuer(String name)
79 {
80 issuerName = name;
81 return this;
82 }
83
84
85 public CertificateBuilder subject(String name)
86 {
87 subjectName = name;
88 return this;
89 }
90
91
92 public CertificateBuilder serial(int number)
93 {
94 return serial(BigInteger.valueOf(number));
95 }
96
97
98 public CertificateBuilder serial(BigInteger number)
99 {
100 this.serial = number;
101 return this;
102 }
103
104
105 public CertificateBuilder canSignOtherCertificates()
106 {
107 canSignOtherCertificates = true;
108 return this;
109 }
110
111
112 public CertificateBuilder key(KeyPair pair)
113 {
114 this.keyPair = pair;
115 return this;
116 }
117
118
119 public CertificateBuilder signedBy(String signatureAlgorithm)
120 {
121 this.signatureAlgorithm = signatureAlgorithm;
122 return this;
123 }
124
125
126 public CertificateBuilder validFrom(LocalDate from)
127 {
128 validFrom = from;
129 return this;
130 }
131
132
133 public CertificateBuilder validFor(long duration, TimeUnit unit)
134 {
135 this.duration = duration;
136 this.unit = unit;
137 return this;
138 }
139
140
141 public X509Certificate toX509()
142 {
143 Objects.requireNonNull(keyPair, "keyPair");
144 Objects.requireNonNull(issuerName, "issuerName");
145 Objects.requireNonNull(subjectName, "subjectName");
146 X500Name issuer = new X500Name(addCnIfMissing(issuerName));
147 X500Name subject = new X500Name(addCnIfMissing(subjectName));
148
149 Date from = javaDate(validFrom);
150 Date to = new Date(from.getTime() + unit.toMillis(duration));
151
152 X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(
153 issuer,
154 serial,
155 from,
156 to,
157 subject,
158 keyPair.getPublic());
159
160 Exceptional.accept(this::addExtensions, certificateBuilder);
161
162 X509CertificateHolder signed = sign(keyPair, signatureAlgorithm, certificateBuilder);
163
164 JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(PROVIDER);
165 return Exceptional.apply(converter::getCertificate, signed);
166 }
167
168
169 static Date javaDate(LocalDate date)
170 {
171 return java.sql.Date.valueOf(date);
172 }
173
174
175 static LocalDate localDate(Date date)
176 {
177 return new java.sql.Date(date.getTime()).toLocalDate();
178 }
179
180
181 private void addExtensions(X509v3CertificateBuilder certificateBuilder) throws CertIOException
182 {
183 certificateBuilder.addExtension(
184 new ASN1ObjectIdentifier(EXTENSION_MAY_ACT_AS_CA),
185 false,
186 new BasicConstraints(canSignOtherCertificates)).addExtension(
187 new ASN1ObjectIdentifier(EXTENSION_KEY_USAGE),
188 true,
189 new X509KeyUsage(
190 X509KeyUsage.digitalSignature |
191 X509KeyUsage.nonRepudiation |
192 X509KeyUsage.keyEncipherment |
193 X509KeyUsage.dataEncipherment));
194 }
195
196
197 private String addCnIfMissing(String name)
198 {
199 return (name.indexOf('=') == -1) ? "CN=" + name : name;
200 }
201
202
203 private static X509CertificateHolder sign(KeyPair keyPair, String signatureAlgorithm, X509v3CertificateBuilder certificateBuilder)
204 {
205 JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
206 ContentSigner sigGen = Exceptional.apply(jcaContentSignerBuilder::build, keyPair.getPrivate());
207 return certificateBuilder.build(sigGen);
208 }
209
210
211 public String toPem()
212 {
213 StringWriter writer = new StringWriter();
214 toPem(writer);
215 return writer.toString();
216 }
217
218
219 public void toPem(Writer writer)
220 {
221 Closing.closeAfterAccepting(JcaPEMWriter::new, writer, toX509(), JcaPEMWriter::writeObject);
222 }
223
224 }
225
226
227 private Certificates()
228 {}
229
230
231 public static CertificateBuilder certificate(KeyPair pair, String subjectName)
232 {
233 return certificate(pair)
234 .subject(subjectName);
235 }
236
237
238 public static CertificateBuilder certificate(KeyPair pair)
239 {
240 return certificate().key(pair);
241 }
242
243
244 public static CertificateBuilder certificate()
245 {
246 return new CertificateBuilder();
247 }
248
249
250 public static X509Certificate localhostCertificate(KeyPair keys)
251 {
252 return hostCertificate(keys, DN_LOCALHOST);
253 }
254
255
256
257
258
259
260 @Deprecated
261 public static X509Certificate hostCertificate(KeyPair keys)
262 {
263 throw new UnsupportedOperationException();
264 }
265
266
267 public static X509Certificate hostCertificate(KeyPair keys, String hostname)
268 {
269 return certificate(keys)
270 .subject(hostname)
271 .toX509();
272 }
273 }