diff --git a/src/main/java/eu/emi/security/authn/x509/helpers/CertificateHelpers.java b/src/main/java/eu/emi/security/authn/x509/helpers/CertificateHelpers.java index 7a88216..2ab80f4 100644 --- a/src/main/java/eu/emi/security/authn/x509/helpers/CertificateHelpers.java +++ b/src/main/java/eu/emi/security/authn/x509/helpers/CertificateHelpers.java @@ -42,15 +42,15 @@ */ public class CertificateHelpers { - public enum PEMContentsType {PRIVATE_KEY, LEGACY_OPENSSL_PRIVATE_KEY, + public enum PEMContentsType {PRIVATE_KEY, LEGACY_OPENSSL_PRIVATE_KEY, CERTIFICATE, CSR, CRL, UNKNOWN}; private static final byte[] TEST = new byte[] {1, 2, 3, 4, 100}; - + /** * Assumes that the input is the contents of the PEM identification line, * after '-----BEGIN ' prefix. - * + * * @param name PEM first line to be checked. * @return the type */ @@ -71,7 +71,7 @@ public static PEMContentsType getPEMType(String name) return PEMContentsType.UNKNOWN; } - + public static Collection readDERCertificates(InputStream input) throws IOException { CertificateFactory factory = getFactory(); @@ -109,7 +109,7 @@ public static Certificate readDERCertificate(InputStream input) throws IOExcepti input.close(); } } - + private static CertificateFactory getFactory() { try @@ -125,9 +125,9 @@ private static CertificateFactory getFactory() "no BouncyCastle provider, it is a BUG!", e); } } - + /** - * Creates a chain of certificates, where the top-most certificate (the one without + * Creates a chain of certificates, where the top-most certificate (the one without * issuing certificate) is the last in the returned array. * @param certificates unsorted certificates of one chain * @return sorted certificate chain @@ -137,11 +137,11 @@ public static X509Certificate[] sortChain(List certificates) th { if (certificates.size() == 0) return new X509Certificate[0]; - + Map certsMapBySubject = new HashMap(); //in this map root CA cert is not stored (as it has the same Issuer as its direct child) Map certsMapByIssuer = new HashMap(); - for (X509Certificate c: certificates) + for (X509Certificate c: certificates) { certsMapBySubject.put(c.getSubjectX500Principal(), c); if (!c.getIssuerX500Principal().equals(c.getSubjectX500Principal())) @@ -167,7 +167,7 @@ public static X509Certificate[] sortChain(List certificates) th } else break; } - + //build path from the first on the list down to the user's certificate current = certsList.get(0); while (true) @@ -180,13 +180,13 @@ public static X509Certificate[] sortChain(List certificates) th } else break; } - + if (certsMapByIssuer.size() > 0) throw new IOException("The keystore is inconsistent as it contains certificates from different chains"); - + return certsList.toArray(new X509Certificate[certsList.size()]); } - + /** * Converts certificates array to {@link CertPath} * @param in array @@ -206,7 +206,7 @@ public static CertPath toCertPath(X509Certificate[] in) throws CertificateExcept } return certFactory.generateCertPath(Arrays.asList(in)); } - + /** * Converts {@link X500Principal} to {@link X500Name} with the {@link JavaAndBCStyle} * style. @@ -223,7 +223,7 @@ public static X500Name toX500Name(X500Principal srcDn) /** * Gets the certificate extension identified by the oid and returns the * value bytes unwrapped by the ASN1OctetString. - * + * * @param cert * The certificate to inspect. * @param oid @@ -243,7 +243,7 @@ public static byte[] getExtensionBytes(X509Certificate cert, String oid) .fromByteArray(bytes); return valueOctets.getOctets(); } - + /** * Throws an exception if the private key is not matching the public key. * The check is done only for known types of keys - RSA and DSA currently. @@ -253,37 +253,42 @@ public static byte[] getExtensionBytes(X509Certificate cert, String oid) */ public static void checkKeysMatching(PrivateKey privKey, PublicKey pubKey) throws InvalidKeyException { - String algorithm = pubKey.getAlgorithm(); + String privAlg = privKey.getAlgorithm(); + String pubAlg = pubKey.getAlgorithm(); + + // "EC" and "ECDSA" are both used for elliptic-curve keys depending on the + // provider and BouncyCastle version — treat them as equivalent. + boolean privIsEC = privAlg.equals("EC") || privAlg.equals("ECDSA"); + boolean pubIsEC = pubAlg.equals("EC") || pubAlg.equals("ECDSA"); - // REVISIT: BouncyCastle uses "ECDSA" as the private key algorithm and "EC" as the public key algorithm names for elliptic curve keys. - if (!privKey.getAlgorithm().equals(algorithm) && !(privKey.getAlgorithm().equals("ECDSA") && algorithm.equals("EC"))) + if (!privAlg.equals(pubAlg) && !(privIsEC && pubIsEC)) throw new InvalidKeyException("Private and public keys are not matching: different algorithms"); - - if (algorithm.equals("DSA")) + + if (pubAlg.equals("DSA")) { if (!checkKeysViaSignature("SHA1withDSA", privKey, pubKey)) throw new InvalidKeyException("Private and public keys are not matching: DSA"); - } else if (algorithm.equals("RSA")) + } else if (pubAlg.equals("RSA")) { RSAPublicKey rpub = (RSAPublicKey)pubKey; RSAPrivateKey rpriv = (RSAPrivateKey)privKey; if (!rpub.getModulus().equals(rpriv.getModulus())) throw new InvalidKeyException("Private and public keys are not matching: RSA parameters"); - } else if (algorithm.equals("GOST3410")) + } else if (pubAlg.equals("GOST3410")) { if (!checkKeysViaSignature("GOST3411withGOST3410", privKey, pubKey)) throw new InvalidKeyException("Private and public keys are not matching: GOST 34.10"); - } else if (algorithm.equals("ECGOST3410")) + } else if (pubAlg.equals("ECGOST3410")) { if (!checkKeysViaSignature("GOST3411withECGOST3410", privKey, pubKey)) throw new InvalidKeyException("Private and public keys are not matching: EC GOST 34.10"); - } else if (algorithm.equals("ECDSA")) + } else if (privIsEC && pubIsEC) { - if (!checkKeysViaSignature("SHA1withECDSA", privKey, pubKey)) + if (!checkKeysViaSignature("SHA256withECDSA", privKey, pubKey)) throw new InvalidKeyException("Private and public keys are not matching: EC DSA"); } } - + private static boolean checkKeysViaSignature(String alg, PrivateKey privKey, PublicKey pubKey) throws InvalidKeyException { try @@ -305,8 +310,3 @@ private static boolean checkKeysViaSignature(String alg, PrivateKey privKey, Pub } } } - - - - - diff --git a/src/test/java/ServerKeyPairValidator.java b/src/test/java/ServerKeyPairValidator.java new file mode 100644 index 0000000..6cd8306 --- /dev/null +++ b/src/test/java/ServerKeyPairValidator.java @@ -0,0 +1,90 @@ +import eu.emi.security.authn.x509.CrlCheckingMode; +import eu.emi.security.authn.x509.NamespaceCheckingMode; +import eu.emi.security.authn.x509.OCSPCheckingMode; +import eu.emi.security.authn.x509.OCSPParametes; +import eu.emi.security.authn.x509.ProxySupport; +import eu.emi.security.authn.x509.RevocationParameters; +import eu.emi.security.authn.x509.ValidationError; +import eu.emi.security.authn.x509.ValidationResult; +import eu.emi.security.authn.x509.X509CertChainValidator; +import eu.emi.security.authn.x509.impl.OpensslCertChainValidator; +import eu.emi.security.authn.x509.impl.PEMCredential; +import eu.emi.security.authn.x509.impl.ValidatorParams; + +import java.security.cert.X509Certificate; + +/** + * Validates that a server private key and certificate are: + * 1. Matching (key pair is consistent) + * 2. Trusted (chain validates against a given truststore) + * + * Usage: + * java ServerKeyPairValidator \ + * /path/to/server.key \ + * /path/to/server.crt \ + * /path/to/custom/certifificates + */ +public class ServerKeyPairValidator { + + public static void main(String[] args) throws Exception { + + if (args.length < 2) { + System.err.println("Usage: ServerKeyPairValidator [caDir]"); + System.exit(1); + } + + String keyPath = args[0]; + String certPath = args[1]; + String caDir = args.length >= 3 ? args[2] : "/etc/grid-security/certificates"; + + // 1. Load credential — also verifies key matches certificate internally + PEMCredential credential; + try { + credential = new PEMCredential(keyPath, certPath, (char[]) null); + } catch (Exception e) { + System.out.println("FAILED: Cannot load key/certificate pair."); + System.out.println(" Reason: " + e.getMessage()); + System.exit(2); + return; + } + + X509Certificate[] chain = credential.getCertificateChain(); + System.out.println("Loaded certificate for: " + credential.getSubjectName()); + System.out.println("Chain length: " + chain.length); + + long trustAnchorRefreshInterval = 3_600_000; + + NamespaceCheckingMode namespaceMode = + NamespaceCheckingMode.valueOf("IGNORE"); + CrlCheckingMode crlCheckingMode = + CrlCheckingMode.valueOf("REQUIRE"); + OCSPCheckingMode ocspCheckingMode = + OCSPCheckingMode.valueOf("REQUIRE"); + ValidatorParams validatorParams = + new ValidatorParams(new RevocationParameters(crlCheckingMode, + new OCSPParametes(ocspCheckingMode)), + ProxySupport.ALLOW); + X509CertChainValidator validator = new OpensslCertChainValidator(caDir, + false, + namespaceMode, + trustAnchorRefreshInterval, + validatorParams, + false); + + + // 3. Validate certificate chain against truststore + ValidationResult result = validator.validate(chain); + + if (result.isValid()) { + System.out.println("SUCCESS: Key/certificate pair is valid and trusted."); + } else { + System.out.println("FAILED: Certificate validation errors:"); + for (ValidationError error : result.getErrors()) { + System.out.println(" [" + error.getErrorCode() + "] " + error.getMessage()); + } + System.exit(3); + } + + ((OpensslCertChainValidator) validator).dispose(); + } +}