mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-12-23 03:17:58 +01:00
Add test for decryption with removed private key
This commit is contained in:
parent
3af6ab1b85
commit
df4fc94ce7
5 changed files with 159 additions and 113 deletions
|
@ -4,18 +4,12 @@
|
|||
|
||||
package org.pgpainless.decryption_verification;
|
||||
|
||||
import org.bouncycastle.bcpg.S2K;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.bouncycastle.openpgp.operator.PGPDataDecryptor;
|
||||
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
|
||||
import org.pgpainless.key.SubkeyIdentifier;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Enable integration of hardware-backed OpenPGP keys.
|
||||
*/
|
||||
|
@ -41,31 +35,6 @@ public class HardwareSecurity {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key-ids of all keys which appear to be stored on a hardware token / smartcard.
|
||||
*
|
||||
* @param secretKeys secret keys
|
||||
* @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K
|
||||
*/
|
||||
public static Set<SubkeyIdentifier> getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) {
|
||||
Set<SubkeyIdentifier> hardwareBackedKeys = new HashSet<>();
|
||||
for (PGPSecretKey secretKey : secretKeys) {
|
||||
S2K s2K = secretKey.getS2K();
|
||||
if (s2K == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int type = s2K.getType();
|
||||
int mode = s2K.getProtectionMode();
|
||||
// TODO: Is GNU_DUMMY_S2K appropriate?
|
||||
if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) {
|
||||
SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID());
|
||||
hardwareBackedKeys.add(hardwareBackedKey);
|
||||
}
|
||||
}
|
||||
return hardwareBackedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys
|
||||
* to a {@link DecryptionCallback}.
|
||||
|
|
|
@ -10,11 +10,14 @@ import org.bouncycastle.bcpg.SecretKeyPacket;
|
|||
import org.bouncycastle.bcpg.SecretSubkeyPacket;
|
||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.pgpainless.key.SubkeyIdentifier;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class can be used to remove private keys from secret software-keys by replacing them with
|
||||
|
@ -29,6 +32,33 @@ public final class GnuPGDummyKeyUtil {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG.
|
||||
* Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required
|
||||
* for dealing with hardware-backed keys.
|
||||
*
|
||||
* @param secretKeys secret keys
|
||||
* @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD
|
||||
*/
|
||||
public static Set<SubkeyIdentifier> getIdsOfKeysWithGnuPGS2KDivertedToCard(PGPSecretKeyRing secretKeys) {
|
||||
Set<SubkeyIdentifier> hardwareBackedKeys = new HashSet<>();
|
||||
for (PGPSecretKey secretKey : secretKeys) {
|
||||
S2K s2K = secretKey.getS2K();
|
||||
if (s2K == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int type = s2K.getType();
|
||||
int mode = s2K.getProtectionMode();
|
||||
// TODO: Is GNU_DUMMY_S2K appropriate?
|
||||
if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) {
|
||||
SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID());
|
||||
hardwareBackedKeys.add(hardwareBackedKey);
|
||||
}
|
||||
}
|
||||
return hardwareBackedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the given {@link PGPSecretKeyRing}.
|
||||
*
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.decryption_verification;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.pgpainless.key.SubkeyIdentifier;
|
||||
import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil;
|
||||
import org.pgpainless.key.util.KeyIdUtil;
|
||||
|
||||
public class HardwareSecurityTest {
|
||||
|
||||
private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
|
||||
"Version: PGPainless\n" +
|
||||
"Comment: DE2E 9AB2 6650 8191 53E7 D599 C176 507F 2B5D 43B3\n" +
|
||||
"Comment: Alice <alice@pgpainless.org>\n" +
|
||||
"\n" +
|
||||
"lFgEY1vjgRYJKwYBBAHaRw8BAQdAXjLoPTOIOdvlFT2Nt3rcvLTVx5ujPBGghZ5S\n" +
|
||||
"D5tEnyoAAP0fAUJTiPrxZYdzs6MP0KFo+Nmr/wb1PJHTkzmYpt4wkRKBtBxBbGlj\n" +
|
||||
"ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmNb44EJEMF2UH8rXUOz\n" +
|
||||
"FiEE3i6asmZQgZFT59WZwXZQfytdQ7MCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" +
|
||||
"AQAAHLYA/AgW+YrpU+UqrwX2dhY6RAfgHTTMU89RHjaTHJx8pLrBAP4gthGof00a\n" +
|
||||
"XEjwTWteDOO049SIp2AUfj9deJqtrQcHD5xdBGNb44ESCisGAQQBl1UBBQEBB0DN\n" +
|
||||
"vUT3awa3YLmwf41LRpPrm7B87AOHfYIP8S9QJ4GDJgMBCAcAAP9bwlSaF+lti8JY\n" +
|
||||
"qKFO3qt3ZYQMu1l/LRBle89ZB4zD+BDOiHUEGBYKAB0FAmNb44ECngECmwwFFgID\n" +
|
||||
"AQAECwkIBwUVCgkICwAKCRDBdlB/K11Ds/TsAP9kvpUrCWnrWGq+a9n1CqEfCMX5\n" +
|
||||
"cT+qzrwNf+J0L22KowD+M9SVO0qssiAqutLE9h9dGYLbEiFvsHzK3WSnjKYbIgac\n" +
|
||||
"WARjW+OBFgkrBgEEAdpHDwEBB0BCPh8M5TnXSmG6Ygwp4j5RR4u3hmxl8CYjX4h/\n" +
|
||||
"XtvvNwAA/RP04coSrLHVI6vUfbJk4MhWYeyhJBRYY0vGp7yq+wVtEpKI1QQYFgoA\n" +
|
||||
"fQUCY1vjgQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNb44EACgkQ\n" +
|
||||
"mlozJSF7rXQW+AD/TA3YBxTd+YbBSwfgqzWNbfT9BBcFrdn3uPCsbvfmqXoA/3oj\n" +
|
||||
"oupkgoaXesrGxn2k9hW9/GBXSvNcgY2txZ6/oYoIAAoJEMF2UH8rXUOziZ4A/0Xl\n" +
|
||||
"xSZJWmkRpBh5AO8Cnqosz6j947IYAxS16ay+sIOHAP9aN9CUNJIIdHnHdFHO4GZz\n" +
|
||||
"ejjknn4wt8NVJP97JxlnBQ==\n" +
|
||||
"=qSQb\n" +
|
||||
"-----END PGP PRIVATE KEY BLOCK-----";
|
||||
|
||||
@Test
|
||||
public void testGetSingleIdOfHardwareBackedKey() throws IOException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY);
|
||||
assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty());
|
||||
long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50");
|
||||
|
||||
PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys)
|
||||
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId));
|
||||
|
||||
Set<SubkeyIdentifier> hardwareBackedKeys = HardwareSecurity
|
||||
.getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey);
|
||||
assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGetIdsOfFullyHardwareBackedKey() throws IOException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY);
|
||||
assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty());
|
||||
|
||||
PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys)
|
||||
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any());
|
||||
Set<SubkeyIdentifier> expected = new HashSet<>();
|
||||
for (PGPSecretKey key : secretKeys) {
|
||||
expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID()));
|
||||
}
|
||||
|
||||
Set<SubkeyIdentifier> hardwareBackedKeys = HardwareSecurity
|
||||
.getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey);
|
||||
|
||||
assertEquals(expected, hardwareBackedKeys);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.decryption_verification;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.bouncycastle.util.io.Streams;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.pgpainless.encryption_signing.EncryptionOptions;
|
||||
import org.pgpainless.encryption_signing.EncryptionStream;
|
||||
import org.pgpainless.encryption_signing.ProducerOptions;
|
||||
import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class TryDecryptWithUnavailableGnuDummyKeyTest {
|
||||
|
||||
@Test
|
||||
public void testAttemptToDecryptWithRemovedPrivateKeysThrows()
|
||||
throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
|
||||
.modernKeyRing("Hardy Hardware <hardy@hard.ware>");
|
||||
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys);
|
||||
|
||||
ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream();
|
||||
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
|
||||
.onOutputStream(ciphertextOut)
|
||||
.withOptions(
|
||||
ProducerOptions.encrypt(EncryptionOptions.get().addRecipient(certificate)));
|
||||
ByteArrayInputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes());
|
||||
Streams.pipeAll(plaintextIn, encryptionStream);
|
||||
encryptionStream.close();
|
||||
|
||||
PGPSecretKeyRing removedKeys = GnuPGDummyKeyUtil.modify(secretKeys)
|
||||
.removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any());
|
||||
|
||||
ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray());
|
||||
assertThrows(PGPException.class, () -> PGPainless.decryptAndOrVerify()
|
||||
.onInputStream(ciphertextIn)
|
||||
.withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys)));
|
||||
}
|
||||
}
|
|
@ -10,12 +10,17 @@ import org.bouncycastle.openpgp.PGPSecretKey;
|
|||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.pgpainless.key.SubkeyIdentifier;
|
||||
import org.pgpainless.key.util.KeyIdUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class GnuPGDummyKeyUtilTest {
|
||||
// normal, non-hw-backed key
|
||||
|
@ -73,6 +78,29 @@ public class GnuPGDummyKeyUtilTest {
|
|||
"=rYoa\n" +
|
||||
"-----END PGP PRIVATE KEY BLOCK-----";
|
||||
|
||||
public static final String ALL_KEYS_REMOVED = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
|
||||
"Version: PGPainless\n" +
|
||||
"Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" +
|
||||
"Comment: Hardy Hardware <hardy@hard.ware>\n" +
|
||||
"\n" +
|
||||
"lDsEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" +
|
||||
"eBwCeTT+AGUAR05VAbQgSGFyZHkgSGFyZHdhcmUgPGhhcmR5QGhhcmQud2FyZT6I\n" +
|
||||
"jwQTFgoAQQUCY1vSiAkQwxLJfan3ak8WIQQB/ats4EpQeHn+ShjDEsl9qfdqTwKe\n" +
|
||||
"AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAD5NgD/dtk+U0O4bpBZacV904TIYniZ\n" +
|
||||
"xAhmORKreVNP7xGNV3YA/3hNTJfaqsekBnGjSnvHXjHtxIU2p7epkvbajB6dv94J\n" +
|
||||
"nEAEY1vSiBIKKwYBBAGXVQEFAQEHQFVwSzbzZhYfSl+oi5nTSTNvGXPTxp8xKAA/\n" +
|
||||
"fk+KdJQ8AwEIB/4AZQBHTlUBiHUEGBYKAB0FAmNb0ogCngECmwwFFgIDAQAECwkI\n" +
|
||||
"BwUVCgkICwAKCRDDEsl9qfdqT8nJAP0YGPS+O1hkB/kWLR4Qp2ICCzTJmtA+Qyzp\n" +
|
||||
"4v7ze17vvQD+MbQN4nL7zx859ZOP6aLE73w9k+dDQzJtYL/VBRO8/QGcOwRjW9KI\n" +
|
||||
"FgkrBgEEAdpHDwEBB0C9JhMPrS3y/HXR1IQEAJSgh9UKl44HfQPqd/Am1sNPRv4A\n" +
|
||||
"ZQBHTlUBiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" +
|
||||
"CgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0y\n" +
|
||||
"Uxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDD\n" +
|
||||
"Esl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r\n" +
|
||||
"0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" +
|
||||
"=GEN/\n" +
|
||||
"-----END PGP PRIVATE KEY BLOCK-----";
|
||||
|
||||
public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
|
||||
"Version: PGPainless\n" +
|
||||
"Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" +
|
||||
|
@ -197,4 +225,53 @@ public class GnuPGDummyKeyUtilTest {
|
|||
|
||||
assertArrayEquals(expected.getEncoded(), onCard.getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveAllKeys() throws IOException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
|
||||
PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_REMOVED);
|
||||
|
||||
PGPSecretKeyRing removedSecretKeys = GnuPGDummyKeyUtil.modify(secretKeys)
|
||||
.removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any());
|
||||
|
||||
for (PGPSecretKey key : removedSecretKeys) {
|
||||
assertEquals(key.getS2KUsage(), SecretKeyPacket.USAGE_SHA1);
|
||||
S2K s2k = key.getS2K();
|
||||
assertEquals(GnuPGDummyExtension.NO_PRIVATE_KEY.getId(), s2k.getProtectionMode());
|
||||
}
|
||||
|
||||
assertArrayEquals(expected.getEncoded(), removedSecretKeys.getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSingleIdOfHardwareBackedKey() throws IOException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
|
||||
assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty());
|
||||
|
||||
PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys)
|
||||
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId));
|
||||
|
||||
Set<SubkeyIdentifier> hardwareBackedKeys = GnuPGDummyKeyUtil
|
||||
.getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey);
|
||||
assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGetIdsOfFullyHardwareBackedKey() throws IOException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
|
||||
assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty());
|
||||
|
||||
PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys)
|
||||
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any());
|
||||
Set<SubkeyIdentifier> expected = new HashSet<>();
|
||||
for (PGPSecretKey key : secretKeys) {
|
||||
expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID()));
|
||||
}
|
||||
|
||||
Set<SubkeyIdentifier> hardwareBackedKeys = GnuPGDummyKeyUtil
|
||||
.getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey);
|
||||
|
||||
assertEquals(expected, hardwareBackedKeys);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue