com.spotify.helios.client.tls.X509CertificateFactory Maven / Gradle / Ivy
/*
* 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 options = ImmutableSet.of(CREATE, WRITE);
final Set perms = ImmutableSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE);
final FileAttribute> 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();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy