diff --git a/cli/src/main/java/ca/weblite/jdeploy/JDeploy.java b/cli/src/main/java/ca/weblite/jdeploy/JDeploy.java index 8de21031..a1e77f2d 100644 --- a/cli/src/main/java/ca/weblite/jdeploy/JDeploy.java +++ b/cli/src/main/java/ca/weblite/jdeploy/JDeploy.java @@ -9,10 +9,7 @@ import ca.weblite.jdeploy.app.JVMSpecification; import ca.weblite.jdeploy.appbundler.Bundler; import ca.weblite.jdeploy.appbundler.BundlerSettings; -import ca.weblite.jdeploy.cli.controllers.CheerpjController; -import ca.weblite.jdeploy.cli.controllers.GitHubRepositoryInitializerCLIController; -import ca.weblite.jdeploy.cli.controllers.JPackageController; -import ca.weblite.jdeploy.cli.controllers.ProjectGeneratorCLIController; +import ca.weblite.jdeploy.cli.controllers.*; import ca.weblite.jdeploy.di.JDeployModule; import ca.weblite.jdeploy.factories.JDeployKeyProviderFactory; import ca.weblite.jdeploy.gui.JDeployMainMenu; @@ -20,10 +17,7 @@ import ca.weblite.jdeploy.helpers.PackageInfoBuilder; import ca.weblite.jdeploy.helpers.PrereleaseHelper; import ca.weblite.jdeploy.npm.NPM; -import ca.weblite.jdeploy.services.DeveloperIdentityKeyStore; -import ca.weblite.jdeploy.services.GithubWorkflowGenerator; -import ca.weblite.jdeploy.services.JavaVersionExtractor; -import ca.weblite.jdeploy.services.PackageSigningService; +import ca.weblite.jdeploy.services.*; import ca.weblite.tools.io.*; import ca.weblite.tools.security.KeyProvider; import com.codename1.io.JSONParser; @@ -1971,6 +1965,13 @@ private void jpackageCLI(String[] args) { private void _package() throws IOException { _package(new BundlerSettings()); } + + private void _verify(String[] args) throws Exception { + String[] verifyArgs = new String[args.length-1]; + System.arraycopy(args, 1, verifyArgs, 0, verifyArgs.length); + CLIVerifyPackageController verifyPackageController = new CLIVerifyPackageController(new VerifyPackageService()); + verifyPackageController.verifyPackage(verifyArgs); + } private void _package(BundlerSettings bundlerSettings) throws IOException { File jdeployBundle = new File(directory, "jdeploy-bundle"); @@ -2514,6 +2515,10 @@ public static void main(String[] args) { prog.generate(generateArgs); return; } + if (args.length > 0 && "verify-package".equals(args[0])) { + prog._verify(args); + return; + } if (args.length > 0 && "github".equals(args[0]) && args.length> 1 && "init".equals(args[1])) { String[] githubInitArgs = new String[args.length-2]; System.arraycopy(args, 2, githubInitArgs, 0, githubInitArgs.length); @@ -2522,7 +2527,7 @@ public static void main(String[] args) { } Options opts = new Options(); - opts.addOption("y", "no-prompt", false,"Indicates not to prompt user "); + opts.addOption("y", "no-prompt", false,"Indicates not to prompt_ user "); opts.addOption("W", "no-workflow", false,"Indicates not to create a github workflow if true"); boolean noPromptFlag = false; boolean noWorkflowFlag = false; diff --git a/cli/src/main/java/ca/weblite/jdeploy/cli/controllers/CLIVerifyPackageController.java b/cli/src/main/java/ca/weblite/jdeploy/cli/controllers/CLIVerifyPackageController.java new file mode 100644 index 00000000..b0671ef0 --- /dev/null +++ b/cli/src/main/java/ca/weblite/jdeploy/cli/controllers/CLIVerifyPackageController.java @@ -0,0 +1,98 @@ +package ca.weblite.jdeploy.cli.controllers; + +import ca.weblite.jdeploy.services.VerifyPackageService; + +public class CLIVerifyPackageController { + private final VerifyPackageService verifyPackageService; + + public CLIVerifyPackageController(VerifyPackageService verifyPackageService) { + this.verifyPackageService = verifyPackageService; + } + + public void verifyPackage(String[] args) { + VerifyPackageService.Result result; + try { + result = verifyPackageService.verifyPackage(parseParameters(args)); + } catch (Exception e) { + System.out.println("Error: " + e.getMessage()); + printUsage(); + System.exit(1); + return; + } + + if (result.verified) { + System.out.println("Package verified successfully"); + System.exit(0); + } else { + System.out.println("Package verification failed: " + result.errorMessage); + if (result.verificationResult != null) { + System.out.println("Verification result: " + result.verificationResult); + System.exit(90 + result.verificationResult.ordinal()); + } else { + printUsage(); + System.exit(1); + } + } + } + + private VerifyPackageService.Parameters parseParameters(String[] args) { + VerifyPackageService.Parameters result = new VerifyPackageService.Parameters(); + + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + + switch (arg) { + case "-v": + case "--version": + if (i + 1 < args.length) { + result.version = args[++i]; + } else { + throw new IllegalArgumentException("Missing value for version"); + } + break; + + case "-k": + case "--keystore": + if (i + 1 < args.length) { + result.keyStore = args[++i]; + } else { + throw new IllegalArgumentException("Missing value for keystore"); + } + break; + + default: + if (result.jdeployBundlePath == null) { + result.jdeployBundlePath = arg; + } else { + throw new IllegalArgumentException("Unknown argument: " + arg); + } + break; + } + } + + if (result.version == null || result.jdeployBundlePath == null || result.keyStore == null) { + throw new IllegalArgumentException("Required parameters: --version, jdeployBundlePath, --keystore"); + } + + return result; + } + + private void printUsage() { + System.out.println("Usage: jdeploy verify-package [options] "); + System.out.println("Options:"); + System.out.println(" -v, --version Specify the version of the package to verify."); + System.out.println(" -k, --keystore Specify the path to the keystore containing trusted certificates."); + System.out.println(); + System.out.println("Arguments:"); + System.out.println(" jdeployBundlePath Path to the jdeploy bundle directory to verify."); + System.out.println(); + System.out.println("Example:"); + System.out.println(" jdeploy verify-package -v 1.0.0 -k /path/to/keystore /path/to/jdeploy-bundle"); + System.out.println(); + System.out.println("Description:"); + System.out.println(" This command verifies the integrity and authenticity of a jdeploy package."); + System.out.println(" The command checks the signature of the package against the provided version and keystore."); + System.out.println(" If the verification is successful, the package is considered authentic and untampered."); + } + +} diff --git a/cli/src/main/java/ca/weblite/jdeploy/services/VerifyPackageService.java b/cli/src/main/java/ca/weblite/jdeploy/services/VerifyPackageService.java new file mode 100644 index 00000000..c3a249b4 --- /dev/null +++ b/cli/src/main/java/ca/weblite/jdeploy/services/VerifyPackageService.java @@ -0,0 +1,224 @@ +package ca.weblite.jdeploy.services; + +import ca.weblite.tools.security.*; + +import java.io.File; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class VerifyPackageService { + + public static class Parameters { + public String version; + public String jdeployBundlePath; + public String keyStore; + } + + public static class Result { + public boolean verified; + public String errorMessage; + public VerificationResult verificationResult; + + private Result(boolean verified, String errorMessage, VerificationResult verificationResult) { + this.verified = verified; + this.errorMessage = errorMessage; + this.verificationResult = verificationResult; + } + } + + public Result verifyPackage(Parameters params) { + try { + validateParameters(params); + KeyStore trustedCertificates = loadTrustedCertificates(params.keyStore); + VerificationResult verificationResult = FileVerifier.verifyDirectory( + params.version, + params.jdeployBundlePath, + createCertificateVerifier(trustedCertificates) + ); + + switch (verificationResult) { + case NOT_SIGNED_AT_ALL: + return new Result(false, "The package is not signed", verificationResult); + case UNTRUSTED_CERTIFICATE: + return new Result( + false, + "The package is signed with an untrusted certificate", + verificationResult + ); + case SIGNED_CORRECTLY: + return new Result(true, null, verificationResult); + case SIGNATURE_MISMATCH: + return new Result( + false, "The package signature does not match the contents", + verificationResult + ); + default: + return new Result( + false, "Unknown verification result: " + verificationResult, + verificationResult + ); + } + + } catch (Exception e) { + return new Result(false, e.getMessage(), null); + } + } + + private void validateParameters(Parameters params) { + if (params.jdeployBundlePath == null || params.jdeployBundlePath.isEmpty()) { + throw new IllegalArgumentException("jdeployBundlePath is required"); + } + if (params.keyStore == null || params.keyStore.isEmpty()) { + throw new IllegalArgumentException("trustedCertificatesPemString is required"); + } + } + + private KeyStore loadTrustedCertificates(String keyStore) throws Exception { + if (isPemString(keyStore)) { + return CertificateUtil.loadCertificatesFromPEM(keyStore); + } else if (isJksFile(keyStore)) { + return loadJksFile(keyStore); + } else if (isPemFile(keyStore)) { + return loadPemFile(keyStore); + } else if (isDerFile(keyStore)) { + return loadDerFile(keyStore); + } else if (isPkcs12File(keyStore)) { + return loadPkcs12File(keyStore); + } else if (isPkcs7File(keyStore)) { + return loadPkcs7File(keyStore); + } else { + throw new IllegalArgumentException("Invalid key store format"); + } + } + + private boolean isPemString(String keyStore) { + return keyStore.startsWith("-----BEGIN CERTIFICATE-----"); + } + + private boolean isJksFile(String keyStore) { + return isFile(keyStore, ".jks"); + } + + private boolean isPemFile(String keyStore) { + return isFile(keyStore, ".pem"); + } + + private boolean isDerFile(String keyStore) { + return isFile(keyStore, ".der") || isFile(keyStore, ".cer") || isFile(keyStore, ".crt"); + } + + private boolean isFile(String path, String extension) { + return path.endsWith(extension) && (new File(path)).exists(); + } + + private boolean isPkcs12File(String keyStore) { + return isFile(keyStore, ".p12") || isFile(keyStore, ".pfx"); + } + + private boolean isPkcs7File(String keyStore) { + return isFile(keyStore, ".p7b") || isFile(keyStore, ".p7c"); + } + + private KeyStore loadDerFile(String filePath) throws Exception { + // Load the DER file into a byte array + FileInputStream fis = new FileInputStream(filePath); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + // Create the X.509 certificate from the input stream + X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(fis); + fis.close(); + + // Extract the common name (CN) from the certificate's subject DN + String subjectDN = certificate.getSubjectX500Principal().getName(); + String alias = getCommonName(subjectDN); + + // Create a new KeyStore instance + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); // Initialize an empty KeyStore + + keyStore.setCertificateEntry(alias, certificate); + + return keyStore; + } + + private KeyStore loadJksFile(String filePath) throws Exception { + // Load the JKS file + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + FileInputStream fis = new FileInputStream(filePath); + keyStore.load(fis, null); + fis.close(); + return keyStore; + } + + private KeyStore loadPkcs7File(String filePath) throws Exception { + // Load the PKCS#7 file + FileInputStream fis = new FileInputStream(filePath); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Collection certificates = certificateFactory.generateCertificates(fis); + fis.close(); + + // Create a new KeyStore instance + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); // Initialize an empty KeyStore + + // Add each certificate to the KeyStore + int i = 1; + for (Certificate certificate : certificates) { + String subjectDN = ((X509Certificate)certificate).getSubjectX500Principal().getName(); + String alias = getCommonName(subjectDN); + keyStore.setCertificateEntry(alias, certificate); + } + + return keyStore; + } + + private KeyStore loadPkcs12File(String filePath) throws Exception { + // Load the PKCS#12 file + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + FileInputStream fis = new FileInputStream(filePath); + keyStore.load(fis, null); + fis.close(); + return keyStore; + } + + private KeyStore loadPemFile(String filePath) throws Exception { + // Load the PEM file + FileInputStream fis = new FileInputStream(filePath); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Collection certificates = certificateFactory.generateCertificates(fis); + fis.close(); + + // Create a new KeyStore instance + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); // Initialize an empty KeyStore + + // Add each certificate to the KeyStore + int i = 1; + for (Certificate certificate : certificates) { + String subjectDN = ((X509Certificate)certificate).getSubjectX500Principal().getName(); + String alias = getCommonName(subjectDN); + keyStore.setCertificateEntry(alias, certificate); + } + + return keyStore; + } + + private String getCommonName(String subjectDN) { + Pattern cnPattern = Pattern.compile("CN=([^,]+)"); + Matcher matcher = cnPattern.matcher(subjectDN); + if (matcher.find()) { + return matcher.group(1); + } + throw new IllegalArgumentException("No CN found in subject DN: " + subjectDN); + } + + private CertificateVerifier createCertificateVerifier(KeyStore trustedCertificates) { + return new SimpleCertificateVerifier(trustedCertificates); + } +} diff --git a/shared/src/main/java/ca/weblite/tools/security/CertificateUtil.java b/shared/src/main/java/ca/weblite/tools/security/CertificateUtil.java index 56475422..d92ab6f4 100644 --- a/shared/src/main/java/ca/weblite/tools/security/CertificateUtil.java +++ b/shared/src/main/java/ca/weblite/tools/security/CertificateUtil.java @@ -210,6 +210,38 @@ public static byte[] getSHA256(byte[] input) throws NoSuchAlgorithmException // and return array of byte return md.digest(input); } + + public static KeyStore loadCertificatesFromPEM(String pemEncodedCertificates) throws Exception { + // Create an empty KeyStore + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); // Initialize the KeyStore with null parameters + + // Split the PEM string into individual certificates + String[] certArray = pemEncodedCertificates.split("(?m)(?=-----BEGIN CERTIFICATE-----)"); + + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + + int certIndex = 0; + + // Process each certificate in the PEM string + for (String certString : certArray) { + // Clean up the PEM string by removing whitespace and newlines + certString = certString.replaceAll("\\s", "").replaceAll("-----BEGINCERTIFICATE-----", "").replaceAll("-----ENDCERTIFICATE-----", ""); + + // Decode the Base64 encoded certificate + byte[] decoded = Base64.getDecoder().decode(certString); + + // Generate the X.509 certificate + X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(decoded)); + + // Add the certificate to the KeyStore with a unique alias + keyStore.setCertificateEntry("cert-" + certIndex, certificate); + certIndex++; + } + + return keyStore; + } + private static String toHexString(byte[] hash) { // Convert byte array into signum representation diff --git a/shared/src/main/java/ca/weblite/tools/security/SimpleCertificateVerifier.java b/shared/src/main/java/ca/weblite/tools/security/SimpleCertificateVerifier.java index cf59063c..2be8bdf9 100644 --- a/shared/src/main/java/ca/weblite/tools/security/SimpleCertificateVerifier.java +++ b/shared/src/main/java/ca/weblite/tools/security/SimpleCertificateVerifier.java @@ -1,10 +1,8 @@ package ca.weblite.tools.security; import java.security.KeyStore; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.util.Collections; -import java.util.List; +import java.security.cert.*; +import java.util.*; public class SimpleCertificateVerifier implements CertificateVerifier { @@ -16,15 +14,20 @@ public SimpleCertificateVerifier(KeyStore keyStore) { @Override public boolean isTrusted(List certificateChain) { + X509Certificate certificate = (X509Certificate) certificateChain.get(0); + Set intermediates = new HashSet<>(); + for (int i = 1; i < certificateChain.size(); i++) { + intermediates.add((X509Certificate) certificateChain.get(i)); + } try { - if (!isValidChain(certificateChain)) { - return false; - } - - for (Certificate cert : certificateChain) { - if (isCertificateTrusted(cert)) { - return true; - } + if ( + isCertificateTrusted( + certificate, + intermediates, + keyStore + ) + ) { + return true; } } catch (Exception e) { e.printStackTrace(); @@ -32,51 +35,42 @@ public boolean isTrusted(List certificateChain) { return false; } - private boolean isValidChain(List certificateChain) { - try { - if (certificateChain.isEmpty()) { - return false; - } - - for (int i = 0; i < certificateChain.size() - 1; i++) { - X509Certificate cert = (X509Certificate) certificateChain.get(i); - X509Certificate issuer = (X509Certificate) certificateChain.get(i + 1); + private static boolean isCertificateTrusted( + X509Certificate certificate, + Set intermediates, + KeyStore rootKeyStore) throws Exception { + // Create a CertPathValidator instance + CertPathValidator validator = CertPathValidator.getInstance(CertPathValidator.getDefaultType()); - // Verify that cert was issued by issuer - cert.verify(issuer.getPublicKey()); - } + // Create a CertPath for the certificate chain + List certChain = new ArrayList<>(); + certChain.add(certificate); // Start with the leaf certificate + certChain.addAll(intermediates); // Add intermediate certificates - return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } + CertPath certPath = CertificateFactory.getInstance("X.509").generateCertPath(certChain); - private boolean isCertificateTrusted(Certificate cert) throws Exception { - if (keyStore == null) { - return false; + // Create a TrustAnchor set from the root certificates in the KeyStore + Set trustAnchors = new HashSet<>(); + Enumeration aliases = rootKeyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (rootKeyStore.isCertificateEntry(alias)) { + X509Certificate rootCert = (X509Certificate) rootKeyStore.getCertificate(alias); + trustAnchors.add(new TrustAnchor(rootCert, null)); + } } - for (String alias : Collections.list(keyStore.aliases())) { - Certificate trustedCert = keyStore.getCertificate(alias); - if (trustedCert != null && trustedCert.equals(cert)) { - return true; - } + // Create PKIX parameters with the trust anchors and any additional constraints + PKIXParameters pkixParams = new PKIXParameters(trustAnchors); + pkixParams.setRevocationEnabled(false); // Disable CRL checks for simplicity - // Check if the certificate is part of a chain that can be validated against the trusted cert - if (trustedCert instanceof X509Certificate) { - X509Certificate x509Cert = (X509Certificate) trustedCert; - try { - x509Cert.checkValidity(); - cert.verify(x509Cert.getPublicKey()); - return true; - } catch (Exception e) { - // Verification failed, continue checking other certificates - } - } + try { + // Validate the certificate path + validator.validate(certPath, pkixParams); + return true; // Certificate chain is trusted + } catch (CertPathValidatorException e) { + return false; // Certificate chain is not trusted } - - return false; } + }