Skip to content

Commit 7c6d862

Browse files
committed
new KeyPairValidator to check if public/private keys match
1 parent 3c072da commit 7c6d862

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package de.hsheilbronn.mi.utils.crypto.keypair;
2+
3+
import java.security.InvalidKeyException;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.PrivateKey;
6+
import java.security.PublicKey;
7+
import java.security.Signature;
8+
import java.security.SignatureException;
9+
import java.util.Arrays;
10+
import java.util.Random;
11+
import java.util.function.Predicate;
12+
13+
import javax.crypto.DecapsulateException;
14+
import javax.crypto.KEM;
15+
import javax.crypto.KEM.Decapsulator;
16+
import javax.crypto.KEM.Encapsulated;
17+
import javax.crypto.KEM.Encapsulator;
18+
19+
public class KeyPairValidator
20+
{
21+
private KeyPairValidator()
22+
{
23+
}
24+
25+
private static final Random RANDOM = new Random();
26+
27+
/**
28+
* Checks if the given <b>privateKey</b> and <b>publicKey</b> match by checking if a generated signature can be
29+
* verified for RSA, EC and EdDSA key pairs or a Diffie-Hellman key agreement produces the same secret key for a XDH
30+
* key pair.
31+
*
32+
* @param privateKey
33+
* may be <code>null</code>
34+
* @param publicKey
35+
* may be <code>null</code>
36+
* @return <code>true</code> if the given keys are not <code>null</code> and match
37+
*/
38+
public static boolean matches(PrivateKey privateKey, PublicKey publicKey)
39+
{
40+
if (privateKey == null || publicKey == null || !privateKey.getAlgorithm().equals(publicKey.getAlgorithm()))
41+
return false;
42+
43+
return match(publicKey).test(privateKey);
44+
}
45+
46+
private static Predicate<PrivateKey> match(PublicKey publicKey)
47+
{
48+
return switch (publicKey.getAlgorithm())
49+
{
50+
case "RSA" -> matchesRsaEcEdDsa(publicKey, "NONEwithRSA");
51+
case "EC" -> matchesRsaEcEdDsa(publicKey, "NONEwithECDSA");
52+
case "EdDSA" -> matchesRsaEcEdDsa(publicKey, "EdDSA");
53+
case "XDH" -> matchesXdh(publicKey);
54+
55+
default -> throw new IllegalArgumentException(
56+
"PublicKey algorithm " + publicKey.getAlgorithm() + " not supported");
57+
};
58+
}
59+
60+
private static Predicate<PrivateKey> matchesRsaEcEdDsa(PublicKey publicKey, String algorithm)
61+
{
62+
return privateKey ->
63+
{
64+
try
65+
{
66+
byte[] b = random(16);
67+
68+
Signature signature = Signature.getInstance(algorithm);
69+
signature.initSign(privateKey);
70+
signature.update(b);
71+
72+
byte[] signed = signature.sign();
73+
74+
signature.initVerify(publicKey);
75+
signature.update(b);
76+
77+
return signature.verify(signed);
78+
}
79+
catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e)
80+
{
81+
throw new RuntimeException(e);
82+
}
83+
};
84+
}
85+
86+
private static byte[] random(int length)
87+
{
88+
byte[] b = new byte[length];
89+
RANDOM.nextBytes(b);
90+
return b;
91+
}
92+
93+
private static Predicate<PrivateKey> matchesXdh(PublicKey publicKey)
94+
{
95+
return privateKey ->
96+
{
97+
try
98+
{
99+
KEM kem = KEM.getInstance("DHKEM");
100+
Encapsulator e = kem.newEncapsulator(publicKey);
101+
Encapsulated en = e.encapsulate();
102+
Decapsulator d = kem.newDecapsulator(privateKey);
103+
104+
return Arrays.equals(en.key().getEncoded(), d.decapsulate(en.encapsulation()).getEncoded());
105+
}
106+
catch (InvalidKeyException | NoSuchAlgorithmException | DecapsulateException e)
107+
{
108+
throw new RuntimeException(e);
109+
}
110+
};
111+
}
112+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package de.hsheilbronn.mi.utils.crypto.keypair;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.security.KeyPair;
6+
import java.security.PrivateKey;
7+
import java.security.PublicKey;
8+
import java.util.stream.Stream;
9+
10+
import org.junit.jupiter.params.ParameterizedTest;
11+
import org.junit.jupiter.params.provider.Arguments;
12+
import org.junit.jupiter.params.provider.MethodSource;
13+
14+
public class KeyPairValidatorTest
15+
{
16+
private static Stream<Arguments> forTestMatches()
17+
{
18+
PrivateKey privateEd25519 = KeyPairGeneratorFactory.ed25519().initialize().generateKeyPair().getPrivate();
19+
PublicKey publicRsa1024 = KeyPairGeneratorFactory.rsa1024().initialize().generateKeyPair().getPublic();
20+
21+
return Stream.concat(Stream.of(KeyPairGeneratorFactory.ed25519(), KeyPairGeneratorFactory.ed448(),
22+
KeyPairGeneratorFactory.rsa1024(), KeyPairGeneratorFactory.rsa2048(), KeyPairGeneratorFactory.rsa3072(),
23+
KeyPairGeneratorFactory.rsa4096(), KeyPairGeneratorFactory.secp256r1(),
24+
KeyPairGeneratorFactory.secp384r1(), KeyPairGeneratorFactory.secp521r1(),
25+
KeyPairGeneratorFactory.x25519(), KeyPairGeneratorFactory.x448()).flatMap(f ->
26+
{
27+
KeyPair kp1 = f.initialize().generateKeyPair();
28+
KeyPair kp2 = f.initialize().generateKeyPair();
29+
return Stream.of(Arguments.of(true, kp1.getPublic(), kp1.getPrivate()),
30+
Arguments.of(false, kp2.getPublic(), kp1.getPrivate()),
31+
Arguments.of(false, kp1.getPublic(), kp2.getPrivate()),
32+
Arguments.of(false, kp1.getPublic(), null), Arguments.of(false, null, kp2.getPrivate()));
33+
}), Stream.of(Arguments.of(false, publicRsa1024, privateEd25519)));
34+
35+
}
36+
37+
@ParameterizedTest
38+
@MethodSource("forTestMatches")
39+
public void testMatches(boolean expected, PublicKey publicKey, PrivateKey privateKey) throws Exception
40+
{
41+
assertEquals(expected, KeyPairValidator.matches(privateKey, publicKey));
42+
}
43+
}

0 commit comments

Comments
 (0)