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

@ -5,13 +5,16 @@
package org.pgpainless.cli.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -77,6 +80,9 @@ public class SignVerifyTest {
Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), dataOut);
dataOut.close();
// Define micalg output file
File micalgOut = new File(tempDir, "micalg");
// Sign test data
FileInputStream dataIn = new FileInputStream(dataFile);
System.setIn(dataIn);
@ -84,7 +90,7 @@ public class SignVerifyTest {
assertTrue(sigFile.createNewFile());
FileOutputStream sigOut = new FileOutputStream(sigFile);
System.setOut(new PrintStream(sigOut));
PGPainlessCLI.execute("sign", "--armor", aliceKeyFile.getAbsolutePath());
PGPainlessCLI.execute("sign", "--armor", "--micalg-out", micalgOut.getAbsolutePath(), aliceKeyFile.getAbsolutePath());
sigOut.close();
// verify test data signature
@ -105,6 +111,15 @@ public class SignVerifyTest {
assertEquals(signingKeyFingerprint.toString(), split[1].trim());
assertEquals(primaryKeyFingerprint.toString(), split[2].trim());
// Test micalg output
assertTrue(micalgOut.exists());
FileReader fileReader = new FileReader(micalgOut);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line = bufferedReader.readLine();
assertNull(bufferedReader.readLine());
bufferedReader.close();
assertEquals("pgp-sha512", line);
System.setIn(originalIn);
}

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

View File

@ -82,8 +82,8 @@ public class EncryptCmd implements Runnable {
throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo);
} catch (SOPGPException.CertCannotSign certCannotSign) {
throw new RuntimeException("Key from " + keyFile.getAbsolutePath() + " cannot sign.", certCannotSign);
} catch (SOPGPException.KeyCannotSign keyCannotSign) {
throw new SOPGPException.KeyCannotSign("Key from " + keyFile.getAbsolutePath() + " cannot sign.", keyCannotSign);
} catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData);
}

View File

@ -7,12 +7,14 @@ package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine;
import sop.Ready;
import sop.MicAlg;
import sop.ReadyWithResult;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.enums.SignAs;
@ -34,9 +36,13 @@ public class SignCmd implements Runnable {
SignAs type;
@CommandLine.Parameters(description = "Secret keys used for signing",
paramLabel = "KEY")
paramLabel = "KEYS")
List<File> secretKeyFile = new ArrayList<>();
@CommandLine.Option(names = "--micalg-out", description = "Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)",
paramLabel = "MICALG")
File micAlgOut;
@Override
public void run() {
Sign sign = SopCLI.getSop().sign();
@ -51,8 +57,12 @@ public class SignCmd implements Runnable {
}
}
if (micAlgOut != null && micAlgOut.exists()) {
throw new SOPGPException.OutputExists(String.format("Target %s of option %s already exists.", micAlgOut.getAbsolutePath(), "--micalg-out"));
}
if (secretKeyFile.isEmpty()) {
Print.errln("Missing required parameter 'KEY'.");
Print.errln("Missing required parameter 'KEYS'.");
System.exit(19);
}
@ -83,8 +93,16 @@ public class SignCmd implements Runnable {
}
try {
Ready ready = sign.data(System.in);
ready.writeTo(System.out);
ReadyWithResult<MicAlg> ready = sign.data(System.in);
MicAlg micAlg = ready.writeTo(System.out);
if (micAlgOut != null) {
// Write micalg out
micAlgOut.createNewFile();
FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut);
micAlg.writeTo(micAlgOutStream);
micAlgOutStream.close();
}
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);

View File

