Compare commits

...

27 Commits

Author SHA1 Message Date
Paul Schaub 7ab65f63a4
SOP-Java 6.1.1-SNAPSHOT 2023-04-27 14:49:13 +02:00
Paul Schaub b8ad6d77a2
SOP-Java 6.1.0 2023-04-27 14:46:52 +02:00
Paul Schaub ab8f44138d
Add missing @throws javadoc 2023-04-27 14:45:42 +02:00
Paul Schaub 419056ba4c
Fix checkstyle issues 2023-04-27 14:32:13 +02:00
Paul Schaub 312cdb69c9
Update changelog 2023-04-27 14:28:07 +02:00
Paul Schaub c479cc8ef3
Profile: Use Optional for description 2023-04-27 14:25:16 +02:00
Paul Schaub aa88904711
Add tests for Verification parsing 2023-04-27 14:24:59 +02:00
Paul Schaub 7ea46a1916
Move tests 2023-04-27 14:23:58 +02:00
Paul Schaub 49fd7143cf
Update CHANGELOG 2023-04-27 13:26:01 +02:00
Paul Schaub 8aded17f10
Use UTF8Util.UTF8 constant 2023-04-27 13:17:58 +02:00
Paul Schaub 8eba099146
UTF8Util.decodeUTF8(): throw CharacterCodingException instead of PasswordNotHumanReadable 2023-04-27 13:15:44 +02:00
Paul Schaub 0308732328
Make UTCUtil.parseUTCDate() throw instead of returning null for malformed inputs 2023-04-27 13:07:08 +02:00
Paul Schaub 8b8863c6df
Verification: Annotate with @Nonnull, @Nullable 2023-04-27 12:52:12 +02:00
Paul Schaub 44e6dd2180
Depend on findbugs:jsr305 for @Nullable etc. annotations 2023-04-27 12:44:40 +02:00
Paul Schaub 19d6b7e142
Verification: Make use of Optional for signature mode and description 2023-04-27 12:44:01 +02:00
Paul Schaub 226b5d99a0
Fix issues with i18n properties 2023-04-26 16:47:53 +02:00
Paul Schaub e336e536a8
Javadoc: Insert <p> tags to preserve newlines 2023-04-26 16:44:49 +02:00
Paul Schaub ed59c713eb
Remove unused 'throws IOException' declarations 2023-04-26 16:28:04 +02:00
Paul Schaub 0aabfac695
DetachedVerifyExternal: Make certs set final 2023-04-26 16:23:18 +02:00
Paul Schaub 790d80ec29
DateParsingTest: make armor command final 2023-04-26 16:22:53 +02:00
Paul Schaub 0fccf3051c
Refactor AbstractSOPTest 2023-04-26 16:21:37 +02:00
Paul Schaub a722e98578
Update changelog 2023-04-26 16:12:32 +02:00
Paul Schaub aeda534f37
Add DSL for testing verification results 2023-04-26 15:53:50 +02:00
Paul Schaub bb2b4e03fb
Add comment to remind future self of adding new exception types to switch-case 2023-04-18 18:15:11 +02:00
Paul Schaub 4a7c2b74da
Add test for listing encrypt profiles, use new shortcut methods 2023-04-18 18:05:10 +02:00
Paul Schaub 78ecf2f554
ListProfiles: Add shortcut methods generateKey() and encrypt() 2023-04-18 18:04:23 +02:00
Paul Schaub d8cac7b9d7
external-sop: Fix error code mapping of new exceptions 2023-04-18 18:03:18 +02:00
57 changed files with 894 additions and 326 deletions

View File

@ -6,6 +6,28 @@ SPDX-License-Identifier: Apache-2.0
# Changelog
## 6.1.0
- `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()`
- Add DSL for testing `Verification` results
- `Verification`
- Return `Optional<SignatureMode>` for `getSignatureMode()`
- Return `Optional<String>` for `getDescription()`
- `Profile`
- Add support for profiles without description
- Return `Optional<String>` for `getDescription()`
- Add `parse(String)` method for parsing profile lines
- `sop-java`: Add dependency on `com.google.code.findbugs:jsr305` for `@Nullable`, `@Nonnull` annotations
- `UTCUtil`: `parseUTCDate()` is now `@Nonnull` and throws a `ParseException` for invalid inputs
- `UTF8Util`: `decodeUTF8()` now throws `CharacterCodingException` instead of `SOPGPException.PasswordNotHumanReadable`
- `external-sop`: Properly map error codes to new exception types (ported from `5.0.1`):
- `UNSUPPORTED_PROFILE`
- `INCOMPATIBLE_OPTIONS`
## 5.0.1
- `external-sop`: Properly map error codes to new exception types:
- `UNSUPPORTED_PROFILE`
- `INCOMPATIBLE_OPTIONS`
## 6.0.0
- Update implementation to [SOP Specification revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html).
- Add option `--profile=XYZ` to `encrypt` subcommand

View File

@ -267,7 +267,16 @@ public class ExternalSOP implements SOP {
throw new SOPGPException.KeyCannotSign("External SOP backend reported error KeyCannotSign (" +
exitCode + "):\n" + errorMessage);
case SOPGPException.IncompatibleOptions.EXIT_CODE:
throw new SOPGPException.IncompatibleOptions("External SOP backend reported error IncompatibleOptions (" +
exitCode + "):\n" + errorMessage);
case SOPGPException.UnsupportedProfile.EXIT_CODE:
throw new SOPGPException.UnsupportedProfile("External SOP backend reported error UnsupportedProfile (" +
exitCode + "):\n" + errorMessage);
default:
// Did you forget to add a case for a new exception type?
throw new RuntimeException("External SOP backend reported unknown exit code (" +
exitCode + "):\n" + errorMessage);
}

View File

@ -10,11 +10,10 @@ import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.Armor;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Properties;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link Armor} operation using an external SOP binary.
@ -37,7 +36,7 @@ public class ArmorExternal implements Armor {
}
@Override
public Ready data(InputStream data) throws SOPGPException.BadData, IOException {
public Ready data(InputStream data) throws SOPGPException.BadData {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data);
}
}

View File

@ -9,7 +9,6 @@ import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.Dearmor;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@ -30,7 +29,7 @@ public class DearmorExternal implements Dearmor {
}
@Override
public Ready data(InputStream data) throws SOPGPException.BadData, IOException {
public Ready data(InputStream data) throws SOPGPException.BadData {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data);
}
}

View File

