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:
parent
5e0ca369bf
commit
1cb49f4b12
21 changed files with 348 additions and 112 deletions
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" +
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue