pgpainless/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest....

336 lines
16 KiB
Java

// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.cli.commands;
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 java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Date;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.generation.type.KeyType;
import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve;
import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec;
import org.pgpainless.key.info.KeyRingInfo;
import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException;
import sop.util.UTCUtil;
public class RoundTripSignVerifyCmdTest extends CLITest {
public RoundTripSignVerifyCmdTest() {
super(LoggerFactory.getLogger(RoundTripSignVerifyCmdTest.class));
}
private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: 9DA0 9423 C9F9 4BA4 CCA3 0951 099B 11BF 296A 373E\n" +
"Comment: Sigmund <sigmund@pgpainless.org>\n" +
"\n" +
"lFgEY2vzkhYJKwYBBAHaRw8BAQdA+Z2OAFQf0k64Au7hIZfXh/ijclabddvwh7Nh\n" +
"kedJ3ZUAAQCZy5p1cvQvRIWUopHwhnrD/oVAa1dNT/nA3cihQ5gkZBHPtCBTaWdt\n" +
"dW5kIDxzaWdtdW5kQHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJja/OSCRAJmxG/\n" +
"KWo3PhYhBJ2glCPJ+UukzKMJUQmbEb8pajc+Ap4BApsBBRYCAwEABAsJCAcFFQoJ\n" +
"CAsCmQEAACM9AP9APloI2waD5gXsJqzenRVU4n/VmZUvcdUyhlbpab/0HQEAlaTw\n" +
"ZvxVyaf8EMFSJOY+LcgacHaZDHRPA1nS3bIfKwycXQRja/OSEgorBgEEAZdVAQUB\n" +
"AQdA1WL4QKgRxbvzW91ICM6PoICSNh2QHK6j0pIdN/cqXz0DAQgHAAD/bOk3WqbF\n" +
"QAE8xxm0w/KDZzL1N0yPcBQ5z4XKmu77FCgQ04h1BBgWCgAdBQJja/OSAp4BApsM\n" +
"BRYCAwEABAsJCAcFFQoJCAsACgkQCZsRvylqNz6rgQEAzoG6HnPCYi2i2c6/ufuy\n" +
"pBkLby2u1JjD0CWSbrM4dZ0A/j/pI4a9b8LcrZcuY2QwHqsXPAJp8QtOOQN6gTvN\n" +
"WcQNnFgEY2vzkhYJKwYBBAHaRw8BAQdAsxcDCvst/GbWxQvvOpChSvmbqWeuBgm3\n" +
"1vRoujFVFcYAAP9Ww46yfWipb8OivTSX+PvgdUhEeVgxENpsyOQLLhQP/RFziNUE\n" +
"GBYKAH0FAmNr85ICngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/OS\n" +
"AAoJENqfQTmGIR3GtsMBAL+b1Zo5giQKJGEyx5aGwAz3AwtGiT6QDS9FH6HyM855\n" +
"AP4uAXDiaNxYTugqnG471jYX/hhJqIROeDGrEIkkAp+qDwAKCRAJmxG/KWo3PhOX\n" +
"AP45LPV6I4+D3h8etdiEA2DVvNcpRA8l4WkNcq4q8H1SjwD/c/rX3FCUIWLlAHoR\n" +
"WxCFj+gDgqDNLzwoA4iNo1VMtQc=\n" +
"=/Np6\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: 9DA0 9423 C9F9 4BA4 CCA3 0951 099B 11BF 296A 373E\n" +
"Comment: Sigmund <sigmund@pgpainless.org>\n" +
"\n" +
"mDMEY2vzkhYJKwYBBAHaRw8BAQdA+Z2OAFQf0k64Au7hIZfXh/ijclabddvwh7Nh\n" +
"kedJ3ZW0IFNpZ211bmQgPHNpZ211bmRAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEF\n" +
"AmNr85IJEAmbEb8pajc+FiEEnaCUI8n5S6TMowlRCZsRvylqNz4CngECmwEFFgID\n" +
"AQAECwkIBwUVCgkICwKZAQAAIz0A/0A+WgjbBoPmBewmrN6dFVTif9WZlS9x1TKG\n" +
"Vulpv/QdAQCVpPBm/FXJp/wQwVIk5j4tyBpwdpkMdE8DWdLdsh8rDLg4BGNr85IS\n" +
"CisGAQQBl1UBBQEBB0DVYvhAqBHFu/Nb3UgIzo+ggJI2HZAcrqPSkh039ypfPQMB\n" +
"CAeIdQQYFgoAHQUCY2vzkgKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEAmbEb8p\n" +
"ajc+q4EBAM6Buh5zwmItotnOv7n7sqQZC28trtSYw9Alkm6zOHWdAP4/6SOGvW/C\n" +
"3K2XLmNkMB6rFzwCafELTjkDeoE7zVnEDbgzBGNr85IWCSsGAQQB2kcPAQEHQLMX\n" +
"Awr7Lfxm1sUL7zqQoUr5m6lnrgYJt9b0aLoxVRXGiNUEGBYKAH0FAmNr85ICngEC\n" +
"mwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/OSAAoJENqfQTmGIR3GtsMB\n" +
"AL+b1Zo5giQKJGEyx5aGwAz3AwtGiT6QDS9FH6HyM855AP4uAXDiaNxYTugqnG47\n" +
"1jYX/hhJqIROeDGrEIkkAp+qDwAKCRAJmxG/KWo3PhOXAP45LPV6I4+D3h8etdiE\n" +
"A2DVvNcpRA8l4WkNcq4q8H1SjwD/c/rX3FCUIWLlAHoRWxCFj+gDgqDNLzwoA4iN\n" +
"o1VMtQc=\n" +
"=KuJ4\n" +
"-----END PGP PUBLIC KEY BLOCK-----";
private static final String PLAINTEXT = "Hello, World!\n";
private static final String BINARY_SIG = "-----BEGIN PGP SIGNATURE-----\n" +
"Version: PGPainless\n" +
"\n" +
"iHUEABYKACcFAmNr9BgJENqfQTmGIR3GFiEEREwQqwEe+EJMg/Cp2p9BOYYhHcYA\n" +
"AKocAP48P2C3TU33T3Zy73clw0eBa1oW9pwxTGuFxhgOBzmoSwEArj0781GlpTB0\n" +
"Vnr2PjPYEqzB+ZuOzOnGhsVGob4c3Ao=\n" +
"=VWAZ\n" +
"-----END PGP SIGNATURE-----";
private static final String BINARY_SIG_VERIFICATION =
"2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:binary\n";
private static final String TEXT_SIG = "-----BEGIN PGP SIGNATURE-----\n" +
"Version: PGPainless\n" +
"\n" +
"iHUEARYKACcFAmNr9E4JENqfQTmGIR3GFiEEREwQqwEe+EJMg/Cp2p9BOYYhHcYA\n" +
"AG+CAQD1B3GAAlyxahSiGhvJv7YAI1m6qGcI7dIXcV7FkAFPSgEAlZ0UpCC8oGR+\n" +
"hi/mQlex4z0hDWSA4abAjclPTJ+qkAI=\n" +
"=s5xn\n" +
"-----END PGP SIGNATURE-----";
private static final String TEXT_SIG_VERIFICATION =
"2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:text\n";
private static final Date TEXT_SIG_CREATION;
static {
try {
TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z");
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
@Test
public void createArmoredSignature() throws IOException {
File keyFile = writeFile("key.asc", KEY);
pipeStringToStdin(PLAINTEXT);
ByteArrayOutputStream out = pipeStdoutToStream();
assertSuccess(executeCommand("sign", "--as", "text", keyFile.getAbsolutePath()));
assertTrue(out.toString().startsWith("-----BEGIN PGP SIGNATURE-----\n"));
}
@Test
public void createUnarmoredSignature() throws IOException {
File keyFile = writeFile("key.asc", KEY);
pipeStringToStdin(PLAINTEXT);
ByteArrayOutputStream out = pipeStdoutToStream();
assertSuccess(executeCommand("sign", "--no-armor", keyFile.getAbsolutePath()));
assertFalse(out.toString().startsWith("-----BEGIN PGP SIGNATURE-----\n"));
}
@Test
public void unarmorArmoredSigAndVerify() throws IOException {
File certFile = writeFile("cert.asc", CERT);
pipeStringToStdin(BINARY_SIG);
File unarmoredSigFile = pipeStdoutToFile("sig.pgp");
assertSuccess(executeCommand("dearmor"));
pipeStringToStdin(PLAINTEXT);
ByteArrayOutputStream out = pipeStdoutToStream();
assertSuccess(executeCommand("verify", unarmoredSigFile.getAbsolutePath(), certFile.getAbsolutePath()));
assertEquals(BINARY_SIG_VERIFICATION, out.toString());
}
@Test
public void testNotBefore() throws IOException {
File cert = writeFile("cert.asc", CERT);
File sigFile = writeFile("sig.asc", TEXT_SIG);
Date plus1Minute = new Date(TEXT_SIG_CREATION.getTime() + 1000 * 60);
pipeStringToStdin(PLAINTEXT);
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(),
"--not-before", UTCUtil.formatUTCDate(plus1Minute));
assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode);
assertEquals(0, out.size());
Date minus1Minute = new Date(TEXT_SIG_CREATION.getTime() - 1000 * 60);
pipeStringToStdin(PLAINTEXT);
out = pipeStdoutToStream();
exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(),
"--not-before", UTCUtil.formatUTCDate(minus1Minute));
assertSuccess(exitCode);
assertEquals(TEXT_SIG_VERIFICATION, out.toString());
}
@Test
public void testNotAfter() throws IOException {
File cert = writeFile("cert.asc", CERT);
File sigFile = writeFile("sig.asc", TEXT_SIG);
Date minus1Minute = new Date(TEXT_SIG_CREATION.getTime() - 1000 * 60);
pipeStringToStdin(PLAINTEXT);
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(),
"--not-after", UTCUtil.formatUTCDate(minus1Minute));
assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode);
assertEquals(0, out.size());
Date plus1Minute = new Date(TEXT_SIG_CREATION.getTime() + 1000 * 60);
pipeStringToStdin(PLAINTEXT);
out = pipeStdoutToStream();
exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(),
"--not-after", UTCUtil.formatUTCDate(plus1Minute));
assertSuccess(exitCode);
assertEquals(TEXT_SIG_VERIFICATION, out.toString());
}
@Test
public void testSignWithIncapableKey()
throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
.addUserId("Cannot Sign <cannot@sign.key>")
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER))
.addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
.build();
File keyFile = writeFile("key.pgp", secretKeys.getEncoded());
pipeStringToStdin("Hello, World!\n");
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("sign", keyFile.getAbsolutePath());
assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode);
assertEquals(0, out.size());
}
@Test
public void testSignatureCreationAndVerification()
throws IOException {
// Create key and cert
File aliceKeyFile = pipeStdoutToFile("alice.key");
assertSuccess(executeCommand("generate-key", "Alice <alice@pgpainless.org>"));
File aliceCertFile = pipeStdoutToFile("alice.cert");
pipeFileToStdin(aliceKeyFile);
assertSuccess(executeCommand("extract-cert"));
File micalgOut = nonExistentFile("micalg");
String msg = "If privacy is outlawed, only outlaws will have privacy.\n";
File dataFile = writeFile("data", msg);
// sign data
File sigFile = pipeStdoutToFile("sig.asc");
pipeFileToStdin(dataFile);
assertSuccess(executeCommand("sign",
"--armor",
"--as", "binary",
"--micalg-out", micalgOut.getAbsolutePath(),
aliceKeyFile.getAbsolutePath()));
// verify test data signature
pipeFileToStdin(dataFile);
ByteArrayOutputStream verificationsOut = pipeStdoutToStream();
assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), aliceCertFile.getAbsolutePath()));
// Test verification output
PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(readBytesFromFile(aliceCertFile));
KeyRingInfo info = PGPainless.inspectKeyRing(cert);
// [date] [signing-key-fp] [primary-key-fp] signed by [key.pub]
String verification = verificationsOut.toString();
String[] split = verification.split(" ");
OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(cert);
OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(info.getSigningSubkeys().get(0));
assertEquals(signingKeyFingerprint.toString(), split[1].trim(), verification);
assertEquals(primaryKeyFingerprint.toString(), split[2].trim());
// Test micalg output
String content = readStringFromFile(micalgOut);
assertEquals("pgp-sha512", content);
}
private static final String PROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: 738E EAB2 503D 322D 613A C42A B18E 8BF8 884F C050\n" +
"Comment: Axel <axel@pgpainless.org>\n" +
"\n" +
"lIYEY2v6aRYJKwYBBAHaRw8BAQdA3PXtH19zYpVQ9zTU3zlY+iXUptelAO3z4vK/\n" +
"M2FkmrP+CQMCYgVa6K+InVJguITSDIA+HQ6vhOZ5Dbanqx7GFbJbJLD2fWrxhTSr\n" +
"BUWGaUWTqN647auD/kUI8phH1cedVL6CzVR+YWvaWj9zZHr/CYXLobQaQXhlbCA8\n" +
"YXhlbEBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCY2v6aQkQsY6L+IhPwFAWIQRz\n" +
"juqyUD0yLWE6xCqxjov4iE/AUAKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAACq\n" +
"zgEAkxB+dUI7Jjcg5zRvT1EfE9DKCI1qTsxOAU/ZXLcSXLkBAJtWRsyptetZvjzB\n" +
"Ze2A7ArOl4q+IvKvun/d783YyRMInIsEY2v6aRIKKwYBBAGXVQEFAQEHQPFmlZ+o\n" +
"jCGEo2X0474vJfRG7blctuZXmCbC0sLO7MgzAwEIB/4JAwJiBVror4idUmDFhBq4\n" +
"lEhJxjCVc6aSD6+EWRT3YdplqCmNdynnrPombUFst6LfJFzns3H3d0rCeXHfQr93\n" +
"GrHTLkHfW8G3x0PJJPiqFkBviHUEGBYKAB0FAmNr+mkCngECmwwFFgIDAQAECwkI\n" +
"BwUVCgkICwAKCRCxjov4iE/AUNC2AP9WDx4lHt9oYFLSrM8vMLRFI31U8TkYrtCe\n" +
"pYICE76cIAEA5+wEbtE5vQrLxOqIRueVVdzwK9kTeMvSIQfc9PNoyQKchgRja/pp\n" +
"FgkrBgEEAdpHDwEBB0CyAEVlCUbFr3dBBG3MQ84hjCPfYqSx9kYsTN8j5Og6uP4J\n" +
"AwJiBVror4idUmCIFuAYXia0YpEhEpB/Lrn/D6/WAUPEgZjNLMvJzL//EmhkWfEa\n" +
"OfQz/fslj1erWNjLKNiW5C/TvGapDfjbn596AkNlcd1JiNUEGBYKAH0FAmNr+mkC\n" +
"ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/ppAAoJELRgil1uCuQj\n" +
"VUYBAJecbedwwqWQITVqucEBIraTRoc6ZGkN8jytDp8z9CsBAQDrb/W/J/kze6ln\n" +
"nRyJSriWF3SjcKOGIRkUslmdJEPPCQAKCRCxjov4iE/AUAvbAQDBBgQFG8acTT5L\n" +
"cyIi1Ix9/XBG7G23SSs6l7Beap8M+wEAmK13NYuq7Mv/mct8iIKZbBFH9aAiY+nX\n" +
"3Uct4Q5f0w0=\n" +
"=K65R\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
private static final String PASSPHRASE = "orange";
private static final String SIGNING_KEY = "9846F3606EE875FB77EC8808B4608A5D6E0AE423 738EEAB2503D322D613AC42AB18E8BF8884FC050";
@Test
public void signWithProtectedKey_missingPassphraseFails() throws IOException {
File key = writeFile("key.asc", PROTECTED_KEY);
pipeStringToStdin(PLAINTEXT);
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("sign", key.getAbsolutePath());
assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode);
assertEquals(0, out.size());
}
@Test
public void signWithProtectedKey_wrongPassphraseFails() throws IOException {
File password = writeFile("password", "blue");
File key = writeFile("key.asc", PROTECTED_KEY);
pipeStringToStdin(PLAINTEXT);
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("sign", key.getAbsolutePath(),
"--with-key-password", password.getAbsolutePath());
assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode);
assertEquals(0, out.size());
}
@Test
public void signWithProtectedKey() throws IOException {
File password = writeFile("password", PASSPHRASE);
File key = writeFile("key.asc", PROTECTED_KEY);
pipeStringToStdin(PROTECTED_KEY);
File cert = pipeStdoutToFile("cert.asc");
assertSuccess(executeCommand("extract-cert"));
pipeStringToStdin(PLAINTEXT);
File sigFile = pipeStdoutToFile("sig.asc");
assertSuccess(executeCommand("sign", key.getAbsolutePath(),
"--with-key-password", password.getAbsolutePath()));
pipeStringToStdin(PLAINTEXT);
ByteArrayOutputStream verificationOut = pipeStdoutToStream();
assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath()));
assertTrue(verificationOut.toString().contains(SIGNING_KEY));
}
}