Adopt changes from SOP-Java and add test for using incapable keys

This commit is contained in:
Paul Schaub 2022-06-10 17:43:51 +02:00
parent 0b69e18715
commit 53df487e59
7 changed files with 137 additions and 41 deletions

View File

@ -99,7 +99,7 @@ public class SignUsingPublicKeyBehaviorTest {
}
@Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
@ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE)
public void testSignatureCreationAndVerification() throws IOException {
originalSout = System.out;
InputStream originalIn = System.in;

View File

@ -21,6 +21,7 @@ import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.bcpg.sig.PrimaryUserID;
import org.bouncycastle.bcpg.sig.RevocationReason;
import org.bouncycastle.openpgp.PGPKeyRing;
@ -1039,6 +1040,32 @@ public class KeyRingInfo {
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) {
if (getPublicKey(keyID) == null) {
throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key.");

View File

@ -12,13 +12,19 @@ import java.io.OutputStream;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.util.io.Streams;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.operation.Dearmor;
public class DearmorImpl implements Dearmor {
@Override
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() {
@Override

View File

@ -25,6 +25,7 @@ import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.exception.KeyException;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.util.ArmoredOutputStreamFactory;
import org.pgpainless.util.Passphrase;
import sop.MicAlg;
@ -54,11 +55,15 @@ public class DetachedSignImpl implements DetachedSign {
}
@Override
public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
try {
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
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);
signingOptions.addDetachedSignature(protector, key, modeToSigType(mode));
}

View File

@ -21,6 +21,7 @@ import org.pgpainless.encryption_signing.EncryptionOptions;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.exception.KeyException;
import org.pgpainless.exception.WrongPassphraseException;
import org.pgpainless.util.Passphrase;
import sop.Ready;
@ -105,6 +106,8 @@ public class EncryptImpl implements Encrypt {
.keyRingCollection(cert, false)
.getPgpPublicKeyRingCollection();
encryptionOptions.addRecipients(certificates);
} catch (KeyException.UnacceptableEncryptionKeyException e) {
throw new SOPGPException.CertCannotEncrypt(e.getMessage(), e);
} catch (IOException | PGPException e) {
throw new SOPGPException.BadData(e);
}

View File

@ -14,20 +14,17 @@ import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.encryption_signing.EncryptionResult;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
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 sop.MicAlg;
import sop.ReadyWithResult;
import sop.SigningResult;
import sop.Ready;
import sop.enums.InlineSignAs;
import sop.exception.SOPGPException;
import sop.operation.InlineSign;
@ -38,6 +35,7 @@ public class InlineSignImpl implements InlineSign {
private InlineSignAs mode = InlineSignAs.Binary;
private final SigningOptions signingOptions = new SigningOptions();
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
private final List<PGPSecretKeyRing> signingKeys = new ArrayList<>();
@Override
public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption {
@ -52,17 +50,17 @@ public class InlineSignImpl implements InlineSign {
}
@Override
public InlineSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
public InlineSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
try {
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
for (PGPSecretKeyRing key : keys) {
protector.addSecretKey(key);
if (mode == InlineSignAs.CleartextSigned) {
signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.BINARY_DOCUMENT);
} else {
signingOptions.addInlineSignature(protector, key, modeToSigType(mode));
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);
signingKeys.add(key);
}
} catch (PGPException | KeyException e) {
throw new SOPGPException.BadData(e);
@ -78,7 +76,20 @@ public class InlineSignImpl implements InlineSign {
}
@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);
if (mode == InlineSignAs.CleartextSigned) {
@ -88,9 +99,9 @@ public class InlineSignImpl implements InlineSign {
producerOptions.setAsciiArmor(armor);
}
return new ReadyWithResult<SigningResult>() {
return new Ready() {
@Override
public SigningResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
public void writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
try {
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
.onOutputStream(outputStream)
@ -102,19 +113,9 @@ public class InlineSignImpl implements InlineSign {
Streams.pipeAll(data, signingStream);
signingStream.close();
EncryptionResult encryptionResult = signingStream.getResult();
// forget passphrases
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) {
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) {
return mode == InlineSignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT
: DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;

View File

@ -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));
}
}