Experimental support for inline-sign, inline-verify

This commit is contained in:
Paul Schaub 2022-06-09 00:44:09 +02:00
parent dd26b5230d
commit 0b69e18715
11 changed files with 222 additions and 87 deletions

View File

@ -43,7 +43,7 @@ public class ArmorTest {
@FailOnSystemExit
public void armorSecretKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
.modernKeyRing("alice@pgpainless.org");
byte[] bytes = secretKey.getEncoded();
System.setIn(new ByteArrayInputStream(bytes));
@ -59,7 +59,7 @@ public class ArmorTest {
@FailOnSystemExit
public void armorPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
.modernKeyRing("alice@pgpainless.org");
PGPPublicKeyRing publicKey = PGPainless.extractCertificate(secretKey);
byte[] bytes = publicKey.getEncoded();

View File

@ -43,7 +43,7 @@ public class DearmorTest {
@FailOnSystemExit
public void dearmorSecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
.modernKeyRing("alice@pgpainless.org");
String armored = PGPainless.asciiArmor(secretKey);
System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)));
@ -59,7 +59,7 @@ public class DearmorTest {
@FailOnSystemExit
public void dearmorCertificate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing()
.modernKeyRing("alice@pgpainless.org", null);
.modernKeyRing("alice@pgpainless.org");
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey);
String armored = PGPainless.asciiArmor(certificate);

View File

