mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-18 02:12:06 +01:00
Adopt changes from SOP-Java and add test for using incapable keys
This commit is contained in:
parent
0b69e18715
commit
53df487e59
7 changed files with 137 additions and 41 deletions
|
@ -99,7 +99,7 @@ public class SignUsingPublicKeyBehaviorTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
@ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE)
|
||||||
public void testSignatureCreationAndVerification() throws IOException {
|
public void testSignatureCreationAndVerification() throws IOException {
|
||||||
originalSout = System.out;
|
originalSout = System.out;
|
||||||
InputStream originalIn = System.in;
|
InputStream originalIn = System.in;
|
||||||
|
|
|
@ -21,6 +21,7 @@ import java.util.regex.Pattern;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.bouncycastle.bcpg.S2K;
|
||||||
import org.bouncycastle.bcpg.sig.PrimaryUserID;
|
import org.bouncycastle.bcpg.sig.PrimaryUserID;
|
||||||
import org.bouncycastle.bcpg.sig.RevocationReason;
|
import org.bouncycastle.bcpg.sig.RevocationReason;
|
||||||
import org.bouncycastle.openpgp.PGPKeyRing;
|
import org.bouncycastle.openpgp.PGPKeyRing;
|
||||||
|
@ -1039,6 +1040,32 @@ public class KeyRingInfo {
|
||||||
return !getEncryptionSubkeys(purpose).isEmpty();
|
return !getEncryptionSubkeys(purpose).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isUsableForSigning() {
|
||||||
|
List<PGPPublicKey> signingKeys = getSigningSubkeys();
|
||||||
|
for (PGPPublicKey pk : signingKeys) {
|
||||||
|
PGPSecretKey sk = getSecretKey(pk.getKeyID());
|
||||||
|
if (sk == null) {
|
||||||
|
// Missing secret key
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
S2K s2K = sk.getS2K();
|
||||||
|
// Unencrypted key
|
||||||
|
if (s2K == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret key on smart-card
|
||||||
|
int s2kType = s2K.getType();
|
||||||
|
if (s2kType >= 100 && s2kType <= 110) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// protected secret key
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// No usable secret key found
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) {
|
private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) {
|
||||||
if (getPublicKey(keyID) == null) {
|
if (getPublicKey(keyID) == null) {
|
||||||
throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key.");
|
throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key.");
|
||||||
|
|
|
@ -12,13 +12,19 @@ import java.io.OutputStream;
|
||||||
import org.bouncycastle.openpgp.PGPUtil;
|
import org.bouncycastle.openpgp.PGPUtil;
|
||||||
import org.bouncycastle.util.io.Streams;
|
import org.bouncycastle.util.io.Streams;
|
||||||
import sop.Ready;
|
import sop.Ready;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
import sop.operation.Dearmor;
|
import sop.operation.Dearmor;
|
||||||
|
|
||||||
public class DearmorImpl implements Dearmor {
|
public class DearmorImpl implements Dearmor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Ready data(InputStream data) throws IOException {
|
public Ready data(InputStream data) throws IOException {
|
||||||
InputStream decoder = PGPUtil.getDecoderStream(data);
|
InputStream decoder;
|
||||||
|
try {
|
||||||
|
decoder = PGPUtil.getDecoderStream(data);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new SOPGPException.BadData(e);
|
||||||
|
}
|
||||||
return new Ready() {
|
return new Ready() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.pgpainless.encryption_signing.ProducerOptions;
|
||||||
import org.pgpainless.encryption_signing.SigningOptions;
|
import org.pgpainless.encryption_signing.SigningOptions;
|
||||||
import org.pgpainless.exception.KeyException;
|
import org.pgpainless.exception.KeyException;
|
||||||
import org.pgpainless.key.SubkeyIdentifier;
|
import org.pgpainless.key.SubkeyIdentifier;
|
||||||
|
import org.pgpainless.key.info.KeyRingInfo;
|
||||||
import org.pgpainless.util.ArmoredOutputStreamFactory;
|
import org.pgpainless.util.ArmoredOutputStreamFactory;
|
||||||
import org.pgpainless.util.Passphrase;
|
import org.pgpainless.util.Passphrase;
|
||||||
import sop.MicAlg;
|
import sop.MicAlg;
|
||||||
|
@ -54,11 +55,15 @@ public class DetachedSignImpl implements DetachedSign {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
|
public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
||||||
try {
|
try {
|
||||||
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
|
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
|
||||||
|
|
||||||
for (PGPSecretKeyRing key : keys) {
|
for (PGPSecretKeyRing key : keys) {
|
||||||
|
KeyRingInfo info = PGPainless.inspectKeyRing(key);
|
||||||
|
if (!info.isUsableForSigning()) {
|
||||||
|
throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys.");
|
||||||
|
}
|
||||||
protector.addSecretKey(key);
|
protector.addSecretKey(key);
|
||||||
signingOptions.addDetachedSignature(protector, key, modeToSigType(mode));
|
signingOptions.addDetachedSignature(protector, key, modeToSigType(mode));
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.pgpainless.encryption_signing.EncryptionOptions;
|
||||||
import org.pgpainless.encryption_signing.EncryptionStream;
|
import org.pgpainless.encryption_signing.EncryptionStream;
|
||||||
import org.pgpainless.encryption_signing.ProducerOptions;
|
import org.pgpainless.encryption_signing.ProducerOptions;
|
||||||
import org.pgpainless.encryption_signing.SigningOptions;
|
import org.pgpainless.encryption_signing.SigningOptions;
|
||||||
|
import org.pgpainless.exception.KeyException;
|
||||||
import org.pgpainless.exception.WrongPassphraseException;
|
import org.pgpainless.exception.WrongPassphraseException;
|
||||||
import org.pgpainless.util.Passphrase;
|
import org.pgpainless.util.Passphrase;
|
||||||
import sop.Ready;
|
import sop.Ready;
|
||||||
|
@ -105,6 +106,8 @@ public class EncryptImpl implements Encrypt {
|
||||||
.keyRingCollection(cert, false)
|
.keyRingCollection(cert, false)
|
||||||
.getPgpPublicKeyRingCollection();
|
.getPgpPublicKeyRingCollection();
|
||||||
encryptionOptions.addRecipients(certificates);
|
encryptionOptions.addRecipients(certificates);
|
||||||
|
} catch (KeyException.UnacceptableEncryptionKeyException e) {
|
||||||
|
throw new SOPGPException.CertCannotEncrypt(e.getMessage(), e);
|
||||||
} catch (IOException | PGPException e) {
|
} catch (IOException | PGPException e) {
|
||||||
throw new SOPGPException.BadData(e);
|
throw new SOPGPException.BadData(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,20 +14,17 @@ import java.util.List;
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
||||||
import org.bouncycastle.openpgp.PGPSignature;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
import org.bouncycastle.util.io.Streams;
|
||||||
import org.pgpainless.PGPainless;
|
import org.pgpainless.PGPainless;
|
||||||
import org.pgpainless.algorithm.DocumentSignatureType;
|
import org.pgpainless.algorithm.DocumentSignatureType;
|
||||||
import org.pgpainless.encryption_signing.EncryptionResult;
|
|
||||||
import org.pgpainless.encryption_signing.EncryptionStream;
|
import org.pgpainless.encryption_signing.EncryptionStream;
|
||||||
import org.pgpainless.encryption_signing.ProducerOptions;
|
import org.pgpainless.encryption_signing.ProducerOptions;
|
||||||
import org.pgpainless.encryption_signing.SigningOptions;
|
import org.pgpainless.encryption_signing.SigningOptions;
|
||||||
import org.pgpainless.exception.KeyException;
|
import org.pgpainless.exception.KeyException;
|
||||||
import org.pgpainless.key.SubkeyIdentifier;
|
import org.pgpainless.key.OpenPgpFingerprint;
|
||||||
|
import org.pgpainless.key.info.KeyRingInfo;
|
||||||
import org.pgpainless.util.Passphrase;
|
import org.pgpainless.util.Passphrase;
|
||||||
import sop.MicAlg;
|
import sop.Ready;
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.SigningResult;
|
|
||||||
import sop.enums.InlineSignAs;
|
import sop.enums.InlineSignAs;
|
||||||
import sop.exception.SOPGPException;
|
import sop.exception.SOPGPException;
|
||||||
import sop.operation.InlineSign;
|
import sop.operation.InlineSign;
|
||||||
|
@ -38,6 +35,7 @@ public class InlineSignImpl implements InlineSign {
|
||||||
private InlineSignAs mode = InlineSignAs.Binary;
|
private InlineSignAs mode = InlineSignAs.Binary;
|
||||||
private final SigningOptions signingOptions = new SigningOptions();
|
private final SigningOptions signingOptions = new SigningOptions();
|
||||||
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
|
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
|
||||||
|
private final List<PGPSecretKeyRing> signingKeys = new ArrayList<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption {
|
public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption {
|
||||||
|
@ -52,17 +50,17 @@ public class InlineSignImpl implements InlineSign {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InlineSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
|
public InlineSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
||||||
try {
|
try {
|
||||||
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
|
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
|
||||||
|
|
||||||
for (PGPSecretKeyRing key : keys) {
|
for (PGPSecretKeyRing key : keys) {
|
||||||
protector.addSecretKey(key);
|
KeyRingInfo info = PGPainless.inspectKeyRing(key);
|
||||||
if (mode == InlineSignAs.CleartextSigned) {
|
if (!info.isUsableForSigning()) {
|
||||||
signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.BINARY_DOCUMENT);
|
throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys.");
|
||||||
} else {
|
|
||||||
signingOptions.addInlineSignature(protector, key, modeToSigType(mode));
|
|
||||||
}
|
}
|
||||||
|
protector.addSecretKey(key);
|
||||||
|
signingKeys.add(key);
|
||||||
}
|
}
|
||||||
} catch (PGPException | KeyException e) {
|
} catch (PGPException | KeyException e) {
|
||||||
throw new SOPGPException.BadData(e);
|
throw new SOPGPException.BadData(e);
|
||||||
|
@ -78,7 +76,20 @@ public class InlineSignImpl implements InlineSign {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReadyWithResult<SigningResult> data(InputStream data) throws IOException, SOPGPException.ExpectedText {
|
public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, IOException, SOPGPException.ExpectedText {
|
||||||
|
for (PGPSecretKeyRing key : signingKeys) {
|
||||||
|
try {
|
||||||
|
if (mode == InlineSignAs.CleartextSigned) {
|
||||||
|
signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.BINARY_DOCUMENT);
|
||||||
|
} else {
|
||||||
|
signingOptions.addInlineSignature(protector, key, modeToSigType(mode));
|
||||||
|
}
|
||||||
|
} catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) {
|
||||||
|
throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e);
|
||||||
|
} catch (PGPException e) {
|
||||||
|
throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ProducerOptions producerOptions = ProducerOptions.sign(signingOptions);
|
ProducerOptions producerOptions = ProducerOptions.sign(signingOptions);
|
||||||
if (mode == InlineSignAs.CleartextSigned) {
|
if (mode == InlineSignAs.CleartextSigned) {
|
||||||
|
@ -88,9 +99,9 @@ public class InlineSignImpl implements InlineSign {
|
||||||
producerOptions.setAsciiArmor(armor);
|
producerOptions.setAsciiArmor(armor);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ReadyWithResult<SigningResult>() {
|
return new Ready() {
|
||||||
@Override
|
@Override
|
||||||
public SigningResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
|
public void writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
|
||||||
try {
|
try {
|
||||||
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
|
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
|
||||||
.onOutputStream(outputStream)
|
.onOutputStream(outputStream)
|
||||||
|
@ -102,19 +113,9 @@ public class InlineSignImpl implements InlineSign {
|
||||||
|
|
||||||
Streams.pipeAll(data, signingStream);
|
Streams.pipeAll(data, signingStream);
|
||||||
signingStream.close();
|
signingStream.close();
|
||||||
EncryptionResult encryptionResult = signingStream.getResult();
|
|
||||||
|
|
||||||
// forget passphrases
|
// forget passphrases
|
||||||
protector.clear();
|
protector.clear();
|
||||||
|
|
||||||
List<PGPSignature> signatures = new ArrayList<>();
|
|
||||||
for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) {
|
|
||||||
signatures.addAll(encryptionResult.getDetachedSignatures().get(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
return SigningResult.builder()
|
|
||||||
.setMicAlg(micAlgFromSignatures(signatures))
|
|
||||||
.build();
|
|
||||||
} catch (PGPException e) {
|
} catch (PGPException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
@ -122,19 +123,6 @@ public class InlineSignImpl implements InlineSign {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private MicAlg micAlgFromSignatures(Iterable<PGPSignature> signatures) {
|
|
||||||
int algorithmId = 0;
|
|
||||||
for (PGPSignature signature : signatures) {
|
|
||||||
int sigAlg = signature.getHashAlgorithm();
|
|
||||||
if (algorithmId == 0 || algorithmId == sigAlg) {
|
|
||||||
algorithmId = sigAlg;
|
|
||||||
} else {
|
|
||||||
return MicAlg.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return algorithmId == 0 ? MicAlg.empty() : MicAlg.fromHashAlgorithmId(algorithmId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DocumentSignatureType modeToSigType(InlineSignAs mode) {
|
private static DocumentSignatureType modeToSigType(InlineSignAs mode) {
|
||||||
return mode == InlineSignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT
|
return mode == InlineSignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT
|
||||||
: DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;
|
: DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop;
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.pgpainless.PGPainless;
|
||||||
|
import org.pgpainless.algorithm.KeyFlag;
|
||||||
|
import org.pgpainless.key.generation.KeySpec;
|
||||||
|
import org.pgpainless.key.generation.type.KeyType;
|
||||||
|
import org.pgpainless.key.generation.type.ecc.EllipticCurve;
|
||||||
|
import org.pgpainless.key.generation.type.eddsa.EdDSACurve;
|
||||||
|
import org.pgpainless.util.ArmorUtils;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
public class IncapableKeysTest {
|
||||||
|
|
||||||
|
private static byte[] nonSigningKey;
|
||||||
|
private static byte[] nonEncryptionKey;
|
||||||
|
private static byte[] nonSigningCert;
|
||||||
|
private static byte[] nonEncryptionCert;
|
||||||
|
|
||||||
|
private static final SOP sop = new SOPImpl();
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void generateKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
|
||||||
|
PGPSecretKeyRing key = PGPainless.buildKeyRing()
|
||||||
|
.addSubkey(KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._P256), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
|
||||||
|
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER))
|
||||||
|
.addUserId("Non Signing <non@signing.key>")
|
||||||
|
.build();
|
||||||
|
nonSigningKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8);
|
||||||
|
nonSigningCert = sop.extractCert().key(nonSigningKey).getBytes();
|
||||||
|
|
||||||
|
key = PGPainless.buildKeyRing()
|
||||||
|
.addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA))
|
||||||
|
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER))
|
||||||
|
.addUserId("Non Encryption <non@encryption.key>")
|
||||||
|
.build();
|
||||||
|
nonEncryptionKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8);
|
||||||
|
nonEncryptionCert = sop.extractCert().key(nonEncryptionKey).getBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encryptionToNonEncryptionKeyFails() {
|
||||||
|
assertThrows(SOPGPException.CertCannotEncrypt.class, () -> sop.encrypt().withCert(nonEncryptionCert));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void signingWithNonSigningKeyFails() {
|
||||||
|
assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.sign().key(nonSigningKey));
|
||||||
|
assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.detachedSign().key(nonSigningKey));
|
||||||
|
assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.inlineSign().key(nonSigningKey));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue