View Javadoc
1   /*-
2    * #%L
3    * io.earcam.utilitarian.security
4    * %%
5    * Copyright (C) 2017 - 2018 earcam
6    * %%
7    * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
8    * 
9    * You <b>must</b> choose to accept, in full - any individual or combination of 
10   * the following licenses:
11   * <ul>
12   * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
13   * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
14   * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
15   * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
16   * </ul>
17   * #L%
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") // SonarQube false-positive; not an IP address
60  		private static final String EXTENSION_KEY_USAGE = "2.5.29.15";
61  		@SuppressWarnings("squid:S1313") // SonarQube false-positive; not an IP address
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 	 * @deprecated
258 	 * @see #localhostCertificate(KeyPair)
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 }