/* * Copyright (c) 2012 Mike Heath. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package cloudeventbus.pki; import cloudeventbus.Subject; import org.apache.commons.codec.binary.Base64InputStream; import org.apache.commons.codec.binary.Base64OutputStream; import javax.crypto.Cipher; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; import java.util.Collection; import java.util.List; /** * @author Mike Heath <elcapo@gmail.com> */ public class CertificateUtils { public static final int KEY_SIZE = 2048; public static final int DEFAULT_CHALLENGE_LENGTH = 64; private static final ThreadLocal<SecureRandom> secureRandom = new ThreadLocal<SecureRandom>() { @Override protected SecureRandom initialValue() { return new SecureRandom(); } }; public static KeyPair generateKeyPair() { try { final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(KEY_SIZE); return keyPairGenerator.generateKeyPair(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static Certificate generateSelfSignedCertificate(KeyPair keyPair, long expirationDate, String comment) { return generateSelfSignedCertificate(keyPair, expirationDate, Arrays.asList(Subject.ALL), Arrays.asList(Subject.ALL), comment); } public static Certificate generateSelfSignedCertificate(KeyPair keyPair, long expirationDate, List<Subject> subscribePermissions, List<Subject> publishPermissions, String comment) { final long serialNumber = secureRandom.get().nextLong(); final Certificate certificate = new Certificate( Certificate.Type.AUTHORITY, serialNumber, serialNumber, expirationDate, keyPair.getPublic(), subscribePermissions, publishPermissions, comment, null); return signCertificate(certificate, keyPair.getPrivate(), certificate); } public static Certificate generateSignedCertificate(Certificate issuer, PrivateKey issuerPrivateKey, PublicKey newCertificatePublicKey, Certificate.Type type, long expirationDate, List<Subject> subscribePermissions, List<Subject> publishPermissions, String comment) { final long serialNumber = secureRandom.get().nextLong(); final Certificate certificate = new Certificate( type, serialNumber, issuer.getSerialNumber(), expirationDate, newCertificatePublicKey, subscribePermissions, publishPermissions, comment, null); return signCertificate(issuer, issuerPrivateKey, certificate); } public static Certificate signCertificate(Certificate issuer, PrivateKey issuerPrivateKey, Certificate certificate) { if (issuer.getSerialNumber() != certificate.getIssuer()) { throw new CertificateIssuerMismatchException("The authority certificate serial number doesn't much the certificate issuer."); } validatePermissions(issuer, certificate); final byte[] hash = certificate.hash(); try { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, issuerPrivateKey); final byte[] signature = cipher.doFinal(hash); return new Certificate( certificate.getType(), certificate.getSerialNumber(), certificate.getIssuer(), certificate.getExpirationDate(), certificate.getPublicKey(), certificate.getSubscribePermissions(), certificate.getPublishPermissions(), certificate.getComment(), signature); } catch (GeneralSecurityException e) { throw new CertificateSecurityException(e); } } public static byte[] generateChallenge() { final byte[] challenge = new byte[DEFAULT_CHALLENGE_LENGTH]; secureRandom.get().nextBytes(challenge); return challenge; } public static byte[] signChallenge(PrivateKey key, byte[] challenge, byte[] salt) { try { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, key); cipher.update(challenge); cipher.update(salt); return cipher.doFinal(); } catch (GeneralSecurityException e) { throw new CertificateSecurityException(e); } } public static void validateSignature(PublicKey key, byte[] challenge, byte[] salt, byte[] signature) { try { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, key); final byte[] decryptedSignature = cipher.doFinal(signature); if (decryptedSignature.length != challenge.length + salt.length) { throw new InvalidSignatureException("Signature doesn't match challenge"); } for (int i = 0; i < challenge.length; i++) { if (decryptedSignature[i] != challenge[i]) { throw new InvalidSignatureException("Signature doesn't match challenge"); } } for (int i = 0; i < salt.length; i++) { if (decryptedSignature[challenge.length + i] != salt[i]) { throw new InvalidSignatureException("Signature doesn't match challenge"); } } } catch (GeneralSecurityException e) { throw new CertificateSecurityException(e); } } private CertificateUtils() { // Don't instantiate me. } public static void validatePermissions(Certificate issuer, Certificate certificate) { validateSubSubjects(issuer.getPublishPermissions(), certificate.getPublishPermissions(), "publish"); validateSubSubjects(issuer.getSubscribePermissions(), certificate.getSubscribePermissions(), "subscribe"); } private static void validateSubSubjects(List<Subject> issuerPermissions, List<Subject> permissions, String type) { outer: for (Subject permission : permissions) { for (Subject parentPermission : issuerPermissions) { if (parentPermission.isSub(permission)) { continue outer; } } throw new CertificatePermissionError ("Permission " + permission + " is not a sub " + type + " permission to any permissions in parent certificate."); } } public static PrivateKey loadPrivateKey(String fileName) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { final Path path = Paths.get(fileName); final byte[] encodedPrivateKey = Files.readAllBytes(path); final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); final PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey); return keyFactory.generatePrivate(privateKeySpec); } public static void savePrivateKey(PrivateKey privateKey, String fileName) throws IOException { try ( final OutputStream outputStream = Files.newOutputStream(Paths.get(fileName), StandardOpenOption.CREATE_NEW) ) { outputStream.write(privateKey.getEncoded()); } } public static TrustStore loadTrustStore(String fileName) throws IOException { final TrustStore trustStore = new TrustStore(); final Path path = Paths.get(fileName); if (Files.notExists(path)) { return trustStore; } loadCertificates(path, trustStore); return trustStore; } private static void loadCertificates(Path path, Collection<Certificate> certificates) throws IOException { try ( final InputStream fileIn = Files.newInputStream(path); final InputStream in = new Base64InputStream(fileIn) ) { CertificateStoreLoader.load(in, certificates); } } /** * Loads a collection of certificates as a {@link CertificateChain} from the specified file. * * @param fileName the file from which the certificates will be loaded * @return a certificate chain holding the certificates in the specified file. * @throws IOException if an I/O error occurs */ public static CertificateChain loadCertificateChain(String fileName) throws IOException { final CertificateChain certificateChain = new CertificateChain(); final Path path = Paths.get(fileName); loadCertificates(path, certificateChain); return certificateChain; } /** * Saves a collection of certificates to the specified file. * * @param fileName the file to which the certificates will be saved * @param certificates the certificate to be saved * @throws IOException if an I/O error occurs */ public static void saveCertificates(String fileName, Collection<Certificate> certificates) throws IOException { final Path path = Paths.get(fileName); final Path directory = path.getParent(); if (directory != null && !Files.exists(directory)) { Files.createDirectories(directory); } try ( final OutputStream fileOut = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); final OutputStream out = new Base64OutputStream(fileOut) ) { CertificateStoreLoader.store(out, certificates); } } }