1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-09 18:29:39 +02:00

Update SOP implementation to the latest spec version

See https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03
This commit is contained in:
Paul Schaub 2022-01-07 14:28:36 +01:00
parent 5e0ca369bf
commit 1cb49f4b12
21 changed files with 348 additions and 112 deletions

View file

@ -100,10 +100,6 @@ public class DecryptImpl implements Decrypt {
PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing()
.secretKeyRingCollection(keyIn);
if (secretKeys.size() != 1) {
throw new SOPGPException.BadData(new AssertionError("Exactly one single secret key expected. Got " + secretKeys.size()));
}
for (PGPSecretKeyRing secretKey : secretKeys) {
KeyRingInfo info = new KeyRingInfo(secretKey);
if (!info.isFullyDecrypted()) {

View file

@ -49,7 +49,7 @@ public class EncryptImpl implements Encrypt {
}
@Override
public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.CertCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
try {
PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
if (keys.size() != 1) {
@ -62,7 +62,7 @@ public class EncryptImpl implements Encrypt {
try {
signingOptions.addInlineSignatures(SecretKeyRingProtector.unprotectedKeys(), keys, DocumentSignatureType.BINARY_DOCUMENT);
} catch (IllegalArgumentException e) {
throw new SOPGPException.CertCannotSign();
throw new SOPGPException.KeyCannotSign();
} catch (WrongPassphraseException e) {
throw new SOPGPException.KeyIsProtected();
}

View file

@ -7,16 +7,18 @@ package org.pgpainless.sop;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.pgpainless.PGPainless;
import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.util.ArmorUtils;
import sop.operation.ExtractCert;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.operation.ExtractCert;
public class ExtractCertImpl implements ExtractCert {
@ -30,21 +32,34 @@ public class ExtractCertImpl implements ExtractCert {
@Override
public Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData {
PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(keyInputStream);
if (key == null) {
PGPSecretKeyRingCollection keys;
try {
keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream);
} catch (PGPException e) {
throw new IOException("Cannot read keys.", e);
}
if (keys == null || keys.size() == 0) {
throw new SOPGPException.BadData(new PGPException("No key data found."));
}
PGPPublicKeyRing cert = KeyRingUtils.publicKeyRingFrom(key);
List<PGPPublicKeyRing> certs = new ArrayList<>();
for (PGPSecretKeyRing key : keys) {
PGPPublicKeyRing cert = PGPainless.extractCertificate(key);
certs.add(cert);
}
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream;
cert.encode(out);
if (armor) {
out.close();
for (PGPPublicKeyRing cert : certs) {
OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream;
cert.encode(out);
if (armor) {
out.close();
}
}
}
};

View file

@ -26,7 +26,8 @@ import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.ArmoredOutputStreamFactory;
import sop.Ready;
import sop.MicAlg;
import sop.ReadyWithResult;
import sop.enums.SignAs;
import sop.exception.SOPGPException;
import sop.operation.Sign;
@ -53,16 +54,14 @@ public class SignImpl implements Sign {
public Sign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
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()));
}
PGPSecretKeyRing key = keys.iterator().next();
KeyRingInfo info = new KeyRingInfo(key);
if (!info.isFullyDecrypted()) {
throw new SOPGPException.KeyIsProtected();
for (PGPSecretKeyRing key : keys) {
KeyRingInfo info = new KeyRingInfo(key);
if (!info.isFullyDecrypted()) {
throw new SOPGPException.KeyIsProtected();
}
signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode));
}
signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode));
} catch (PGPException e) {
throw new SOPGPException.BadData(e);
}
@ -70,7 +69,7 @@ public class SignImpl implements Sign {
}
@Override
public Ready data(InputStream data) throws IOException {
public ReadyWithResult<MicAlg> data(InputStream data) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try {
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
@ -78,9 +77,9 @@ public class SignImpl implements Sign {
.withOptions(ProducerOptions.sign(signingOptions)
.setAsciiArmor(armor));
return new Ready() {
return new ReadyWithResult<MicAlg>() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
public MicAlg writeTo(OutputStream outputStream) throws IOException {
if (signingStream.isClosed()) {
throw new IllegalStateException("EncryptionStream is already closed.");
@ -106,6 +105,8 @@ public class SignImpl implements Sign {
}
out.close();
outputStream.close(); // armor out does not close underlying stream
return micAlgFromSignatures(signatures);
}
};
@ -115,6 +116,19 @@ public class SignImpl implements Sign {
}
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(SignAs mode) {
return mode == SignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT
: DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;

View file

@ -8,6 +8,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import sop.operation.Version;
public class VersionImpl implements Version {
@ -33,4 +34,20 @@ public class VersionImpl implements Version {
}
return version;
}
@Override
public String getBackendVersion() {
double bcVersion = new BouncyCastleProvider().getVersion();
return String.format("Bouncycastle %,.2f", bcVersion);
}
@Override
public String getExtendedVersion() {
return getName() + " " + getVersion() + "\n" +
"Based on PGPainless " + getVersion() + "\n" +
"Using " + getBackendVersion() + "\n" +
"See https://pgpainless.org\n" +
"Implementing Stateless OpenPGP Protocol Version 3\n" +
"See https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03";
}
}

View file

@ -51,7 +51,7 @@ public class EncryptDecryptRoundTripTest {
}
@Test
public void basicRoundTripWithKey() throws IOException, SOPGPException.CertCannotSign {
public void basicRoundTripWithKey() throws IOException, SOPGPException.KeyCannotSign {
byte[] encrypted = sop.encrypt()
.signWith(aliceKey)
.withCert(aliceCert)
@ -74,7 +74,7 @@ public class EncryptDecryptRoundTripTest {
}
@Test
public void basicRoundTripWithoutArmorUsingKey() throws IOException, SOPGPException.CertCannotSign {
public void basicRoundTripWithoutArmorUsingKey() throws IOException, SOPGPException.KeyCannotSign {
byte[] aliceKeyNoArmor = sop.generateKey()
.userId("Alice <alice@unarmored.org>")
.noArmor()
@ -189,16 +189,6 @@ public class EncryptDecryptRoundTripTest {
.toByteArrayAndResult());
}
@Test
public void decrypt_withKeyWithMultipleKeysFails() {
byte[] keys = new byte[aliceKey.length + bobKey.length];
System.arraycopy(aliceKey, 0, keys, 0 , aliceKey.length);
System.arraycopy(bobKey, 0, keys, aliceKey.length, bobKey.length);
assertThrows(SOPGPException.BadData.class, () -> sop.decrypt()
.withKey(keys));
}
@Test
public void decrypt_withKeyWithPasswordProtectionFails() {
String passwordProtectedKey = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +

View file

@ -13,13 +13,11 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Date;
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.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@ -56,7 +54,7 @@ public class SignTest {
byte[] signature = sop.sign()
.key(key)
.data(data)
.getBytes();
.toByteArrayAndResult().getBytes();
assertTrue(new String(signature).startsWith("-----BEGIN PGP SIGNATURE-----"));
@ -76,7 +74,7 @@ public class SignTest {
.key(key)
.noArmor()
.data(data)
.getBytes();
.toByteArrayAndResult().getBytes();
assertFalse(new String(signature).startsWith("-----BEGIN PGP SIGNATURE-----"));
@ -95,7 +93,7 @@ public class SignTest {
byte[] signature = sop.sign()
.key(key)
.data(data)
.getBytes();
.toByteArrayAndResult().getBytes();
assertThrows(SOPGPException.NoSignature.class, () -> sop.verify()
.cert(cert)
@ -109,7 +107,7 @@ public class SignTest {
byte[] signature = sop.sign()
.key(key)
.data(data)
.getBytes();
.toByteArrayAndResult().getBytes();
assertThrows(SOPGPException.NoSignature.class, () -> sop.verify()
.cert(cert)
@ -124,22 +122,12 @@ public class SignTest {
.mode(SignAs.Text)
.key(key)
.data(data)
.getBytes();
.toByteArrayAndResult().getBytes();
PGPSignature sig = SignatureUtils.readSignatures(signature).get(0);
assertEquals(SignatureType.CANONICAL_TEXT_DOCUMENT.getCode(), sig.getSignatureType());
}
@Test
public void rejectKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing key1 = PGPainless.generateKeyRing().modernKeyRing("Alice", null);
PGPSecretKeyRing key2 = PGPainless.generateKeyRing().modernKeyRing("Bob", null);
PGPSecretKeyRingCollection collection = new PGPSecretKeyRingCollection(Arrays.asList(key1, key2));
byte[] keys = collection.getEncoded();
assertThrows(SOPGPException.BadData.class, () -> sop.sign().key(keys));
}
@Test
public void rejectEncryptedKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing key = PGPainless.generateKeyRing()

View file

@ -5,19 +5,48 @@
package org.pgpainless.sop;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import sop.SOP;
public class VersionTest {
private static SOP sop;
@BeforeAll
public static void setup() {
sop = new SOPImpl();
}
@Test
public void testGetVersion() {
assertNotNull(new SOPImpl().version().getVersion());
String version = sop.version().getVersion();
assertNotNull(version);
assertFalse(version.isEmpty());
}
@Test
public void assertNameEqualsPGPainless() {
assertEquals("PGPainless-SOP", new SOPImpl().version().getName());
assertEquals("PGPainless-SOP", sop.version().getName());
}
@Test
public void testGetBackendVersion() {
String backendVersion = sop.version().getBackendVersion();
assertNotNull(backendVersion);
assertFalse(backendVersion.isEmpty());
}
@Test
public void testGetExtendedVersion() {
String extendedVersion = sop.version().getExtendedVersion();
assertNotNull(extendedVersion);
assertFalse(extendedVersion.isEmpty());
String firstLine = extendedVersion.split("\n")[0];
assertEquals(sop.version().getName() + " " + sop.version().getVersion(), firstLine);
}
}