View Javadoc
1   /*-
2    * #%L
3    * io.earcam.instrumental.archive.sign
4    * %%
5    * Copyright (C) 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.instrumental.archive.sign;
20  
21  import static io.earcam.instrumental.archive.AbstractAsJarBuilder.CREATED_BY;
22  import static io.earcam.instrumental.archive.AbstractAsJarBuilder.V1_0;
23  import static io.earcam.instrumental.archive.ArchiveResourceSource.ResourceSourceLifecycle.FINAL;
24  import static java.nio.charset.StandardCharsets.UTF_8;
25  import static java.util.jar.Attributes.Name.SIGNATURE_VERSION;
26  
27  import java.io.ByteArrayOutputStream;
28  import java.security.KeyStore;
29  import java.security.MessageDigest;
30  import java.util.jar.Attributes;
31  import java.util.jar.Attributes.Name;
32  import java.util.jar.Manifest;
33  import java.util.stream.Stream;
34  
35  import io.earcam.instrumental.archive.ArchiveConfigurationPlugin;
36  import io.earcam.instrumental.archive.ArchiveRegistrar;
37  import io.earcam.instrumental.archive.ArchiveResource;
38  import io.earcam.instrumental.archive.ArchiveResourceListener;
39  import io.earcam.instrumental.archive.ArchiveResourceSource;
40  import io.earcam.instrumental.archive.ManifestProcessor;
41  import io.earcam.unexceptional.Closing;
42  import io.earcam.utilitarian.security.OpenedKeyStore;
43  import io.earcam.utilitarian.security.Signatures;
44  
45  // TODO file extension for DSA and make sure this can run if WithDigest invoked separately previously
46  /**
47   * <p>
48   * WithSignature archive extension
49   * </p>
50   *
51   */
52  public class WithSignature extends WithDigest implements ArchiveConfigurationPlugin, ManifestProcessor, ArchiveResourceSource, ArchiveResourceListener {
53  
54  	private static final String META_INF = "META-INF/";
55  
56  	private static final String DEFAULT_SIGNATURE_FILENAME = "SIGNATURE";
57  	private static final String DEFAULT_SIGNATURE_ALGORITHM = "SHA512withRSA";
58  
59  	private KeyStore keyStore;
60  	private String keyAlias;
61  	private char[] keyPassword;
62  	private String signatureFilename = DEFAULT_SIGNATURE_FILENAME;
63  	private String signatureAlgorithm = DEFAULT_SIGNATURE_ALGORITHM;
64  	private Manifest signatures = new Manifest();
65  	private String createdBy = WithSignature.class.getPackage().getName();
66  
67  
68  	WithSignature()
69  	{}
70  
71  
72  	/**
73  	 * <p>
74  	 * withSignature.
75  	 * </p>
76  	 *
77  	 * @return a {@link io.earcam.instrumental.archive.sign.WithSignature} object.
78  	 */
79  	public static WithSignature withSignature()
80  	{
81  		return new WithSignature();
82  	}
83  
84  
85  	@Override
86  	public void added(ArchiveResource resource)
87  	{
88  		if(!resource.name().startsWith(META_INF + signatureFilename + '.')) {
89  			super.added(resource);
90  		}
91  	}
92  
93  
94  	@Override
95  	public Stream<ArchiveResource> drain(ResourceSourceLifecycle stage)
96  	{
97  		if(stage == FINAL) {
98  			byte[] sign = sign();
99  			ArchiveResource sf = new ArchiveResource(META_INF + signatureFilename + ".SF", sign);
100 			OpenedKeyStore openedKeyStore = new OpenedKeyStore(keyStore, keyAlias, keyPassword);
101 			ArchiveResource cert = new ArchiveResource(META_INF + signatureFilename + ".RSA", Signatures.sign(sign, openedKeyStore, signatureAlgorithm));
102 			return Stream.of(sf, cert);
103 		}
104 		return Stream.empty();
105 	}
106 
107 
108 	private byte[] sign()
109 	{
110 		Attributes mainAttributes = signatures.getMainAttributes();
111 		mainAttributes.put(SIGNATURE_VERSION, V1_0);
112 		mainAttributes.put(new Name(CREATED_BY), createdBy);
113 		mainAttributes.put(new Name(digester.getAlgorithm() + "-Digest-Manifest"), manifestDigest());
114 
115 		mainAttributes.put(new Name(digester.getAlgorithm() + "-Digest-Manifest-Main-Attributes"), manifestMainDigest());
116 
117 		manifest.getEntries().keySet().forEach(this::addSigned);
118 
119 		ByteArrayOutputStream out = new ByteArrayOutputStream();
120 		Closing.closeAfterAccepting(out, signatures::write);
121 		return out.toByteArray();
122 	}
123 
124 
125 	private String manifestDigest()
126 	{
127 		ByteArrayOutputStream out = new ByteArrayOutputStream();
128 		Closing.closeAfterAccepting(out, manifest::write);
129 		return base64Digest(out.toByteArray());
130 	}
131 
132 
133 	private String manifestMainDigest()
134 	{
135 		StringBuilder main = new StringBuilder();
136 		attributesAsString(manifest.getMainAttributes(), main);
137 		return base64Digest(newline(main).toString().getBytes(UTF_8));
138 	}
139 
140 
141 	private void addSigned(String resourceName)
142 	{
143 		Attributes signature = new Attributes();
144 		signatures.getEntries().put(resourceName, signature);
145 
146 		Attributes attributes = manifest.getAttributes(resourceName);
147 
148 		StringBuilder sig = make72Safe(newline(new StringBuilder("Name: ").append(resourceName)));
149 		attributesAsString(attributes, sig);
150 
151 		String digested = base64Digest(sig.toString().getBytes(UTF_8));
152 		signature.putValue(digester.getAlgorithm() + "-Digest", digested);
153 	}
154 
155 
156 	private static final StringBuilder newline(StringBuilder string)
157 	{
158 		return string.append("\r\n");
159 	}
160 
161 
162 	private void attributesAsString(Attributes attributes, StringBuilder string)
163 	{
164 		attributes.forEach((k, v) -> string.append(make72Safe(newline(new StringBuilder(k.toString()).append(": ").append(v)))));
165 		newline(string);
166 	}
167 
168 
169 	private static StringBuilder make72Safe(StringBuilder line)
170 	{
171 		int length = line.length();
172 		if(length > 72) {
173 			int i = 70;
174 			while(i < length - 2) {
175 				line.insert(i, "\r\n ");
176 				i += 72;
177 				length += 3;
178 			}
179 		}
180 		return line;
181 	}
182 
183 
184 	@Override
185 	public void attach(ArchiveRegistrar core)
186 	{
187 		super.attach(core);
188 		core.registerResourceSource(this);
189 	}
190 
191 
192 	/* WithDigest API */
193 
194 	@Override
195 	public WithSignature digestedBy(StandardDigestAlgorithms hash)
196 	{
197 		super.digestedBy(hash);
198 		return this;
199 	}
200 
201 
202 	@Override
203 	public WithSignature digestedBy(MessageDigest digest)
204 	{
205 		super.digestedBy(digest);
206 		return this;
207 	}
208 
209 
210 	/* WithSignature API */
211 
212 	/**
213 	 * The store containing the key and cert(s)
214 	 *
215 	 * @param store the key store
216 	 * @return this plugin
217 	 */
218 	public WithSignature store(KeyStore store)
219 	{
220 		this.keyStore = store;
221 		return this;
222 	}
223 
224 
225 	/**
226 	 * The alias for the key in store
227 	 *
228 	 * @param alias by which the key shall be known
229 	 * @return this plugin
230 	 */
231 	public WithSignature alias(String alias)
232 	{
233 		this.keyAlias = alias;
234 		return this;
235 	}
236 
237 
238 	/**
239 	 * The password for the key in store
240 	 *
241 	 * @param password an array of {@link char} objects.
242 	 * @return this plugin
243 	 */
244 	public WithSignature password(char[] password)
245 	{
246 		this.keyPassword = password;
247 		return this;
248 	}
249 
250 
251 	/**
252 	 * Defaults to {@value DEFAULT_SIGNATURE_FILENAME} if not set
253 	 *
254 	 * @param signatureFilename a {@link java.lang.String} object.
255 	 * @return a {@link io.earcam.instrumental.archive.sign.WithSignature} object.
256 	 */
257 	public WithSignature signatureFileName(String signatureFilename)
258 	{
259 		this.signatureFilename = signatureFilename;
260 		return this;
261 	}
262 
263 
264 	/**
265 	 * <p>
266 	 * signatureAlgorithm.
267 	 * </p>
268 	 *
269 	 * @param algorithm a {@link java.lang.String} object.
270 	 * @return a {@link io.earcam.instrumental.archive.sign.WithSignature} object.
271 	 */
272 	public WithSignature signatureAlgorithm(StandardSignatureAlgorithms algorithm)
273 	{
274 		return signatureAlgorithm(algorithm.algorithm());
275 	}
276 
277 
278 	/**
279 	 * <p>
280 	 * signatureAlgorithm.
281 	 * </p>
282 	 *
283 	 * @param algorithm a {@link java.lang.String} object.
284 	 * @return a {@link io.earcam.instrumental.archive.sign.WithSignature} object.
285 	 */
286 	public WithSignature signatureAlgorithm(String algorithm)
287 	{
288 		this.signatureAlgorithm = algorithm;
289 		return this;
290 	}
291 
292 
293 	/**
294 	 * <p>
295 	 * createdBy.
296 	 * </p>
297 	 *
298 	 * @param author a {@link java.lang.String} object.
299 	 * @return a {@link io.earcam.instrumental.archive.sign.WithSignature} object.
300 	 */
301 	public WithSignature createdBy(String author)
302 	{
303 		this.createdBy = author;
304 		return this;
305 	}
306 
307 }