Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support adding CA Certificates #1

Merged
merged 10 commits into from
Apr 29, 2022
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
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-1.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: feature
feature:
description: Provide a way to add CA certs to all installed JDKs.
links:
- https://github.com/palantir/gradle-jdks/pull/1
50 changes: 50 additions & 0 deletions gradle-jdks/src/main/java/com/palantir/gradle/jdks/CaCerts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* (c) Copyright 2022 Palantir Technologies Inc. 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 com.palantir.gradle.jdks;

import java.util.Comparator;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import org.immutables.value.Value;

@Value.Immutable
interface CaCerts {
NavigableMap<String, String> caCerts();

default String combinedInSortedOrder() {
StringBuilder stringBuilder = new StringBuilder();

caCerts().forEach((alias, caCert) -> {
stringBuilder.append(alias).append(": ").append(caCert).append('\n');
});

return stringBuilder.toString();
}

class Builder extends ImmutableCaCerts.Builder {}

static Builder builder() {
return new Builder();
}

static CaCerts from(Map<String, String> caCerts) {
NavigableMap<String, String> sortedMap = new TreeMap<>(Comparator.naturalOrder());
sortedMap.putAll(caCerts);
return builder().caCerts(sortedMap).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
package com.palantir.gradle.jdks;

import com.palantir.gradle.jdks.JdkPath.Extension;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -29,6 +32,7 @@
import org.gradle.api.file.Directory;
import org.gradle.api.file.FileTree;
import org.gradle.api.provider.Provider;
import org.gradle.process.ExecResult;

public final class JdkManager {
private final Project project;
Expand Down Expand Up @@ -72,8 +76,14 @@ public Path jdk(JdkSpec jdkSpec) {
copy.into(temporaryJdkPath);
});

Path javaHome = findJavaHome(temporaryJdkPath);

jdkSpec.caCerts().caCerts().forEach((name, caCertFile) -> {
addCaCert(javaHome, name, caCertFile);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, given two projects which use azul-17 x86_64, what happens if both projects specify completely different certificates? Do we need to include the certificates in our cache key? I'm not sure that we should mutate the jvm in a potentially leaky way such that tests pass on project B only if you've run project A first.

Copy link
Contributor Author

@CRogers CRogers Apr 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the CA certificates configurable at the root level/all jdks level, so all the jdks will have the same CA certs. misread what you wrote

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certificates are included in the cache key:

String.join("\n", "Distribution: %s", "Version: %s", "Os: %s", "Arch: %s", "CaCerts: %s"),
distributionName().uiName(),
release().version(),
release().os(),
release().arch(),
caCerts().combinedInSortedOrder());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah fantastic, I missed that in the block above :-)


try {
Files.move(findJavaHome(temporaryJdkPath), diskPath, StandardCopyOption.ATOMIC_MOVE);
Files.move(javaHome, diskPath, StandardCopyOption.ATOMIC_MOVE);
} catch (FileAlreadyExistsException e) {
// This means another process has successfully installed this JDK, and we can just use their one.
return diskPath;
Expand Down Expand Up @@ -117,4 +127,34 @@ private Path findJavaHome(Path temporaryJdkPath) {
throw new RuntimeException("Failed to find java home in " + temporaryJdkPath, e);
}
}

private void addCaCert(Path javaHome, String alias, String caCert) {
ByteArrayOutputStream output = new ByteArrayOutputStream();

ExecResult keytoolResult = project.exec(exec -> {
exec.setCommandLine(
"bin/keytool",
"-import",
"-trustcacerts",
"-alias",
alias,
"-cacerts",
"-storepass",
"changeit",
"-noprompt");

exec.environment("JAVA_HOME", javaHome);
exec.setWorkingDir(javaHome);
exec.setStandardInput(new ByteArrayInputStream(caCert.getBytes(StandardCharsets.UTF_8)));
exec.setStandardOutput(output);
exec.setErrorOutput(output);
exec.setIgnoreExitValue(true);
});

if (keytoolResult.getExitValue() != 0) {
throw new RuntimeException(String.format(
"Failed to add ca cert '%s' to java installation at '%s'. Keytool output: %s\n\n",
alias, javaHome, output.toString(StandardCharsets.UTF_8)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,16 @@ interface JdkSpec {

JdkRelease release();

// Later: CA certs
CaCerts caCerts();

default String consistentShortHash() {
String infoBlock = String.format(
String.join("\n", "Distribution: %s", "Version: %s", "Os: %s", "Arch: %s"),
String.join("\n", "Distribution: %s", "Version: %s", "Os: %s", "Arch: %s", "CaCerts: %s"),
distributionName().uiName(),
release().version(),
release().os(),
release().arch());
release().arch(),
caCerts().combinedInSortedOrder());

try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public abstract class JdksExtension {

protected abstract MapProperty<JavaLanguageVersion, JdkExtension> getJdks();

public abstract MapProperty<String, String> getCaCerts();

public abstract DirectoryProperty getJdkStorageLocation();

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public void apply(Project rootProject) {
ret.put(
javaLanguageVersion,
javaInstallationForLanguageVersion(
rootProject, jdkExtension, jdkManager, javaLanguageVersion));
rootProject, jdksExtension, jdkExtension, jdkManager, javaLanguageVersion));
});

return ret;
Expand Down Expand Up @@ -88,8 +88,9 @@ private JdksExtension extension(Project rootProject, JdkDistributions jdkDistrib
return jdksExtension;
}

private ImmutableGradleJdksJavaInstallationMetadata javaInstallationForLanguageVersion(
private GradleJdksJavaInstallationMetadata javaInstallationForLanguageVersion(
Project rootProject,
JdksExtension jdksExtension,
JdkExtension jdkExtension,
JdkManager jdkManager,
JavaLanguageVersion javaLanguageVersion) {
Expand All @@ -101,6 +102,7 @@ private ImmutableGradleJdksJavaInstallationMetadata javaInstallationForLanguageV
Path jdk = jdkManager.jdk(JdkSpec.builder()
.distributionName(jdkDistributionName)
.release(JdkRelease.builder().version(version).build())
.caCerts(CaCerts.from(jdksExtension.getCaCerts().get()))
.build());

return GradleJdksJavaInstallationMetadata.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import nebula.test.functional.ExecutionResult


class JdksPluginIntegrationSpec extends IntegrationSpec {
def 'can download + run an Azul Zulu JDK'() {
def setup() {
// language=gradle
buildFile << '''
apply plugin: 'com.palantir.jdks'
Expand All @@ -39,7 +39,9 @@ class JdksPluginIntegrationSpec extends IntegrationSpec {
libraryTarget = 11
}
'''.stripIndent(true)
}

def 'can download + run an Azul Zulu JDK'() {
// language=gradle
def subprojectDir = addSubproject 'subproject', '''
apply plugin: 'java-library'
Expand Down Expand Up @@ -73,6 +75,72 @@ class JdksPluginIntegrationSpec extends IntegrationSpec {
stdout.contains 'version: 11.0.14.1, vendor: Azul Systems, Inc.'
}

def 'can add ca certs to a JDK'() {
def amazonRootCa1Serial = '143266978916655856878034712317230054538369994'

// language=gradle
buildFile << '''
jdks {
caCerts.put 'Our_Amazon_CA_Cert_1', """
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj
ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM
9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw
IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6
VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L
93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm
jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA
A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI
U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs
N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv
o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU
5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy
rqXRfboQnoZsG4q5WTP468SQvvG5
-----END CERTIFICATE-----
""".stripIndent(true)
}

apply plugin: 'java-library'

task printCaTruststoreAliases(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'foo.OutputCaCerts'
logging.captureStandardOutput LogLevel.LIFECYCLE
logging.captureStandardError LogLevel.LIFECYCLE
}
'''.stripIndent(true)

// language=java
writeJavaSourceFile '''
package foo;

import java.io.File;
import java.security.KeyStore;import java.security.cert.X509Certificate;

public final class OutputCaCerts {
public static void main(String... args) throws Exception {
KeyStore keyStore = KeyStore.getInstance(
new File(System.getProperty("java.home"), "lib/security/cacerts"),
"changeit".toCharArray());
System.out.println(
((X509Certificate) keyStore.getCertificate("our_amazon_ca_cert_1")).getSerialNumber());
}
}
'''.stripIndent(true)

when:

def stdout = runTasksSuccessfully('printCaTruststoreAliases').standardOutput

then:
stdout.contains amazonRootCa1Serial
}

@Override
ExecutionResult runTasksSuccessfully(String... tasks) {
def result = super.runTasks(tasks)
Expand Down