Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 34 additions & 47 deletions src/main/java/com/venafi/vcert/sdk/certificate/PEMCollection.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.openssl.PEMEncryptor;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import lombok.Data;
Expand All @@ -27,12 +27,20 @@

@Data
public class PEMCollection {
// We don't use AES-256-CBC. Default JDK installations are limited
// to 128-bit keys, but when AES-256-CBC is specified as algorithm
// then BouncyCastle will automatically use a 256-bit key size,
// resulting in an "illegal key size" exception.
// https://deveshsharmablogs.wordpress.com/2012/10/09/fixing-java-security-invalidkeyexception-illegal-key-size-exception/
public static final String BOUNCY_CASTLE_ENCRYPTION_ALGORITHM = "AES-128-CBC";

private Certificate certificate;
private PrivateKey privateKey;
private String privateKeyPassword;
private List<Certificate> chain = new ArrayList<>();

public static PEMCollection fromResponse(String body, ChainOption chainOption,
PrivateKey privateKey) throws VCertException {
PrivateKey privateKey, String privateKeyPassword) throws VCertException {
List<Certificate> chain = new ArrayList<>();

PEMParser pemParser = new PEMParser(new StringReader(body));
Expand All @@ -59,15 +67,17 @@ public static PEMCollection fromResponse(String body, ChainOption chainOption,
if (chain.size() > 0) {
switch (chainOption) {
case ChainOptionRootFirst:
pemCollection = newPemCollection(chain.get(chain.size() - 1), null, null);
pemCollection = new PEMCollection();
pemCollection.certificate(chain.get(chain.size() - 1));
if (chain.size() > 1 && chainOption != ChainOption.ChainOptionIgnore) {
for (int i = 0; i < chain.size() - 1; i++) {
pemCollection.chain().add(chain.get(i));
}
}
break;
default:
pemCollection = newPemCollection(chain.get(0), null, null);
pemCollection = new PEMCollection();
pemCollection.certificate(chain.get(0));
if (chain.size() > 1 && chainOption != ChainOption.ChainOptionIgnore) {
for (int i = 1; i < chain.size(); i++) {
pemCollection.chain().add(chain.get(i));
Expand All @@ -79,26 +89,11 @@ public static PEMCollection fromResponse(String body, ChainOption chainOption,
pemCollection = new PEMCollection();
}
pemCollection.privateKey(privateKey);
pemCollection.privateKeyPassword(privateKeyPassword);

return pemCollection;
}

public static PEMCollection fromResponse(String body, ChainOption chainOption)
throws VCertException {
return fromResponse(body, chainOption, null);
}

// TODO deal with password? is it required?
public static PEMCollection newPemCollection(Certificate certificate, PrivateKey privateKey,
byte[] privateKeyPassword) {
PEMCollection pemCollection = new PEMCollection();
pemCollection.certificate(certificate);
if (privateKey != null) {
pemCollection.privateKey(privateKey);
}
return pemCollection;
}

public String pemCertificate() {
String pem = null;
if (!Objects.isNull(this.certificate)) {
Expand All @@ -116,33 +111,25 @@ public String pemCertificate() {
}

public String pemPrivateKey() {
String pem = null;
if (!Objects.isNull(this.privateKey)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
switch (KeyType.from(this.privateKey.getAlgorithm())) {
case RSA:
try (PemWriter pemWriter = new PemWriter(new OutputStreamWriter(outputStream))) {
PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(this.privateKey.getEncoded());
ASN1Encodable privateKeyPKCS1ASN1Encodable = pkInfo.parsePrivateKey();
ASN1Primitive privateKeyPKCS1ASN1 = privateKeyPKCS1ASN1Encodable.toASN1Primitive();
pemWriter
.writeObject(new PemObject("RSA PRIVATE KEY", privateKeyPKCS1ASN1.getEncoded()));
} catch (IOException e) {
throw new RuntimeException(e);
}
pem = new String(outputStream.toByteArray());
break;
case ECDSA:
try (PemWriter pemWriter = new PemWriter(new OutputStreamWriter(outputStream))) {
pemWriter.writeObject(new PemObject("EC PRIVATE KEY", this.privateKey.getEncoded()));
} catch (IOException e) {
throw new RuntimeException(e);
}
pem = new String(outputStream.toByteArray());
break;
if (Objects.isNull(this.privateKey)) {
return null;
}

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (PemWriter pemWriter = new PemWriter(new OutputStreamWriter(outputStream))) {
PEMEncryptor encryptor = null;

if (privateKeyPassword != null) {
encryptor = new JcePEMEncryptorBuilder(BOUNCY_CASTLE_ENCRYPTION_ALGORITHM)
.build(privateKeyPassword.toCharArray());
}

JcaMiscPEMGenerator gen = new JcaMiscPEMGenerator(this.privateKey, encryptor);
pemWriter.writeObject(gen.generate());
} catch (IOException e) {
throw new RuntimeException(e);
}
return pem;
return new String(outputStream.toByteArray());
}

public String pemCertificateChain() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,12 +316,14 @@ public PEMCollection retrieveCertificate(CertificateRequest request) throws VCer
}
String body = certificateViaCSR(request.pickupId(), chainOption);
PEMCollection pemCollection =
PEMCollection.fromResponse(body, request.chainOption(), request.privateKey());
PEMCollection.fromResponse(body, request.chainOption(), request.privateKey(),
request.keyPassword());
request.checkCertificate(pemCollection.certificate());
return pemCollection;
} else {
String body = certificateAsPem(certId);
return PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore);
return PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore,
request.privateKey(), request.keyPassword());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ public PEMCollection retrieveCertificate(CertificateRequest request) throws VCer
PEMCollection pemCollection = PEMCollection.fromResponse(
org.bouncycastle.util.Strings
.fromByteArray(Base64.getDecoder().decode(retrieveResponse.certificateData())),
request.chainOption(), request.privateKey());
request.chainOption(), request.privateKey(), request.keyPassword());
request.checkCertificate(pemCollection.certificate());
return pemCollection;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ public PEMCollection retrieveCertificate(CertificateRequest request) throws VCer
PEMCollection pemCollection = PEMCollection.fromResponse(
org.bouncycastle.util.Strings
.fromByteArray(Base64.getDecoder().decode(retrieveResponse.certificateData())),
request.chainOption(), request.privateKey());
request.chainOption(), request.privateKey(), request.keyPassword());
request.checkCertificate(pemCollection.certificate());
return pemCollection;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,71 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.Security;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.Test;
import com.venafi.vcert.sdk.VCertException;

class PEMCollectionTest {
private static final String KEY_PASSWORD = "my secret";

@Test
void fromResponse() throws VCertException, IOException {
static {
Security.addProvider(new BouncyCastleProvider());
}

private String readResourceAsString(String name) throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
String path = classLoader.getResource("certificates/certWithKey.pem").getPath();
String path = classLoader.getResource(name).getPath();
// windows platform: if it starts with /C: then remove the leading slash
if (path.charAt(0) == '/' && path.charAt(2) == ':') {
path = path.substring(1);
}
String body = new String(Files.readAllBytes(Paths.get(path).toAbsolutePath()));
PEMCollection pemCollection = PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore);
return new String(Files.readAllBytes(Paths.get(path).toAbsolutePath()));
}

@Test
void fromResponseRSA() throws VCertException, IOException {
String body = readResourceAsString("certificates/certWithKey.pem");
PEMCollection pemCollection = PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore, null, null);
assertThat(pemCollection.certificate()).isNotNull();
assertThat(pemCollection.chain()).hasSize(0);
assertThat(pemCollection.privateKey()).isNotNull();
}

@Test
void fromResponseECDSA() throws VCertException, IOException {
String body = readResourceAsString("certificates/certWithKeyECDSA.pem");
PEMCollection pemCollection = PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore, null, null);
assertThat(pemCollection.certificate()).isNotNull();
assertThat(pemCollection.chain()).hasSize(0);
assertThat(pemCollection.privateKey()).isNotNull();
}

@Test
void keyPasswordRSA() throws VCertException, IOException {
String body = readResourceAsString("certificates/certWithKey.pem");
PEMCollection pemCollection = PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore, null, null);
PrivateKey privateKey = pemCollection.privateKey();

PEMCollection pemCollection2 = PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore, privateKey,
KEY_PASSWORD);
String pemPrivateKey = pemCollection2.pemPrivateKey();
assertThat(pemPrivateKey).contains("BEGIN RSA PRIVATE KEY");
assertThat(pemPrivateKey).contains("ENCRYPTED");
}

@Test
void keyPasswordECDSA() throws VCertException, IOException {
String body = readResourceAsString("certificates/certWithKeyECDSA.pem");
PEMCollection pemCollection = PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore, null, null);
PrivateKey privateKey = pemCollection.privateKey();

PEMCollection pemCollection2 = PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore, privateKey,
KEY_PASSWORD);
String pemPrivateKey = pemCollection2.pemPrivateKey();
assertThat(pemPrivateKey).contains("BEGIN EC PRIVATE KEY");
assertThat(pemPrivateKey).contains("ENCRYPTED");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,17 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.Security;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -20,10 +29,17 @@
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import feign.Request;
import feign.Response;

