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

feat: implement java_keystore #9

Merged
merged 5 commits into from
Dec 6, 2023
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
11 changes: 11 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ common --enable_bzlmod
# https://bazelbuild.slack.com/archives/C014RARENH0/p1691158021917459?thread_ts=1691156601.420349&cid=C014RARENH0
common --check_direct_dependencies=off

# Enable platform specific options
build --enable_platform_specific_config

# Use a hermetic Java version
build --java_runtime_version=remotejdk_11

# Newer versions jdk creates collisions on /tmp
# See: https://github.com/bazelbuild/bazel/issues/3236
# https://github.com/GoogleContainerTools/rules_distroless/actions/runs/7118944984/job/19382981899?pr=9#step:8:51
common:linux --sandbox_tmpfs_path=/tmp

# Load any settings specific to the current user.
# .bazelrc.user should appear in .gitignore so that settings are not shared with team members
# This needs to be last statement in this
Expand Down
2 changes: 1 addition & 1 deletion .bazelversion
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
6.2.1
6.4.0
# The first line of this file is used by Bazelisk and Bazel to be sure
# the right version of Bazel is used to build and test this repo.
# This also defines which version is used on CI.
Expand Down
1 change: 1 addition & 0 deletions distroless/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ bzl_library(
deps = [
"//distroless/private:cacerts",
"//distroless/private:group",
"//distroless/private:java_keystore",
"//distroless/private:locale",
"//distroless/private:os_release",
"//distroless/private:passwd",
Expand Down
2 changes: 2 additions & 0 deletions distroless/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

load("//distroless/private:cacerts.bzl", _cacerts = "cacerts")
load("//distroless/private:group.bzl", _group = "group")
load("//distroless/private:java_keystore.bzl", _java_keystore = "java_keystore")
load("//distroless/private:locale.bzl", _locale = "locale")
load("//distroless/private:os_release.bzl", _os_release = "os_release")
load("//distroless/private:passwd.bzl", _passwd = "passwd")
Expand All @@ -11,3 +12,4 @@ locale = _locale
os_release = _os_release
group = _group
passwd = _passwd
java_keystore = _java_keystore
21 changes: 20 additions & 1 deletion distroless/private/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")

exports_files(["cacerts.sh"])
exports_files([
"cacerts.sh",
])

java_binary(
name = "keystore_binary",
srcs = ["JavaKeyStore.java"],
javacopts = [
"-Xlint:-options",
],
main_class = "JavaKeyStore",
visibility = ["//visibility:public"],
)

bzl_library(
name = "cacerts",
Expand Down Expand Up @@ -52,6 +64,13 @@ bzl_library(
],
)

bzl_library(
name = "java_keystore",
srcs = ["java_keystore.bzl"],
visibility = ["//distroless:__subpackages__"],
deps = [":tar"],
)

bzl_library(
name = "tar",
srcs = ["tar.bzl"],
Expand Down
117 changes: 117 additions & 0 deletions distroless/private/JavaKeyStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@

// Parts taken from https://github.com/openjdk/jdk17u-dev/blob/a028120220f6fd28e39fe0f6190eb1f5da6a788d/make/jdk/src/classes/build/tools/generatecacerts/GenerateCacerts.java
// https://github.com/GoogleContainerTools/distroless/tree/b1e2203eceb9cc91de0500d71c648e346e1d7b89/cacerts/jksutil
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map.Entry;

import javax.security.auth.x500.X500Principal;

/**
* Generate cacerts
*/
class JavaKeyStore {

private static final int MAGIC = 0xfeedfeed;
private static final int VERSION = 0x02;
private static final int TRUSTED_CERT_TAG = 0x02;
private static final char[] PASSWORD = "changeit".toCharArray();
private static final String SALT = "Mighty Aphrodite";

public static void main(String[] args) throws Exception {
try (FileOutputStream output = new FileOutputStream(args[0])) {
store(output, Arrays.copyOfRange(args, 1, args.length));
}
}

public static void store(OutputStream stream, String[] entries)
throws IOException, NoSuchAlgorithmException, CertificateException {
byte[] encoded; // the certificate encoding
CertificateFactory cf = CertificateFactory.getInstance("X509");

MessageDigest md = getPreKeyedHash(PASSWORD);
DataOutputStream dos = new DataOutputStream(new DigestOutputStream(stream, md));

HashMap<String, X509Certificate> certs = new HashMap<String, X509Certificate>();

for (String entry : entries) {
try (InputStream fis = Files.newInputStream(Path.of(entry))) {
for (Certificate rcert : cf.generateCertificates(fis)) {
X509Certificate cert = (X509Certificate) rcert;
String alias = cert.getSubjectX500Principal().getName(X500Principal.CANONICAL);
certs.put(alias, cert);
}
}
}

dos.writeInt(MAGIC);
dos.writeInt(VERSION);
dos.writeInt(certs.size());

for (Entry<String, X509Certificate> entry : certs.entrySet()) {

X509Certificate cert = entry.getValue();
String alias = entry.getKey();

dos.writeInt(TRUSTED_CERT_TAG);

// Write the alias
dos.writeUTF(alias);

// Write the (entry creation) date, which is notBefore of the cert
dos.writeLong(cert.getNotBefore().getTime());

// Write the trusted certificate
encoded = cert.getEncoded();
dos.writeUTF(cert.getType());
dos.writeInt(encoded.length);
dos.write(encoded);
}

/*
* Write the keyed hash which is used to detect tampering with
* the keystore (such as deleting or modifying key or
* certificate entries).
*/
byte[] digest = md.digest();

dos.write(digest);
dos.flush();
}

private static MessageDigest getPreKeyedHash(char[] password)
throws NoSuchAlgorithmException, UnsupportedEncodingException {

MessageDigest md = MessageDigest.getInstance("SHA");
byte[] passwdBytes = convertToBytes(password);
md.update(passwdBytes);
Arrays.fill(passwdBytes, (byte) 0x00);
md.update(SALT.getBytes("UTF8"));
return md;
}

private static byte[] convertToBytes(char[] password) {
int i, j;
byte[] passwdBytes = new byte[password.length * 2];
for (i = 0, j = 0; i < password.length; i++) {
passwdBytes[j++] = (byte) (password[i] >> 8);
passwdBytes[j++] = (byte) password[i];
}
return passwdBytes;
}
}
54 changes: 54 additions & 0 deletions distroless/private/java_keystore.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"jks"

load(":tar.bzl", "tar_lib")

_DOC = """Create a java keystore (database) of cryptographic keys, X.509 certificate chains, and trusted certificates.

Currently only public X.509 are supported as part of the PUBLIC API contract.
"""

def _java_keystore_impl(ctx):
jks = ctx.actions.declare_file(ctx.attr.name + ".jks")

args = ctx.actions.args()
args.add(jks)
args.add_all(ctx.files.certificates)

ctx.actions.run(
executable = ctx.executable._java_keystore,
inputs = ctx.files.certificates,
outputs = [jks],
arguments = [args],
)

output = ctx.actions.declare_file(ctx.attr.name + ".tar.gz")
mtree = tar_lib.create_mtree(ctx)
mtree.add_file_with_parents("/etc/ssl/certs/java/cacerts", jks)
mtree.build(output = output, mnemonic = "JavaKeyStore", inputs = [jks])

return [
DefaultInfo(files = depset([output])),
OutputGroupInfo(
jks = depset([jks]),
),
]

java_keystore = rule(
doc = _DOC,
attrs = {
"_java_keystore": attr.label(
executable = True,
cfg = "exec",
default = ":keystore_binary",
),
"certificates": attr.label_list(
allow_files = True,
mandatory = True,
allow_empty = False,
),
},
implementation = _java_keystore_impl,
toolchains = [
tar_lib.TOOLCHAIN_TYPE,
],
)
30 changes: 30 additions & 0 deletions distroless/tests/asserts.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,33 @@ def assert_tar_listing(name, actual, expected):
file2 = expected_listing,
timeout = "short",
)

# buildifier: disable=function-docstring
def assert_jks_listing(name, actual, expected):
actual_listing = "_{}_listing".format(name)

native.genrule(
name = actual_listing,
srcs = [
actual,
"@rules_java//toolchains:current_java_runtime",
],
outs = ["_{}.listing".format(name)],
cmd = """
#!/usr/bin/env bash
set -o pipefail -o errexit -o nounset

BINS=($(locations @rules_java//toolchains:current_java_runtime))
KEYTOOL=$$(dirname $${BINS[1]})/keytool

$$KEYTOOL -J-Duser.language=en -J-Duser.country=US -J-Duser.timezone=UTC \\
-list -rfc -keystore $(location %s) -storepass changeit > $@
""" % actual,
)

diff_test(
name = name,
file1 = actual_listing,
file2 = expected,
timeout = "short",
)
22 changes: 22 additions & 0 deletions docs/rules.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions examples/java_keystore/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
load("//distroless:defs.bzl", "java_keystore")
load("//distroless/tests:asserts.bzl", "assert_jks_listing", "assert_tar_listing")

java_keystore(
name = "java_keystore",
certificates = [
# asserting that we support both bundle x509 certs
# and single x509 certs
"amazon.crt",
loosebazooka marked this conversation as resolved.
Show resolved Hide resolved
"bundle.crt",
],
)

filegroup(
name = "java_keystore_jks",
srcs = [":java_keystore"],
output_group = "jks",
)

assert_jks_listing(
name = "test_java_keystore_jks",
actual = "java_keystore_jks",
expected = "expected.jks.output",
)

assert_tar_listing(
name = "test_java_keystore",
actual = "java_keystore",
expected = """\
#mtree
./etc time=1672560000.0 mode=755 gid=0 uid=0 type=dir
loosebazooka marked this conversation as resolved.
Show resolved Hide resolved
./etc/ssl time=1672560000.0 mode=755 gid=0 uid=0 type=dir
./etc/ssl/certs time=1672560000.0 mode=755 gid=0 uid=0 type=dir
./etc/ssl/certs/java time=1672560000.0 mode=755 gid=0 uid=0 type=dir
./etc/ssl/certs/java/cacerts nlink=0 time=1672560000.0 mode=755 gid=0 uid=0 type=file size=5349 cksum=3752477219 sha1digest=015078faa5537fcabb6c7e73fe2dedf8241b106d
""",
)
20 changes: 20 additions & 0 deletions examples/java_keystore/amazon.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----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-----
Loading