@ -91,7 +91,7 @@ public class EncryptCmdTest {
}
@Test
public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData {
public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
File keyFile1 = File.createTempFile("sign-with-1-", ".asc");
File keyFile2 = File.createTempFile("sign-with-2-", ".asc");
@ -107,7 +107,7 @@ public class EncryptCmdTest {
@Test
@ExpectSystemExitWithStatus(67)
public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException {
public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
File keyFile = File.createTempFile("sign-with", ".asc");
SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", "starship"});
@ -115,23 +115,23 @@ public class EncryptCmdTest {
@Test
@ExpectSystemExitWithStatus(13)
public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException {
public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
File keyFile = File.createTempFile("sign-with", ".asc");
SopCLI.main(new String[] {"encrypt", "--with-password", "123456", "--sign-with", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(1)
public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.CertCannotSign());
@ExpectSystemExitWithStatus(79)
public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign());
File keyFile = File.createTempFile("sign-with", ".asc");
SopCLI.main(new String[] {"encrypt", "--with-password", "dragon", "--sign-with", keyFile.getAbsolutePath()});
}
@Test
@ExpectSystemExitWithStatus(41)
public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException {
public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
File keyFile = File.createTempFile("sign-with", ".asc");
SopCLI.main(new String[] {"encrypt", "--with-password", "orange", "--sign-with", keyFile.getAbsolutePath()});

View File

@ -19,7 +19,8 @@ import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sop.Ready;
import sop.MicAlg;
import sop.ReadyWithResult;
import sop.SOP;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
@ -33,10 +34,10 @@ public class SignCmdTest {
@BeforeEach
public void mockComponents() throws IOException, SOPGPException.ExpectedText {
sign = mock(Sign.class);
when(sign.data((InputStream) any())).thenReturn(new Ready() {
when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult<MicAlg>() {
@Override
public void writeTo(OutputStream outputStream) {
public MicAlg writeTo(OutputStream outputStream) {
return MicAlg.fromHashAlgorithmId(10);
}
});
@ -109,9 +110,9 @@ public class SignCmdTest {
@Test
@ExpectSystemExitWithStatus(1)
public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText {
when(sign.data((InputStream) any())).thenReturn(new Ready() {
when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult<MicAlg>() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
public MicAlg writeTo(OutputStream outputStream) throws IOException {
throw new IOException();
}
});

View File

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import java.io.OutputStream;
import java.io.PrintWriter;
public class MicAlg {
private final String micAlg;
public MicAlg(String micAlg) {
if (micAlg == null) {
throw new IllegalArgumentException("MicAlg String cannot be null.");
}
this.micAlg = micAlg;
}
public static MicAlg empty() {
return new MicAlg("");
}
public static MicAlg fromHashAlgorithmId(int id) {
switch (id) {
case 1:
return new MicAlg("pgp-md5");
case 2:
return new MicAlg("pgp-sha1");
case 3:
return new MicAlg("pgp-ripemd160");
case 8:
return new MicAlg("pgp-sha256");
case 9:
return new MicAlg("pgp-sha384");
case 10:
return new MicAlg("pgp-sha512");
case 11:
return new MicAlg("pgp-sha224");
default:
throw new IllegalArgumentException("Unsupported hash algorithm ID: " + id);
}
}
public String getMicAlg() {
return micAlg;
}
public void writeTo(OutputStream outputStream) {
PrintWriter pw = new PrintWriter(outputStream);
pw.write(getMicAlg());
pw.close();
}
}

View File

@ -24,6 +24,9 @@ public abstract class SOPGPException extends RuntimeException {
public abstract int getExitCode();
/**
* No acceptable signatures found (sop verify).
*/
public static class NoSignature extends SOPGPException {
public static final int EXIT_CODE = 3;
@ -38,6 +41,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Asymmetric algorithm unsupported (sop encrypt).
*/
public static class UnsupportedAsymmetricAlgo extends SOPGPException {
public static final int EXIT_CODE = 13;
@ -56,6 +62,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Certificate not encryption capable (e,g, expired, revoked, unacceptable usage).
*/
public static class CertCannotEncrypt extends SOPGPException {
public static final int EXIT_CODE = 17;
@ -69,10 +78,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
public static class CertCannotSign extends Exception {
}
/**
* Missing required argument.
*/
public static class MissingArg extends SOPGPException {
public static final int EXIT_CODE = 19;
@ -87,6 +95,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Incomplete verification instructions (sop decrypt).
*/
public static class IncompleteVerification extends SOPGPException {
public static final int EXIT_CODE = 23;
@ -101,6 +112,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Unable to decrypt (sop decrypt).
*/
public static class CannotDecrypt extends SOPGPException {
public static final int EXIT_CODE = 29;
@ -111,6 +125,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Non-UTF-8 or otherwise unreliable password (sop encrypt).
*/
public static class PasswordNotHumanReadable extends SOPGPException {
public static final int EXIT_CODE = 31;
@ -121,6 +138,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Unsupported option.
*/
public static class UnsupportedOption extends SOPGPException {
public static final int EXIT_CODE = 37;
@ -139,6 +159,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Invalid data type (no secret key where KEYS expected, etc.).
*/
public static class BadData extends SOPGPException {
public static final int EXIT_CODE = 41;
@ -157,6 +180,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Non-Text input where text expected.
*/
public static class ExpectedText extends SOPGPException {
public static final int EXIT_CODE = 53;
@ -167,6 +193,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Output file already exists.
*/
public static class OutputExists extends SOPGPException {
public static final int EXIT_CODE = 59;
@ -181,6 +210,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Input file does not exist.
*/
public static class MissingInput extends SOPGPException {
public static final int EXIT_CODE = 61;
@ -195,6 +227,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* A KEYS input is protected (locked) with a password, and sop cannot unlock it.
*/
public static class KeyIsProtected extends SOPGPException {
public static final int EXIT_CODE = 67;
@ -213,6 +248,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* Unsupported subcommand.
*/
public static class UnsupportedSubcommand extends SOPGPException {
public static final int EXIT_CODE = 69;
@ -227,6 +265,9 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* An indirect parameter is a special designator (it starts with @), but sop does not know how to handle the prefix.
*/
public static class UnsupportedSpecialPrefix extends SOPGPException {
public static final int EXIT_CODE = 71;
@ -237,7 +278,10 @@ public abstract class SOPGPException extends RuntimeException {
}
}
/**
* A indirect input parameter is a special designator (it starts with @),
* and a filename matching the designator is actually present.
*/
public static class AmbiguousInput extends SOPGPException {
public static final int EXIT_CODE = 73;
@ -251,4 +295,30 @@ public abstract class SOPGPException extends RuntimeException {
return EXIT_CODE;
}
}
/**
* Key not signature-capable (e.g. expired, revoked, unacceptable usage flags)
* (sop sign and sop encrypt with --sign-with).
*/
public static class KeyCannotSign extends SOPGPException {
public static final int EXIT_CODE = 79;
public KeyCannotSign() {
super();
}
public KeyCannotSign(String message) {
super(message);
}
public KeyCannotSign(String s, KeyCannotSign keyCannotSign) {
super(s, keyCannotSign);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
}

View File

@ -35,9 +35,9 @@ public interface Decrypt {
throws SOPGPException.UnsupportedOption;
/**
* Adds the verification cert.
* Adds one or more verification cert.
*
* @param cert input stream containing the cert
* @param cert input stream containing the cert(s)
* @return builder instance
*/
Decrypt verifyWithCert(InputStream cert)
@ -45,9 +45,9 @@ public interface Decrypt {
IOException;
/**
* Adds the verification cert.
* Adds one or more verification cert.
*
* @param cert byte array containing the cert
* @param cert byte array containing the cert(s)
* @return builder instance
*/
default Decrypt verifyWithCert(byte[] cert)
@ -75,9 +75,9 @@ public interface Decrypt {
SOPGPException.UnsupportedOption;
/**
* Adds the decryption key.
* Adds one or more decryption key.
*
* @param key input stream containing the key
* @param key input stream containing the key(s)
* @return builder instance
*/
Decrypt withKey(InputStream key)
@ -86,9 +86,9 @@ public interface Decrypt {
SOPGPException.UnsupportedAsymmetricAlgo;
/**
* Adds the decryption key.
* Adds one or more decryption key.
*
* @param key byte array containing the key
* @param key byte array containing the key(s)
* @return builder instance
*/
default Decrypt withKey(byte[] key)

View File

@ -38,7 +38,7 @@ public interface Encrypt {
*/
Encrypt signWith(InputStream key)
throws SOPGPException.KeyIsProtected,
SOPGPException.CertCannotSign,
SOPGPException.KeyCannotSign,
SOPGPException.UnsupportedAsymmetricAlgo,
SOPGPException.BadData;
@ -50,7 +50,7 @@ public interface Encrypt {
*/
default Encrypt signWith(byte[] key)
throws SOPGPException.KeyIsProtected,
SOPGPException.CertCannotSign,
SOPGPException.KeyCannotSign,
SOPGPException.UnsupportedAsymmetricAlgo,
SOPGPException.BadData {
return signWith(new ByteArrayInputStream(key));

View File

@ -21,18 +21,18 @@ public interface ExtractCert {
ExtractCert noArmor();
/**
* Extract the cert from the provided key.
* Extract the cert(s) from the provided key(s).
*
* @param keyInputStream input stream containing the encoding of an OpenPGP key
* @return result containing the encoding of the keys cert
* @param keyInputStream input stream containing the encoding of one or more OpenPGP keys
* @return result containing the encoding of the keys certs
*/
Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData;
/**
* Extract the cert from the provided key.
* Extract the cert(s) from the provided key(s).
*
* @param key byte array containing the encoding of an OpenPGP key
* @return result containing the encoding of the keys cert
* @param key byte array containing the encoding of one or more OpenPGP key
* @return result containing the encoding of the keys certs
*/
default Ready key(byte[] key) throws IOException, SOPGPException.BadData {
return key(new ByteArrayInputStream(key));

View File

@ -8,7 +8,8 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import sop.Ready;
import sop.MicAlg;
import sop.ReadyWithResult;
import sop.enums.SignAs;
import sop.exception.SOPGPException;
@ -31,17 +32,17 @@ public interface Sign {
Sign mode(SignAs mode) throws SOPGPException.UnsupportedOption;
/**
* Adds the signer key.
* Add one or more signing keys.
*
* @param key input stream containing encoded key
* @param key input stream containing encoded keys
* @return builder instance
*/
Sign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException;
/**
* Adds the signer key.
* Add one or more signing keys.
*
* @param key byte array containing encoded key
* @param key byte array containing encoded keys
* @return builder instance
*/
default Sign key(byte[] key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
@ -54,7 +55,7 @@ public interface Sign {
* @param data input stream containing data
* @return ready
*/
Ready data(InputStream data) throws IOException, SOPGPException.ExpectedText;
ReadyWithResult<MicAlg> data(InputStream data) throws IOException, SOPGPException.ExpectedText;
/**
* Signs data.
@ -62,7 +63,7 @@ public interface Sign {
* @param data byte array containing data
* @return ready
*/
default Ready data(byte[] data) throws IOException, SOPGPException.ExpectedText {
default ReadyWithResult<MicAlg> data(byte[] data) throws IOException, SOPGPException.ExpectedText {
return data(new ByteArrayInputStream(data));
}
}

View File

@ -29,17 +29,17 @@ public interface Verify extends VerifySignatures {
Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption;
/**
* Adds the verification cert.
* Add one or more verification cert.
*
* @param cert input stream containing the encoded cert
* @param cert input stream containing the encoded certs
* @return builder instance
*/
Verify cert(InputStream cert) throws SOPGPException.BadData;
/**
* Adds the verification cert.
* Add one or more verification cert.
*
* @param cert byte array containing the encoded cert
* @param cert byte array containing the encoded certs
* @return builder instance
*/
default Verify cert(byte[] cert) throws SOPGPException.BadData {

View File

@ -8,15 +8,42 @@ public interface Version {
/**
* Return the implementations name.
* e.g. "SOP",
*
* @return implementation name
*/
String getName();
/**
* Return the implementations version string.
* Return the implementations short version string.
* e.g. "1.0"
*
* @return version string
*/
String getVersion();
/**
* Return version information about the used OpenPGP backend.
* e.g. "Bouncycastle 1.70"
*
* @return backend version string
*/
String getBackendVersion();
/**
* Return an extended version string containing multiple lines of version information.
* The first line MUST match the information produced by {@link #getName()} and {@link #getVersion()}, but the rest of the text
* has no defined structure.
* Example:
* <pre>
* "SOP 1.0
* Awesome PGP!
* Using Bouncycastle 1.70
* LibFoo 1.2.2
* See https://pgp.example.org/sop/ for more information"
* </pre>
*
* @return extended version string
*/
String getExtendedVersion();
}