@ -31,7 +31,7 @@ public class DetachedVerifyExternal implements DetachedVerify {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private Set<InputStream> certs = new HashSet<>();
private final Set<InputStream> certs = new HashSet<>();
private InputStream signatures;
private int certCounter = 0;
@ -54,13 +54,13 @@ public class DetachedVerifyExternal implements DetachedVerify {
}
@Override
public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData, IOException {
public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData {
this.certs.add(cert);
return this;
}
@Override
public VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData, IOException {
public VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData {
this.signatures = signatures;
return this;
}

View File

@ -93,7 +93,7 @@ public class EncryptExternal implements Encrypt {
@Override
public Ready plaintext(InputStream plaintext)
throws IOException, SOPGPException.KeyIsProtected {
throws SOPGPException.KeyIsProtected {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, plaintext);
}
}

View File

@ -62,7 +62,7 @@ public class InlineSignExternal implements InlineSign {
}
@Override
public Ready data(InputStream data) throws IOException, SOPGPException.KeyIsProtected, SOPGPException.ExpectedText {
public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data);
}
}

View File

@ -38,8 +38,10 @@ public class ListProfilesExternal implements ListProfiles {
private static List<Profile> toProfiles(String output) {
List<Profile> profiles = new ArrayList<>();
for (String line : output.split("\n")) {
String[] split = line.split(": ");
profiles.add(new Profile(split[0], split[1]));
if (line.trim().isEmpty()) {
continue;
}
profiles.add(Profile.parse(line));
}
return profiles;
}

View File

@ -18,6 +18,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.ParseException;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
@ -246,21 +247,36 @@ public abstract class AbstractSopCmd implements Runnable {
}
public Date parseNotAfter(String notAfter) {
Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter);
if (date == null) {
if (notAfter.equals("now")) {
return new Date();
}
if (notAfter.equals("-")) {
return END_OF_TIME;
}
try {
return UTCUtil.parseUTCDate(notAfter);
} catch (ParseException e) {
String errorMsg = getMsg("sop.error.input.malformed_not_after");
throw new IllegalArgumentException(errorMsg);
}
return date;
}
public Date parseNotBefore(String notBefore) {
Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore);
if (date == null) {
if (notBefore.equals("now")) {
return new Date();
}
if (notBefore.equals("-")) {
return BEGINNING_OF_TIME;
}
try {
return UTCUtil.parseUTCDate(notBefore);
} catch (ParseException e) {
String errorMsg = getMsg("sop.error.input.malformed_not_before");
throw new IllegalArgumentException(errorMsg);
}
return date;
}
}

View File

@ -5,10 +5,10 @@ usage.header=Decrypt a message from standard input
session-key-out=Can be used to learn the session key on successful decryption
with-session-key.0=Symmetric message key (session key).
with-session-key.1=Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet.
with-session-key.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...)
with-session-key.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
with-password.0=Symmetric passphrase to decrypt the message with.
with-password.1=Enables decryption based on any "SKESK" packets in the "CIPHERTEXT".
with-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...)
with-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
verify-out=Emits signature verification status to the designated output
verify-with=Certificates for signature verification
not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)

View File

@ -5,7 +5,7 @@ usage.header=Entschl
session-key-out=Extrahiere den Nachrichtenschlüssel nach erfolgreicher Entschlüsselung
with-session-key.0=Symmetrischer Nachrichtenschlüssel (Sitzungsschlüssel).
with-session-key.1=Ermöglicht direkte Entschlüsselung des im "CIPHERTEXT" enhaltenen "SEIPD" Paketes mithilfe des Nachrichtenschlüssels.
with_session-key.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
with-session-key.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
with-password.0=Symmetrisches Passwort zur Entschlüsselung der Nachricht.
with-password.1=Ermöglicht Entschlüsselung basierend auf im "CIPHERTEXT" enthaltenen "SKESK" Paketen.
with-password.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).

View File

@ -3,12 +3,12 @@
# SPDX-License-Identifier: Apache-2.0
usage.header=Create a detached signature on the data from standard input
no-armor=ASCII armor the output
as.0=Specify the output format of the signed message
as.0=Specify the output format of the signed message.
as.1=Defaults to 'binary'.
as.2=If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.
with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
micalg-out=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)
micalg-out=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).
KEYS[0..*]=Secret keys used for signing
# Generic TODO: Remove when bumping picocli to 4.7.0

View File

@ -5,10 +5,10 @@ usage.header=Verify a detached signature over the data from standard input
not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)
not-before.1=Reject signatures with a creation date not in range.
not-before.2=Defaults to beginning of time ("-").
not-after.1=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)
not-after.2=Reject signatures with a creation date not in range.
not-after.3=Defaults to current system time ("now").\
not-after.4 = Accepts special value "-" for end of time.
not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)
not-after.1=Reject signatures with a creation date not in range.
not-after.2=Defaults to current system time ("now").
not-after.3=Accepts special value "-" for end of time.
SIGNATURE[0]=Detached signature
CERT[0..*]=Public key certificates for signature verification

View File

@ -5,7 +5,7 @@ usage.header=Encrypt a message from standard input
no-armor=ASCII armor the output
as=Type of the input data. Defaults to 'binary'
with-password.0=Encrypt the message with a password.
with-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...)
with-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
sign-with=Sign the output with a private key
profile=Profile identifier to switch between profiles
with-key-password.0=Passphrase to unlock the secret key(s).

View File

@ -3,14 +3,14 @@
# SPDX-License-Identifier: Apache-2.0
usage.header=Create an inline-signed message from data on standard input
no-armor=ASCII armor the output
as.0=Specify the signature format of the signed message
as.0=Specify the signature format of the signed message.
as.1='text' and 'binary' will produce inline-signed messages.
as.2='cleartextsigned' will make use of the cleartext signature framework.
as.2='clearsigned' will make use of the cleartext signature framework.
as.3=Defaults to 'binary'.
as.4=If '--as=text' and the input data is not valid UTF-8, inline-sign fails with return code 53.
with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
micalg=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)
micalg=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).
KEYS[0..*]=Secret keys used for signing
# Generic TODO: Remove when bumping picocli to 4.7.0

View File

@ -5,7 +5,7 @@ usage.header=Signiere eine Nachricht von Standard-Eingabe mit eingebetteten Sign
no-armor=Schütze Ausgabe mit ASCII Armor
as.0=Bestimme Signaturformat der Nachricht.
as.1='text' und 'binary' resultieren in eingebettete Signaturen.
as.2='cleartextsigned' wird die Nachricht Klartext-signieren.
as.2='clearsigned' wird die Nachricht Klartext-signieren.
as.3=Standardmäßig: 'binary'.
as.4=Ist die Standard-Eingabe nicht UTF-8 kodiert und '--as=text' gesetzt, so wird inline-sign Fehlercode 53 zurückgeben.
with-key-password.0=Passwort zum Entsperren des privaten Schlüssels

View File

@ -47,7 +47,7 @@ sop.error.input.malformed_not_after=Invalid date string supplied as value of '--
sop.error.input.malformed_not_before=Invalid date string supplied as value of '--not-before'.
sop.error.input.stdin_not_a_message=Standard Input appears not to contain a valid OpenPGP message.
sop.error.input.stdin_not_a_private_key=Standard Input appears not to contain a valid OpenPGP secret key.
sop.error.input.stdin_not_openpgp_data=Standard Input appears not to contain valid OpenPGP data
sop.error.input.stdin_not_openpgp_data=Standard Input appears not to contain valid OpenPGP data.
## Indirect Data Types
sop.error.indirect_data_type.ambiguous_filename=File name '%s' is ambiguous. File with the same name exists on the filesystem.
sop.error.indirect_data_type.environment_variable_not_set=Environment variable '%s' not set.

View File

@ -4,7 +4,7 @@
usage.header=Zeige Versionsinformationen über das Programm
extended=Gebe erweiterte Versionsinformationen aus
backend=Gebe Informationen über das kryptografische Backend aus
sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird.
sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020

View File

