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 }