001/* 002 * $HeadURL: http://juliusdavies.ca/svn/not-yet-commons-ssl/tags/commons-ssl-0.3.9/src/java/org/apache/commons/ssl/KeyStoreBuilder.java $ 003 * $Revision: 129 $ 004 * $Date: 2007-11-14 19:21:33 -0800 (Wed, 14 Nov 2007) $ 005 * 006 * ==================================================================== 007 * Licensed to the Apache Software Foundation (ASF) under one 008 * or more contributor license agreements. See the NOTICE file 009 * distributed with this work for additional information 010 * regarding copyright ownership. The ASF licenses this file 011 * to you under the Apache License, Version 2.0 (the 012 * "License"); you may not use this file except in compliance 013 * with the License. You may obtain a copy of the License at 014 * 015 * http://www.apache.org/licenses/LICENSE-2.0 016 * 017 * Unless required by applicable law or agreed to in writing, 018 * software distributed under the License is distributed on an 019 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 020 * KIND, either express or implied. See the License for the 021 * specific language governing permissions and limitations 022 * under the License. 023 * ==================================================================== 024 * 025 * This software consists of voluntary contributions made by many 026 * individuals on behalf of the Apache Software Foundation. For more 027 * information on the Apache Software Foundation, please see 028 * <http://www.apache.org/>. 029 * 030 */ 031 032package org.apache.commons.ssl; 033 034import org.apache.commons.ssl.asn1.ASN1EncodableVector; 035import org.apache.commons.ssl.asn1.DERInteger; 036import org.apache.commons.ssl.asn1.DERSequence; 037import org.apache.commons.ssl.util.Hex; 038 039import java.io.ByteArrayInputStream; 040import java.io.File; 041import java.io.FileInputStream; 042import java.io.FileOutputStream; 043import java.io.IOException; 044import java.math.BigInteger; 045import java.security.GeneralSecurityException; 046import java.security.InvalidKeyException; 047import java.security.Key; 048import java.security.KeyStore; 049import java.security.KeyStoreException; 050import java.security.NoSuchAlgorithmException; 051import java.security.NoSuchProviderException; 052import java.security.PrivateKey; 053import java.security.PublicKey; 054import java.security.UnrecoverableKeyException; 055import java.security.cert.Certificate; 056import java.security.cert.CertificateException; 057import java.security.cert.CertificateFactory; 058import java.security.cert.X509Certificate; 059import java.security.interfaces.DSAParams; 060import java.security.interfaces.DSAPrivateKey; 061import java.security.interfaces.RSAPrivateCrtKey; 062import java.security.interfaces.RSAPublicKey; 063import java.util.Arrays; 064import java.util.Collection; 065import java.util.Collections; 066import java.util.Enumeration; 067import java.util.Iterator; 068import java.util.LinkedList; 069import java.util.List; 070 071/** 072 * Builds Java Key Store files out of pkcs12 files, or out of pkcs8 files + 073 * certificate chains. Also supports OpenSSL style private keys (encrypted or 074 * unencrypted). 075 * 076 * @author Credit Union Central of British Columbia 077 * @author <a href="http://www.cucbc.com/">www.cucbc.com</a> 078 * @author <a href="mailto:juliusdavies@cucbc.com">juliusdavies@cucbc.com</a> 079 * @since 4-Nov-2006 080 */ 081public class KeyStoreBuilder { 082 private final static String PKCS7_ENCRYPTED = "1.2.840.113549.1.7.6"; 083 084 public static KeyStore build(byte[] jksOrCerts, char[] password) 085 throws IOException, CertificateException, KeyStoreException, 086 NoSuchAlgorithmException, InvalidKeyException, 087 NoSuchProviderException, ProbablyBadPasswordException, 088 UnrecoverableKeyException { 089 return build(jksOrCerts, null, password); 090 } 091 092 public static KeyStore build(byte[] jksOrCerts, byte[] privateKey, 093 char[] password) 094 throws IOException, CertificateException, KeyStoreException, 095 NoSuchAlgorithmException, InvalidKeyException, 096 NoSuchProviderException, ProbablyBadPasswordException, 097 UnrecoverableKeyException { 098 BuildResult br1 = parse(jksOrCerts, password); 099 BuildResult br2 = null; 100 KeyStore jks = null; 101 if (br1.jks != null) { 102 jks = br1.jks; 103 } else if (privateKey != null && privateKey.length > 0) { 104 br2 = parse(privateKey, password); 105 if (br2.jks != null) { 106 jks = br2.jks; 107 } 108 } 109 110 // If we happened to find a JKS file, let's just return that. 111 // JKS files get priority (in case some weirdo specifies both a PKCS12 112 // and a JKS file!). 113 if (jks != null) { 114 // Make sure the keystore we found is not corrupt. 115 validate(jks, password); 116 return jks; 117 } 118 119 Key key = br1.key; 120 X509Certificate[] chain = br1.chain; 121 boolean atLeastOneNotSet = key == null || chain == null; 122 if (atLeastOneNotSet && br2 != null) { 123 if (br2.key != null) { 124 // Notice that the key from build-result-2 gets priority over the 125 // key from build-result-1 (if both had valid keys). 126 key = br2.key; 127 } 128 if (chain == null) { 129 chain = br2.chain; 130 } 131 } 132 133 atLeastOneNotSet = key == null || chain == null; 134 if (atLeastOneNotSet) { 135 String missing = ""; 136 if (key == null) { 137 missing = " [Private key missing (bad password?)]"; 138 } 139 if (chain == null) { 140 missing += " [Certificate chain missing]"; 141 } 142 throw new KeyStoreException("Can't build keystore:" + missing); 143 } else { 144 145 X509Certificate theOne = buildChain(key, chain); 146 String alias = "alias"; 147 // The theOne is not null, then our chain was probably altered. 148 // Need to trim out the newly introduced null entries at the end of 149 // our chain. 150 if (theOne != null) { 151 chain = Certificates.trimChain(chain); 152 alias = Certificates.getCN(theOne); 153 alias = alias.replace(' ', '_'); 154 } 155 156 KeyStore ks = KeyStore.getInstance("jks"); 157 ks.load(null, password); 158 ks.setKeyEntry(alias, key, password, chain); 159 return ks; 160 } 161 } 162 163 /** 164 * Builds the chain up such that chain[ 0 ] contains the public key 165 * corresponding to the supplied private key. 166 * 167 * @param key private key 168 * @param chain array of certificates to build chain from 169 * @return theOne! 170 * @throws KeyStoreException no certificates correspond to private key 171 * @throws CertificateException java libraries complaining 172 * @throws NoSuchAlgorithmException java libraries complaining 173 * @throws InvalidKeyException java libraries complaining 174 * @throws NoSuchProviderException java libraries complaining 175 */ 176 public static X509Certificate buildChain(Key key, Certificate[] chain) 177 throws CertificateException, KeyStoreException, 178 NoSuchAlgorithmException, InvalidKeyException, 179 NoSuchProviderException { 180 X509Certificate theOne = null; 181 if (key instanceof RSAPrivateCrtKey) { 182 final RSAPrivateCrtKey rsa = (RSAPrivateCrtKey) key; 183 BigInteger publicExponent = rsa.getPublicExponent(); 184 BigInteger modulus = rsa.getModulus(); 185 for (int i = 0; i < chain.length; i++) { 186 X509Certificate c = (X509Certificate) chain[i]; 187 PublicKey pub = c.getPublicKey(); 188 if (pub instanceof RSAPublicKey) { 189 RSAPublicKey certKey = (RSAPublicKey) pub; 190 BigInteger pe = certKey.getPublicExponent(); 191 BigInteger mod = certKey.getModulus(); 192 if (publicExponent.equals(pe) && modulus.equals(mod)) { 193 theOne = c; 194 } 195 } 196 } 197 if (theOne == null) { 198 throw new KeyStoreException("Can't build keystore: [No certificates belong to the private-key]"); 199 } 200 X509Certificate[] newChain; 201 newChain = X509CertificateChainBuilder.buildPath(theOne, chain); 202 Arrays.fill(chain, null); 203 System.arraycopy(newChain, 0, chain, 0, newChain.length); 204 } 205 return theOne; 206 } 207 208 public static void validate(KeyStore jks, char[] password) 209 throws CertificateException, KeyStoreException, 210 NoSuchAlgorithmException, InvalidKeyException, 211 NoSuchProviderException, UnrecoverableKeyException { 212 Enumeration en = jks.aliases(); 213 String privateKeyAlias = null; 214 while (en.hasMoreElements()) { 215 String alias = (String) en.nextElement(); 216 boolean isKey = jks.isKeyEntry(alias); 217 if (isKey) { 218 if (privateKeyAlias != null) { 219 throw new KeyStoreException("Only 1 private key per keystore allowed for Commons-SSL"); 220 } else { 221 privateKeyAlias = alias; 222 } 223 } 224 } 225 if (privateKeyAlias == null) { 226 throw new KeyStoreException("No private keys found in keystore!"); 227 } 228 PrivateKey key = (PrivateKey) jks.getKey(privateKeyAlias, password); 229 Certificate[] chain = jks.getCertificateChain(privateKeyAlias); 230 X509Certificate[] x509Chain = Certificates.x509ifyChain(chain); 231 X509Certificate theOne = buildChain(key, x509Chain); 232 // The theOne is not null, then our chain was probably altered. 233 // Need to trim out the newly introduced null entries at the end of 234 // our chain. 235 if (theOne != null) { 236 x509Chain = Certificates.trimChain(x509Chain); 237 jks.deleteEntry(privateKeyAlias); 238 jks.setKeyEntry(privateKeyAlias, key, password, x509Chain); 239 } 240 } 241 242 protected static class BuildResult { 243 protected final Key key; 244 protected final X509Certificate[] chain; 245 protected final KeyStore jks; 246 247 protected BuildResult(Key key, Certificate[] chain, KeyStore jks) { 248 this.key = key; 249 this.jks = jks; 250 if (chain == null) { 251 this.chain = null; 252 } else if (chain instanceof X509Certificate[]) { 253 this.chain = (X509Certificate[]) chain; 254 } else { 255 X509Certificate[] x509 = new X509Certificate[chain.length]; 256 System.arraycopy(chain, 0, x509, 0, chain.length); 257 this.chain = x509; 258 } 259 } 260 } 261 262 263 public static BuildResult parse(byte[] stuff, char[] password) 264 throws IOException, CertificateException, KeyStoreException, 265 ProbablyBadPasswordException { 266 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 267 Key key = null; 268 Certificate[] chain = null; 269 try { 270 PKCS8Key pkcs8Key = new PKCS8Key(stuff, password); 271 key = pkcs8Key.getPrivateKey(); 272 } 273 catch (ProbablyBadPasswordException pbpe) { 274 throw pbpe; 275 } 276 catch (GeneralSecurityException gse) { 277 // no luck 278 } 279 280 List pemItems = PEMUtil.decode(stuff); 281 Iterator it = pemItems.iterator(); 282 LinkedList certificates = new LinkedList(); 283 while (it.hasNext()) { 284 PEMItem item = (PEMItem) it.next(); 285 byte[] derBytes = item.getDerBytes(); 286 String type = item.pemType.trim().toUpperCase(); 287 if (type.startsWith("CERT") || 288 type.startsWith("X509") || 289 type.startsWith("PKCS7")) { 290 ByteArrayInputStream in = new ByteArrayInputStream(derBytes); 291 X509Certificate c = (X509Certificate) cf.generateCertificate(in); 292 certificates.add(c); 293 } 294 chain = toChain(certificates); 295 } 296 297 if (chain != null || key != null) { 298 return new BuildResult(key, chain, null); 299 } 300 301 boolean isProbablyPKCS12 = false; 302 boolean isASN = false; 303 boolean isProbablyJKS = stuff.length >= 4 && 304 stuff[0] == (byte) 0xFE && 305 stuff[1] == (byte) 0xED && 306 stuff[2] == (byte) 0xFE && 307 stuff[3] == (byte) 0xED; 308 309 ASN1Structure asn1 = null; 310 try { 311 asn1 = ASN1Util.analyze(stuff); 312 isASN = true; 313 isProbablyPKCS12 = asn1.oids.contains(PKCS7_ENCRYPTED); 314 if (!isProbablyPKCS12 && asn1.bigPayload != null) { 315 asn1 = ASN1Util.analyze(asn1.bigPayload); 316 isProbablyPKCS12 = asn1.oids.contains(PKCS7_ENCRYPTED); 317 } 318 } 319 catch (Exception e) { 320 // isProbablyPKCS12 and isASN are set properly by now. 321 } 322 323 ByteArrayInputStream stuffStream = new ByteArrayInputStream(stuff); 324 if (isProbablyJKS) { 325 try { 326 return tryJKS("jks", stuffStream, password); 327 } 328 catch (ProbablyBadPasswordException pbpe) { 329 throw pbpe; 330 } 331 catch (GeneralSecurityException gse) { 332 // jks didn't work. 333 } 334 catch (IOException ioe) { 335 // jks didn't work. 336 } 337 } 338 if (isASN) { 339 if (isProbablyPKCS12) { 340 try { 341 return tryJKS("pkcs12", stuffStream, password); 342 } 343 catch (ProbablyBadPasswordException pbpe) { 344 throw pbpe; 345 } 346 catch (GeneralSecurityException gse) { 347 // pkcs12 didn't work. 348 } 349 catch (IOException ioe) { 350 // pkcs12 didn't work. 351 } 352 } else { 353 // Okay, it's ASN.1, but it's not PKCS12. Only one possible 354 // interesting things remains: X.509. 355 stuffStream.reset(); 356 357 try { 358 certificates = new LinkedList(); 359 Collection certs = cf.generateCertificates(stuffStream); 360 it = certs.iterator(); 361 while (it.hasNext()) { 362 X509Certificate x509 = (X509Certificate) it.next(); 363 certificates.add(x509); 364 } 365 chain = toChain(certificates); 366 if (chain != null && chain.length > 0) { 367 return new BuildResult(null, chain, null); 368 } 369 } 370 catch (CertificateException ce) { 371 // oh well 372 } 373 374 stuffStream.reset(); 375 // Okay, still no luck. Maybe it's an ASN.1 DER stream 376 // containing only a single certificate? (I don't completely 377 // trust CertificateFactory.generateCertificates). 378 try { 379 Certificate c = cf.generateCertificate(stuffStream); 380 X509Certificate x509 = (X509Certificate) c; 381 chain = toChain(Collections.singleton(x509)); 382 if (chain != null && chain.length > 0) { 383 return new BuildResult(null, chain, null); 384 } 385 } 386 catch (CertificateException ce) { 387 // oh well 388 } 389 } 390 } 391 392 if (!isProbablyJKS) { 393 String hex = Hex.encode(stuff, 0, 4); 394 try { 395 BuildResult br = tryJKS("jks", stuffStream, password); 396 // no exception thrown, so must be JKS. 397 System.out.println("Please report bug!"); 398 System.out.println("JKS usually start with binary FE ED FE ED, but this JKS started with: [" + hex + "]"); 399 return br; 400 } 401 catch (ProbablyBadPasswordException pbpe) { 402 System.out.println("Please report bug!"); 403 System.out.println("JKS usually start with binary FE ED FE ED, but this JKS started with: [" + hex + "]"); 404 throw pbpe; 405 } 406 catch (GeneralSecurityException gse) { 407 // jks didn't work. 408 } 409 catch (IOException ioe) { 410 // jks didn't work. 411 } 412 } 413 414 if (!isProbablyPKCS12) { 415 try { 416 BuildResult br = tryJKS("pkcs12", stuffStream, password); 417 // no exception thrown, so must be PKCS12. 418 System.out.println("Please report bug!"); 419 System.out.println("PKCS12 detection failed to realize this was PKCS12!"); 420 System.out.println(asn1); 421 return br; 422 } 423 catch (ProbablyBadPasswordException pbpe) { 424 System.out.println("Please report bug!"); 425 System.out.println("PKCS12 detection failed to realize this was PKCS12!"); 426 System.out.println(asn1); 427 throw pbpe; 428 } 429 catch (GeneralSecurityException gse) { 430 // pkcs12 didn't work. 431 } 432 catch (IOException ioe) { 433 // pkcs12 didn't work. 434 } 435 } 436 throw new KeyStoreException("failed to extract any certificates or private keys - maybe bad password?"); 437 } 438 439 private static BuildResult tryJKS(String keystoreType, 440 ByteArrayInputStream in, 441 char[] password) 442 throws GeneralSecurityException, IOException { 443 in.reset(); 444 keystoreType = keystoreType.trim().toLowerCase(); 445 boolean isPKCS12 = "pkcs12".equals(keystoreType); 446 KeyStore jksKeyStore = KeyStore.getInstance(keystoreType); 447 try { 448 Key key = null; 449 Certificate[] chain = null; 450 jksKeyStore.load(in, password); 451 Enumeration en = jksKeyStore.aliases(); 452 while (en.hasMoreElements()) { 453 String alias = (String) en.nextElement(); 454 if (jksKeyStore.isKeyEntry(alias)) { 455 key = jksKeyStore.getKey(alias, password); 456 if (key != null && key instanceof PrivateKey) { 457 chain = jksKeyStore.getCertificateChain(alias); 458 break; 459 } 460 } 461 if (isPKCS12 && en.hasMoreElements()) { 462 System.out.println("what kind of weird pkcs12 file has more than one alias?"); 463 } 464 } 465 if (isPKCS12) { 466 // PKCS12 is supposed to be just a key and a chain, anyway. 467 jksKeyStore = null; 468 } 469 return new BuildResult(key, chain, jksKeyStore); 470 } 471 catch (GeneralSecurityException gse) { 472 throw gse; 473 } 474 catch (IOException ioe) { 475 ioe.printStackTrace(); 476 477 String msg = ioe.getMessage(); 478 msg = msg != null ? msg.trim().toLowerCase() : ""; 479 if (isPKCS12) { 480 int x = msg.indexOf("failed to decrypt"); 481 int y = msg.indexOf("verify mac"); 482 x = Math.max(x, y); 483 if (x >= 0) { 484 throw new ProbablyBadPasswordException("Probably bad PKCS12 password: " + ioe); 485 } 486 } else { 487 int x = msg.indexOf("password"); 488 if (x >= 0) { 489 throw new ProbablyBadPasswordException("Probably bad JKS password: " + ioe); 490 } 491 } 492 ioe.printStackTrace(); 493 throw ioe; 494 } 495 } 496 497 private static X509Certificate[] toChain(Collection certs) { 498 if (certs != null && !certs.isEmpty()) { 499 X509Certificate[] x509Chain = new X509Certificate[certs.size()]; 500 certs.toArray(x509Chain); 501 return x509Chain; 502 } else { 503 return null; 504 } 505 } 506 507 508 public static void main(String[] args) throws Exception { 509 if (args.length < 2) { 510 System.out.println("KeyStoreBuilder: creates '[alias].jks' (Java Key Store)"); 511 System.out.println(" -topk8 mode: creates '[alias].pem' (x509 chain + unencrypted pkcs8)"); 512 System.out.println("[alias] will be set to the first CN value of the X509 certificate."); 513 System.out.println("-------------------------------------------------------------------"); 514 System.out.println("Usage1: [password] [file:pkcs12]"); 515 System.out.println("Usage2: [password] [file:private-key] [file:certificate-chain]"); 516 System.out.println("Usage3: -topk8 [password] [file:jks]"); 517 System.out.println("-------------------------------------------------------------------"); 518 System.out.println("[private-key] can be openssl format, or pkcs8."); 519 System.out.println("[password] decrypts [private-key], and also encrypts outputted JKS file."); 520 System.out.println("All files can be PEM or DER."); 521 System.exit(1); 522 } 523 char[] password = args[0].toCharArray(); 524 boolean toPKCS8 = false; 525 if ("-topk8".equalsIgnoreCase(args[0])) { 526 toPKCS8 = true; 527 password = args[1].toCharArray(); 528 args[1] = args[2]; 529 args[2] = null; 530 } 531 532 FileInputStream fin1 = new FileInputStream(args[1]); 533 byte[] bytes1 = Util.streamToBytes(fin1); 534 byte[] bytes2 = null; 535 if (args.length > 2 && args[2] != null) { 536 FileInputStream fin2 = new FileInputStream(args[2]); 537 bytes2 = Util.streamToBytes(fin2); 538 } 539 540 KeyStore ks = build(bytes1, bytes2, password); 541 Enumeration en = ks.aliases(); 542 String alias = null; 543 while (en.hasMoreElements()) { 544 if (alias == null) { 545 alias = (String) en.nextElement(); 546 } else { 547 System.out.println("Generated keystore contains more than 1 alias!?!?"); 548 } 549 } 550 551 String suffix = toPKCS8 ? ".pem" : ".jks"; 552 File f = new File(alias + suffix); 553 int count = 1; 554 while (f.exists()) { 555 f = new File(alias + "_" + count + suffix); 556 count++; 557 } 558 559 FileOutputStream jks = new FileOutputStream(f); 560 if (toPKCS8) { 561 List pemItems = new LinkedList(); 562 PrivateKey key = (PrivateKey) ks.getKey(alias, password); 563 Certificate[] chain = ks.getCertificateChain(alias); 564 byte[] pkcs8DerBytes = null; 565 if (key instanceof RSAPrivateCrtKey) { 566 RSAPrivateCrtKey rsa = (RSAPrivateCrtKey) key; 567 ASN1EncodableVector vec = new ASN1EncodableVector(); 568 vec.add(new DERInteger(BigInteger.ZERO)); 569 vec.add(new DERInteger(rsa.getModulus())); 570 vec.add(new DERInteger(rsa.getPublicExponent())); 571 vec.add(new DERInteger(rsa.getPrivateExponent())); 572 vec.add(new DERInteger(rsa.getPrimeP())); 573 vec.add(new DERInteger(rsa.getPrimeQ())); 574 vec.add(new DERInteger(rsa.getPrimeExponentP())); 575 vec.add(new DERInteger(rsa.getPrimeExponentQ())); 576 vec.add(new DERInteger(rsa.getCrtCoefficient())); 577 DERSequence seq = new DERSequence(vec); 578 byte[] derBytes = PKCS8Key.encode(seq); 579 PKCS8Key pkcs8 = new PKCS8Key(derBytes, null); 580 pkcs8DerBytes = pkcs8.getDecryptedBytes(); 581 } else if (key instanceof DSAPrivateKey) { 582 DSAPrivateKey dsa = (DSAPrivateKey) key; 583 DSAParams params = dsa.getParams(); 584 BigInteger g = params.getG(); 585 BigInteger p = params.getP(); 586 BigInteger q = params.getQ(); 587 BigInteger x = dsa.getX(); 588 BigInteger y = q.modPow(x, p); 589 590 ASN1EncodableVector vec = new ASN1EncodableVector(); 591 vec.add(new DERInteger(BigInteger.ZERO)); 592 vec.add(new DERInteger(p)); 593 vec.add(new DERInteger(q)); 594 vec.add(new DERInteger(g)); 595 vec.add(new DERInteger(y)); 596 vec.add(new DERInteger(x)); 597 DERSequence seq = new DERSequence(vec); 598 byte[] derBytes = PKCS8Key.encode(seq); 599 PKCS8Key pkcs8 = new PKCS8Key(derBytes, null); 600 pkcs8DerBytes = pkcs8.getDecryptedBytes(); 601 } 602 if (chain != null && chain.length > 0) { 603 for (int i = 0; i < chain.length; i++) { 604 X509Certificate x509 = (X509Certificate) chain[i]; 605 byte[] derBytes = x509.getEncoded(); 606 PEMItem item = new PEMItem(derBytes, "CERTIFICATE"); 607 pemItems.add(item); 608 } 609 } 610 if (pkcs8DerBytes != null) { 611 PEMItem item = new PEMItem(pkcs8DerBytes, "PRIVATE KEY"); 612 pemItems.add(item); 613 } 614 byte[] pem = PEMUtil.encode(pemItems); 615 jks.write(pem); 616 } else { 617 ks.store(jks, password); 618 } 619 jks.flush(); 620 jks.close(); 621 System.out.println("Successfuly wrote: [" + f.getPath() + "]"); 622 } 623 624 625}