mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2025-09-08 17:59:43 +02:00
Add test for JSON data parsing and serializing using a dummy implementation
This commit is contained in:
parent
ebfde35422
commit
cdcbae7e5f
4 changed files with 208 additions and 29 deletions
|
@ -45,7 +45,8 @@ class SopVCLI {
|
||||||
@JvmField var EXECUTABLE_NAME = "sopv"
|
@JvmField var EXECUTABLE_NAME = "sopv"
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
@CommandLine.Option(names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT)
|
@CommandLine.Option(
|
||||||
|
names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT)
|
||||||
var stacktrace = false
|
var stacktrace = false
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -10,6 +10,15 @@ import sop.enums.SignatureMode
|
||||||
import sop.util.Optional
|
import sop.util.Optional
|
||||||
import sop.util.UTCUtil
|
import sop.util.UTCUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about a verified signature.
|
||||||
|
*
|
||||||
|
* @param creationTime creation time of the signature
|
||||||
|
* @param signingKeyFingerprint fingerprint of the (sub-)key that issued the signature
|
||||||
|
* @param signingCertFingerprint fingerprint of the certificate that contains the signing key
|
||||||
|
* @param signatureMode optional signature mode (text/binary)
|
||||||
|
* @param jsonOrDescription arbitrary text or JSON data
|
||||||
|
*/
|
||||||
data class Verification(
|
data class Verification(
|
||||||
val creationTime: Date,
|
val creationTime: Date,
|
||||||
val signingKeyFingerprint: String,
|
val signingKeyFingerprint: String,
|
||||||
|
@ -47,21 +56,22 @@ data class Verification(
|
||||||
Optional.ofNullable(signatureMode),
|
Optional.ofNullable(signatureMode),
|
||||||
Optional.of(jsonSerializer.serialize(json)))
|
Optional.of(jsonSerializer.serialize(json)))
|
||||||
|
|
||||||
@Deprecated("Replaced by jsonOrDescription",
|
@Deprecated("Replaced by jsonOrDescription", replaceWith = ReplaceWith("jsonOrDescription"))
|
||||||
replaceWith = ReplaceWith("jsonOrDescription")
|
|
||||||
)
|
|
||||||
val description = jsonOrDescription
|
val description = jsonOrDescription
|
||||||
|
|
||||||
|
/** This value is `true` if the [Verification] contains extension JSON. */
|
||||||
|
val containsJson: Boolean =
|
||||||
|
jsonOrDescription.get()?.trim()?.let { it.startsWith("{") && it.endsWith("}") } ?: false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the result.
|
* Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the
|
||||||
* This method returns `null` if parsing fails.
|
* result. This method returns `null` if parsing fails.
|
||||||
*
|
*
|
||||||
* @param parser [JSONParser] implementation
|
* @param parser [JSONParser] implementation
|
||||||
* @return successfully parsed [JSON] POJO or `null`.
|
* @return successfully parsed [JSON] POJO or `null`.
|
||||||
*/
|
*/
|
||||||
fun getJson(parser: JSONParser): JSON? {
|
fun getJson(parser: JSONParser): JSON? {
|
||||||
return jsonOrDescription.get()
|
return jsonOrDescription.get()?.let {
|
||||||
?.let {
|
|
||||||
try {
|
try {
|
||||||
parser.parse(it)
|
parser.parse(it)
|
||||||
} catch (e: ParseException) {
|
} catch (e: ParseException) {
|
||||||
|
@ -116,35 +126,37 @@ data class Verification(
|
||||||
/**
|
/**
|
||||||
* POJO data class representing JSON metadata.
|
* POJO data class representing JSON metadata.
|
||||||
*
|
*
|
||||||
* @param signers list of supplied CERTS objects that could have issued the signature, identified by
|
* @param signers list of supplied CERTS objects that could have issued the signature,
|
||||||
* the name given on the command line.
|
* identified by the name given on the command line.
|
||||||
* @param comment a freeform UTF-8 encoded text describing the verification
|
* @param comment a freeform UTF-8 encoded text describing the verification
|
||||||
* @param ext an extension object containing arbitrary, implementation-specific data
|
* @param ext an extension object containing arbitrary, implementation-specific data
|
||||||
*/
|
*/
|
||||||
data class JSON(
|
data class JSON(val signers: List<String>, val comment: String?, val ext: Any?) {
|
||||||
val signers: List<String>,
|
|
||||||
val comment: String?,
|
|
||||||
val ext: Any?)
|
|
||||||
|
|
||||||
/**
|
/** Create a JSON object with only a list of signers. */
|
||||||
* Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings.
|
constructor(signers: List<String>) : this(signers, null, null)
|
||||||
*/
|
|
||||||
|
/** Create a JSON object with only a single signer. */
|
||||||
|
constructor(signer: String) : this(listOf(signer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. */
|
||||||
fun interface JSONParser {
|
fun interface JSONParser {
|
||||||
/**
|
/**
|
||||||
* Parse a [JSON] POJO from the given single-line [string].
|
* Parse a [JSON] POJO from the given single-line [string]. If the string does not represent
|
||||||
* If the string does not represent a JSON object matching the [JSON] definition,
|
* a JSON object matching the [JSON] definition, this method throws a [ParseException].
|
||||||
* this method throws a [ParseException].
|
|
||||||
*
|
*
|
||||||
* @param string [String] representation of the [JSON] object.
|
* @param string [String] representation of the [JSON] object.
|
||||||
* @return parsed [JSON] POJO
|
* @return parsed [JSON] POJO
|
||||||
* @throws ParseException if the [string] is not a JSON string representing the [JSON] object.
|
* @throws ParseException if the [string] is not a JSON string representing the [JSON]
|
||||||
|
* object.
|
||||||
*/
|
*/
|
||||||
@Throws(ParseException::class)
|
@Throws(ParseException::class) fun parse(string: String): JSON
|
||||||
fun parse(string: String): JSON
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON strings.
|
* Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON
|
||||||
|
* strings.
|
||||||
*/
|
*/
|
||||||
fun interface JSONSerializer {
|
fun interface JSONSerializer {
|
||||||
|
|
||||||
|
|
163
sop-java/src/test/java/sop/VerificationJSONTest.java
Normal file
163
sop-java/src/test/java/sop/VerificationJSONTest.java
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sop;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.enums.SignatureMode;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public class VerificationJSONTest {
|
||||||
|
|
||||||
|
// A hacky self-made "JSON parser" stand-in.
|
||||||
|
// Only used for testing, do not use in production!
|
||||||
|
private Verification.JSONParser dummyParser = new Verification.JSONParser() {
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Verification.JSON parse(@NotNull String string) throws ParseException {
|
||||||
|
if (!string.startsWith("{")) {
|
||||||
|
throw new ParseException("Alleged JSON String does not begin with '{'", 0);
|
||||||
|
}
|
||||||
|
if (!string.endsWith("}")) {
|
||||||
|
throw new ParseException("Alleged JSON String does not end with '}'", string.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> signersList = new ArrayList<>();
|
||||||
|
Matcher signersMat = Pattern.compile("\"signers\": \\[(.*?)\\]").matcher(string);
|
||||||
|
if (signersMat.find()) {
|
||||||
|
String signersCat = signersMat.group(1);
|
||||||
|
String[] split = signersCat.split(",");
|
||||||
|
for (String s : split) {
|
||||||
|
s = s.trim();
|
||||||
|
signersList.add(s.substring(1, s.length() - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String comment = null;
|
||||||
|
Matcher commentMat = Pattern.compile("\"comment\": \"(.*?)\"").matcher(string);
|
||||||
|
if (commentMat.find()) {
|
||||||
|
comment = commentMat.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String ext = null;
|
||||||
|
Matcher extMat = Pattern.compile("\"ext\": (.*?})}").matcher(string);
|
||||||
|
if (extMat.find()) {
|
||||||
|
ext = extMat.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Verification.JSON(signersList, comment, ext);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// A just as hacky "JSON Serializer" lookalike.
|
||||||
|
// Also don't use in production, for testing only!
|
||||||
|
private Verification.JSONSerializer dummySerializer = new Verification.JSONSerializer() {
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public String serialize(@NotNull Verification.JSON json) {
|
||||||
|
if (json.getSigners().isEmpty() && json.getComment() == null && json.getExt() == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder("{");
|
||||||
|
boolean comma = false;
|
||||||
|
|
||||||
|
if (!json.getSigners().isEmpty()) {
|
||||||
|
comma = true;
|
||||||
|
sb.append("\"signers\": [");
|
||||||
|
for (Iterator<String> iterator = json.getSigners().iterator(); iterator.hasNext(); ) {
|
||||||
|
String signer = iterator.next();
|
||||||
|
sb.append("\"").append(signer).append("\"");
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.getComment() != null) {
|
||||||
|
if (comma) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
comma = true;
|
||||||
|
sb.append("\"comment\": \"").append(json.getComment()).append("\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.getExt() != null) {
|
||||||
|
if (comma) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
comma = true;
|
||||||
|
sb.append("\"ext\": ").append(json.getExt().toString());
|
||||||
|
}
|
||||||
|
return sb.append("}").toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSimpleSerializeParse() throws ParseException {
|
||||||
|
String signer = "alice.pub";
|
||||||
|
Verification.JSON json = new Verification.JSON(signer);
|
||||||
|
|
||||||
|
String string = dummySerializer.serialize(json);
|
||||||
|
assertEquals("{\"signers\": [\"alice.pub\"]}", string);
|
||||||
|
|
||||||
|
Verification.JSON parsed = dummyParser.parse(string);
|
||||||
|
assertEquals(signer, parsed.getSigners().get(0));
|
||||||
|
assertEquals(1, parsed.getSigners().size());
|
||||||
|
assertNull(parsed.getComment());
|
||||||
|
assertNull(parsed.getExt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAdvancedSerializeParse() throws ParseException {
|
||||||
|
Verification.JSON json = new Verification.JSON(
|
||||||
|
Arrays.asList("../certs/alice.pub", "/etc/pgp/certs.pgp"),
|
||||||
|
"This is a comment",
|
||||||
|
"{\"Foo\": \"Bar\"}");
|
||||||
|
|
||||||
|
String serialized = dummySerializer.serialize(json);
|
||||||
|
assertEquals("{\"signers\": [\"../certs/alice.pub\", \"/etc/pgp/certs.pgp\"], \"comment\": \"This is a comment\", \"ext\": {\"Foo\": \"Bar\"}}",
|
||||||
|
serialized);
|
||||||
|
|
||||||
|
Verification.JSON parsed = dummyParser.parse(serialized);
|
||||||
|
assertEquals(json.getSigners(), parsed.getSigners());
|
||||||
|
assertEquals(json.getComment(), parsed.getComment());
|
||||||
|
assertEquals(json.getExt(), parsed.getExt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerificationWithSimpleJson() {
|
||||||
|
String string = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E mode:text {\"signers\": [\"alice.pgp\"]}";
|
||||||
|
Verification verification = Verification.fromString(string);
|
||||||
|
|
||||||
|
assertTrue(verification.getContainsJson());
|
||||||
|
assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningKeyFingerprint());
|
||||||
|
assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningCertFingerprint());
|
||||||
|
assertEquals(SignatureMode.text, verification.getSignatureMode().get());
|
||||||
|
|
||||||
|
Verification.JSON json = verification.getJson(dummyParser);
|
||||||
|
assertNotNull(json, "The verification string MUST contain valid extension json");
|
||||||
|
|
||||||
|
assertEquals(Collections.singletonList("alice.pgp"), json.getSigners());
|
||||||
|
assertNull(json.getComment());
|
||||||
|
assertNull(json.getExt());
|
||||||
|
|
||||||
|
verification = new Verification(verification.getCreationTime(), verification.getSigningKeyFingerprint(), verification.getSigningCertFingerprint(), verification.getSignatureMode().get(), json, dummySerializer);
|
||||||
|
assertEquals(string, verification.toString());
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import java.text.ParseException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.assertThrows;
|
||||||
|
|
||||||
public class VerificationTest {
|
public class VerificationTest {
|
||||||
|
@ -25,6 +26,8 @@ public class VerificationTest {
|
||||||
Verification verification = new Verification(signDate, keyFP, certFP);
|
Verification verification = new Verification(signDate, keyFP, certFP);
|
||||||
assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString());
|
assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString());
|
||||||
|
|
||||||
|
assertFalse(verification.getContainsJson());
|
||||||
|
|
||||||
VerificationAssert.assertThatVerification(verification)
|
VerificationAssert.assertThatVerification(verification)
|
||||||
.issuedBy(certFP)
|
.issuedBy(certFP)
|
||||||
.isBySigningKey(keyFP)
|
.isBySigningKey(keyFP)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue