Skip to content

Commit 6eaecc2

Browse files
kofemannmksahakyan
authored andcommitted
common-security: add workaround bouncycastle EC algorithm name mismatch
Motivation: BouncyCastle uses "ECDSA" as the private key algorithm and "EC" as the public key algorithm names for elliptic curve keys. Thus, CertificateHelpers#checkKeysMatching throws an exception. Modification: Update CanlContextFactory with a custom implementations of PEMCredential and KeyAndCertCredential that handle the mismatch between private and public key algorithms. Result: Elliptic curve keys are supported. Fixes: #7928 Acked-by: Dmitry Litvintsev Target: master, 11.1, 11.0, 10.2 Require-book: no Require-notes: yes (cherry picked from commit 8de5c6f) Signed-off-by: Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
1 parent f4bc000 commit 6eaecc2

2 files changed

Lines changed: 252 additions & 4 deletions

File tree

modules/common-security/src/main/java/org/dcache/ssl/CanlContextFactory.java

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* dCache - http://www.dcache.org/
22
*
3-
* Copyright (C) 2015 - 2023 Deutsches Elektronen-Synchrotron
3+
* Copyright (C) 2015 - 2025 Deutsches Elektronen-Synchrotron
44
*
55
* This program is free software: you can redistribute it and/or modify
66
* it under the terms of the GNU Affero General Public License as
@@ -39,20 +39,37 @@
3939
import eu.emi.security.authn.x509.ValidationErrorCategory;
4040
import eu.emi.security.authn.x509.X509CertChainValidator;
4141
import eu.emi.security.authn.x509.X509Credential;
42+
import eu.emi.security.authn.x509.helpers.AbstractDelegatingX509Credential;
43+
import eu.emi.security.authn.x509.helpers.AbstractX509Credential;
44+
import eu.emi.security.authn.x509.helpers.KeyStoreHelper;
45+
import eu.emi.security.authn.x509.helpers.PasswordSupplier;
4246
import eu.emi.security.authn.x509.helpers.ssl.SSLTrustManager;
47+
import eu.emi.security.authn.x509.impl.CertificateUtils;
48+
import eu.emi.security.authn.x509.impl.KeystoreCredential;
4349
import eu.emi.security.authn.x509.impl.OpensslCertChainValidator;
44-
import eu.emi.security.authn.x509.impl.PEMCredential;
4550
import eu.emi.security.authn.x509.impl.ValidatorParams;
4651
import io.netty.handler.ssl.SslContext;
4752
import io.netty.handler.ssl.SslContextBuilder;
4853
import java.io.File;
54+
import java.io.FileInputStream;
4955
import java.io.FileNotFoundException;
5056
import java.io.IOException;
57+
import java.io.InputStream;
5158
import java.nio.file.FileSystems;
5259
import java.nio.file.Path;
5360
import java.security.GeneralSecurityException;
61+
import java.security.InvalidKeyException;
62+
import java.security.KeyStoreException;
63+
import java.security.NoSuchAlgorithmException;
64+
import java.security.PrivateKey;
65+
import java.security.PublicKey;
5466
import java.security.SecureRandom;
67+
import java.security.Signature;
68+
import java.security.SignatureException;
69+
import java.security.cert.CertificateException;
5570
import java.security.cert.X509Certificate;
71+
import java.security.interfaces.RSAPrivateKey;
72+
import java.security.interfaces.RSAPublicKey;
5673
import java.util.EnumSet;
5774
import java.util.concurrent.Callable;
5875
import java.util.concurrent.TimeUnit;
@@ -313,8 +330,8 @@ public <T> Callable<T> buildWithCaching(Class<T> contextType) throws Exception {
313330
* https://github.com/eu-emi/canl-java/issues/114
314331
*/
315332
Callable newContext = () -> {
316-
PEMCredential credential
317-
= new PEMCredential(keyPath.toString(), certificatePath.toString(), new char[]{});
333+
PEMCredential0 credential
334+
= new PEMCredential0(keyPath.toString(), certificatePath.toString(), new char[]{});
318335
LOGGER.info("Reloading host credential {} {}", certificatePath, keyPath);
319336
return factory.getContext(contextType, credential);
320337
};
@@ -326,4 +343,115 @@ public <T> Callable<T> buildWithCaching(Class<T> contextType) throws Exception {
326343
credentialUpdateIntervalUnit);
327344
}
328345
}
346+
347+
// REVISIT: PEMCredential0 is a copy of PEMCredential that uses custom KeyAndCertCredential
348+
// This workaround is needed as long as CaNL library with desired fix ( > 2.8.3 ) is not released.
349+
350+
private static class PEMCredential0 extends AbstractDelegatingX509Credential {
351+
352+
public PEMCredential0(String keyPath, String certificatePath, char[] keyPasswd) throws IOException, CertificateException, KeyStoreException {
353+
this(new FileInputStream(keyPath), new FileInputStream(certificatePath), keyPasswd);
354+
}
355+
356+
public PEMCredential0(InputStream privateKeyStream, InputStream certificateStream, char[] keyPasswd)
357+
throws IOException, KeyStoreException, CertificateException {
358+
this(privateKeyStream, certificateStream, CertificateUtils.getPF(keyPasswd));
359+
}
360+
361+
public PEMCredential0(InputStream privateKeyStream, InputStream certificateStream, PasswordSupplier pf)
362+
throws IOException, KeyStoreException {
363+
X509Certificate[] chain = CertificateUtils.loadCertificateChain(
364+
certificateStream, CertificateUtils.Encoding.PEM);
365+
PrivateKey pk = CertificateUtils.loadPEMPrivateKey(privateKeyStream, pf);
366+
privateKeyStream.close();
367+
delegate = new KeyAndCertCredential0(pk, chain);
368+
}
369+
}
370+
371+
/**
372+
* KeyAndCertCredential0 is a copy of KeyAndCertCredential that support EC and ECDSA keys.
373+
*/
374+
private static class KeyAndCertCredential0 extends AbstractX509Credential {
375+
376+
// a semirandom byte array for ecrypt/decrypt testing
377+
private static final byte[] TEST = new byte[]{1, 2, 3, 4, 100};
378+
379+
/**
380+
* Creates a new instance from the provided key and certificates.
381+
*
382+
* @param privateKey private key to be placed in this {@link X509Credential}'s KeyStore
383+
* @param certificateChain certificates to be placed in this {@link X509Credential}'s KeyStore.
384+
* those certificates must match the provided privateKey. The user's certificate is assumed
385+
* to be the first entry in the chain.
386+
* @throws KeyStoreException if private key is invalid or doesn't match the certificate.
387+
*/
388+
public KeyAndCertCredential0(PrivateKey privateKey, X509Certificate[] certificateChain)
389+
throws KeyStoreException {
390+
try {
391+
ks = KeyStoreHelper.getInstanceForCredential("JKS");
392+
} catch (KeyStoreException e) {
393+
throw new RuntimeException("Can't create JKS KeyStore - JDK is misconfgured?", e);
394+
}
395+
396+
try {
397+
ks.load(null);
398+
} catch (Exception e) {
399+
throw new RuntimeException("Can't init JKS KeyStore - JDK is misconfgured?", e);
400+
}
401+
402+
PublicKey pubKey = certificateChain[0].getPublicKey();
403+
String pubKeyAlgorithm = pubKey.getAlgorithm();
404+
// REVISIT: BouncyCastle uses "ECDSA" as the private key algorithm and "EC" as the public key algorithm names for elliptic curve keys.
405+
if (!privateKey.getAlgorithm().equals(pubKeyAlgorithm) && !(privateKey.getAlgorithm().equals("ECDSA") && pubKeyAlgorithm.equals("EC")))
406+
throw new KeyStoreException("Private and public keys are not matching: different algorithms: "
407+
+ privateKey.getAlgorithm() + " vs. " + pubKeyAlgorithm);
408+
409+
switch (pubKeyAlgorithm) {
410+
case "DSA":
411+
if (!checkKeysViaSignature("SHA1withDSA", privateKey, pubKey))
412+
throw new KeyStoreException("Private and public keys are not matching: DSA");
413+
break;
414+
case "RSA":
415+
RSAPublicKey rpub = (RSAPublicKey) pubKey;
416+
RSAPrivateKey rpriv = (RSAPrivateKey) privateKey;
417+
if (!rpub.getModulus().equals(rpriv.getModulus()))
418+
throw new KeyStoreException("Private and public keys are not matching: RSA parameters");
419+
break;
420+
case "GOST3410":
421+
if (!checkKeysViaSignature("GOST3411withGOST3410", privateKey, pubKey))
422+
throw new KeyStoreException("Private and public keys are not matching: GOST 34.10");
423+
break;
424+
case "ECGOST3410":
425+
if (!checkKeysViaSignature("GOST3411withECGOST3410", privateKey, pubKey))
426+
throw new KeyStoreException("Private and public keys are not matching: EC GOST 34.10");
427+
break;
428+
case "ECDSA":
429+
if (!checkKeysViaSignature("SHA1withECDSA", privateKey, pubKey))
430+
throw new KeyStoreException("Private and public keys are not matching: EC DSA");
431+
break;
432+
}
433+
434+
ks.setKeyEntry(KeystoreCredential.ALIAS, privateKey,
435+
KeystoreCredential.KEY_PASSWD, certificateChain);
436+
}
437+
438+
private static boolean checkKeysViaSignature(String alg, PrivateKey privKey, PublicKey pubKey) throws KeyStoreException {
439+
try {
440+
Signature s = Signature.getInstance(alg);
441+
s.initSign(privKey);
442+
s.update(TEST);
443+
byte[] signature = s.sign();
444+
Signature s2 = Signature.getInstance(alg);
445+
s2.initVerify(pubKey);
446+
s2.update(TEST);
447+
return s2.verify(signature);
448+
} catch (InvalidKeyException e) {
449+
throw new KeyStoreException("Invalid key when checking key match", e);
450+
} catch (NoSuchAlgorithmException e) {
451+
throw new RuntimeException("Bug: BC provider not available in checkKeysMatching()", e);
452+
} catch (SignatureException e) {
453+
throw new RuntimeException("Bug: can't sign/verify test data", e);
454+
}
455+
}
456+
}
329457
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.dcache.ssl;
2+
3+
import eu.emi.security.authn.x509.CrlCheckingMode;
4+
import eu.emi.security.authn.x509.NamespaceCheckingMode;
5+
import eu.emi.security.authn.x509.OCSPCheckingMode;
6+
import eu.emi.security.authn.x509.impl.CertificateUtils;
7+
import io.netty.handler.ssl.SslContext;
8+
import org.bouncycastle.asn1.x500.X500Name;
9+
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
10+
import org.bouncycastle.cert.X509CertificateHolder;
11+
import org.bouncycastle.cert.X509v3CertificateBuilder;
12+
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
13+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
14+
import org.bouncycastle.operator.ContentSigner;
15+
import org.bouncycastle.operator.OperatorCreationException;
16+
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
17+
import org.junit.Before;
18+
import org.junit.BeforeClass;
19+
import org.junit.Test;
20+
21+
import java.io.File;
22+
import java.io.IOException;
23+
import java.io.OutputStream;
24+
import java.math.BigInteger;
25+
import java.nio.file.Files;
26+
import java.security.GeneralSecurityException;
27+
import java.security.KeyPair;
28+
import java.security.KeyPairGenerator;
29+
import java.security.SecureRandom;
30+
import java.security.Security;
31+
import java.security.spec.ECGenParameterSpec;
32+
import java.util.Date;
33+
import java.util.concurrent.TimeUnit;
34+
35+
import static java.nio.file.StandardOpenOption.CREATE;
36+
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
37+
import static java.nio.file.StandardOpenOption.WRITE;
38+
39+
public class CanlContextFactoryTest {
40+
41+
private File keyFile;
42+
private File certFile;
43+
private File ca;
44+
45+
46+
@BeforeClass
47+
public static void setupClass() {
48+
Security.addProvider(new BouncyCastleProvider());
49+
}
50+
51+
@Before
52+
public void setup() throws IOException, GeneralSecurityException, OperatorCreationException {
53+
keyFile = File.createTempFile("hostkey-", ".pem");
54+
certFile = File.createTempFile("hostcert-", ".pem");
55+
ca = Files.createTempDirectory("ca-").toFile();
56+
generateSelfSignedCert();
57+
}
58+
59+
@Test
60+
public void testEcdsaKey() throws Exception {
61+
62+
var builder = CanlContextFactory.custom()
63+
.withCertificatePath(certFile.toPath())
64+
.withKeyPath(keyFile.toPath())
65+
.withCertificateAuthorityPath(ca.toPath())
66+
.withCrlCheckingMode(CrlCheckingMode.REQUIRE)
67+
.withOcspCheckingMode(OCSPCheckingMode.IF_AVAILABLE)
68+
.withNamespaceMode(NamespaceCheckingMode.EUGRIDPMA_AND_GLOBUS)
69+
.withLazy(false)
70+
.buildWithCaching(SslContext.class);
71+
72+
builder.call();
73+
}
74+
75+
private void generateSelfSignedCert()
76+
throws GeneralSecurityException, OperatorCreationException, IOException {
77+
78+
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", "BC");
79+
80+
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
81+
82+
keyPairGenerator.initialize(ecSpec, new SecureRandom());
83+
KeyPair keyPair = keyPairGenerator.generateKeyPair();
84+
85+
long notBefore = System.currentTimeMillis();
86+
long notAfter = notBefore + TimeUnit.DAYS.toMillis(1);
87+
88+
X500Name subjectDN = new X500Name("CN=localhost, O=dCache.org");
89+
// explicit self-signed certificate
90+
X500Name issuerDN = subjectDN;
91+
92+
SubjectPublicKeyInfo subjectPublicKeyInfo =
93+
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
94+
95+
X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(issuerDN,
96+
BigInteger.ONE,
97+
new Date(notBefore),
98+
new Date(notAfter), subjectDN,
99+
subjectPublicKeyInfo);
100+
101+
String signatureAlgorithm = "SHA1withECDSA";
102+
103+
// sign with own key
104+
ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm)
105+
.build(keyPair.getPrivate());
106+
107+
X509CertificateHolder certificateHolder = certificateBuilder.build(contentSigner);
108+
var cert = new JcaX509CertificateConverter().getCertificate(certificateHolder);
109+
110+
try (OutputStream certOut = Files.newOutputStream(
111+
certFile.toPath(), CREATE, TRUNCATE_EXISTING,
112+
WRITE); OutputStream keyOut = Files.newOutputStream(keyFile.toPath(), CREATE,
113+
TRUNCATE_EXISTING, WRITE)) {
114+
115+
CertificateUtils.saveCertificate(certOut, cert, CertificateUtils.Encoding.PEM);
116+
CertificateUtils.savePrivateKey(keyOut, keyPair.getPrivate(), CertificateUtils.Encoding.PEM, null, null);
117+
}
118+
}
119+
120+
}

0 commit comments

Comments
 (0)