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