@ -14,7 +14,7 @@ import sop.cli.picocli.commands.ArmorCmd;
import sop.util.UTCUtil;
public class DateParsingTest {
private AbstractSopCmd cmd = new ArmorCmd(); // we use ArmorCmd as a concrete implementation.
private final AbstractSopCmd cmd = new ArmorCmd(); // we use ArmorCmd as a concrete implementation.
@Test
public void parseNotAfterDashReturnsEndOfTime() {

View File

@ -31,6 +31,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
@ -187,7 +188,7 @@ public class DecryptCmdTest {
}
@Test
public void assertSessionKeyAndVerificationsIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException {
public void assertSessionKeyAndVerificationsIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException, ParseException {
Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z");
String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209";
String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B";
@ -281,7 +282,7 @@ public class DecryptCmdTest {
}
@Test
public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData {
public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, ParseException {
File certFile = File.createTempFile("verify-out-cert", ".asc");
File verifyOut = new File(certFile.getParent(), "verify-out.txt");
if (verifyOut.exists()) {

View File

@ -16,6 +16,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@ -41,7 +42,7 @@ public class VerifyCmdTest {
PrintStream originalSout;
@BeforeEach
public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException {
public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException, ParseException {
originalSout = System.out;
detachedVerify = mock(DetachedVerify.class);
@ -73,7 +74,7 @@ public class VerifyCmdTest {
}
@Test
public void notAfter_passedDown() throws SOPGPException.UnsupportedOption {
public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(detachedVerify, times(1)).notAfter(date);
@ -100,7 +101,7 @@ public class VerifyCmdTest {
}
@Test
public void notBefore_passedDown() throws SOPGPException.UnsupportedOption {
public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(detachedVerify, times(1)).notBefore(date);
@ -178,7 +179,7 @@ public class VerifyCmdTest {
}
@Test
public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData, ParseException {
when(detachedVerify.data((InputStream) any())).thenReturn(Arrays.asList(
new Verification(UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"),
"EB85BB5FA33A75E15E944E63F231550C4F47E38E",

View File

@ -19,6 +19,10 @@ dependencies {
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
testFixturesImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testFixturesImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
// @Nullable, @Nonnull annotations
implementation "com.google.code.findbugs:jsr305:3.0.2"
}
test {

View File

@ -4,7 +4,11 @@
package sop;
import java.nio.charset.Charset;
import sop.util.Optional;
import sop.util.UTF8Util;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Tuple class bundling a profile name and description.
@ -15,7 +19,7 @@ import java.nio.charset.Charset;
public class Profile {
private final String name;
private final String description;
private final Optional<String> description;
/**
* Create a new {@link Profile} object.
@ -24,15 +28,62 @@ public class Profile {
* @param name profile name
* @param description profile description
*/
public Profile(String name, String description) {
public Profile(@Nonnull String name, @Nullable String description) {
if (name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty.");
}
if (name.contains(":")) {
throw new IllegalArgumentException("Name cannot contain ':'.");
}
if (name.contains(" ") || name.contains("\n") || name.contains("\t") || name.contains("\r")) {
throw new IllegalArgumentException("Name cannot contain whitespace characters.");
}
this.name = name;
this.description = description;
if (description == null) {
this.description = Optional.ofEmpty();
} else {
String trimmedDescription = description.trim();
if (trimmedDescription.isEmpty()) {
this.description = Optional.ofEmpty();
} else {
this.description = Optional.of(trimmedDescription);
}
}
if (exceeds1000CharLineLimit(this)) {
throw new IllegalArgumentException("The line representation of a profile MUST NOT exceed 1000 bytes.");
}
}
public Profile(String name) {
this(name, null);
}
/**
* Parse a {@link Profile} from its string representation.
*
* @param string string representation
* @return profile
*/
public static Profile parse(String string) {
if (string.contains(": ")) {
// description after colon, e.g. "default: Use implementers recommendations."
String name = string.substring(0, string.indexOf(": "));
String description = string.substring(string.indexOf(": ") + 2);
return new Profile(name, description.trim());
}
if (string.endsWith(":")) {
// empty description, e.g. "default:"
return new Profile(string.substring(0, string.length() - 1));
}
// no description
return new Profile(string.trim());
}
/**
* Return the name (also known as identifier) of the profile.
* A profile name is a UTF-8 string that has no whitespace in it.
@ -48,6 +99,7 @@ public class Profile {
*
* @return name
*/
@Nonnull
public String getName() {
return name;
}
@ -57,17 +109,26 @@ public class Profile {
*
* @return description
*/
public String getDescription() {
@Nonnull
public Optional<String> getDescription() {
return description;
}
public boolean hasDescription() {
return description.isPresent();
}
/**
* Convert the profile into a String for displaying.
*
* @return string
*/
@Override
public String toString() {
return getName() + ": " + getDescription();
if (getDescription().isEmpty()) {
return getName();
}
return getName() + ": " + getDescription().get();
}
/**
@ -77,6 +138,6 @@ public class Profile {
*/
private static boolean exceeds1000CharLineLimit(Profile profile) {
String line = profile.toString();
return line.getBytes(Charset.forName("UTF8")).length > 1000;
return line.getBytes(UTF8Util.UTF8).length > 1000;
}
}

View File

@ -54,7 +54,7 @@ public interface SOP {
/**
* Create detached signatures.
* Customize the operation using the builder {@link DetachedSign}.
*
* <p>
* If you want to sign a message inline, use {@link #inlineSign()} instead.
*
* @return builder instance
@ -66,7 +66,7 @@ public interface SOP {
/**
* Create detached signatures.
* Customize the operation using the builder {@link DetachedSign}.
*
* <p>
* If you want to sign a message inline, use {@link #inlineSign()} instead.
*
* @return builder instance
@ -75,7 +75,7 @@ public interface SOP {
/**
* Sign a message using inline signatures.
*
* <p>
* If you need to create detached signatures, use {@link #detachedSign()} instead.
*
* @return builder instance
@ -85,7 +85,7 @@ public interface SOP {
/**
* Verify detached signatures.
* Customize the operation using the builder {@link DetachedVerify}.
*
* <p>
* If you need to verify an inline-signed message, use {@link #inlineVerify()} instead.
*
* @return builder instance
@ -97,7 +97,7 @@ public interface SOP {
/**
* Verify detached signatures.
* Customize the operation using the builder {@link DetachedVerify}.
*
* <p>
* If you need to verify an inline-signed message, use {@link #inlineVerify()} instead.
*
* @return builder instance
@ -106,7 +106,7 @@ public interface SOP {
/**
* Verify signatures of an inline-signed message.
*
* <p>
* If you need to verify detached signatures over a message, use {@link #detachedVerify()} instead.
*
* @return builder instance

View File

@ -19,7 +19,7 @@ public final class SigningResult {
* Return a string identifying the digest mechanism used to create the signed message.
* This is useful for setting the micalg= parameter for the multipart/signed
* content type of a PGP/MIME object as described in section 5 of [RFC3156].
*
* <p>
* If more than one signature was generated and different digest mechanisms were used,
* the value of the micalg object is an empty string.
*

View File

@ -4,11 +4,15 @@
package sop;
import java.util.Date;
import sop.enums.SignatureMode;
import sop.util.Optional;
import sop.util.UTCUtil;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.text.ParseException;
import java.util.Date;
/**
* Class bundling information about a verified signature.
*/
@ -17,8 +21,8 @@ public class Verification {
private final Date creationTime;
private final String signingKeyFingerprint;
private final String signingCertFingerprint;
private final SignatureMode signatureMode;
private final String description;
private final Optional<SignatureMode> signatureMode;
private final Optional<String> description;
private static final String MODE = "mode:";
@ -29,8 +33,10 @@ public class Verification {
* @param signingKeyFingerprint fingerprint of the signing (sub-) key
* @param signingCertFingerprint fingerprint of the certificate
*/
public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint) {
this(creationTime, signingKeyFingerprint, signingCertFingerprint, null, null);
public Verification(@Nonnull Date creationTime,
@Nonnull String signingKeyFingerprint,
@Nonnull String signingCertFingerprint) {
this(creationTime, signingKeyFingerprint, signingCertFingerprint, Optional.ofEmpty(), Optional.ofEmpty());
}
/**
@ -42,15 +48,41 @@ public class Verification {
* @param signatureMode signature mode (optional, may be <pre>null</pre>)
* @param description free-form description, e.g. <pre>certificate from dkg.asc</pre> (optional, may be <pre>null</pre>)
*/
public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint, SignatureMode signatureMode, String description) {
public Verification(@Nonnull Date creationTime,
@Nonnull String signingKeyFingerprint,
@Nonnull String signingCertFingerprint,
@Nullable SignatureMode signatureMode,
@Nullable String description) {
this(
creationTime,
signingKeyFingerprint,
signingCertFingerprint,
Optional.ofNullable(signatureMode),
Optional.ofNullable(nullSafeTrim(description))
);
}
private Verification(@Nonnull Date creationTime,
@Nonnull String signingKeyFingerprint,
@Nonnull String signingCertFingerprint,
@Nonnull Optional<SignatureMode> signatureMode,
@Nonnull Optional<String> description) {
this.creationTime = creationTime;
this.signingKeyFingerprint = signingKeyFingerprint;
this.signingCertFingerprint = signingCertFingerprint;
this.signatureMode = signatureMode;
this.description = description == null ? null : description.trim();
this.description = description;
}
public static Verification fromString(String toString) {
private static String nullSafeTrim(@Nullable String string) {
if (string == null) {
return null;
}
return string.trim();
}
@Nonnull
public static Verification fromString(@Nonnull String toString) {
String[] split = toString.trim().split(" ");
if (split.length < 3) {
throw new IllegalArgumentException("Verification must be of the format 'UTC-DATE OpenPGPFingerprint OpenPGPFingerprint [mode] [info]'");
@ -58,7 +90,7 @@ public class Verification {
if (split.length == 3) {
return new Verification(
UTCUtil.parseUTCDate(split[0]), // timestamp
parseUTCDate(split[0]), // timestamp
split[1], // key FP
split[2] // cert FP
);
@ -80,7 +112,7 @@ public class Verification {
}
return new Verification(
UTCUtil.parseUTCDate(split[0]), // timestamp
parseUTCDate(split[0]), // timestamp
split[1], // key FP
split[2], // cert FP
mode, // signature mode
@ -88,11 +120,20 @@ public class Verification {
);
}
private static Date parseUTCDate(String utcFormatted) {
try {
return UTCUtil.parseUTCDate(utcFormatted);
} catch (ParseException e) {
throw new IllegalArgumentException("Malformed UTC timestamp.", e);
}
}
/**
* Return the signatures' creation time.
*
* @return signature creation time
*/
@Nonnull
public Date getCreationTime() {
return creationTime;
}
@ -102,6 +143,7 @@ public class Verification {
*
* @return signing key fingerprint
*/
@Nonnull
public String getSigningKeyFingerprint() {
return signingKeyFingerprint;
}
@ -111,6 +153,7 @@ public class Verification {
*
* @return signing certificate fingerprint
*/
@Nonnull
public String getSigningCertFingerprint() {
return signingCertFingerprint;
}
@ -121,7 +164,8 @@ public class Verification {
*
* @return signature mode
*/
public SignatureMode getSignatureMode() {
@Nonnull
public Optional<SignatureMode> getSignatureMode() {
return signatureMode;
}
@ -131,7 +175,8 @@ public class Verification {
*
* @return description
*/
public String getDescription() {
@Nonnull
public Optional<String> getDescription() {
return description;
}
@ -144,12 +189,12 @@ public class Verification {
.append(' ')
.append(getSigningCertFingerprint());
if (signatureMode != null) {
sb.append(' ').append(MODE).append(signatureMode);
if (signatureMode.isPresent()) {
sb.append(' ').append(MODE).append(signatureMode.get());
}
if (description != null) {
sb.append(' ').append(description);
if (description.isPresent()) {
sb.append(' ').append(description.get());
}
return sb.toString();

View File

@ -337,7 +337,7 @@ public abstract class SOPGPException extends RuntimeException {
/**
* Exception that gets thrown if a special designator (starting with @) is given, but the filesystem contains
* a file matching the designator.
*
* <p>
* E.g. <pre>@ENV:FOO</pre> is given, but <pre>./@ENV:FOO</pre> exists on the filesystem.
*/
public static class AmbiguousInput extends SOPGPException {

View File

@ -5,11 +5,11 @@
package sop.operation;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
public interface AbstractSign<T> {
@ -67,7 +67,7 @@ public interface AbstractSign<T> {
default T withKeyPassword(String password)
throws SOPGPException.UnsupportedOption,
SOPGPException.PasswordNotHumanReadable {
return withKeyPassword(password.getBytes(Charset.forName("UTF8")));
return withKeyPassword(password.getBytes(UTF8Util.UTF8));
}
/**

View File

@ -8,11 +8,11 @@ import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Date;
public interface Decrypt {
@ -138,7 +138,7 @@ public interface Decrypt {
default Decrypt withKeyPassword(String password)
throws SOPGPException.UnsupportedOption,
SOPGPException.PasswordNotHumanReadable {
return withKeyPassword(password.getBytes(Charset.forName("UTF8")));
return withKeyPassword(password.getBytes(UTF8Util.UTF8));
}
/**

View File

@ -4,15 +4,15 @@
package sop.operation;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import sop.Profile;
import sop.Ready;
import sop.enums.EncryptAs;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public interface Encrypt {
@ -82,7 +82,7 @@ public interface Encrypt {
default Encrypt withKeyPassword(String password)
throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption {
return withKeyPassword(password.getBytes(Charset.forName("UTF8")));
return withKeyPassword(password.getBytes(UTF8Util.UTF8));
}
/**

View File

@ -6,6 +6,7 @@ package sop.operation;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.CharacterCodingException;
import sop.Profile;
import sop.Ready;
@ -54,7 +55,11 @@ public interface GenerateKey {
default GenerateKey withKeyPassword(byte[] password)
throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption {
return withKeyPassword(UTF8Util.decodeUTF8(password));
try {
return withKeyPassword(UTF8Util.decodeUTF8(password));
} catch (CharacterCodingException e) {
throw new SOPGPException.PasswordNotHumanReadable();
}
}
/**

View File

@ -22,4 +22,22 @@ public interface ListProfiles {
*/
List<Profile> subcommand(String command);
/**
* Return a list of {@link Profile Profiles} supported by the {@link GenerateKey} implementation.
*
* @return profiles
*/
default List<Profile> generateKey() {
return subcommand("generate-key");
}
/**
* Return a list of {@link Profile Profiles} supported by the {@link Encrypt} implementation.
*
* @return profiles
*/
default List<Profile> encrypt() {
return subcommand("encrypt");
}
}

View File

@ -12,7 +12,7 @@ import java.io.OutputStream;
* {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced.
* At that point, first all the buffered data is being written to the underlying stream, followed by any successive
* data that may get written to the {@link ProxyOutputStream}.
*
* <p>
* This class is useful if we need to provide an {@link OutputStream} at one point in time when the final
* target output stream is not yet known.
*/

View File

@ -4,6 +4,7 @@
package sop.util;
import javax.annotation.Nonnull;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -32,16 +33,24 @@ public class UTCUtil {
*
* @param dateString string
* @return date
* @throws ParseException if the date string is malformed and cannot be parsed
*/
public static Date parseUTCDate(String dateString) {
@Nonnull
public static Date parseUTCDate(String dateString) throws ParseException {
ParseException exception = null;
for (SimpleDateFormat parser : UTC_PARSERS) {
try {
return parser.parse(dateString);
} catch (ParseException e) {
// Store first exception (that of UTC_FORMATTER) to throw if we fail to parse the date
if (exception == null) {
exception = e;
}
// Try next parser
}
}
return null;
// No parser worked, so we throw the store exception
throw exception;
}
/**

View File

@ -4,8 +4,6 @@
package sop.util;
import sop.exception.SOPGPException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
@ -15,7 +13,8 @@ import java.nio.charset.CodingErrorAction;
public class UTF8Util {
private static final CharsetDecoder UTF8Decoder = Charset.forName("UTF8")
public static final Charset UTF8 = Charset.forName("UTF8");
private static final CharsetDecoder UTF8Decoder = UTF8
.newDecoder()
.onUnmappableCharacter(CodingErrorAction.REPORT)
.onMalformedInput(CodingErrorAction.REPORT);
@ -27,14 +26,12 @@ public class UTF8Util {
* @param data utf-8 encoded bytes
*
* @return decoded string
* @throws CharacterCodingException if the input data does not resemble UTF8
*/
public static String decodeUTF8(byte[] data) {
public static String decodeUTF8(byte[] data)
throws CharacterCodingException {
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
try {
CharBuffer charBuffer = UTF8Decoder.decode(byteBuffer);
return charBuffer.toString();
} catch (CharacterCodingException e) {
throw new SOPGPException.PasswordNotHumanReadable();
}
CharBuffer charBuffer = UTF8Decoder.decode(byteBuffer);
return charBuffer.toString();
}
}

View File

@ -2,23 +2,23 @@
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
package sop;
import org.junit.jupiter.api.Test;
import sop.util.UTCUtil;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import sop.ByteArrayAndResult;
import sop.Verification;
public class ByteArrayAndResultTest {
@Test
public void testCreationAndGetters() {
public void testCreationAndGetters() throws ParseException {
byte[] bytes = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
List<Verification> result = Collections.singletonList(
new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"),

View File

@ -2,19 +2,18 @@
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
package sop;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import sop.MicAlg;
public class MicAlgTest {
@Test

View File

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ProfileTest {
@Test
public void toStringFull() {
Profile profile = new Profile("default", "Use the implementers recommendations.");
assertEquals("default: Use the implementers recommendations.", profile.toString());
}
@Test
public void toStringNameOnly() {
Profile profile = new Profile("default");
assertEquals("default", profile.toString());
}
@Test
public void parseFull() {
String string = "default: Use the implementers recommendations.";
Profile profile = Profile.parse(string);
assertEquals("default", profile.getName());
assertTrue(profile.hasDescription());
assertEquals("Use the implementers recommendations.", profile.getDescription().get());
}
@Test
public void parseNameOnly() {
String string = "rfc4880";
Profile profile = Profile.parse(string);
assertEquals("rfc4880", profile.getName());
assertFalse(profile.hasDescription());
}
@Test
public void parseEmptyDescription() {
String string = "rfc4880: ";
Profile profile = Profile.parse(string);
assertEquals("rfc4880", profile.getName());
assertFalse(profile.hasDescription());
string = "rfc4880:";
profile = Profile.parse(string);
assertEquals("rfc4880", profile.getName());
assertFalse(profile.hasDescription());
}
@Test
public void parseTooLongProfile() {
// 1200 chars
String string = "longDescription: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.";
assertThrows(IllegalArgumentException.class, () -> Profile.parse(string));
}
@Test
public void constructTooLongProfile() {
// name + description = 1200 chars
String name = "longDescription";
String description = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.";
assertThrows(IllegalArgumentException.class, () -> new Profile(name, description));
}
@Test
public void nameCannotBeEmpty() {
assertThrows(IllegalArgumentException.class, () -> new Profile(""));
assertThrows(IllegalArgumentException.class, () -> new Profile(""), "Description Text.");
}
@Test
public void nameCannotContainColons() {
assertThrows(IllegalArgumentException.class, () -> new Profile("default:"));
assertThrows(IllegalArgumentException.class, () -> new Profile("default:", "DescriptionText"));
assertThrows(IllegalArgumentException.class, () -> new Profile("rfc:4880"));
assertThrows(IllegalArgumentException.class, () -> new Profile("rfc:4880", "OpenPGP Message Format"));
}
@Test
public void nameCannotContainWhitespace() {
assertThrows(IllegalArgumentException.class, () -> new Profile("default profile"));
assertThrows(IllegalArgumentException.class, () -> new Profile("default profile", "With description."));
assertThrows(IllegalArgumentException.class, () -> new Profile("default\nprofile"));
assertThrows(IllegalArgumentException.class, () -> new Profile("default\nprofile", "With description"));
assertThrows(IllegalArgumentException.class, () -> new Profile("default\tprofile"));
assertThrows(IllegalArgumentException.class, () -> new Profile("default\tprofile", "With description"));
assertThrows(IllegalArgumentException.class, () -> new Profile("default\r\nprofile"));
assertThrows(IllegalArgumentException.class, () -> new Profile("default\r\nprofile", "With description"));
assertThrows(IllegalArgumentException.class, () -> new Profile("default\rprofile"));
assertThrows(IllegalArgumentException.class, () -> new Profile("default\rprofile", "With description"));
}
}

View File

@ -2,16 +2,15 @@
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
package sop;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import sop.Ready;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
public class ReadyTest {

View File

@ -2,27 +2,26 @@
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
package sop;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import sop.exception.SOPGPException;
import sop.util.UTCUtil;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import sop.ByteArrayAndResult;
import sop.ReadyWithResult;
import sop.Verification;
import sop.exception.SOPGPException;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ReadyWithResultTest {
@Test
public void testReadyWithResult() throws SOPGPException.NoSignature, IOException {
public void testReadyWithResult() throws SOPGPException.NoSignature, IOException, ParseException {
byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
List<Verification> result = Collections.singletonList(
new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"),

View File

@ -2,15 +2,15 @@
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
package sop;
import org.junit.jupiter.api.Test;
import sop.util.HexUtil;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import sop.SessionKey;
public class SessionKeyTest {
@Test

View File

@ -2,13 +2,11 @@
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
package sop;
import org.junit.jupiter.api.Test;
import sop.MicAlg;
import sop.SigningResult;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SigningResultTest {

View File

@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import org.junit.jupiter.api.Test;
import sop.enums.SignatureMode;
import sop.testsuite.assertions.VerificationAssert;
import sop.util.UTCUtil;
import java.text.ParseException;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class VerificationTest {
@Test
public void limitedConstructorTest() throws ParseException {
Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z");
String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209";
String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B";
Verification verification = new Verification(signDate, keyFP, certFP);
assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString());
VerificationAssert.assertThatVerification(verification)
.issuedBy(certFP)
.isBySigningKey(keyFP)
.isCreatedAt(signDate)
.hasMode(null)
.hasDescription(null);
}
@Test
public void limitedParsingTest() throws ParseException {
String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B";
Verification verification = Verification.fromString(string);
assertEquals(string, verification.toString());
VerificationAssert.assertThatVerification(verification)
.isCreatedAt(UTCUtil.parseUTCDate("2022-11-07T15:01:24Z"))
.issuedBy("F9E6F53F7201C60A87064EAB0B27F2B0760A1209", "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B")
.hasMode(null)
.hasDescription(null);
}
@Test
public void parsingWithModeTest() throws ParseException {
String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:text";
Verification verification = Verification.fromString(string);
assertEquals(string, verification.toString());
VerificationAssert.assertThatVerification(verification)
.isCreatedAt(UTCUtil.parseUTCDate("2022-11-07T15:01:24Z"))
.issuedBy("F9E6F53F7201C60A87064EAB0B27F2B0760A1209", "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B")
.hasMode(SignatureMode.text)
.hasDescription(null);
}
@Test
public void extendedConstructorTest() throws ParseException {
Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z");
String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209";
String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B";
SignatureMode mode = SignatureMode.binary;
String description = "certificate from dkg.asc";
Verification verification = new Verification(signDate, keyFP, certFP, mode, description);
assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc", verification.toString());
VerificationAssert.assertThatVerification(verification)
.isCreatedAt(signDate)
.issuedBy(keyFP, certFP)
.hasMode(SignatureMode.binary)
.hasDescription(description);
}
@Test
public void extendedParsingTest() throws ParseException {
String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc";
Verification verification = Verification.fromString(string);
assertEquals(string, verification.toString());
// no mode
string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B certificate from dkg.asc";
verification = Verification.fromString(string);
assertEquals(string, verification.toString());
VerificationAssert.assertThatVerification(verification)
.isCreatedAt(UTCUtil.parseUTCDate("2022-11-07T15:01:24Z"))
.issuedBy("F9E6F53F7201C60A87064EAB0B27F2B0760A1209", "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B")
.hasMode(null)
.hasDescription("certificate from dkg.asc");
}
@Test
public void missingFingerprintFails() {
String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209";
assertThrows(IllegalArgumentException.class, () -> Verification.fromString(string));
}
@Test
public void malformedTimestampFails() {
String shorter = "'99-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B";
assertThrows(IllegalArgumentException.class, () -> Verification.fromString(shorter));
String longer = "'99-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc";
assertThrows(IllegalArgumentException.class, () -> Verification.fromString(longer));
}
}

View File

@ -4,12 +4,13 @@
package sop.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.Test;
import java.text.ParseException;
import java.util.Date;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Test parsing some date examples from the stateless OpenPGP CLI spec.
@ -19,30 +20,29 @@ import org.junit.jupiter.api.Test;
public class UTCUtilTest {
@Test
public void parseExample1() {
public void parseExample1() throws ParseException {
String timestamp = "2019-10-29T12:11:04+00:00";
Date date = UTCUtil.parseUTCDate(timestamp);
assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date));
}
@Test
public void parseExample2() {
public void parseExample2() throws ParseException {
String timestamp = "2019-10-24T23:48:29Z";
Date date = UTCUtil.parseUTCDate(timestamp);
assertEquals("2019-10-24T23:48:29Z", UTCUtil.formatUTCDate(date));
}
@Test
public void parseExample3() {
public void parseExample3() throws ParseException {
String timestamp = "20191029T121104Z";
Date date = UTCUtil.parseUTCDate(timestamp);
assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date));
}
@Test
public void invalidDateReturnsNull() {
public void invalidDateThrows() {
String invalidTimestamp = "foobar";
Date expectNull = UTCUtil.parseUTCDate(invalidTimestamp);
assertNull(expectNull);
assertThrows(ParseException.class, () -> UTCUtil.parseUTCDate(invalidTimestamp));
}
}

View File

@ -5,8 +5,8 @@
package sop.util;
import org.junit.jupiter.api.Test;
import sop.exception.SOPGPException;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -15,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class UTF8UtilTest {
@Test
public void testValidUtf8Decoding() {
public void testValidUtf8Decoding() throws CharacterCodingException {
String utf8String = "Hello, World\n";
String decoded = UTF8Util.decodeUTF8(utf8String.getBytes(StandardCharsets.UTF_8));
@ -29,11 +29,11 @@ public class UTF8UtilTest {
*/
@Test
public void testInvalidUtf8StringThrows() {
assertThrows(SOPGPException.PasswordNotHumanReadable.class,
assertThrows(CharacterCodingException.class,
() -> UTF8Util.decodeUTF8(new byte[] {(byte) 0xa0, (byte) 0xa1}));
assertThrows(SOPGPException.PasswordNotHumanReadable.class,
assertThrows(CharacterCodingException.class,
() -> UTF8Util.decodeUTF8(new byte[] {(byte) 0xc0, (byte) 0xaf}));
assertThrows(SOPGPException.PasswordNotHumanReadable.class,
assertThrows(CharacterCodingException.class,
() -> UTF8Util.decodeUTF8(new byte[] {(byte) 0x80, (byte) 0xbf}));
}
}

View File

@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
import org.junit.jupiter.api.Test;
import sop.Verification;
import sop.enums.SignatureMode;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class VerificationTest {
@Test
public void limitedConstructorTest() {
Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z");
String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209";
String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B";
Verification verification = new Verification(signDate, keyFP, certFP);
assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString());
}
public void limitedParsingTest() {
String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B";
Verification verification = Verification.fromString(string);
assertEquals(string, verification.toString());
}
@Test
public void extendedConstructorTest() {
Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z");
String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209";
String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B";
SignatureMode mode = SignatureMode.binary;
String description = "certificate from dkg.asc";
Verification verification = new Verification(signDate, keyFP, certFP, mode, description);
assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc", verification.toString());
assertEquals(SignatureMode.binary, verification.getSignatureMode());
assertEquals(description, verification.getDescription());
}
@Test
public void extendedParsingTest() {
String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc";
Verification verification = Verification.fromString(string);
assertEquals(string, verification.toString());
// no mode
string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B certificate from dkg.asc";
verification = Verification.fromString(string);
assertEquals(string, verification.toString());
}
}

View File

@ -4,15 +4,14 @@
package sop.testsuite;
import sop.Verification;
import sop.util.UTCUtil;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class JUtils {
@ -157,66 +156,12 @@ public class JUtils {
return string.getBytes(StandardCharsets.UTF_8);
}
public static void assertSignedBy(List<Verification> verifications, String primaryFingerprint) {
for (Verification verification : verifications) {
if (verification.getSigningCertFingerprint().equals(primaryFingerprint)) {
return;
}
}
if (verifications.isEmpty()) {
fail("Verification list is empty.");
}
fail("Verification list does not contain verification by cert " + primaryFingerprint + ":\n" +
Arrays.toString(verifications.toArray(new Verification[0])));
public static void assertDateEquals(Date expected, Date actual) {
assertEquals(UTCUtil.formatUTCDate(expected), UTCUtil.formatUTCDate(actual));
}
public static void assertSignedBy(List<Verification> verifications, String signingFingerprint, String primaryFingerprint) {
for (Verification verification : verifications) {
if (verification.getSigningCertFingerprint().equals(primaryFingerprint) && verification.getSigningKeyFingerprint().equals(signingFingerprint)) {
return;
}
}
if (verifications.isEmpty()) {
fail("Verification list is empty.");
}
fail("Verification list does not contain verification by key " + signingFingerprint + " on cert " + primaryFingerprint + ":\n" +
Arrays.toString(verifications.toArray(new Verification[0])));
public static boolean dateEquals(Date expected, Date actual) {
return UTCUtil.formatUTCDate(expected).equals(UTCUtil.formatUTCDate(actual));
}
public static void assertSignedBy(List<Verification> verifications, String primaryFingerprint, Date signatureDate) {
for (Verification verification : verifications) {
if (verification.getSigningCertFingerprint().equals(primaryFingerprint) &&
verification.getCreationTime().equals(signatureDate)) {
return;
}
}
if (verifications.isEmpty()) {
fail("Verification list is empty.");
}
fail("Verification list does not contain verification by cert " + primaryFingerprint + " made at " + UTCUtil.formatUTCDate(signatureDate) + ":\n" +
Arrays.toString(verifications.toArray(new Verification[0])));
}
public static void assertSignedBy(List<Verification> verifications, String signingFingerprint, String primaryFingerprint, Date signatureDate) {
for (Verification verification : verifications) {
if (verification.getSigningCertFingerprint().equals(primaryFingerprint) &&
verification.getSigningKeyFingerprint().equals(signingFingerprint) &&
verification.getCreationTime().equals(signatureDate)) {
return;
}
}
if (verifications.isEmpty()) {
fail("Verification list is empty.");
}
fail("Verification list does not contain verification by key" + signingFingerprint + " on cert " + primaryFingerprint + " made at " + UTCUtil.formatUTCDate(signatureDate) + ":\n" +
Arrays.toString(verifications.toArray(new Verification[0])));
}
}

View File

@ -7,6 +7,7 @@ package sop.testsuite;
import sop.util.UTCUtil;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.Date;
public class TestData {
@ -59,7 +60,7 @@ public class TestData {
"sfcfswMA\n" +
"=RDAo\n" +
"-----END PGP MESSAGE-----";
public static final Date ALICE_INLINE_SIGNED_MESSAGE_DATE = UTCUtil.parseUTCDate("2023-01-13T17:20:47Z");
public static final Date ALICE_INLINE_SIGNED_MESSAGE_DATE = parseUTCDate("2023-01-13T17:20:47Z");
// signature over PLAINTEXT
public static final String ALICE_DETACHED_SIGNED_MESSAGE = "-----BEGIN PGP SIGNATURE-----\n" +
"\n" +
@ -68,7 +69,7 @@ public class TestData {
"0K1UgT5roym9Fln8U5W8R03TSbfNiwE=\n" +
"=bxPN\n" +
"-----END PGP SIGNATURE-----";
public static final Date ALICE_DETACHED_SIGNED_MESSAGE_DATE = UTCUtil.parseUTCDate("2023-01-13T16:57:57Z");
public static final Date ALICE_DETACHED_SIGNED_MESSAGE_DATE = parseUTCDate("2023-01-13T16:57:57Z");
// 'Bob' key from draft-bre-openpgp-samples-00
public static final String BOB_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
@ -421,4 +422,12 @@ public class TestData {
public static final byte[] BEGIN_PGP_SIGNATURE = "-----BEGIN PGP SIGNATURE-----\n".getBytes(StandardCharsets.UTF_8);
public static final byte[] END_PGP_SIGNATURE = "-----END PGP SIGNATURE-----".getBytes(StandardCharsets.UTF_8);
public static final byte[] BEGIN_PGP_SIGNED_MESSAGE = "-----BEGIN PGP SIGNED MESSAGE-----\n".getBytes(StandardCharsets.UTF_8);
private static Date parseUTCDate(String utcFormatted) {
try {
return UTCUtil.parseUTCDate(utcFormatted);
} catch (ParseException e) {
throw new IllegalArgumentException("Malformed UTC timestamp.", e);
}
}
}

View File

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.assertions;
import sop.Verification;
import sop.enums.SignatureMode;
import sop.testsuite.JUtils;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
public final class VerificationAssert {
private final Verification verification;
public static VerificationAssert assertThatVerification(Verification verification) {
return new VerificationAssert(verification);
}
private VerificationAssert(Verification verification) {
this.verification = verification;
}
public VerificationAssert issuedBy(String signingKeyFingerprint, String primaryFingerprint) {
return isBySigningKey(signingKeyFingerprint)
.issuedBy(primaryFingerprint);
}
public VerificationAssert issuedBy(String primaryFingerprint) {
assertEquals(primaryFingerprint, verification.getSigningCertFingerprint());
return this;
}
public VerificationAssert isBySigningKey(String signingKeyFingerprint) {
assertEquals(signingKeyFingerprint, verification.getSigningKeyFingerprint());
return this;
}
public VerificationAssert isCreatedAt(Date creationDate) {
JUtils.assertDateEquals(creationDate, verification.getCreationTime());
return this;
}
public VerificationAssert hasDescription(String description) {
assertEquals(description, verification.getDescription().get());
return this;
}
public VerificationAssert hasDescriptionOrNull(String description) {
if (verification.getDescription().isEmpty()) {
return this;
}
return hasDescription(description);
}
public VerificationAssert hasMode(SignatureMode mode) {
assertEquals(mode, verification.getSignatureMode().get());
return this;
}
public VerificationAssert hasModeOrNull(SignatureMode mode) {
if (verification.getSignatureMode().isEmpty()) {
return this;
}
return hasMode(mode);
}
}

View File

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.assertions;
import sop.Verification;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public final class VerificationListAssert {
private final List<Verification> verificationList = new ArrayList<>();
private VerificationListAssert(List<Verification> verifications) {
this.verificationList.addAll(verifications);
}
public static VerificationListAssert assertThatVerificationList(List<Verification> verifications) {
return new VerificationListAssert(verifications);
}
public VerificationListAssert isEmpty() {
assertTrue(verificationList.isEmpty());
return this;
}
public VerificationListAssert isNotEmpty() {
assertFalse(verificationList.isEmpty());
return this;
}
public VerificationListAssert sizeEquals(int size) {
assertEquals(size, verificationList.size());
return this;
}
public VerificationAssert hasSingleItem() {
sizeEquals(1);
return VerificationAssert.assertThatVerification(verificationList.get(0));
}
public VerificationListAssert containsVerificationByCert(String primaryFingerprint) {
for (Verification verification : verificationList) {
if (primaryFingerprint.equals(verification.getSigningCertFingerprint())) {
return this;
}
}
fail("No verification was issued by certificate " + primaryFingerprint);
return this;
}
public VerificationListAssert containsVerificationBy(String signingKeyFingerprint, String primaryFingerprint) {
for (Verification verification : verificationList) {
if (primaryFingerprint.equals(verification.getSigningCertFingerprint()) &&
signingKeyFingerprint.equals(verification.getSigningKeyFingerprint())) {
return this;
}
}
fail("No verification was issued by key " + signingKeyFingerprint + " of cert " + primaryFingerprint);
return this;
}
}

View File

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* DSL for assertions on SOP objects.
*/
package sop.testsuite.assertions;

View File

@ -9,6 +9,7 @@ import org.junit.jupiter.params.provider.Arguments;
import sop.SOP;
import sop.testsuite.SOPInstanceFactory;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -19,24 +20,29 @@ public abstract class AbstractSOPTest {
private static final List<Arguments> backends = new ArrayList<>();
static {
// populate instances list via configured test subject factory
String factoryName = System.getenv("test.implementation");
if (factoryName != null) {
try {
Class testSubjectFactoryClass = Class.forName(factoryName);
SOPInstanceFactory factory = (SOPInstanceFactory) testSubjectFactoryClass.newInstance();
Map<String, SOP> testSubjects = factory.provideSOPInstances();
initBackends();
}
for (String key : testSubjects.keySet()) {
backends.add(Arguments.of(Named.of(key, testSubjects.get(key))));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
// populate instances list via configured test subject factory
private static void initBackends() {
String factoryName = System.getenv("test.implementation");
if (factoryName == null) {
return;
}
SOPInstanceFactory factory;
try {
Class<?> testSubjectFactoryClass = Class.forName(factoryName);
factory = (SOPInstanceFactory) testSubjectFactoryClass
.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException |
InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
Map<String, SOP> testSubjects = factory.provideSOPInstances();
for (String key : testSubjects.keySet()) {
backends.add(Arguments.of(Named.of(key, testSubjects.get(key))));
}
}

View File

@ -11,9 +11,11 @@ import org.junit.jupiter.params.provider.MethodSource;
import sop.SOP;
import sop.Verification;
import sop.enums.SignAs;
import sop.enums.SignatureMode;
import sop.exception.SOPGPException;
import sop.testsuite.JUtils;
import sop.testsuite.TestData;
import sop.testsuite.assertions.VerificationListAssert;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -21,7 +23,6 @@ import java.util.Date;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
@ -47,8 +48,11 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.signatures(signature)
.data(message);
assertFalse(verificationList.isEmpty());
JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT)
.hasModeOrNull(SignatureMode.binary);
}
@ParameterizedTest
@ -68,8 +72,11 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.signatures(signature)
.data(message);
assertFalse(verificationList.isEmpty());
JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT)
.hasModeOrNull(SignatureMode.text);
}
@ParameterizedTest
@ -83,8 +90,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.signatures(signature)
.data(message);
assertFalse(verificationList.isEmpty());
JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT, TestData.ALICE_DETACHED_SIGNED_MESSAGE_DATE);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -103,8 +112,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.signatures(signature)
.data(message);
assertFalse(verificationList.isEmpty());
JUtils.assertSignedBy(verificationList, TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -123,8 +134,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.signatures(signature)
.data(message);
assertFalse(verificationList.isEmpty());
JUtils.assertSignedBy(verificationList, TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -146,7 +159,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.signatures(signature)
.data(message);
assertFalse(verificationList.isEmpty());
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -170,8 +186,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.signatures(armored)
.data(message);
assertFalse(verificationList.isEmpty());
JUtils.assertSignedBy(verificationList, TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -202,6 +220,21 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.data(message));
}
@ParameterizedTest
@MethodSource("provideInstances")
public void signWithAliceVerifyWithBobThrowsNoSignature(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signatures = sop.detachedSign()
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.toByteArrayAndResult()
.getBytes();
assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify()
.cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signatures)
.data(message));
}
@ParameterizedTest
@MethodSource("provideInstances")
@ -229,11 +262,15 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.toByteArrayAndResult()
.getBytes();
assertFalse(sop.verify()
List<Verification> verificationList = sop.verify()
.cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signature)
.data(message)
.isEmpty());
.data(message);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -247,4 +284,29 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.data(message));
}
@ParameterizedTest
@MethodSource("provideInstances")
public void signVerifyWithMultipleKeys(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signatures = sop.detachedSign()
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.toByteArrayAndResult()
.getBytes();
List<Verification> verificationList = sop.detachedVerify()
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signatures)
.data(message);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.sizeEquals(2)
.containsVerificationBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT)
.containsVerificationBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT);
}
}

View File

@ -13,20 +13,20 @@ import sop.DecryptionResult;
import sop.SOP;
import sop.Verification;
import sop.enums.EncryptAs;
import sop.enums.SignatureMode;
import sop.exception.SOPGPException;
import sop.testsuite.JUtils;
import sop.testsuite.TestData;
import sop.testsuite.assertions.VerificationListAssert;
import sop.util.UTCUtil;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
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 static org.junit.jupiter.api.Assertions.assertThrows;
@ -142,6 +142,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
byte[] ciphertext = sop.encrypt()
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(EncryptAs.Binary)
.plaintext(message)
.getBytes();
@ -156,9 +157,13 @@ public class EncryptDecryptTest extends AbstractSOPTest {
DecryptionResult result = bytesAndResult.getResult();
assertNotNull(result.getSessionKey().get());
List<Verification> verificationList = result.getVerifications();
assertEquals(1, verificationList.size());
JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT)
.hasModeOrNull(SignatureMode.binary);
}
@ParameterizedTest
@ -183,9 +188,12 @@ public class EncryptDecryptTest extends AbstractSOPTest {
DecryptionResult result = bytesAndResult.getResult();
assertNotNull(result.getSessionKey().get());
List<Verification> verificationList = result.getVerifications();
assertEquals(1, verificationList.size());
JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.hasSingleItem()
.issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT)
.hasModeOrNull(SignatureMode.text);
}
@ParameterizedTest
@ -216,13 +224,15 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.ciphertext(ciphertext)
.toByteArrayAndResult();
assertFalse(bytesAndResult.getResult().getVerifications().isEmpty());
assertArrayEquals(message, bytesAndResult.getBytes());
List<Verification> verifications = bytesAndResult.getResult().getVerifications();
VerificationListAssert.assertThatVerificationList(verifications)
.isNotEmpty()
.hasSingleItem();
}
@ParameterizedTest
@MethodSource("provideInstances")
public void decryptVerifyNotAfterTest(SOP sop) {
public void decryptVerifyNotAfterTest(SOP sop) throws ParseException {
byte[] message = ("-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"wV4DR2b2udXyHrYSAQdAwlOwwyxFDJta5+H9abgSj8jum9v7etUc9usdrElESmow\n" +
@ -247,6 +257,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.ciphertext(message)
.toByteArrayAndResult();
// Some implementations do not throw NoSignature and instead return an empty list.
if (bytesAndResult.getResult().getVerifications().isEmpty()) {
throw new SOPGPException.NoSignature("No verifiable signature found.");
}
@ -255,7 +266,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void decryptVerifyNotBeforeTest(SOP sop) {
public void decryptVerifyNotBeforeTest(SOP sop) throws ParseException {
byte[] message = ("-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"wV4DR2b2udXyHrYSAQdAwlOwwyxFDJta5+H9abgSj8jum9v7etUc9usdrElESmow\n" +
@ -280,6 +291,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.ciphertext(message)
.toByteArrayAndResult();
// Some implementations do not throw NoSignature and instead return an empty list.
if (bytesAndResult.getResult().getVerifications().isEmpty()) {
throw new SOPGPException.NoSignature("No verifiable signature found.");
}

View File

@ -13,9 +13,11 @@ import sop.ByteArrayAndResult;
import sop.SOP;
import sop.Verification;
import sop.enums.InlineSignAs;
import sop.enums.SignatureMode;
import sop.exception.SOPGPException;
import sop.testsuite.JUtils;
import sop.testsuite.TestData;
import sop.testsuite.assertions.VerificationListAssert;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -51,8 +53,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
.toByteArrayAndResult();
assertArrayEquals(message, bytesAndResult.getBytes());
List<Verification> verificationList = bytesAndResult.getResult();
JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -74,8 +80,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
.toByteArrayAndResult();
assertArrayEquals(message, bytesAndResult.getBytes());
List<Verification> verificationList = bytesAndResult.getResult();
JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -97,8 +107,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
.toByteArrayAndResult();
assertArrayEquals(message, bytesAndResult.getBytes());
List<Verification> verificationList = bytesAndResult.getResult();
JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.hasSingleItem()
.issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT)
.hasModeOrNull(SignatureMode.text);
}
@ParameterizedTest
@ -111,8 +125,13 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.data(message)
.toByteArrayAndResult();
List<Verification> verificationList = bytesAndResult.getResult();
JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT, signatureDate);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.isCreatedAt(signatureDate)
.issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -161,8 +180,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
.toByteArrayAndResult();
assertArrayEquals(message, bytesAndResult.getBytes());
List<Verification> verificationList = bytesAndResult.getResult();
JUtils.assertSignedBy(verificationList, TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -183,8 +206,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
.toByteArrayAndResult();
assertArrayEquals(message, bytesAndResult.getBytes());
List<Verification> verificationList = bytesAndResult.getResult();
JUtils.assertSignedBy(verificationList, TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.isNotEmpty()
.hasSingleItem()
.issuedBy(TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT);
}
@ParameterizedTest
@ -205,7 +232,9 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
.toByteArrayAndResult();
List<Verification> verificationList = bytesAndResult.getResult();
JUtils.assertSignedBy(verificationList, TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT);
VerificationListAssert.assertThatVerificationList(verificationList)
.hasSingleItem()
.issuedBy(TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT);
}
}

View File

@ -9,12 +9,13 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import sop.Profile;
import sop.SOP;
import sop.exception.SOPGPException;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ListProfilesTest extends AbstractSOPTest {
@ -24,10 +25,29 @@ public class ListProfilesTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void listGenerateKeyProfiles(SOP sop) throws IOException {
List<Profile> profiles = sop.listProfiles()
.subcommand("generate-key");
public void listGenerateKeyProfiles(SOP sop) {
List<Profile> profiles = sop
.listProfiles()
.generateKey();
assertFalse(profiles.isEmpty());
}
@ParameterizedTest
@MethodSource("provideInstances")
public void listEncryptProfiles(SOP sop) {
List<Profile> profiles = sop
.listProfiles()
.encrypt();
assertFalse(profiles.isEmpty());
}
@ParameterizedTest
@MethodSource("provideInstances")
public void listUnsupportedProfiles(SOP sop) {
assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop
.listProfiles()
.subcommand("invalid"));
}
}

View File

@ -4,7 +4,7 @@
allprojects {
ext {
shortVersion = '6.0.1'
shortVersion = '6.1.1'
isSnapshot = true
minAndroidSdk = 10
javaSourceCompatibility = 1.8