1 /*
2 * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package org.openjdk.jigsaw.cli;
27
28 import java.io.*;
29 import java.net.URI;
30 import java.nio.channels.FileChannel;
31 import java.nio.file.Files;
32 import java.nio.file.StandardCopyOption;
33 import java.security.*;
34 import java.security.cert.X509Certificate;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.List;
38 import javax.security.auth.DestroyFailedException;
39
40 import static java.lang.System.err;
41 import static java.lang.System.in;
42 import static java.lang.System.out;
43 import static java.security.KeyStore.PasswordProtection;
44 import static java.security.KeyStore.PrivateKeyEntry;
45 import java.util.Map;
46
47 import org.openjdk.jigsaw.*;
48 import org.openjdk.jigsaw.ModuleFileParserException;
49 import org.openjdk.jigsaw.ModuleFileParser.Event;
50 import org.openjdk.internal.joptsimple.OptionException;
51 import org.openjdk.internal.joptsimple.OptionParser;
52 import org.openjdk.internal.joptsimple.OptionSet;
53 import org.openjdk.internal.joptsimple.OptionSpec;
54
55 import static org.openjdk.jigsaw.ModuleFile.*;
56 import static org.openjdk.jigsaw.FileConstants.ModuleFile.*;
57
58 import sun.security.pkcs.PKCS7;
59 import sun.security.util.Password;
60
61 /* Interface:
62
63 jsign [-v] [--keystore <keystore-location>] \
64 [--storetype <keystore-type>] [--protected] \
65 [--tsa <url>] [--signedmodulefile <signed-module-file>] \
66 <module-file> <signer-alias>
67
68 */
69
70 public final class Signer {
71
72 private OptionParser parser;
73 private boolean verbose;
74
75 // If true, do not prompt for a keystore password (for example, with a
76 // keystore provider that is configured with its own PIN entry device).
77 private boolean protectedPath;
78
79 // Module signer's alias
80 private String signer;
81
82 // Module signer's keystore location
83 private String keystore;
84
85 // Module signer's keystore type
86 private String storetype;
87
88 // Time Stamping Authority URI
89 private URI tsaURI;
90
91 // Signed Module File (if not specified, use module file path)
92 private File signedModuleFile;
93
94 public static void main(String[] args) throws Exception {
95 try {
96 run(args);
97 } catch (OptionException x) {
98 err.println(x.getMessage());
99 System.exit(1);
100 } catch (Command.Exception x) {
101 err.println(x.getMessage());
102 x.printStackTrace();
103 System.exit(1);
104 }
105 }
106
107 public static void run(String[] args)
108 throws OptionException, Command.Exception
109 {
110 new Signer().exec(args);
111 }
112
113 private Signer() { }
114
115 private void exec(String[] args) throws OptionException, Command.Exception
116 {
117 parser = new OptionParser();
118
119 parser.acceptsAll(Arrays.asList("v", "verbose"),
120 "Enable verbose output");
121 parser.acceptsAll(Arrays.asList("h", "?", "help"),
122 "Show this help message");
123 parser.acceptsAll(Arrays.asList("p", "protected"),
124 "Do not prompt for a keystore password");
125
126 OptionSpec<String> keystoreUrl
127 = (parser.acceptsAll(Arrays.asList("k", "keystore"),
128 "URL or file name of module signer's"
129 + " keystore")
130 .withRequiredArg()
131 .describedAs("location")
132 .ofType(String.class));
133
134 OptionSpec<String> keystoreType
135 = (parser.acceptsAll(Arrays.asList("s", "storetype"),
136 "Module signer's keystore type")
137 .withRequiredArg()
138 .describedAs("type")
139 .ofType(String.class));
140
141 OptionSpec<URI> tsa
142 = (parser.acceptsAll(Arrays.asList("t", "tsa"),
143 "URL of Time Stamping Authority")
144 .withRequiredArg()
145 .describedAs("location")
146 .ofType(URI.class));
147
148 OptionSpec<File> signedModule
149 = (parser.acceptsAll(Arrays.asList("f", "signedmodulefile"),
150 "File name of signed module file")
151 .withRequiredArg()
152 .describedAs("path")
153 .ofType(File.class));
154
155 if (args.length == 0) {
156 usage();
157 return;
158 }
159
160 OptionSet opts = parser.parse(args);
161 if (opts.has("h")) {
162 usage();
163 return;
164 }
165 verbose = opts.has("v");
166 if (opts.has(keystoreUrl)) {
167 keystore = opts.valueOf(keystoreUrl);
168 // NONE is for non-file based keystores, ex. PKCS11 tokens
169 if (keystore.equals("NONE"))
170 keystore = null;
171 } else {
172 // default is $HOME/.keystore
173 keystore = System.getProperty("user.home")
174 + File.separator + ".keystore";
175 }
176 storetype = opts.has(keystoreType) ? opts.valueOf(keystoreType)
177 : KeyStore.getDefaultType();
178 protectedPath = opts.has("protected");
179 if (opts.has(tsa))
180 tsaURI = opts.valueOf(tsa);
181 if (opts.has(signedModule))
182 signedModuleFile = opts.valueOf(signedModule);
183
184 new Jsign().run(null, opts);
185 }
186
187 private void usage() {
188 out.format("%n");
189 out.format("usage: jsign [-v] [--keystore <keystore-location>] "
190 + "[--storetype <keystore-type>] [--protected] "
191 + "[--tsa <url>] [--signedmodulefile <signed-module-file>] "
192 + "<module-file> <signer-alias>%n");
193 out.format("%n");
194 try {
195 parser.printHelpOn(out);
196 } catch (IOException x) {
197 throw new AssertionError(x);
198 }
199 out.format("%n");
200 }
201
202 class Jsign extends Command<SimpleLibrary> {
203 protected void go(SimpleLibrary lib)
204 throws Command.Exception
205 {
206 String moduleFile = command;
207 String signer = takeArg();
208 if (verbose)
209 out.println("Signing module using '" + signer + "' from "
210 + " keystore " + keystore);
211
212 PrivateKeyEntry pke = null;
213 try {
214 pke = getPrivateKeyEntry(signer);
215 } catch (GeneralSecurityException | IOException x) {
216 throw new Command.Exception("unable to extract private key " +
217 "entry from keystore", x);
218 }
219
220 // First, read in module file and get the hashes
221 List<byte[]> hashes = new ArrayList<>();
222 int moduleInfoLength = 0;
223 try (FileInputStream mfis = new FileInputStream(moduleFile)) {
224 ValidatingModuleFileParser parser =
225 ModuleFile.newValidatingParser(mfis);
226 while (parser.hasNext()) {
227 Event event = parser.next();
228 if (event == Event.END_SECTION) {
229 SectionHeader header = parser.getSectionHeader();
230 if (header.getType() == SectionType.SIGNATURE)
231 throw new Command.Exception("module file is already signed");
232 if (header.getType() == SectionType.MODULE_INFO)
233 moduleInfoLength = header.getCSize();
234 }
235 }
236 hashes.add(parser.getHeaderHash());
237 for (byte[] hash: parser.getHashes().values())
238 hashes.add(hash); // section hashes
239 hashes.add(parser.getFileHash());
240 } catch (IOException | ModuleFileParserException x) {
241 throw new Command.Exception("unable to read module file", x);
242 }
243
244 // Next, generate signature and insert into signed module file
245 File tmpFile = (signedModuleFile == null)
246 ? new File(moduleFile + ".sig") : signedModuleFile;
247 try (RandomAccessFile mraf = new RandomAccessFile(moduleFile, "r");
248 RandomAccessFile raf = new RandomAccessFile(tmpFile, "rw"))
249 {
250 raf.setLength(0);
251
252 // Transfer header and module-info from module file
253 // to signed module file.
254 long remainderStart = ModuleFileHeader.LENGTH
255 + SectionHeader.LENGTH
256 + moduleInfoLength;
257 FileChannel source = mraf.getChannel();
258 FileChannel dest = raf.getChannel();
259 for (long pos = 0; pos < remainderStart;) {
260 pos += source.transferTo(pos, remainderStart - pos, dest);
261 }
262
263 // Write out the Signature Section
264 writeSignatureSection(raf, hashes, pke);
265
266 // Transfer the remainder of the file
267 for (long pos = remainderStart; pos < mraf.length();) {
268 pos += source.transferTo(pos, mraf.length() - pos, dest);
269 }
270
271 } catch (IOException | GeneralSecurityException x) {
272 try {
273 Files.deleteIfExists(tmpFile.toPath());
274 } catch (IOException ioe) {
275 x.addSuppressed(ioe);
276 }
277 throw new Command.Exception("unable to sign module", x);
278 }
279
280 if (signedModuleFile == null) {
281 try {
282 Files.move(tmpFile.toPath(), new File(moduleFile).toPath(),
283 StandardCopyOption.REPLACE_EXISTING);
284 } catch (IOException ioe) {
285 throw new Command.Exception("unable to sign module", ioe);
286 }
287 }
288 }
289
290 private PrivateKeyEntry getPrivateKeyEntry(String signer)
291 throws GeneralSecurityException, IOException
292 {
293 PasswordProtection storePassword = null;
294 PasswordProtection keyPassword = null;
295
296 try (InputStream inStream = new FileInputStream(keystore)) {
297
298 // Prompt user for the keystore password (except when
299 // protected is true or when using Windows MY native keystore)
300 if (!protectedPath || !isWindowsKeyStore(storetype)) {
301 err.print("Enter password for " + storetype
302 + " keystore: ");
303 err.flush();
304 storePassword
305 = new PasswordProtection(Password.readPassword(in));
306 }
307
308 // Load the keystore
309 KeyStore ks = KeyStore.getInstance(storetype);
310 ks.load(inStream, storePassword.getPassword());
311
312 if (!ks.containsAlias(signer))
313 throw new KeyStoreException("Signer alias " + signer
314 + "does not exist");
315
316 if (!ks.entryInstanceOf(signer, PrivateKeyEntry.class))
317 throw new KeyStoreException("Signer alias " + signer
318 + "is not a private key");
319
320 // First try to recover the key using keystore password
321 try {
322 return (PrivateKeyEntry)ks.getEntry(signer, storePassword);
323 } catch (UnrecoverableKeyException e) {
324 if (protectedPath ||
325 storetype.equalsIgnoreCase("PKCS11") ||
326 storetype.equalsIgnoreCase("Windows-MY")) {
327 throw e;
328 }
329 // Otherwise prompt the user for key password
330 err.print("Enter password for '" + signer + "' key: ");
331 err.flush();
332 keyPassword =
333 new PasswordProtection(Password.readPassword(in));
334 return (PrivateKeyEntry)ks.getEntry(signer, keyPassword);
335 }
336 } finally {
337 try {
338 if (storePassword != null) {
339 storePassword.destroy();
340 }
341 } catch (DestroyFailedException x) {
342 if (verbose)
343 err.println("Could not destroy keystore password: "
344 + x);
345 }
346 try {
347 if (keyPassword != null) {
348 keyPassword.destroy();
349 }
350 } catch (DestroyFailedException x) {
351 if (verbose)
352 err.println("Could not destroy private key password: "
353 + x);
354 }
355 }
356 }
357
358 /*
359 * The signature algorithm is derived from the signer key.
360 */
361 private String getSignatureAlg(PrivateKey privateKey)
362 throws SignatureException
363 {
364 switch (privateKey.getAlgorithm()) {
365 case "RSA":
366 return "SHA256withRSA";
367 case "DSA":
368 return "SHA256withDSA";
369 case "EC":
370 return "SHA256withECDSA";
371 }
372 throw new SignatureException(privateKey.getAlgorithm()
373 + " private keys are not supported");
374 }
375
376 /*
377 * Generates the module file signature and writes the Signature Section.
378 *
379 * The data to be signed is a list of hash values:
380 *
381 * ToBeSignedContent {
382 * u2 moduleHeaderHashLength;
383 * b* moduleHeaderHash;
384 * u2 moduleInfoHashLength;
385 * b* moduleInfoHash;
386 * u2 sectionHashLength;
387 * b* sectionHash;
388 * ...
389 * // other section hashes (in same order as module file)
390 * ...
391 * u2 moduleFileHashLength;
392 * b* moduleFileHash;
393 * }
394 *
395 */
396 private void writeSignatureSection(DataOutput out,
397 List<byte[]> hashes,
398 PrivateKeyEntry pke)
399 throws GeneralSecurityException, IOException
400 {
401 ByteArrayOutputStream baos = new ByteArrayOutputStream();
402 short hashLength;
403 for (byte[] hash : hashes) {
404 hashLength = (short)hash.length;
405 baos.write((byte) ((hashLength >>> 8) & 0xFF));
406 baos.write((byte) ((hashLength >>> 0) & 0xFF));
407 baos.write(hash, 0, hashLength);
408 }
409 byte[] toBeSigned = baos.toByteArray();
410
411 // Compute the signature
412 PrivateKey privateKey = pke.getPrivateKey();
413 Signature sig = Signature.getInstance(getSignatureAlg(privateKey));
414 sig.initSign(privateKey);
415 sig.update(toBeSigned);
416
417 // Create the PKCS #7 signed data message
418 X509Certificate[] signerChain =
419 (X509Certificate[])pke.getCertificateChain();
420 byte[] signedData = PKCS7.generateSignedData(sig.sign(),
421 signerChain,
422 toBeSigned,
423 sig.getAlgorithm(),
424 tsaURI);
425
426 // Generate the hash for the signature header and content
427 baos = new ByteArrayOutputStream();
428 DataOutputStream dos = new DataOutputStream(baos);
429 short signatureType = (short)SignatureType.PKCS7.value();
430 dos.writeShort(signatureType);
431 dos.writeInt(signedData.length);
432 byte[] signatureHeader = baos.toByteArray();
433 MessageDigest md =
434 MessageDigest.getInstance(HashType.SHA256.algorithm());
435 md.update(signatureHeader);
436 md.update(signedData);
437 byte[] hash = md.digest();
438
439 // Write out the Signature Section
440 SectionHeader header = new SectionHeader(SectionType.SIGNATURE,
441 Compressor.NONE,
442 signedData.length + 6,
443 (short)0, hash);
444 header.write(out);
445 out.write(signatureHeader);
446 out.write(signedData);
447 }
448 }
449
450 /**
451 * Returns true if KeyStore has a password. This is true except for
452 * MSCAPI KeyStores.
453 */
454 private static boolean isWindowsKeyStore(String storetype) {
455 return storetype.equalsIgnoreCase("Windows-MY")
456 || storetype.equalsIgnoreCase("Windows-ROOT");
457 }
458 }
--- EOF ---