import com.venafi.vcert.sdk.VCertException;
import com.venafi.vcert.sdk.certificate.CertificateRequest;
import com.venafi.vcert.sdk.certificate.CertificateStatus;
import com.venafi.vcert.sdk.certificate.ChainOption;
import com.venafi.vcert.sdk.certificate.KeyType;
import com.venafi.vcert.sdk.certificate.ManagedCertificate;
import com.venafi.vcert.sdk.certificate.PEMCollection;
import com.venafi.vcert.sdk.certificate.RenewalRequest;
import com.venafi.vcert.sdk.connectors.ZoneConfiguration;
import com.venafi.vcert.sdk.connectors.cloud.domain.CertificateIssuingTemplate;
Expand All @@ -38,6 +54,7 @@

@ExtendWith(MockitoExtension.class)
class CloudConnectorTest {
private static final String KEY_SECRET = "my secret";

@Mock
private Cloud cloud;
Expand All @@ -49,6 +66,17 @@ class CloudConnectorTest {
UserDetails userDetails;


private String readResourceAsString(String name) throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
String path = classLoader.getResource(name).getPath();
// windows platform: if it starts with /C: then remove the leading slash
if (path.charAt(0) == '/' && path.charAt(2) == ':') {
path = path.substring(1);
}
return new String(Files.readAllBytes(Paths.get(path).toAbsolutePath()));
}


@BeforeEach
void setUp() {
classUnderTest = new CloudConnector(cloud);
Expand Down Expand Up @@ -112,6 +140,45 @@ void requestCertificate() throws VCertException {
assertThat(actual).isEqualTo("jackpot");
}

@Test
void retrieveCertificate() throws VCertException, IOException {
Security.addProvider(new BouncyCastleProvider());

String apiKey = "12345678-1234-1234-1234-123456789012";
final Authentication auth = new Authentication(null, null, apiKey);
classUnderTest.authenticate(auth);

String body = readResourceAsString("certificates/certWithKey.pem");
PEMCollection pemCollection = PEMCollection.fromResponse(body, ChainOption.ChainOptionIgnore, null, null);

CertificateRequest request = new CertificateRequest().subject(
new CertificateRequest.PKIXName()
.commonName("random name").organization(singletonList("Venafi, Inc."))
.organizationalUnit(singletonList("Automated Tests")));
request
.pickupId("jackpot")
.keyType(KeyType.RSA)
.keyPair(new KeyPair(pemCollection.certificate().getPublicKey(),
pemCollection.privateKey()))
.keyPassword(KEY_SECRET);

when(cloud.certificateStatus(eq("jackpot"), eq(apiKey)))
.thenReturn(new CertificateStatus().status("ISSUED"));
when(cloud.certificateViaCSR(eq("jackpot"), eq(apiKey), eq("ROOT_FIRST")))
.thenReturn(Response.builder()
.request(Request.create(Request.HttpMethod.GET, "http://localhost",
new HashMap<String, Collection<String>>(), null, null))
.status(200)
.body(body, Charset.forName("UTF-8"))
.build());

PEMCollection pemCollection2 = classUnderTest.retrieveCertificate(request);
assertThat(pemCollection2).isNotNull();
assertThat(pemCollection2.certificate()).isNotNull();
assertThat(pemCollection2.privateKey()).isNotNull();
assertThat(pemCollection2.privateKeyPassword()).isEqualTo(KEY_SECRET);
}

@Test
@DisplayName("Renew a certificate that do not exists in Cloud should fail")
void renewCertificateNotFound() throws VCertException {
Expand Down
20 changes: 20 additions & 0 deletions src/test/resources/certificates/certWithKeyECDSA.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIBxjCCAWwCCQCWb+I1jjdO1DAKBggqhkjOPQQDAjBqMQswCQYDVQQGEwJOTDEL
MAkGA1UECAwCTkIxEzARBgNVBAcMCk9vc3RlcmhvdXQxETAPBgNVBAoMCEZ1bGxz
dGFxMQ8wDQYDVQQLDAZEZXZPcHMxFTATBgNVBAMMDGZ1bGxzdGFxLmNvbTAgFw0y
MDEwMjgyMDQ0NDZaGA8yMjk0MDgxMjIwNDQ0NlowajELMAkGA1UEBhMCTkwxCzAJ
BgNVBAgMAk5CMRMwEQYDVQQHDApPb3N0ZXJob3V0MREwDwYDVQQKDAhGdWxsc3Rh
cTEPMA0GA1UECwwGRGV2T3BzMRUwEwYDVQQDDAxmdWxsc3RhcS5jb20wWTATBgcq
hkjOPQIBBggqhkjOPQMBBwNCAASn/I7KzLRrc1h1KpJ3rO+3qb1SEBa6q8axRjuH
o7rY5WqQ7zA0pxKzBGK2isdNl+32/IOXUBGeM0kqgrnpL4zJMAoGCCqGSM49BAMC
A0gAMEUCIQCFARO1Jmg21vi6Re55Wv7KVW040N/3pHFzad7Q8E9i2QIgMtNNGBYc
LCSPnaeYL4OHHovVy/7cG8EwfO8s9g8ZQFg=
-----END CERTIFICATE-----
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIINX1RybUM04kiXpguJjy/3Yqnwqxvmf3SPqzhU+CA70oAoGCCqGSM49
AwEHoUQDQgAEp/yOysy0a3NYdSqSd6zvt6m9UhAWuqvGsUY7h6O62OVqkO8wNKcS
swRitorHTZft9vyDl1ARnjNJKoK56S+MyQ==
-----END EC PRIVATE KEY-----