Skip to content

Commit

Permalink
feat: implement java_keystore (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
thesayyn authored Dec 6, 2023
1 parent 5b47360 commit 2db6073
Show file tree
Hide file tree
Showing 13 changed files with 576 additions and 2 deletions.
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",
"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
./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

0 comments on commit 2db6073

Please sign in to comment.