Skip to content
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 gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
# =============================================================================
# Core XDK dependencies
# =============================================================================
acme4j = "3.5.1"
apache-commons-cli = "1.11.0"
bouncycastle = "1.82"
gson = "2.13.2"
jakarta-activation = "2.1.4"
jakarta-xml-bind-api = "4.0.5"
Expand Down Expand Up @@ -152,7 +154,10 @@ xdk-xunit-engine = { group = "org.xtclang", name = "lib-xunit-engine" }
# =============================================================================
# Core XDK third-party libraries (alphabetical)
# =============================================================================
acme4j-client = { module = "org.shredzone.acme4j:acme4j-client", version.ref = "acme4j" }
apache-commons-cli = { module = "commons-cli:commons-cli", version.ref = "apache-commons-cli" }
bouncycastle-pkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" }
bouncycastle-provider = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
jakarta-activation-api = { module = "jakarta.activation:jakarta.activation-api", version.ref = "jakarta-activation" }
jakarta-xml-bind-api = { module = "jakarta.xml.bind:jakarta.xml.bind-api", version.ref = "jakarta-xml-bind-api" }
Expand Down
10 changes: 10 additions & 0 deletions javatools/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ dependencies {
implementation(libs.jline)
implementation(libs.apache.commons.cli)
implementation(libs.gson)
implementation(libs.acme4j.client)
implementation(libs.bouncycastle.pkix)
implementation(libs.bouncycastle.provider)
testCompileOnly(libs.jetbrains.annotations)
testImplementation(libs.javatools.utils)
}
Expand Down Expand Up @@ -187,7 +190,14 @@ val jar by tasks.existing(Jar::class) {
cfg.filter { it.name.endsWith(".jar") }.map { file ->
zipTree(file).matching {
// Exclude module-info files from dependencies - fat JARs should not be JPMS modules
// Exclude signature files from signed jars (e.g. BouncyCastle) that break fat jars
exclude("module-info.class", "META-INF/versions/*/module-info.class")
// Exclude signature files from signed jars (e.g. BouncyCastle) that break fat jars
exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA")
// Exclude dependency metadata that causes duplicates in fat jars
exclude("META-INF/MANIFEST.MF", "META-INF/**/MANIFEST.MF")
exclude("META-INF/OSGI-INF/**", "META-INF/versions/*/OSGI-INF/**")
exclude("META-INF/LICENSE*", "META-INF/NOTICE*", "META-INF/maven/**")
}
}
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package org.xvm.runtime.template._native.crypto;


import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import java.math.BigInteger;

import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;

import java.time.Duration;
import java.time.Instant;

import java.util.Date;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import org.shredzone.acme4j.exception.AcmeException;


/**
* Pure Java keystore and certificate operations that replace the previous native tool
* dependencies ({@code keytool}, {@code openssl}). Every method in this class produces
* PKCS12 keystore entries that are byte-compatible with those created by the corresponding
* native commands — verified by bidirectional cross-tool tests (Java-created keystores
* readable by keytool/openssl, and vice versa). See {@code KeyStoreCompatibilityTest}.
* <p>
* This class has no XVM runtime dependencies and can be unit tested independently.
*/
public class KeyStoreOperations {

/**
* Load an existing PKCS12 keystore or create a new empty one.
*/
public static KeyStore loadOrCreateKeyStore(String sPath, char[] achPwd)
throws GeneralSecurityException, IOException {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
File file = new File(sPath);
if (file.exists()) {
try (FileInputStream in = new FileInputStream(file)) {
keyStore.load(in, achPwd);
}
} else {
keyStore.load(null, achPwd);
}
return keyStore;
}

/**
* Save a keystore to disk.
*/
public static void saveKeyStore(KeyStore keyStore, String sPath, char[] achPwd)
throws GeneralSecurityException, IOException {
try (FileOutputStream out = new FileOutputStream(sPath)) {
keyStore.store(out, achPwd);
}
}

/**
* Delete an entry from a keystore, silently ignoring errors (e.g. if the alias
* does not exist or the keystore file does not exist).
*/
public static void deleteKeyStoreEntry(String sPath, char[] achPwd, String sAlias) {
try {
File file = new File(sPath);
if (file.exists()) {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (FileInputStream in = new FileInputStream(file)) {
keyStore.load(in, achPwd);
}
if (keyStore.containsAlias(sAlias)) {
keyStore.deleteEntry(sAlias);
saveKeyStore(keyStore, sPath, achPwd);
}
}
} catch (GeneralSecurityException | IOException ignore) {
// intentionally silent; enttry may not exist
}
}

/**
* Create a self-signed certificate and store it in a PKCS12 keystore.
* <p>
* Equivalent to {@code keytool -genkeypair -keyalg RSA -keysize 2048 -validity 90}.
* Uses the same JDK {@link KeyPairGenerator} for RSA-2048 key generation and
* BouncyCastle for X.509 certificate construction with SHA256WithRSA signing.
*/
public static void createSelfSignedCertificate(String sStorePath, char[] achPwd,
String sName, String sDName)
throws AcmeException, OperatorCreationException,
GeneralSecurityException, IOException, InterruptedException {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(2048, new SecureRandom());
KeyPair keyPair = keyPairGen.generateKeyPair();

X500Name x500Name = new X500Name(sDName);
Instant now = Instant.now();
BigInteger serial = BigInteger.valueOf(now.toEpochMilli());
var keyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());

X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
x500Name, serial,
Date.from(now), Date.from(now.plus(Duration.ofDays(90))),
x500Name, keyInfo);

ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate());
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer));

