com.spotify.helios.client.tls.X509CertificateFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.helios.client.tls.X509CertificateFactory.java

Source

/*
 * Copyright (c) 2015 Spotify AB.
 *
 * 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 com.spotify.helios.client.tls;

import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;

import com.eaio.uuid.UUID;
import com.spotify.sshagentproxy.AgentProxy;
import com.spotify.sshagentproxy.Identity;

import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509ExtensionUtils;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.DigestCalculator;
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.StringWriter;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static com.spotify.helios.common.Hash.sha1;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.WRITE;

public class X509CertificateFactory {

    private static final Path HELIOS_HOME = Paths.get(System.getProperty("user.home"), ".helios");

    private static final BaseEncoding HEX_ENCODING = BaseEncoding.base16().lowerCase();

    private static final Logger log = LoggerFactory.getLogger(X509CertificateFactory.class);

    private static final JcaX509CertificateConverter CERTIFICATE_CONVERTER = new JcaX509CertificateConverter()
            .setProvider("BC");

    private static final BaseEncoding KEY_ID_ENCODING = BaseEncoding.base16().upperCase().withSeparator(":", 2);

    private static final int KEY_SIZE = 2048;

    private final Path cacheDirectory;
    private final int validBeforeMilliseconds;
    private final int validAfterMilliseconds;

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    public X509CertificateFactory() {
        this(HELIOS_HOME, (int) TimeUnit.HOURS.toMillis(1), (int) TimeUnit.HOURS.toMillis(48));
    }

    public X509CertificateFactory(final Path cacheDirectory, final int validBeforeMilliseconds,
            final int validAfterMillieconds) {
        this.cacheDirectory = cacheDirectory;
        this.validBeforeMilliseconds = validBeforeMilliseconds;
        this.validAfterMilliseconds = validAfterMillieconds;
    }

    public CertificateAndPrivateKey get(final AgentProxy agentProxy, final Identity identity,
            final String username) {
        final MessageDigest identityHash = sha1();
        identityHash.update(identity.getKeyBlob());
        identityHash.update(username.getBytes());

        final String identityHex = HEX_ENCODING.encode(identityHash.digest()).substring(0, 8);
        final Path cacheCertPath = cacheDirectory.resolve(identityHex + ".crt");
        final Path cacheKeyPath = cacheDirectory.resolve(identityHex + ".pem");

        boolean useCached = false;
        CertificateAndPrivateKey cached = null;

        try {
            if (Files.exists(cacheCertPath) && Files.exists(cacheKeyPath)) {
                cached = CertificateAndPrivateKey.from(cacheCertPath, cacheKeyPath);
            }
        } catch (IOException | GeneralSecurityException e) {
            // some sort of issue with cached certificate, that's fine
            log.debug("error reading cached certificate and key from {} for identity={}", cacheDirectory,
                    identity.getComment(), e);
        }

        if ((cached != null) && (cached.getCertificate() instanceof X509Certificate)) {
            final X509Certificate cachedX509 = (X509Certificate) cached.getCertificate();
            final Date now = new Date();

            if (now.after(cachedX509.getNotBefore()) && now.before(cachedX509.getNotAfter())) {
                useCached = true;
            }
        }

        if (useCached) {
            log.debug("using existing certificate for {} from {}", username, cacheCertPath);
            return cached;
        } else {
            final CertificateAndPrivateKey generated = generate(agentProxy, identity, username);
            saveToCache(cacheDirectory, cacheCertPath, cacheKeyPath, generated);

            return generated;
        }
    }

    private CertificateAndPrivateKey generate(final AgentProxy agentProxy, final Identity identity,
            final String username) {

        final UUID uuid = new UUID();
        final Calendar calendar = Calendar.getInstance();
        final X500Name issuerDN = new X500Name("C=US,O=Spotify,CN=helios-client");
        final X500Name subjectDN = new X500NameBuilder().addRDN(BCStyle.UID, username).build();

        calendar.add(Calendar.MILLISECOND, -validBeforeMilliseconds);
        final Date notBefore = calendar.getTime();

        calendar.add(Calendar.MILLISECOND, validBeforeMilliseconds + validAfterMilliseconds);
        final Date notAfter = calendar.getTime();

        // Reuse the UUID time as a SN
        final BigInteger serialNumber = BigInteger.valueOf(uuid.getTime()).abs();

        try {
            final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
            keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());

            final KeyPair keyPair = keyPairGenerator.generateKeyPair();
            final SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo
                    .getInstance(ASN1Sequence.getInstance(keyPair.getPublic().getEncoded()));

            final X509v3CertificateBuilder builder = new X509v3CertificateBuilder(issuerDN, serialNumber, notBefore,
                    notAfter, subjectDN, subjectPublicKeyInfo);

            final DigestCalculator digestCalculator = new BcDigestCalculatorProvider()
                    .get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));
            final X509ExtensionUtils utils = new X509ExtensionUtils(digestCalculator);

            final SubjectKeyIdentifier keyId = utils.createSubjectKeyIdentifier(subjectPublicKeyInfo);
            final String keyIdHex = KEY_ID_ENCODING.encode(keyId.getKeyIdentifier());
            log.info("generating an X509 certificate for {} with key ID={} and identity={}", username, keyIdHex,
                    identity.getComment());

            builder.addExtension(Extension.subjectKeyIdentifier, false, keyId);
            builder.addExtension(Extension.authorityKeyIdentifier, false,
                    utils.createAuthorityKeyIdentifier(subjectPublicKeyInfo));
            builder.addExtension(Extension.keyUsage, false,
                    new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign));
            builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));

            final X509CertificateHolder holder = builder.build(new SshAgentContentSigner(agentProxy, identity));

            final X509Certificate certificate = CERTIFICATE_CONVERTER.getCertificate(holder);
            log.debug("generated certificate:\n{}", asPEMString(certificate));

            return new CertificateAndPrivateKey(certificate, keyPair.getPrivate());
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    private static void saveToCache(final Path cacheDirectory, final Path cacheCertPath, final Path cacheKeyPath,
            final CertificateAndPrivateKey certificateAndPrivateKey) {
        try {
            Files.createDirectories(cacheDirectory);

            final String certPem = asPEMString(certificateAndPrivateKey.getCertificate());
            final String keyPem = asPEMString(certificateAndPrivateKey.getPrivateKey());

            // overwrite any existing file, and make sure it's only readable by the current user
            final Set<StandardOpenOption> options = ImmutableSet.of(CREATE, WRITE);
            final Set<PosixFilePermission> perms = ImmutableSet.of(PosixFilePermission.OWNER_READ,
                    PosixFilePermission.OWNER_WRITE);
            final FileAttribute<Set<PosixFilePermission>> attrs = PosixFilePermissions.asFileAttribute(perms);

            try (final SeekableByteChannel sbc = Files.newByteChannel(cacheCertPath, options, attrs)) {
                sbc.write(ByteBuffer.wrap(certPem.getBytes()));
            }

            try (final SeekableByteChannel sbc = Files.newByteChannel(cacheKeyPath, options, attrs)) {
                sbc.write(ByteBuffer.wrap(keyPem.getBytes()));
            }

            log.debug("cached generated certificate to {}", cacheCertPath);
        } catch (IOException e) {
            // couldn't save to the cache, oh well
            log.warn("error caching generated certificate", e);
        }
    }

    private static String asPEMString(final Object o) throws IOException {
        final StringWriter sw = new StringWriter();

        try (final JcaPEMWriter pw = new JcaPEMWriter(sw)) {
            pw.writeObject(o);
        }

        return sw.toString();
    }
}