@ -115,7 +115,7 @@ public class DetachInbandSignatureAndMessageTest {
// Detach
File tempSigFile = new File(tempDir, "sig.out");
PGPainlessCLI.main(new String[] {"detach-inband-signature-and-message", "--signatures-out=" + tempSigFile.getAbsolutePath()});
PGPainlessCLI.main(new String[] {"inline-detach", "--signatures-out=" + tempSigFile.getAbsolutePath()});
// Test equality with expected values
assertEquals(CLEAR_SIGNED_BODY, msgOut.toString());
@ -150,7 +150,7 @@ public class DetachInbandSignatureAndMessageTest {
// Detach
File tempSigFile = new File(tempDir, "sig.asc");
PGPainlessCLI.main(new String[] {"detach-inband-signature-and-message", "--signatures-out=" + tempSigFile.getAbsolutePath(), "--no-armor"});
PGPainlessCLI.main(new String[] {"inline-detach", "--signatures-out=" + tempSigFile.getAbsolutePath(), "--no-armor"});
// Test equality with expected values
assertEquals(CLEAR_SIGNED_BODY, msgOut.toString());
@ -187,7 +187,7 @@ public class DetachInbandSignatureAndMessageTest {
// Detach
File existingSigFile = new File(tempDir, "sig.existing");
assertTrue(existingSigFile.createNewFile());
PGPainlessCLI.main(new String[] {"detach-inband-signature-and-message", "--signatures-out=" + existingSigFile.getAbsolutePath()});
PGPainlessCLI.main(new String[] {"inline-detach", "--signatures-out=" + existingSigFile.getAbsolutePath()});
}
}

View File

@ -59,7 +59,7 @@ public class SignVerifyTest {
File aliceKeyFile = new File(tempDir, "alice.key");
assertTrue(aliceKeyFile.createNewFile());
PGPSecretKeyRing aliceKeys = PGPainless.generateKeyRing()
.modernKeyRing("alice", null);
.modernKeyRing("alice");
OutputStream aliceKeyOut = new FileOutputStream(aliceKeyFile);
Streams.pipeAll(new ByteArrayInputStream(aliceKeys.getEncoded()), aliceKeyOut);
aliceKeyOut.close();
@ -108,7 +108,7 @@ public class SignVerifyTest {
String[] split = verification.split(" ");
OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(aliceKeys);
OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(new KeyRingInfo(alicePub, new Date()).getSigningSubkeys().get(0));
assertEquals(signingKeyFingerprint.toString(), split[1].trim());
assertEquals(signingKeyFingerprint.toString(), split[1].trim(), verification);
assertEquals(primaryKeyFingerprint.toString(), split[2].trim());
// Test micalg output

View File

@ -134,7 +134,7 @@ public class SignUsingPublicKeyBehaviorTest {
assertTrue(sigFile.createNewFile());
FileOutputStream sigOut = new FileOutputStream(sigFile);
System.setOut(new PrintStream(sigOut));
PGPainlessCLI.execute("sign", "--armor", aliceKeyFile.getAbsolutePath());
PGPainlessCLI.main(new String[] {"sign", "--armor", aliceKeyFile.getAbsolutePath()});
System.setIn(originalIn);
}

View File

@ -10,9 +10,7 @@ import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
@ -39,7 +37,6 @@ import sop.operation.Decrypt;
public class DecryptImpl implements Decrypt {
private final ConsumerOptions consumerOptions = new ConsumerOptions();
private final Set<PGPSecretKeyRing> keys = new HashSet<>();
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
@Override
@ -105,7 +102,8 @@ public class DecryptImpl implements Decrypt {
PGPSecretKeyRingCollection secretKeyCollection = PGPainless.readKeyRing()
.secretKeyRingCollection(keyIn);
for (PGPSecretKeyRing key : secretKeyCollection) {
keys.add(key);
protector.addSecretKey(key);
consumerOptions.addDecryptionKey(key, protector);
}
} catch (IOException | PGPException e) {
throw new SOPGPException.BadData(e);
@ -124,10 +122,6 @@ public class DecryptImpl implements Decrypt {
public ReadyWithResult<DecryptionResult> ciphertext(InputStream ciphertext)
throws SOPGPException.BadData,
SOPGPException.MissingArg {
for (PGPSecretKeyRing key : keys) {
protector.addSecretKey(key);
consumerOptions.addDecryptionKey(key, protector);
}
if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty() && consumerOptions.getSessionKey() == null) {
throw new SOPGPException.MissingArg("Missing decryption key, passphrase or session key.");
@ -145,6 +139,7 @@ public class DecryptImpl implements Decrypt {
} catch (PGPException | IOException e) {
throw new SOPGPException.BadData(e);
} finally {
// Forget passphrases after decryption
protector.clear();
}

View File

@ -8,6 +8,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
@ -24,9 +25,8 @@ 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.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.ArmoredOutputStreamFactory;
import org.pgpainless.util.Passphrase;
import sop.MicAlg;
import sop.ReadyWithResult;
import sop.SigningResult;
@ -39,6 +39,7 @@ public class DetachedSignImpl implements DetachedSign {
private boolean armor = true;
private SignAs mode = SignAs.Binary;
private final SigningOptions signingOptions = new SigningOptions();
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
@Override
public DetachedSign noArmor() {
@ -58,11 +59,8 @@ public class DetachedSignImpl implements DetachedSign {
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
for (PGPSecretKeyRing key : keys) {
KeyRingInfo info = new KeyRingInfo(key);
if (!info.isFullyDecrypted()) {
throw new SOPGPException.KeyIsProtected();
}
signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode));
protector.addSecretKey(key);
signingOptions.addDetachedSignature(protector, key, modeToSigType(mode));
}
} catch (PGPException | KeyException e) {
throw new SOPGPException.BadData(e);
@ -72,7 +70,9 @@ public class DetachedSignImpl implements DetachedSign {
@Override
public DetachedSign withKeyPassword(byte[] password) {
return null;
String string = new String(password, Charset.forName("UTF8"));
protector.addPassphrase(Passphrase.fromPassword(string));
return this;
}
@Override
@ -96,6 +96,9 @@ public class DetachedSignImpl implements DetachedSign {
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));

View File

@ -8,8 +8,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Set;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
@ -25,18 +23,16 @@ import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.exception.WrongPassphraseException;
import org.pgpainless.util.Passphrase;
import sop.util.ProxyOutputStream;
import sop.Ready;
import sop.enums.EncryptAs;
import sop.exception.SOPGPException;
import sop.operation.Encrypt;
import sop.util.ProxyOutputStream;
public class EncryptImpl implements Encrypt {
EncryptionOptions encryptionOptions = new EncryptionOptions();
SigningOptions signingOptions = null;
Set<PGPSecretKeyRing> signingKeys = new HashSet<>();
MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
private EncryptAs encryptAs = EncryptAs.Binary;
@ -55,17 +51,34 @@ public class EncryptImpl implements Encrypt {
}
@Override
public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
public Encrypt signWith(InputStream keyIn)
throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
if (signingOptions == null) {
signingOptions = SigningOptions.get();
}
try {
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
if (keys.size() != 1) {
throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size()));
}
if (signingOptions == null) {
signingOptions = SigningOptions.get();
PGPSecretKeyRing signingKey = keys.iterator().next();
protector.addSecretKey(signingKey);
try {
signingOptions.addInlineSignature(
protector,
signingKey,
(encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)
);
} catch (IllegalArgumentException e) {
throw new SOPGPException.KeyCannotSign();
} catch (WrongPassphraseException e) {
throw new SOPGPException.KeyIsProtected();
} catch (PGPException e) {
throw new SOPGPException.BadData(e);
}
signingKeys.add(keys.getKeyRings().next());
} catch (IOException | PGPException e) {
throw new SOPGPException.BadData(e);
}
@ -100,26 +113,6 @@ public class EncryptImpl implements Encrypt {
@Override
public Ready plaintext(InputStream plaintext) throws IOException {
for (PGPSecretKeyRing signingKey : signingKeys) {
protector.addSecretKey(signingKey);
}
if (signingOptions != null) {
try {
signingOptions.addInlineSignatures(
protector,
signingKeys,
(encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)
);
} catch (IllegalArgumentException e) {
throw new SOPGPException.KeyCannotSign();
} catch (WrongPassphraseException e) {
throw new SOPGPException.KeyIsProtected();
} catch (PGPException e) {
throw new SOPGPException.BadData(e);
}
}
ProducerOptions producerOptions = signingOptions != null ?
ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) :
ProducerOptions.encrypt(encryptionOptions);

View File

@ -4,39 +4,139 @@
package org.pgpainless.sop;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
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.util.Passphrase;
import sop.MicAlg;
import sop.ReadyWithResult;
import sop.SigningResult;
import sop.enums.InlineSignAs;
import sop.exception.SOPGPException;
import sop.operation.DetachedSign;
import sop.operation.InlineSign;
import java.io.IOException;
import java.io.InputStream;
public class InlineSignImpl implements InlineSign {
private boolean armor = true;
private InlineSignAs mode = InlineSignAs.Binary;
private final SigningOptions signingOptions = new SigningOptions();
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
@Override
public DetachedSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption {
return null;
public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption {
this.mode = mode;
return this;
}
@Override
public DetachedSign noArmor() {
return null;
public InlineSign noArmor() {
this.armor = false;
return this;
}
@Override
public InlineSign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
return null;
public InlineSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, 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));
}
}
} catch (PGPException | KeyException e) {
throw new SOPGPException.BadData(e);
}
return this;
}
@Override
public InlineSign withKeyPassword(byte[] password) {
return null;
String string = new String(password, Charset.forName("UTF8"));
protector.addPassphrase(Passphrase.fromPassword(string));
return this;
}
@Override
public ReadyWithResult<SigningResult> data(InputStream data) throws IOException, SOPGPException.ExpectedText {
return null;
ProducerOptions producerOptions = ProducerOptions.sign(signingOptions);
if (mode == InlineSignAs.CleartextSigned) {
producerOptions.setCleartextSigned();
producerOptions.setAsciiArmor(true);
} else {
producerOptions.setAsciiArmor(armor);
}
return new ReadyWithResult<SigningResult>() {
@Override
public SigningResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
try {
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
.onOutputStream(outputStream)
.withOptions(producerOptions);
if (signingStream.isClosed()) {
throw new IllegalStateException("EncryptionStream is already closed.");
}
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);
}
}
};
}
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

@ -4,6 +4,15 @@
package org.pgpainless.sop;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.decryption_verification.ConsumerOptions;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.key.SubkeyIdentifier;
import sop.ReadyWithResult;
import sop.Verification;
import sop.exception.SOPGPException;
@ -11,27 +20,75 @@ import sop.operation.InlineVerify;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class InlineVerifyImpl implements InlineVerify {
@Override
public ReadyWithResult<List<Verification>> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
return null;
}
private final ConsumerOptions options = new ConsumerOptions();
@Override
public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
return null;
options.verifyNotBefore(timestamp);
return this;
}
@Override
public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
return null;
options.verifyNotAfter(timestamp);
return this;
}
@Override
public InlineVerify cert(InputStream cert) throws SOPGPException.BadData {
return null;
PGPPublicKeyRingCollection certificates;
try {
certificates = PGPainless.readKeyRing().publicKeyRingCollection(cert);
} catch (IOException | PGPException e) {
throw new SOPGPException.BadData(e);
}
options.addVerificationCerts(certificates);
return this;
}
@Override
public ReadyWithResult<List<Verification>> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
return new ReadyWithResult<List<Verification>>() {
@Override
public List<Verification> writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
DecryptionStream decryptionStream;
try {
decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(data)
.withOptions(options);
Streams.pipeAll(decryptionStream, outputStream);
decryptionStream.close();
OpenPgpMetadata metadata = decryptionStream.getResult();
List<Verification> verificationList = new ArrayList<>();
for (SubkeyIdentifier verifiedSigningKey : metadata.getVerifiedSignatures().keySet()) {
PGPSignature signature = metadata.getVerifiedSignatures().get(verifiedSigningKey);
verificationList.add(new Verification(
signature.getCreationTime(),
verifiedSigningKey.getSubkeyFingerprint().toString(),
verifiedSigningKey.getPrimaryKeyFingerprint().toString()));
}
if (!options.getCertificates().isEmpty()) {
if (verificationList.isEmpty()) {
throw new SOPGPException.NoSignature();
}
}
return verificationList;
} catch (PGPException e) {
throw new SOPGPException.BadData(e);
}
}
};
}
}

View File

@ -11,17 +11,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.signature.SignatureUtils;
import sop.SOP;
@ -128,13 +124,4 @@ public class SignTest {
assertEquals(SignatureType.CANONICAL_TEXT_DOCUMENT.getCode(), sig.getSignatureType());
}
@Test
public void rejectEncryptedKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing key = PGPainless.generateKeyRing()
.modernKeyRing("Alice", "passphrase");
byte[] bytes = key.getEncoded();
assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.sign().key(bytes));
}
}