KeyStore keyStore = loadOrCreateKeyStore(sStorePath, achPwd);
keyStore.setKeyEntry(sName, keyPair.getPrivate(), achPwd, new Certificate[]{cert});
saveKeyStore(keyStore, sStorePath, achPwd);
}

/**
* Generate an AES-256 symmetric key and store it in a PKCS12 keystore.
* <p>
* Equivalent to {@code keytool -genseckey -keyalg AES -keysize 256}. Uses the same
* JDK {@link javax.crypto.KeyGenerator} API that keytool uses internally.
*/
public static void createSymmetricKey(String sPath, char[] achPwd, String sName)
throws GeneralSecurityException, IOException {
deleteKeyStoreEntry(sPath, achPwd, sName);

KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256, new SecureRandom());
SecretKey secretKey = keyGen.generateKey();

KeyStore keyStore = loadOrCreateKeyStore(sPath, achPwd);
keyStore.setEntry(sName,
new KeyStore.SecretKeyEntry(secretKey),
new KeyStore.PasswordProtection(achPwd));
saveKeyStore(keyStore, sPath, achPwd);
}

/**
* Store a password value as a PBE secret key entry in a PKCS12 keystore.
* <p>
* Equivalent to {@code keytool -importpass}. Creates a PBE secret key from the
* password using {@link javax.crypto.SecretKeyFactory} and stores it as a
* {@link KeyStore.SecretKeyEntry} — the same internal representation.
*/
public static void createPassword(String sPath, char[] achPwd,
String sName, String sPwdValue)
throws GeneralSecurityException, IOException {
deleteKeyStoreEntry(sPath, achPwd, sName);

SecretKey pbeKey = SecretKeyFactory.getInstance("PBE")
.generateSecret(new PBEKeySpec(sPwdValue.toCharArray()));

KeyStore keyStore = loadOrCreateKeyStore(sPath, achPwd);
keyStore.setEntry(sName,
new KeyStore.SecretKeyEntry(pbeKey),
new KeyStore.PasswordProtection(achPwd));
saveKeyStore(keyStore, sPath, achPwd);
}

/**
* Change the password on a PKCS12 keystore by loading with the old password and
* saving with the new one.
* <p>
* Equivalent to {@code keytool -storepasswd -keystore <path> -storepass <old> -new <new>}.
*/
public static void changeStorePassword(String sPath, char[] achPwd, char[] achPwdNew)
throws GeneralSecurityException, IOException {
KeyStore keyStore = loadOrCreateKeyStore(sPath, achPwd);
saveKeyStore(keyStore, sPath, achPwdNew);
}

/**
* Extract a key (private or secret) from a PKCS12 keystore file.
*
* @return the key, or null if not found or inaccessible
*/
public static Key extractKey(String sPath, char[] achPwd, String sName)
throws GeneralSecurityException, IOException {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (FileInputStream in = new FileInputStream(sPath)) {
keyStore.load(in, achPwd);
}
return keyStore.getKey(sName, achPwd);
}
}
Loading
Loading