Compare commits

...

71 commits
14.0.0 ... main

Author SHA1 Message Date
b28e234f21
ExternalTestSuite: Add license header 2025-07-25 14:09:57 +02:00
cc08b76b68
Introduce sop-java-json-gson module 2025-07-25 14:09:06 +02:00
e680f3450a
SOPV: Document since when operations are available 2025-07-25 12:21:06 +02:00
d4e8c14b08
Update documentation, add withKeyPassphrase(CharArray) methods 2025-07-24 12:53:27 +02:00
d32d9b54d7
Depend on junit-platform-suite to avoid needing to inherit test suite for external-sop tests 2025-07-01 15:06:50 +02:00
c651adc0b3
Add VerificationAssert methods, disable tests for unsupported operations 2025-07-01 14:38:13 +02:00
b3223372c6
SOP-Java 14.0.1-SNAPSHOT 2025-06-17 13:45:10 +02:00
9762f1f043
SOP-Java 14.0.0 2025-06-17 13:45:10 +02:00
191ec8c07d
Improve CHANGELOG again 2025-06-17 13:45:09 +02:00
07d0aa6941
Update CHANGELOG 2025-06-17 13:45:09 +02:00
12835bfb8e
Add/fix missing localizations for new SOP commands 2025-06-17 13:45:09 +02:00
04d154f63d
Version: Fix getSopJavaVersion() 2025-06-17 13:45:09 +02:00
01be696f75
Bump version to 14.0.0-SNAPSHOT 2025-06-17 13:45:09 +02:00
86718a2690
Bump logback to 1.5.13 2025-06-17 13:45:08 +02:00
e72e5a15c0
Fix: Pass chars to StringBuilder.append() 2025-06-17 13:45:08 +02:00
ac17000ff1
Clean up unused version literal 2025-06-17 13:45:08 +02:00
2a22cea29b
Remove animalsniffer 2025-06-17 13:45:08 +02:00
8a7fd5cb58
Move validate-userid to SOPV 2025-06-17 13:45:08 +02:00
21766a1f39
Delete ProxyOutputStream and test 2025-06-17 13:45:07 +02:00
cdcbae7e5f
Add test for JSON data parsing and serializing using a dummy implementation 2025-06-17 13:45:07 +02:00
ebfde35422
Add support for JSON POJOs 2025-06-17 13:45:07 +02:00
bff4423f93
Verification: Rename description to jsonOrDescription 2025-06-17 13:45:07 +02:00
5a7a8ae901
MergeCertsTest: do not pass unarmored data
This is done to fix external-sop tests, which rely on environment variables,
which do not play nicely with binary data
2025-06-17 13:45:07 +02:00
79aece6f04
SOP, SOPV: Add --debug option 2025-06-17 13:45:07 +02:00
28d06c330d
ExternalSOP: Map UnspecificError 2025-06-17 13:45:06 +02:00
ab13cc1de1
CertifyUserIdExternal: add separator before passing keys 2025-06-17 13:45:06 +02:00
e1d048225b
CertifyValidateUserIdTest: unbound User-IDs do throw exceptions 2025-06-17 13:45:06 +02:00
61206dde53
GenerateKeyTest: Provoke exception for CertCannotEncrypt test case 2025-06-17 13:45:06 +02:00
589884672a
External-SOP: Properly map KeyCannotCertify error code 2025-06-17 13:45:06 +02:00
47a6db8702
External-SOP: Fix error message typo 2025-06-17 13:45:05 +02:00
0df80470c6
External-SOP: Extend test suite with new test classes 2025-06-17 13:45:05 +02:00
00a02686c8
External-SOP: Fix command names 2025-06-17 13:45:05 +02:00
c5d9e57f69
Add test for encrypt-decrypt using all available generate-key profiles 2025-06-17 13:45:05 +02:00
e481717421
Add Profile.withAliases() utility function 2025-06-17 13:45:05 +02:00
9677f1fd0b
Fix profile constructors 2025-06-17 13:45:05 +02:00
e5cb58468b
Add aliases to Profile 2025-06-17 13:45:04 +02:00
4ef5444e78
Test key generation with supported profiles 2025-06-17 13:45:04 +02:00
77106942d1
Add tests for MergeCerts command 2025-06-17 13:45:04 +02:00
6d23d3771d
Remove unused import 2025-06-17 13:45:04 +02:00
9360b0e8ce
Remove println statements 2025-06-17 13:45:04 +02:00
38c5a947dd
Add MergeCertsTest 2025-06-17 13:45:03 +02:00
be460fabab
Add test for certifying without ASCII armor 2025-06-17 13:45:03 +02:00
138e275bb6
Fix formatting issues 2025-06-17 13:45:03 +02:00
091b5f9a5e
Add test for certifying with revoked key 2025-06-17 13:45:03 +02:00
dea7e905a9
Document update key 2025-06-17 13:45:03 +02:00
8c077a9c13
SOP update-key: Rename --no-new-mechanisms option to --no-added-capabilities 2025-06-17 13:45:03 +02:00
a8cfb8fbf4
Improve test 2025-06-17 13:45:02 +02:00
a8497617d5
Add basic test for certify-userid and validate-userid subcommands 2025-06-17 13:45:02 +02:00
68bab9cbb4
reuse: convert dep5 file to toml file 2025-06-17 13:45:02 +02:00
65aa0afd4e
Add new Exception types 2025-06-17 13:45:02 +02:00
a72545e3b9
Fix formatting 2025-06-17 13:45:02 +02:00
082cbde869
MergeCertsCmd: Fix default value of armor 2025-06-17 13:45:01 +02:00
b300be42a4
validate-userid: Add --validate-at option 2025-06-17 13:45:01 +02:00
5105b6f4ad
Remove call to explicitly set bundle to fix native image 2025-06-17 13:45:01 +02:00
7f1c1b1aae
Fix documentation of merge-certs command 2025-06-17 13:45:01 +02:00
4cf410a9f9
Bump version 2025-06-17 13:45:01 +02:00
f1bdce99cb
Document endOfOptionsDelimiter 2025-06-17 13:45:00 +02:00
f7cc9ab816
Fix nullability of sop commands 2025-06-17 13:45:00 +02:00
40ccb8cc99
Add first test for new commands 2025-06-17 13:45:00 +02:00
0bb50952c5
Show endOfOptions delimiter in help 2025-06-17 13:45:00 +02:00
69fbfc09a7
Implement external variants of new subcommands 2025-06-17 13:45:00 +02:00
dd12e28926
Checkstyle 2025-06-17 13:45:00 +02:00
4ed326a142
Implement validate-userid command 2025-06-17 13:44:59 +02:00
122cd016a1
Update msg files with input/output information 2025-06-17 13:44:59 +02:00
2b6a5dd651
Checkstyle and exception handling improvements 2025-06-17 13:44:59 +02:00
d6c1330874
Implement certify-userid command 2025-06-17 13:44:59 +02:00
ada77be955
Add support for rendering help info for input and output 2025-06-17 13:44:59 +02:00
84404d629f
Add implementation of merge-certs command 2025-06-17 13:44:58 +02:00
4115a5041d
Add implementation of update-key command 2025-06-17 13:44:58 +02:00
1dcf13244d
Add new exceptions 2025-06-17 13:44:58 +02:00
ad137d6351
Try to fix coveralls repo token 2025-05-15 02:16:52 +02:00
128 changed files with 2840 additions and 586 deletions

View file

@ -17,5 +17,5 @@ steps:
# Code has coverage
- gradle jacocoRootReport coveralls
environment:
coveralls_repo_token:
COVERALLS_REPO_TOKEN:
from_secret: coveralls_repo_token

View file

@ -6,6 +6,25 @@ SPDX-License-Identifier: Apache-2.0
# Changelog
## 14.0.0
- Update implementation to [SOP Specification revision 14](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-14.html),
including changes from revisions `11`, `12`, `13`, `14`.
- Implement newly introduced operations
- `update-key` 'fixes' everything wrong with a key
- `merge-certs` merges a certificate with other copies
- `certify-userid` create signatures over user-ids on certificates
- `validate-userid` validate signatures over user-ids
- Add new exceptions
- `UnspecificFailure` maps generic application errors
- `KeyCannotCertify` signals that a key cannot be used for third-party certifications
- `NoHardwareKeyFound` signals that a key backed by a hardware device cannot be found
- `HardwareKeyFailure` signals a hardware device failure
- `PrimaryKeyBad` signals an unusable or bad primary key
- `CertUserIdNoMatch` signals that a user-id cannot be found/validated on a certificate
- `Verification`: Add support for JSON description extensions
- Remove `animalsniffer` from build dependencies
- Bump `logback` to `1.5.13`
## 10.1.1
- Prepare jar files for use in native images, e.g. using GraalVM by generating and including
configuration files for reflection, resources and dynamic proxies.

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0
# SOP for Java
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java)
[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/)
[![Spec Revision: 14](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/14/)
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main)
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java)

View file

@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
# SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
#
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: CC0-1.0
version = 1
SPDX-PackageName = "SOP-Java"

View file

@ -18,7 +18,6 @@ buildscript {
}
plugins {
id 'ru.vyarus.animalsniffer' version '2.0.0'
id 'org.jetbrains.kotlin.jvm' version "1.9.21"
id 'com.diffplug.spotless' version '6.22.0' apply false
}
@ -35,18 +34,6 @@ allprojects {
apply plugin: 'kotlin-kapt'
apply plugin: 'com.diffplug.spotless'
// For non-cli modules enable android api compatibility check
if (it.name.equals('sop-java')) {
// animalsniffer
apply plugin: 'ru.vyarus.animalsniffer'
dependencies {
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature"
}
animalsniffer {
sourceSets = [sourceSets.main]
}
}
// Only generate jar for submodules
// https://stackoverflow.com/a/25445035
jar {

View file

@ -15,7 +15,9 @@ repositories {
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
testImplementation "org.junit.platform:junit-platform-suite-api:1.13.2"
testRuntimeOnly 'org.junit.platform:junit-platform-suite:1.13.2'
api project(":sop-java")
api "org.slf4j:slf4j-api:$slf4jVersion"

View file

@ -69,6 +69,14 @@ class ExternalSOP(
override fun changeKeyPassword(): ChangeKeyPassword =
ChangeKeyPasswordExternal(binaryName, properties)
override fun updateKey(): UpdateKey = UpdateKeyExternal(binaryName, properties)
override fun mergeCerts(): MergeCerts = MergeCertsExternal(binaryName, properties)
override fun certifyUserId(): CertifyUserId = CertifyUserIdExternal(binaryName, properties)
override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties)
/**
* This interface can be used to provide a directory in which external SOP binaries can
* temporarily store additional results of OpenPGP operations such that the binding classes can
@ -112,6 +120,9 @@ class ExternalSOP(
val errorMessage = readString(errIn)
when (exitCode) {
UnspecificFailure.EXIT_CODE ->
throw UnspecificFailure(
"External SOP backend reported an unspecific error ($exitCode):\n$errorMessage")
NoSignature.EXIT_CODE ->
throw NoSignature(
"External SOP backend reported error NoSignature ($exitCode):\n$errorMessage")
@ -169,6 +180,21 @@ class ExternalSOP(
UnsupportedProfile.EXIT_CODE ->
throw UnsupportedProfile(
"External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage")
NoHardwareKeyFound.EXIT_CODE ->
throw NoHardwareKeyFound(
"External SOP backend reported error NoHardwareKeyFound ($exitCode):\n$errorMessage")
HardwareKeyFailure.EXIT_CODE ->
throw HardwareKeyFailure(
"External SOP backend reported error HardwareKeyFailure ($exitCode):\n$errorMessage")
PrimaryKeyBad.EXIT_CODE ->
throw PrimaryKeyBad(
"External SOP backend reported error PrimaryKeyBad ($exitCode):\n$errorMessage")
CertUserIdNoMatch.EXIT_CODE ->
throw CertUserIdNoMatch(
"External SOP backend reported error CertUserIdNoMatch ($exitCode):\n$errorMessage")
KeyCannotCertify.EXIT_CODE ->
throw KeyCannotCertify(
"External SOP backend reported error KeyCannotCertify ($exitCode):\n$errorMessage")
// Did you forget to add a case for a new exception type?
else ->

View file

@ -10,9 +10,11 @@ import sop.SOPV
import sop.external.ExternalSOP.TempDirProvider
import sop.external.operation.DetachedVerifyExternal
import sop.external.operation.InlineVerifyExternal
import sop.external.operation.ValidateUserIdExternal
import sop.external.operation.VersionExternal
import sop.operation.DetachedVerify
import sop.operation.InlineVerify
import sop.operation.ValidateUserId
import sop.operation.Version
/**
@ -37,6 +39,8 @@ class ExternalSOPV(
override fun inlineVerify(): InlineVerify =
InlineVerifyExternal(binaryName, properties, tempDirProvider)
override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties)
companion object {
/**

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.*
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.CertifyUserId
class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUserId {
private val commandList = mutableListOf(binary, "certify-userid")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCount = 0
private val keys: MutableList<String> = mutableListOf()
override fun noArmor(): CertifyUserId = apply { commandList.add("--no-armor") }
override fun userId(userId: String): CertifyUserId = apply {
commandList.add("--userid")
commandList.add(userId)
}
override fun withKeyPassword(password: ByteArray): CertifyUserId = apply {
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount")
envList.add("KEY_PASSWORD_$argCount=${String(password)}")
argCount += 1
}
override fun noRequireSelfSig(): CertifyUserId = apply {
commandList.add("--no-require-self-sig")
}
override fun keys(keys: InputStream): CertifyUserId = apply {
this.keys.add("@ENV:KEY_$argCount")
envList.add("KEY_$argCount=${ExternalSOP.readString(keys)}")
argCount += 1
}
override fun certs(certs: InputStream): Ready =
ExternalSOP.executeTransformingOperation(
Runtime.getRuntime(), commandList.plus("--").plus(keys), envList, certs)
}

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.*
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.MergeCerts
class MergeCertsExternal(binary: String, environment: Properties) : MergeCerts {
private val commandList = mutableListOf(binary, "merge-certs")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCount = 0
override fun noArmor(): MergeCerts = apply { commandList.add("--no-armor") }
override fun updates(updateCerts: InputStream): MergeCerts = apply {
commandList.add("@ENV:CERT_$argCount")
envList.add("CERT_$argCount=${ExternalSOP.readString(updateCerts)}")
argCount += 1
}
override fun baseCertificates(certs: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, certs)
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.*
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.UpdateKey
class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey {
private val commandList = mutableListOf(binary, "update-key")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCount = 0
override fun noArmor(): UpdateKey = apply { commandList.add("--no-armor") }
override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") }
override fun noAddedCapabilities(): UpdateKey = apply {
commandList.add("--no-added-capabilities")
}
override fun withKeyPassword(password: ByteArray): UpdateKey = apply {
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount")
envList.add("KEY_PASSWORD_$argCount=${String(password)}")
argCount += 1
}
override fun mergeCerts(certs: InputStream): UpdateKey = apply {
commandList.add("--merge-certs")
commandList.add("@ENV:CERT_$argCount")
envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}")
argCount += 1
}
override fun key(key: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, key)
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.*
import sop.external.ExternalSOP
import sop.operation.ValidateUserId
import sop.util.UTCUtil
class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId {
private val commandList = mutableListOf(binary, "validate-userid")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCount = 0
private var userId: String? = null
private val authorities: MutableList<String> = mutableListOf()
override fun addrSpecOnly(): ValidateUserId = apply { commandList.add("--addr-spec-only") }
override fun userId(userId: String): ValidateUserId = apply { this.userId = userId }
override fun authorities(certs: InputStream): ValidateUserId = apply {
this.authorities.add("@ENV:CERT_$argCount")
envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}")
argCount += 1
}
override fun subjects(certs: InputStream): Boolean {
ExternalSOP.executeTransformingOperation(
Runtime.getRuntime(), commandList.plus(userId!!).plus(authorities), envList, certs)
.bytes
return true
}
override fun validateAt(date: Date): ValidateUserId = apply {
commandList.add("--validate-at=${UTCUtil.formatUTCDate(date)}")
}
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external;
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("External SOP Tests")
@SelectPackages("sop.testsuite.operation")
@IncludeClassNamePatterns(".*Test")
public class ExternalTestSuite {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.ArmorDearmorTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalArmorDearmorTest extends ArmorDearmorTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.DecryptWithSessionKeyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalDecryptWithSessionKeyTest extends DecryptWithSessionKeyTest {
}

View file

@ -1,12 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.DetachedSignDetachedVerifyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.EncryptDecryptTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalEncryptDecryptTest extends EncryptDecryptTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.ExtractCertTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalExtractCertTest extends ExtractCertTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.GenerateKeyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalGenerateKeyTest extends GenerateKeyTest {
}

View file

@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.InlineSignInlineDetachDetachedVerifyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalInlineSignInlineDetachDetachedVerifyTest
extends InlineSignInlineDetachDetachedVerifyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.InlineSignInlineVerifyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.ListProfilesTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalListProfilesTest extends ListProfilesTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.RevokeKeyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalRevokeKeyTest extends RevokeKeyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.VersionTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalVersionTest extends VersionTest {
}

View file

@ -7,5 +7,6 @@ rootProject.name = 'SOP-Java'
include 'sop-java',
'sop-java-picocli',
'sop-java-testfixtures',
'external-sop'
'external-sop',
'sop-java-json-gson'

View file

@ -0,0 +1,13 @@
<!--
SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
SPDX-License-Identifier: Apache-2.0
-->
# SOP-Java-JSON-GSON
## JSON Parsing VERIFICATION extension JSON using Gson
Since revision 11, the SOP specification defines VERIFICATIONS extension JSON.
This module implements the `JSONParser` and `JSONSerializer` interfaces using Googles Gson library.

View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
plugins {
id 'java-library'
}
group 'org.pgpainless'
repositories {
mavenCentral()
}
dependencies {
implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
implementation project(":sop-java")
api "org.slf4j:slf4j-api:$slf4jVersion"
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
// @Nonnull, @Nullable...
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
api "com.google.code.gson:gson:$gsonVersion"
}

View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import java.text.ParseException
class GsonParser(
private val gson: Gson = Gson()
) : Verification.JSONParser {
override fun parse(string: String): Verification.JSON {
try {
return gson.fromJson(string, object : TypeToken<Verification.JSON>(){}.type)
} catch (e: JsonSyntaxException) {
throw ParseException(e.message, 0)
}
}
}

View file

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import com.google.gson.Gson
class GsonSerializer(
private val gson: Gson = Gson()
) : Verification.JSONSerializer {
override fun serialize(json: Verification.JSON): String {
return gson.toJson(json)
}
}

View file

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.text.ParseException
class GsonSerializerAndParserTest {
private val serializer: GsonSerializer = GsonSerializer()
private val parser: GsonParser = GsonParser()
@Test
fun simpleSingleTest() {
val before = Verification.JSON("/tmp/alice.pgp")
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"]}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun simpleListTest() {
val before = Verification.JSON(listOf("/tmp/alice.pgp", "/tmp/bob.asc"))
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\",\"/tmp/bob.asc\"]}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun withCommentTest() {
val before = Verification.JSON(
listOf("/tmp/alice.pgp"),
"This is a comment.",
null)
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\"}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun withExtStringTest() {
val before = Verification.JSON(
listOf("/tmp/alice.pgp"),
"This is a comment.",
"This is an ext object string.")
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":\"This is an ext object string.\"}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun withExtListTest() {
val before = Verification.JSON(
listOf("/tmp/alice.pgp"),
"This is a comment.",
listOf(1.0,2.0,3.0))
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":[1.0,2.0,3.0]}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun parseInvalidJSON() {
assertThrows<ParseException> { parser.parse("Invalid") }
}
@Test
fun parseMalformedJSON() {
// Missing '}'
assertThrows<ParseException> { parser.parse("{\"signers\":[\"Alice\"]") }
}
}

View file

@ -27,6 +27,10 @@ import sop.exception.SOPGPException
ChangeKeyPasswordCmd::class,
RevokeKeyCmd::class,
ExtractCertCmd::class,
UpdateKeyCmd::class,
MergeCertsCmd::class,
CertifyUserIdCmd::class,
ValidateUserIdCmd::class,
// Messaging subcommands
SignCmd::class,
VerifyCmd::class,
@ -60,7 +64,7 @@ class SopCLI {
@JvmField var EXECUTABLE_NAME = "sop"
@JvmField
@Option(names = ["--stacktrace"], scope = ScopeType.INHERIT)
@Option(names = ["--stacktrace", "--debug"], scope = ScopeType.INHERIT)
var stacktrace = false
@JvmStatic
@ -83,6 +87,12 @@ class SopCLI {
.apply {
// Hide generate-completion command
subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true)
// render Input/Output sections in help command
subcommands.values
.filter {
(it.getCommand() as Any) is AbstractSopCmd
} // Only for AbstractSopCmd objects
.forEach { (it.getCommand() as AbstractSopCmd).installIORenderer(it) }
// overwrite executable name
commandName = EXECUTABLE_NAME
// setup exception handling

View file

@ -45,7 +45,8 @@ class SopVCLI {
@JvmField var EXECUTABLE_NAME = "sopv"
@JvmField
@CommandLine.Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT)
@CommandLine.Option(
names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT)
var stacktrace = false
@JvmStatic

View file

@ -7,6 +7,11 @@ package sop.cli.picocli.commands
import java.io.*
import java.text.ParseException
import java.util.*
import picocli.CommandLine
import picocli.CommandLine.Help
import picocli.CommandLine.Help.Column
import picocli.CommandLine.Help.TextTable
import picocli.CommandLine.IHelpSectionRenderer
import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver
import sop.exception.SOPGPException.*
import sop.util.UTCUtil.Companion.parseUTCDate
@ -215,11 +220,106 @@ abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable {
}
}
/**
* See
* [Example](https://github.com/remkop/picocli/blob/main/picocli-examples/src/main/java/picocli/examples/customhelp/EnvironmentVariablesSection.java)
*/
class InputOutputHelpSectionRenderer(private val argument: Pair<String?, String?>) :
IHelpSectionRenderer {
override fun render(help: Help): String {
return argument.let {
val calcLen =
help.calcLongOptionColumnWidth(
help.commandSpec().options(),
help.commandSpec().positionalParameters(),
help.colorScheme())
val keyLength =
help
.commandSpec()
.usageMessage()
.longOptionsMaxWidth()
.coerceAtMost(calcLen - 1)
val table =
TextTable.forColumns(
help.colorScheme(),
Column(keyLength + 7, 6, Column.Overflow.SPAN),
Column(width(help) - (keyLength + 7), 0, Column.Overflow.WRAP))
table.setAdjustLineBreaksForWideCJKCharacters(adjustCJK(help))
table.addRowValues("@|yellow ${argument.first}|@", argument.second ?: "")
table.toString()
}
}
private fun adjustCJK(help: Help) =
help.commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()
private fun width(help: Help) = help.commandSpec().usageMessage().width()
}
fun installIORenderer(cmd: CommandLine) {
val inputName = getResString(cmd, "standardInput")
if (inputName != null) {
cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_HEADING] = IHelpSectionRenderer {
getResString(cmd, "standardInputHeading")
}
cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_DETAILS] =
InputOutputHelpSectionRenderer(
inputName to getResString(cmd, "standardInputDescription"))
cmd.helpSectionKeys =
insertKey(
cmd.helpSectionKeys,
SECTION_KEY_STANDARD_INPUT_HEADING,
SECTION_KEY_STANDARD_INPUT_DETAILS)
}
val outputName = getResString(cmd, "standardOutput")
if (outputName != null) {
cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_HEADING] = IHelpSectionRenderer {
getResString(cmd, "standardOutputHeading")
}
cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_DETAILS] =
InputOutputHelpSectionRenderer(
outputName to getResString(cmd, "standardOutputDescription"))
cmd.helpSectionKeys =
insertKey(
cmd.helpSectionKeys,
SECTION_KEY_STANDARD_OUTPUT_HEADING,
SECTION_KEY_STANDARD_OUTPUT_DETAILS)
}
}
private fun insertKey(keys: List<String>, header: String, details: String): List<String> {
val index =
keys.indexOf(CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING)
val result = keys.toMutableList()
result.add(index, header)
result.add(index + 1, details)
return result
}
private fun getResString(cmd: CommandLine, key: String): String? =
try {
cmd.resourceBundle.getString(key)
} catch (m: MissingResourceException) {
try {
cmd.parent.resourceBundle.getString(key)
} catch (m: MissingResourceException) {
null
}
}
?.let { String.format(it) }
companion object {
const val PRFX_ENV = "@ENV:"
const val PRFX_FD = "@FD:"
const val SECTION_KEY_STANDARD_INPUT_HEADING = "standardInputHeading"
const val SECTION_KEY_STANDARD_INPUT_DETAILS = "standardInput"
const val SECTION_KEY_STANDARD_OUTPUT_HEADING = "standardOutputHeading"
const val SECTION_KEY_STANDARD_OUTPUT_DETAILS = "standardOutput"
@JvmField val DAWN_OF_TIME = Date(0)
@JvmField

View file

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.UnsupportedOption
@Command(
name = "certify-userid",
resourceBundle = "msg_certify-userid",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE,
showEndOfOptionsDelimiterInUsageHelp = true)
class CertifyUserIdCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true
@Option(names = ["--userid"], required = true, arity = "1..*", paramLabel = "USERID")
var userIds: List<String> = listOf()
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: List<String> = listOf()
@Option(names = ["--no-require-self-sig"]) var noRequireSelfSig = false
@Parameters(paramLabel = "KEYS", arity = "1..*") var keys: List<String> = listOf()
override fun run() {
val certifyUserId =
throwIfUnsupportedSubcommand(SopCLI.getSop().certifyUserId(), "certify-userid")
if (!armor) {
certifyUserId.noArmor()
}
if (noRequireSelfSig) {
certifyUserId.noRequireSelfSig()
}
for (userId in userIds) {
certifyUserId.userId(userId)
}
for (passwordFileName in withKeyPassword) {
try {
val password = stringFromInputStream(getInput(passwordFileName))
certifyUserId.withKeyPassword(password)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw UnsupportedOption(errorMsg, unsupportedOption)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
for (keyInput in keys) {
try {
getInput(keyInput).use { certifyUserId.keys(it) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput)
throw BadData(errorMsg, badData)
}
}
try {
val ready = certifyUserId.certs(System.`in`)
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN")
throw BadData(errorMsg, badData)
}
}
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine
import picocli.CommandLine.Command
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException
@Command(
name = "merge-certs",
resourceBundle = "msg_merge-certs",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class MergeCertsCmd : AbstractSopCmd() {
@CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = true
@CommandLine.Parameters(paramLabel = "CERTS") var updates: List<String> = listOf()
override fun run() {
val mergeCerts = throwIfUnsupportedSubcommand(SopCLI.getSop().mergeCerts(), "merge-certs")
if (!armor) {
mergeCerts.noArmor()
}
for (certFileName in updates) {
try {
getInput(certFileName).use { mergeCerts.updates(it) }
} catch (e: IOException) {
throw RuntimeException(e)
}
}
try {
val ready = mergeCerts.baseCertificates(System.`in`)
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException.*
@Command(
name = "update-key",
resourceBundle = "msg_update-key",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
class UpdateKeyCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true
@Option(names = ["--signing-only"]) var signingOnly = false
@Option(names = ["--no-added-capabilities"]) var noAddedCapabilities = false
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: List<String> = listOf()
@Option(names = ["--merge-certs"], paramLabel = "CERTS") var mergeCerts: List<String> = listOf()
override fun run() {
val updateKey = throwIfUnsupportedSubcommand(SopCLI.getSop().updateKey(), "update-key")
if (!armor) {
updateKey.noArmor()
}
if (signingOnly) {
updateKey.signingOnly()
}
if (noAddedCapabilities) {
updateKey.noAddedCapabilities()
}
for (passwordFileName in withKeyPassword) {
try {
val password = stringFromInputStream(getInput(passwordFileName))
updateKey.withKeyPassword(password)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw UnsupportedOption(errorMsg, unsupportedOption)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
for (certInput in mergeCerts) {
try {
getInput(certInput).use { updateKey.mergeCerts(it) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput)
throw BadData(errorMsg, badData)
}
}
try {
val ready = updateKey.key(System.`in`)
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN")
throw BadData(errorMsg, badData)
}
}
}

View file

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import java.util.*
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException
import sop.util.HexUtil.Companion.bytesToHex
@Command(
name = "validate-userid",
resourceBundle = "msg_validate-userid",
exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE,
showEndOfOptionsDelimiterInUsageHelp = true)
class ValidateUserIdCmd : AbstractSopCmd() {
@Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false
@Option(names = ["--validate-at"]) var validateAt: Date? = null
@Parameters(index = "0", arity = "1", paramLabel = "USERID") lateinit var userId: String
@Parameters(index = "1..*", arity = "1..*", paramLabel = "CERTS")
var authorities: List<String> = listOf()
override fun run() {
val validateUserId =
throwIfUnsupportedSubcommand(SopCLI.getSop().validateUserId(), "validate-userid")
if (addrSpecOnly) {
validateUserId.addrSpecOnly()
}
if (validateAt != null) {
validateUserId.validateAt(validateAt!!)
}
validateUserId.userId(userId)
for (authority in authorities) {
try {
getInput(authority).use { validateUserId.authorities(it) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (b: SOPGPException.BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", authority)
throw SOPGPException.BadData(errorMsg, b)
}
}
try {
val valid = validateUserId.subjects(System.`in`)
if (!valid) {
val errorMsg = getMsg("sop.error.runtime.any_cert_user_id_no_match", userId)
throw SOPGPException.CertUserIdNoMatch(errorMsg)
}
} catch (e: SOPGPException.CertUserIdNoMatch) {
val errorMsg =
if (e.fingerprint != null) {
getMsg(
"sop.error.runtime.cert_user_id_no_match",
bytesToHex(e.fingerprint!!),
userId)
} else {
getMsg("sop.error.runtime.any_cert_user_id_no_match", userId)
}
throw SOPGPException.CertUserIdNoMatch(errorMsg, e)
} catch (e: SOPGPException.BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", "STDIN")
throw SOPGPException.BadData(errorMsg, e)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -3,9 +3,13 @@
# SPDX-License-Identifier: Apache-2.0
usage.header=Add ASCII Armor to standard input
standardInput=BINARY
standardInputDescription=OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
standardOutput=ARMORED
standardOutputDescription=Same material, but with ASCII-armoring added, if not already present
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -3,9 +3,11 @@
# SPDX-License-Identifier: Apache-2.0
usage.header=Schütze Standard-Eingabe mit ASCII Armor
standardInputDescription=OpenPGP Material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
standardOutputDescription=Dasselbe Material, aber mit ASCII Armor kodiert, falls noch nicht geschehen
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Certify OpenPGP Certificate User IDs
no-armor=ASCII armor the output
userid=Identities that shall be certified
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...).
no-require-self-sig=Certify the UserID regardless of whether self-certifications are present
KEYS[0..*]=Private keys
standardInput=CERTS
standardInputDescription=Certificates that shall be certified
standardOutput=CERTS
standardOutputDescription=Certified certificates
picocli.endofoptions.description=End of options. Remainder are positional parameters. Fixes 'Missing required parameter' error
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Zertifiziere OpenPGP Zertifikat Identitäten
no-armor=Schütze Ausgabe mit ASCII Armor
userid=Identität, die zertifiziert werden soll
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
no-require-self-sig=Zertifiziere die Identität, unabhängig davon, ob eine Selbstzertifizierung vorhanden ist
KEYS[0..*]=Private Schlüssel
standardInputDescription=Zertifikate, auf denen Identitäten zertifiziert werden sollen
standardOutputDescription=Zertifizierte Zertifikate
picocli.endofoptions.description=Ende der Optionen. Der Rest sind Positionsparameter. Behebt 'Missing required parameter' Fehler
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -12,10 +12,15 @@ old-key-password.0=Old passwords to unlock the keys with.
old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys.
old-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
standardInput=KEYS
standardInputDescription=OpenPGP keys whose passphrases shall be changed
standardOutput=KEYS
standardOutputDescription=OpenPGP keys with changed passphrases
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -12,10 +12,13 @@ old-key-password.0=Alte Passw
old-key-password.1=Mehrere Passwortkandidaten können übergeben werden, welche der Reihe nach durchprobiert werden, um Unterschlüssel zu entsperren.
old-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
standardInputDescription=OpenPGP Schlüssel deren Passwörter geändert werden sollen
standardOutputDescription=OpenPGP Schlüssel mit geänderten Passwörtern
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -3,9 +3,14 @@
# SPDX-License-Identifier: Apache-2.0
usage.header=Remove ASCII Armor from standard input
standardInput=ARMORED
standardInputDescription=Armored OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
standardOutput=BINARY
standardOutputDescription=Same material, but with ASCII-armoring removed
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -3,9 +3,12 @@
# SPDX-License-Identifier: Apache-2.0
usage.header=Entferne ASCII Armor von Standard-Eingabe
standardInputDescription=OpenPGP Material mit ASCII Armor (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
standardOutputDescription=Dasselbe Material, aber mit entfernter ASCII Armor
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -22,10 +22,15 @@ 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...).
KEY[0..*]=Secret keys to attempt decryption with
standardInput=CIPHERTEXT
standardInputDescription=Encrypted OpenPGP message
standardOutput=DATA
standardOutputDescription=Decrypted OpenPGP message
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -22,10 +22,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht
standardInputDescription=Verschlüsselte OpenPGP Nachricht
standardOutputDescription=Entschlüsselte OpenPGP Nachricht
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -11,10 +11,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f
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
standardInput=DATA
standardInputDescription=Data that shall be signed
standardOutput=SIGNATURES
standardOutputDescription=Detached OpenPGP signature(s)
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -11,10 +11,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable,
micalg-out=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann.
KEYS[0..*]=Private Signaturschlüssel
standardInputDescription=Daten die signiert werden sollen
standardOutputDescription=Abgetrennte OpenPGP Signatur(en)
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -13,11 +13,16 @@ not-after.3=Accepts special value "-" for end of time.
SIGNATURE[0]=Detached signature
CERT[1..*]=Public key certificates for signature verification
standardInput=DATA
standardInputDescription=Data over which the detached signatures were calculated
standardOutput=VERIFICATIONS
standardOutputDescription=Information about successfully verified signatures
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -13,11 +13,14 @@ not-after.3=Akzeptiert speziellen Wert '-' f
SIGNATURE[0]=Abgetrennte Signatur
CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung
standardInputDescription=Daten, über die die abgetrennten Signaturen erstellt wurden
standardOutputDescription=Informationen über erfolgreich verifizierte Signaturen
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -12,10 +12,15 @@ 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...).
CERTS[0..*]=Certificates the message gets encrypted to
standardInput=DATA
standardInputDescription=Data that shall be encrypted
standardOutput=CIPHERTEXT
standardOutputDescription=Encrypted OpenPGP message
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -12,10 +12,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
CERTS[0..*]=Zertifikate für die die Nachricht verschlüsselt werden soll
standardInputDescription=Daten, die verschlüsselt werden sollen
standardOutputDescription=Verschlüsselte OpenPGP Nachricht
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -5,10 +5,15 @@ usage.header=Extract a public key certificate from a secret key
usage.description=Read a secret key from STDIN and emit the public key certificate to STDOUT.
no-armor=ASCII armor the output
standardInput=KEYS
standardInputDescription=Private key(s), from which certificate(s) shall be extracted
standardOutput=CERTS
standardOutputDescription=Extracted certificate(s)
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -5,10 +5,13 @@ usage.header=Extrahiere Zertifikat (
usage.description=Lese einen Schlüssel von Standard-Eingabe und gebe das Zertifikat auf Standard-Ausgabe aus.
no-armor=Schütze Ausgabe mit ASCII Armor
standardInputDescription=Private Schlüssel, deren Zertifikate extrahiert werden sollen
standardOutputDescription=Extrahierte Zertifikate
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -9,10 +9,13 @@ signing-only=Generate a key that can only be used for signing
with-key-password.0=Password to protect the private key with
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
standardOutput=KEYS
standardOutputDescription=Generated OpenPGP key
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -9,10 +9,12 @@ signing-only=Generiere einen Schl
with-key-password.0=Passwort zum Schutz des privaten Schlüssels
with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
standardOutputDescription=Erzeugter OpenPGP Schlüssel
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -6,6 +6,6 @@ usage.header=Display usage information for the specified subcommand
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -7,5 +7,5 @@ stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -5,9 +5,14 @@ usage.header=Split signatures from a clearsigned message
no-armor=ASCII armor the output
signatures-out=Destination to which a detached signatures block will be written
standardInput=INLINESIGNED
standardInputDescription=Inline-signed OpenPGP message
standardOutput=DATA
standardOutputDescription=The message without any signatures
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -5,9 +5,12 @@ usage.header=Trenne Signaturen von Klartext-signierter Nachricht
no-armor=Schütze Ausgabe mit ASCII Armor
signatures-out=Schreibe abgetrennte Signaturen in Ausgabe
standardInputDescription=Klartext-signierte OpenPGP Nachricht
standardOutputDescription=Nachricht ohne Signaturen
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -13,10 +13,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f
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
standardInput=DATA
standardInputDescription=Data that shall be signed
standardOutput=INLINESIGNED
standardOutputDescription=Inline-signed OpenPGP message
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -13,10 +13,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable,
micalg=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann.
KEYS[0..*]=Private Signaturschlüssel
standardInputDescription=Daten, die signiert werden sollen
standardOutputDescription=Inline-signierte OpenPGP Nachricht
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -12,10 +12,15 @@ not-after.3=Accepts special value "-" for end of time.
verifications-out=File to write details over successful verifications to
CERT[0..*]=Public key certificates for signature verification
standardInput=INLINESIGNED
standardInputDescription=Inline-signed OpenPGP message
standardOutput=DATA
standardOutputDescription=The message without any signatures
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -12,10 +12,13 @@ not-after.3=Akzeptiert speziellen Wert '-' f
verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe
CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung
standardInputDescription=Inline-signierte OpenPGP Nachricht
standardOutputDescription=Nachricht ohne Signaturen
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -4,10 +4,13 @@
usage.header=Emit a list of profiles supported by the identified subcommand
subcommand=Subcommand for which to list profiles
standardOutput=PROFILELIST
standardOutputDescription=List of profiles supported by the identified subcommand
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -4,10 +4,12 @@
usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden
subcommand=Unterbefehl, für welchen Profile gelistet werden sollen
standardOutputDescription=Liste von Profilen, die der identifizierte Unterbefehl unterstützt
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.headerHeading=Merge OpenPGP certificates%n
usage.header=Merge OpenPGP certificates from standard input with related elements from CERTS and emit the result to standard output
usage.description=Only certificates that were part of standard input will be emitted to standard output
no-armor=ASCII armor the output
CERTS[0..*]=OpenPGP certificates from which updates shall be merged into the base certificates from standard input
standardInput=CERTS
standardInputDescription=Base certificates into which additional elements from the command line shall be merged
standardOutput=CERTS
standardOutputDescription=Merged certificates
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.descriptionHeading=%nNote:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.headerHeading=OpenPGP Zertifikate zusammenführen%n
usage.header=Führe OpenPGP Zertifikate aus der Standardeingabe mit ensprechenden Elementen aus CERTS zusammen und gebe das Ergebnis auf der Standardausgabe aus
usage.description=Es werden nur Zertifikate auf die Standardausgabe geschrieben, welche Teil der Standardeingabe waren
no-armor=Schütze Ausgabe mit ASCII Armor
CERTS[0..*]=OpenPGP Zertifikate aus denen neue Elemente in die Basiszertifikate aus der Standardeingabe übernommen werden sollen
standardInputDescription=Basis-Zertifikate, in welche zusätzliche Elemente von der Kommandozeile zusammengeführt werden sollen
standardOutputDescription=Zusammengeführte Zertifikate
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.descriptionHeading=%nHinweis:%n
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -7,10 +7,15 @@ no-armor=ASCII armor the output
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...).
standardInput=KEYS
standardInputDescription=OpenPGP key that shall be revoked
standardOutput=CERTS
standardOutputDescription=Revocation certificate
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n
usage.descriptionHeading=D%nescription:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -7,10 +7,13 @@ no-armor=Sch
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
standardInputDescription=OpenPGP Schlüssel, der widerrufen werden soll
standardOutputDescription=Widerrufszertifikat
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -9,10 +9,14 @@ locale=Locale for description texts
# Generic
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.parameterListHeading=%nParameters:%n
usage.footerHeading=Powered by picocli%n
standardInputHeading=%nInput:%n
standardOutputHeading=%nOutput:%n
# Exit Codes
usage.exitCodeListHeading=%nExit Codes:%n
usage.exitCodeList.0=\u00200:Successful program execution
@ -38,6 +42,8 @@ usage.exitCodeList.19=83:Options were supplied that are incompatible with each o
usage.exitCodeList.20=89:The requested profile is unsupported, or the indicated subcommand does not accept profiles
usage.exitCodeList.21=97:The implementation supports some form of hardware-backed secret keys, but could not identify the hardware device
usage.exitCodeList.22=101:The implementation tried to use a hardware-backed secret key, but the cryptographic hardware refused the operation for some reason other than a bad PIN or password
usage.exitCodeList.23=103:The primary key of a KEYS object is too weak or revoked
usage.exitCodeList.24=107:The CERTS object has no matching User ID
## SHARED RESOURCES
stacktrace=Print stacktrace
@ -74,6 +80,8 @@ sop.error.runtime.cert_cannot_encrypt=Certificate from input '%s' cannot encrypt
sop.error.runtime.no_session_key_extracted=Session key not extracted. Feature potentially not supported.
sop.error.runtime.no_verifiable_signature_found=No verifiable signature found.
sop.error.runtime.cannot_decrypt_message=Message could not be decrypted.
sop.error.runtime.cert_user_id_no_match=Certificate '%s' does not contain a valid binding for user id '%s'.
sop.error.runtime.any_cert_user_id_no_match=Any certificate does not contain a valid binding for user id '%s'.
## Usage errors
sop.error.usage.password_or_cert_required=At least one password file or cert file required for encryption.
sop.error.usage.argument_required=Argument '%s' is required.

View file

@ -10,9 +10,13 @@ locale=Gebietsschema f
# Generic
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.parameterListHeading=%nParameter:%n
usage.footerHeading=Powered by Picocli%n
standardInputHeading=%nEingabe:%n
standardOutputHeading=%nAusgabe:%n
# Exit Codes
usage.exitCodeListHeading=%nExit Codes:%n
usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung
@ -38,6 +42,8 @@ usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert
usage.exitCodeList.20=89:Das angeforderte Profil wird nicht unterstützt, oder der angegebene Unterbefehl akzeptiert keine Profile
usage.exitCodeList.21=97:Die Anwendung unterstützt hardwaregestützte private Schlüssel, aber kann das Gerät nicht identifizieren
usage.exitCodeList.22=101:Die Anwendung versuchte, einen hardwaregestützten Schlüssel zu verwenden, aber das Gerät lehnte den Vorgang aus einem anderen Grund als einer falschen PIN oder einem falschen Passwort ab
usage.exitCodeList.23=103:Der primäre private Schlüssel ist zu schwach oder widerrufen
usage.exitCodeList.24=107:Das Zertifikat hat keine übereinstimmende User ID
## SHARED RESOURCES
stacktrace=Stacktrace ausgeben

View file

@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Keep a secret key up-to-date
no-armor=ASCII armor the output
signing-only=TODO: Document
no-added-capabilities=Do not add feature support for new mechanisms, which the key did not previously support
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...).
merge-certs.0=Merge additional elements found in the corresponding CERTS objects into the updated secret keys
merge-certs.1=This can be used, for example, to absorb a third-party certification into the Transferable Secret Key
standardInput=KEYS
standardInputDescription=OpenPGP key that shall be kept up-to-date
standardOutput=KEYS
standardOutputDescription=Updated OpenPGP key
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Halte einen Schlüssel auf dem neusten Stand
no-armor=Schütze Ausgabe mit ASCII Armor
signing-only=TODO: Dokumentieren
no-added-capabilities=Füge keine neuen Funktionen hinzu, die der Schlüssel nicht bereits zuvor unterstützt hat
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
merge-certs.0=Führe zusätzliche Elemente aus entsprechenden CERTS Objekten mit dem privaten Schlüssel zusammen
merge-certs.1=Dies kann zum Beispiel dazu genutzt werden, Zertifizierungen dritter in den privaten Schlüssel zu übernehmen
standardInputDescription=OpenPGP Schlüssel, der auf den neusten Stand gebracht werden soll
standardOutputDescription=Erneuerter OpenPGP Schlüssel
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Validate a UserID in an OpenPGP certificate
addr-spec-only=Treat the USERID as an email address, match only against the email address part of each correctly bound UserID
USERID[0]=UserID
CERTS[1..*]=Authority OpenPGP certificates
standardInput=CERTS
standardInputDescription=OpenPGP certificates in which UserID bindings shall be validated
picocli.endofoptions.description=End of options. Remainder are positional parameters. Fixes 'Missing required parameter' error
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Validiere eine UserID auf OpenPGP Zertifikaten
addr-spec-only=Behandle die USERID als E-Mail-Adresse, vergleiche sie nur mit dem E-Mail-Adressen-Teil jeder korrekten UserID
USERID[0]=UserID
CERTS[1..*]=Autoritäre OpenPGP Zertifikate
standardInput=CERTS
standardInputDescription=OpenPGP Zertifikate auf denen UserIDs validiert werden sollen
picocli.endofoptions.description=Ende der Optionen. Der Rest sind Positionsparameter. Behebt 'Missing required parameter' Fehler
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -5,10 +5,13 @@ usage.header=Display version information about the tool
extended=Print an extended version string
backend=Print information about the cryptographic backend
sop-spec=Print the latest revision of the SOP specification targeted by the implementation
sopv=Print the SOPV API version
standardOutput=version information
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -5,10 +5,13 @@ usage.header=Zeige Versionsinformationen
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
sopv=Gebe die SOPV API Version aus
standardOutput=Versionsinformationen
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test;
import sop.SOP;
import sop.exception.SOPGPException;
import sop.operation.Armor;
import sop.operation.CertifyUserId;
import sop.operation.ChangeKeyPassword;
import sop.operation.Dearmor;
import sop.operation.Decrypt;
@ -29,7 +30,10 @@ import sop.operation.InlineVerify;
import sop.operation.DetachedSign;
import sop.operation.DetachedVerify;
import sop.operation.ListProfiles;
import sop.operation.MergeCerts;
import sop.operation.RevokeKey;
import sop.operation.UpdateKey;
import sop.operation.ValidateUserId;
import sop.operation.Version;
public class SOPTest {
@ -52,6 +56,26 @@ public class SOPTest {
@Test
public void UnsupportedSubcommandsTest() {
SOP nullCommandSOP = new SOP() {
@Override
public ValidateUserId validateUserId() {
return null;
}
@Override
public CertifyUserId certifyUserId() {
return null;
}
@Override
public MergeCerts mergeCerts() {
return null;
}
@Override
public UpdateKey updateKey() {
return null;
}
@Override
public Version version() {
return null;
@ -140,6 +164,11 @@ public class SOPTest {
commands.add(new String[] {"sign"});
commands.add(new String[] {"verify", "signature.asc", "cert.asc"});
commands.add(new String[] {"version"});
commands.add(new String[] {"list-profiles", "generate-key"});
commands.add(new String[] {"certify-userid", "--userid", "Alice <alice@pgpainless.org>", "--", "alice.pgp"});
commands.add(new String[] {"validate-userid", "Alice <alice@pgpainless.org>", "bob.pgp", "--", "alice.pgp"});
commands.add(new String[] {"update-key"});
commands.add(new String[] {"merge-certs"});
for (String[] command : commands) {
int exit = SopCLI.execute(command);

View file

@ -8,9 +8,13 @@ import sop.Verification;
import sop.enums.SignatureMode;
import sop.testsuite.JUtils;
import java.text.ParseException;
import java.util.Date;
import java.util.function.Predicate;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public final class VerificationAssert {
@ -45,18 +49,39 @@ public final class VerificationAssert {
}
public VerificationAssert hasDescription(String description) {
assertEquals(description, verification.getDescription().get());
assertEquals(description, verification.getJsonOrDescription().get());
return this;
}
public VerificationAssert hasDescriptionOrNull(String description) {
if (verification.getDescription().isEmpty()) {
if (verification.getJsonOrDescription().isEmpty()) {
return this;
}
return hasDescription(description);
}
public VerificationAssert hasValidJSONOrNull(Verification.JSONParser parser)
throws ParseException {
if (!verification.getJsonOrDescription().isPresent()) {
// missing description
return this;
}
return hasJSON(parser, null);
}
public VerificationAssert hasJSON(Verification.JSONParser parser, Predicate<Verification.JSON> predicate) {
assertTrue(verification.getContainsJson(), "Verification does not appear to contain JSON extension");
Verification.JSON json = verification.getJson(parser);
assertNotNull(verification.getJson(parser), "Verification does not appear to contain valid JSON extension.");
if (predicate != null) {
assertTrue(predicate.test(json), "JSON object does not match predicate.");
}
return this;
}
public VerificationAssert hasMode(SignatureMode mode) {
assertEquals(mode, verification.getSignatureMode().get());
return this;

View file

@ -4,10 +4,13 @@
package sop.testsuite.operation;
import kotlin.jvm.functions.Function0;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.provider.Arguments;
import sop.SOP;
import sop.exception.SOPGPException;
import sop.testsuite.AbortOnUnsupportedOption;
import sop.testsuite.AbortOnUnsupportedOptionExtension;
import sop.testsuite.SOPInstanceFactory;
@ -18,6 +21,8 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@ExtendWith(AbortOnUnsupportedOptionExtension.class)
@AbortOnUnsupportedOption
public abstract class AbstractSOPTest {
@ -51,6 +56,17 @@ public abstract class AbstractSOPTest {
}
}
public <T> T assumeSupported(Function0<T> f) {
try {
T t = f.invoke();
assumeTrue(t != null, "Unsupported operation.");
return t;
} catch (SOPGPException.UnsupportedSubcommand e) {
assumeTrue(false, e.getMessage());
return null;
}
}
public static Stream<Arguments> provideBackends() {
return backends.stream();
}

View file

@ -20,7 +20,7 @@ import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ArmorDearmorTest {
public class ArmorDearmorTest extends AbstractSOPTest {
static Stream<Arguments> provideInstances() {
return AbstractSOPTest.provideBackends();
@ -31,13 +31,13 @@ public class ArmorDearmorTest {
public void dearmorArmorAliceKey(SOP sop) throws IOException {
byte[] aliceKey = TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8);
byte[] dearmored = sop.dearmor()
byte[] dearmored = assumeSupported(sop::dearmor)
.data(aliceKey)
.getBytes();
Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK));
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(dearmored)
.getBytes();
@ -52,13 +52,13 @@ public class ArmorDearmorTest {
public void dearmorArmorAliceCert(SOP sop) throws IOException {
byte[] aliceCert = TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8);
byte[] dearmored = sop.dearmor()
byte[] dearmored = assumeSupported(sop::dearmor)
.data(aliceCert)
.getBytes();
Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK));
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(dearmored)
.getBytes();
@ -73,13 +73,13 @@ public class ArmorDearmorTest {
public void dearmorArmorBobKey(SOP sop) throws IOException {
byte[] bobKey = TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8);
byte[] dearmored = sop.dearmor()
byte[] dearmored = assumeSupported(sop::dearmor)
.data(bobKey)
.getBytes();
Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK));
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(dearmored)
.getBytes();
@ -94,13 +94,13 @@ public class ArmorDearmorTest {
public void dearmorArmorBobCert(SOP sop) throws IOException {
byte[] bobCert = TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8);
byte[] dearmored = sop.dearmor()
byte[] dearmored = assumeSupported(sop::dearmor)
.data(bobCert)
.getBytes();
Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK));
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(dearmored)
.getBytes();
@ -115,13 +115,13 @@ public class ArmorDearmorTest {
public void dearmorArmorCarolKey(SOP sop) throws IOException {
byte[] carolKey = TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8);
byte[] dearmored = sop.dearmor()
byte[] dearmored = assumeSupported(sop::dearmor)
.data(carolKey)
.getBytes();
Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK));
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(dearmored)
.getBytes();
@ -136,13 +136,13 @@ public class ArmorDearmorTest {
public void dearmorArmorCarolCert(SOP sop) throws IOException {
byte[] carolCert = TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8);
byte[] dearmored = sop.dearmor()
byte[] dearmored = assumeSupported(sop::dearmor)
.data(carolCert)
.getBytes();
Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK));
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(dearmored)
.getBytes();
@ -163,13 +163,13 @@ public class ArmorDearmorTest {
"CePQFpprprnGEzpE3flQLUc=\n" +
"=ZiFR\n" +
"-----END PGP MESSAGE-----\n").getBytes(StandardCharsets.UTF_8);
byte[] dearmored = sop.dearmor()
byte[] dearmored = assumeSupported(sop::dearmor)
.data(message)
.getBytes();
Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_MESSAGE));
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(dearmored)
.getBytes();
@ -191,13 +191,13 @@ public class ArmorDearmorTest {
"=GHvQ\n" +
"-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8);
byte[] dearmored = sop.dearmor()
byte[] dearmored = assumeSupported(sop::dearmor)
.data(signature)
.getBytes();
Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_SIGNATURE));
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(dearmored)
.getBytes();
@ -210,11 +210,11 @@ public class ArmorDearmorTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void testDearmoringTwiceIsIdempotent(SOP sop) throws IOException {
byte[] dearmored = sop.dearmor()
byte[] dearmored = assumeSupported(sop::dearmor)
.data(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.getBytes();
byte[] dearmoredAgain = sop.dearmor()
byte[] dearmoredAgain = assumeSupported(sop::dearmor)
.data(dearmored)
.getBytes();
@ -233,7 +233,7 @@ public class ArmorDearmorTest {
"=GHvQ\n" +
"-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8);
byte[] armoredAgain = sop.armor()
byte[] armoredAgain = assumeSupported(sop::armor)
.data(armored)
.getBytes();

View file

@ -0,0 +1,193 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import sop.SOP;
import sop.exception.SOPGPException;
import java.io.IOException;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class CertifyValidateUserIdTest extends AbstractSOPTest {
static Stream<Arguments> provideInstances() {
return AbstractSOPTest.provideBackends();
}
@ParameterizedTest
@MethodSource("provideInstances")
public void certifyUserId(SOP sop) throws IOException {
byte[] aliceKey = assumeSupported(sop::generateKey)
.withKeyPassword("sw0rdf1sh")
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] aliceCert = assumeSupported(sop::extractCert)
.key(aliceKey)
.getBytes();
byte[] bobKey = assumeSupported(sop::generateKey)
.userId("Bob <bob@pgpainless.org>")
.generate()
.getBytes();
byte[] bobCert = assumeSupported(sop::extractCert)
.key(bobKey)
.getBytes();
// Alice has her own user-id self-certified
assertTrue(assumeSupported(sop::validateUserId)
.authorities(aliceCert)
.userId("Alice <alice@pgpainless.org>")
.subjects(aliceCert),
"Alice accepts her own self-certified user-id");
// Alice has not yet certified Bobs user-id
assertThrows(SOPGPException.CertUserIdNoMatch.class, () ->
assumeSupported(sop::validateUserId)
.authorities(aliceCert)
.userId("Bob <bob@pgpainless.org>")
.subjects(bobCert),
"Alice has not yet certified Bobs user-id");
byte[] bobCertifiedByAlice = assumeSupported(sop::certifyUserId)
.userId("Bob <bob@pgpainless.org>")
.withKeyPassword("sw0rdf1sh")
.keys(aliceKey)
.certs(bobCert)
.getBytes();
assertTrue(assumeSupported(sop::validateUserId)
.userId("Bob <bob@pgpainless.org>")
.authorities(aliceCert)
.subjects(bobCertifiedByAlice),
"Alice accepts Bobs user-id after she certified it");
}
@ParameterizedTest
@MethodSource("provideInstances")
public void certifyUserIdUnarmored(SOP sop) throws IOException {
byte[] aliceKey = assumeSupported(sop::generateKey)
.noArmor()
.withKeyPassword("sw0rdf1sh")
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] aliceCert = assumeSupported(sop::extractCert)
.noArmor()
.key(aliceKey)
.getBytes();
byte[] bobKey = assumeSupported(sop::generateKey)
.noArmor()
.userId("Bob <bob@pgpainless.org>")
.generate()
.getBytes();
byte[] bobCert = assumeSupported(sop::extractCert)
.noArmor()
.key(bobKey)
.getBytes();
byte[] bobCertifiedByAlice = assumeSupported(sop::certifyUserId)
.noArmor()
.userId("Bob <bob@pgpainless.org>")
.withKeyPassword("sw0rdf1sh")
.keys(aliceKey)
.certs(bobCert)
.getBytes();
assertTrue(assumeSupported(sop::validateUserId)
.userId("Bob <bob@pgpainless.org>")
.authorities(aliceCert)
.subjects(bobCertifiedByAlice),
"Alice accepts Bobs user-id after she certified it");
}
@ParameterizedTest
@MethodSource("provideInstances")
public void addPetName(SOP sop) throws IOException {
byte[] aliceKey = assumeSupported(sop::generateKey)
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] aliceCert = assumeSupported(sop::extractCert)
.key(aliceKey)
.getBytes();
byte[] bobKey = assumeSupported(sop::generateKey)
.userId("Bob <bob@pgpainless.org>")
.generate()
.getBytes();
byte[] bobCert = assumeSupported(sop::extractCert)
.key(bobKey)
.getBytes();
assertThrows(SOPGPException.CertUserIdNoMatch.class, () ->
assumeSupported(sop::certifyUserId)
.userId("Bobby")
.keys(aliceKey)
.certs(bobCert)
.getBytes(),
"Alice cannot create a pet-name for Bob without the --no-require-self-sig flag");
byte[] bobWithPetName = assumeSupported(sop::certifyUserId)
.userId("Bobby")
.noRequireSelfSig()
.keys(aliceKey)
.certs(bobCert)
.getBytes();
assertTrue(assumeSupported(sop::validateUserId)
.userId("Bobby")
.authorities(aliceCert)
.subjects(bobWithPetName),
"Alice accepts the pet-name she gave to Bob");
assertThrows(SOPGPException.CertUserIdNoMatch.class, () ->
assumeSupported(sop::validateUserId)
.userId("Bobby")
.authorities(bobWithPetName)
.subjects(bobWithPetName),
"Bob does not accept the pet-name Alice gave him");
}
@ParameterizedTest
@MethodSource("provideInstances")
public void certifyWithRevokedKey(SOP sop) throws IOException {
byte[] aliceKey = assumeSupported(sop::generateKey)
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] aliceRevokedCert = assumeSupported(sop::revokeKey)
.keys(aliceKey)
.getBytes();
byte[] aliceRevokedKey = assumeSupported(sop::updateKey)
.mergeCerts(aliceRevokedCert)
.key(aliceKey)
.getBytes();
byte[] bobKey = assumeSupported(sop::generateKey)
.userId("Bob <bob@pgpainless.org>")
.generate()
.getBytes();
byte[] bobCert = assumeSupported(sop::extractCert)
.key(bobKey)
.getBytes();
assertThrows(SOPGPException.KeyCannotCertify.class, () ->
assumeSupported(sop::certifyUserId)
.userId("Bob <bob@pgpainless.org>")
.keys(aliceRevokedKey)
.certs(bobCert)
.getBytes());
}
}

View file

@ -32,18 +32,18 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void changePasswordFromUnprotectedToProtected(SOP sop) throws IOException {
byte[] unprotectedKey = sop.generateKey().generate().getBytes();
byte[] unprotectedKey = assumeSupported(sop::generateKey).generate().getBytes();
byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8);
byte[] protectedKey = sop.changeKeyPassword().newKeyPassphrase(password).keys(unprotectedKey).getBytes();
byte[] protectedKey = assumeSupported(sop::changeKeyPassword).newKeyPassphrase(password).keys(unprotectedKey).getBytes();
sop.sign().withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8));
assumeSupported(sop::sign).withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8));
}
@ParameterizedTest
@MethodSource("provideInstances")
public void changePasswordFromUnprotectedToUnprotected(SOP sop) throws IOException {
byte[] unprotectedKey = sop.generateKey().noArmor().generate().getBytes();
byte[] stillUnprotectedKey = sop.changeKeyPassword().noArmor().keys(unprotectedKey).getBytes();
byte[] unprotectedKey = assumeSupported(sop::generateKey).noArmor().generate().getBytes();
byte[] stillUnprotectedKey = assumeSupported(sop::changeKeyPassword).noArmor().keys(unprotectedKey).getBytes();
assertArrayEquals(unprotectedKey, stillUnprotectedKey);
}
@ -52,12 +52,12 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void changePasswordFromProtectedToUnprotected(SOP sop) throws IOException {
byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8);
byte[] protectedKey = sop.generateKey().withKeyPassword(password).generate().getBytes();
byte[] unprotectedKey = sop.changeKeyPassword()
byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(password).generate().getBytes();
byte[] unprotectedKey = assumeSupported(sop::changeKeyPassword)
.oldKeyPassphrase(password)
.keys(protectedKey).getBytes();
sop.sign().key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8));
assumeSupported(sop::sign).key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8));
}
@ParameterizedTest
@ -65,13 +65,13 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest {
public void changePasswordFromProtectedToDifferentProtected(SOP sop) throws IOException {
byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8);
byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8);
byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes();
byte[] reprotectedKey = sop.changeKeyPassword()
byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes();
byte[] reprotectedKey = assumeSupported(sop::changeKeyPassword)
.oldKeyPassphrase(oldPassword)
.newKeyPassphrase(newPassword)
.keys(protectedKey).getBytes();
sop.sign().key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8));
assumeSupported(sop::sign).key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8));
}
@ -82,8 +82,8 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest {
byte[] newPassword = "monkey123".getBytes(UTF8Util.UTF8);
byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8);
byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes();
assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.changeKeyPassword()
byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes();
assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::changeKeyPassword)
.oldKeyPassphrase(wrongPassword)
.newKeyPassphrase(newPassword)
.keys(protectedKey).getBytes());
@ -93,9 +93,9 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void nonUtf8PasswordsFail(SOP sop) {
assertThrows(SOPGPException.PasswordNotHumanReadable.class, () ->
sop.changeKeyPassword().oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe}));
assumeSupported(sop::changeKeyPassword).oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe}));
assertThrows(SOPGPException.PasswordNotHumanReadable.class, () ->
sop.changeKeyPassword().newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe}));
assumeSupported(sop::changeKeyPassword).newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe}));
}
@ -104,16 +104,16 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest {
public void testNoArmor(SOP sop) throws IOException {
byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8);
byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8);
byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes();
byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes();
byte[] armored = sop.changeKeyPassword()
byte[] armored = assumeSupported(sop::changeKeyPassword)
.oldKeyPassphrase(oldPassword)
.newKeyPassphrase(newPassword)
.keys(protectedKey)
.getBytes();
JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK);
byte[] unarmored = sop.changeKeyPassword()
byte[] unarmored = assumeSupported(sop::changeKeyPassword)
.noArmor()
.oldKeyPassphrase(oldPassword)
.newKeyPassphrase(newPassword)

View file

@ -41,7 +41,7 @@ public class DecryptWithSessionKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void testDecryptAndExtractSessionKey(SOP sop) throws IOException {
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
ByteArrayAndResult<DecryptionResult> bytesAndResult = assumeSupported(sop::decrypt)
.withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8))
.toByteArrayAndResult();
@ -54,7 +54,7 @@ public class DecryptWithSessionKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void testDecryptWithSessionKey(SOP sop) throws IOException {
byte[] decrypted = sop.decrypt()
byte[] decrypted = assumeSupported(sop::decrypt)
.withSessionKey(SessionKey.fromString(SESSION_KEY))
.ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8))
.toByteArrayAndResult()

View file

@ -37,13 +37,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
public void signVerifyWithAliceKey(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signature = sop.detachedSign()
byte[] signature = assumeSupported(sop::detachedSign)
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.toByteArrayAndResult()
.getBytes();
List<Verification> verificationList = sop.detachedVerify()
List<Verification> verificationList = assumeSupported(sop::detachedVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signature)
.data(message);
@ -60,14 +60,14 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
public void signVerifyTextModeWithAliceKey(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signature = sop.detachedSign()
byte[] signature = assumeSupported(sop::detachedSign)
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(SignAs.text)
.data(message)
.toByteArrayAndResult()
.getBytes();
List<Verification> verificationList = sop.detachedVerify()
List<Verification> verificationList = assumeSupported(sop::detachedVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signature)
.data(message);
@ -85,7 +85,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8);
List<Verification> verificationList = sop.detachedVerify()
List<Verification> verificationList = assumeSupported(sop::detachedVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signature)
.data(message);
@ -101,13 +101,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
public void signVerifyWithBobKey(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signature = sop.detachedSign()
byte[] signature = assumeSupported(sop::detachedSign)
.key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.toByteArrayAndResult()
.getBytes();
List<Verification> verificationList = sop.detachedVerify()
List<Verification> verificationList = assumeSupported(sop::detachedVerify)
.cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signature)
.data(message);
@ -123,13 +123,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
public void signVerifyWithCarolKey(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signature = sop.detachedSign()
byte[] signature = assumeSupported(sop::detachedSign)
.key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.toByteArrayAndResult()
.getBytes();
List<Verification> verificationList = sop.detachedVerify()
List<Verification> verificationList = assumeSupported(sop::detachedVerify)
.cert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signature)
.data(message);
@ -145,7 +145,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
public void signVerifyWithEncryptedKey(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signature = sop.detachedSign()
byte[] signature = assumeSupported(sop::detachedSign)
.key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8))
.withKeyPassword(TestData.PASSWORD)
.data(message)
@ -154,7 +154,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
JUtils.assertArrayStartsWith(signature, TestData.BEGIN_PGP_SIGNATURE);
List<Verification> verificationList = sop.detachedVerify()
List<Verification> verificationList = assumeSupported(sop::detachedVerify)
.cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signature)
.data(message);
@ -170,18 +170,18 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
public void signArmorVerifyWithBobKey(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signature = sop.detachedSign()
byte[] signature = assumeSupported(sop::detachedSign)
.key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8))
.noArmor()
.data(message)
.toByteArrayAndResult()
.getBytes();
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(signature)
.getBytes();
List<Verification> verificationList = sop.detachedVerify()
List<Verification> verificationList = assumeSupported(sop::detachedVerify)
.cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(armored)
.data(message);
@ -199,7 +199,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8);
Date beforeSignature = new Date(TestData.ALICE_DETACHED_SIGNED_MESSAGE_DATE.getTime() - 1000); // 1 sec before sig
assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify()
assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.notAfter(beforeSignature)
.signatures(signature)
@ -213,7 +213,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8);
Date afterSignature = new Date(TestData.ALICE_DETACHED_SIGNED_MESSAGE_DATE.getTime() + 1000); // 1 sec after sig
assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify()
assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.notBefore(afterSignature)
.signatures(signature)
@ -224,13 +224,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void signWithAliceVerifyWithBobThrowsNoSignature(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signatures = sop.detachedSign()
byte[] signatures = assumeSupported(sop::detachedSign)
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.toByteArrayAndResult()
.getBytes();
assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify()
assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify)
.cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signatures)
.data(message));
@ -240,7 +240,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void signVerifyWithEncryptedKeyWithoutPassphraseFails(SOP sop) {
assertThrows(SOPGPException.KeyIsProtected.class, () ->
sop.detachedSign()
assumeSupported(sop::detachedSign)
.key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8))
.data(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))
.toByteArrayAndResult()
@ -253,7 +253,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signature = sop.sign()
byte[] signature = assumeSupported(sop::sign)
.key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8))
.withKeyPassword("wrong")
.withKeyPassword(TestData.PASSWORD) // correct
@ -262,7 +262,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
.toByteArrayAndResult()
.getBytes();
List<Verification> verificationList = sop.verify()
List<Verification> verificationList = assumeSupported(sop::verify)
.cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signature)
.data(message);
@ -279,7 +279,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
assertThrows(SOPGPException.MissingArg.class, () ->
sop.verify()
assumeSupported(sop::verify)
.signatures(TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8))
.data(message));
}
@ -288,14 +288,14 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void signVerifyWithMultipleKeys(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signatures = sop.detachedSign()
byte[] signatures = assumeSupported(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()
List<Verification> verificationList = assumeSupported(sop::detachedVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signatures)

View file

@ -11,12 +11,15 @@ import org.junit.jupiter.params.provider.MethodSource;
import sop.ByteArrayAndResult;
import sop.DecryptionResult;
import sop.EncryptionResult;
import sop.Profile;
import sop.SOP;
import sop.SessionKey;
import sop.Verification;
import sop.enums.EncryptAs;
import sop.enums.SignatureMode;
import sop.exception.SOPGPException;
import sop.operation.Decrypt;
import sop.operation.Encrypt;
import sop.testsuite.TestData;
import sop.testsuite.assertions.VerificationListAssert;
import sop.util.Optional;
@ -25,6 +28,7 @@ import sop.util.UTCUtil;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Stream;
@ -45,7 +49,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void encryptDecryptRoundTripPasswordTest(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
ByteArrayAndResult<EncryptionResult> encResult = sop.encrypt()
ByteArrayAndResult<EncryptionResult> encResult = assumeSupported(sop::encrypt)
.withPassword("sw0rdf1sh")
.plaintext(message)
.toByteArrayAndResult();
@ -53,7 +57,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
byte[] ciphertext = encResult.getBytes();
Optional<SessionKey> encSessionKey = encResult.getResult().getSessionKey();
ByteArrayAndResult<DecryptionResult> decResult = sop.decrypt()
ByteArrayAndResult<DecryptionResult> decResult = assumeSupported(sop::decrypt)
.withPassword("sw0rdf1sh")
.ciphertext(ciphertext)
.toByteArrayAndResult();
@ -61,9 +65,10 @@ public class EncryptDecryptTest extends AbstractSOPTest {
byte[] plaintext = decResult.getBytes();
Optional<SessionKey> decSessionKey = decResult.getResult().getSessionKey();
assertArrayEquals(message, plaintext);
assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext.");
if (encSessionKey.isPresent() && decSessionKey.isPresent()) {
assertEquals(encSessionKey.get(), decSessionKey.get());
assertEquals(encSessionKey.get(), decSessionKey.get(),
"Extracted Session Key mismatch.");
}
}
@ -71,91 +76,93 @@ public class EncryptDecryptTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void encryptDecryptRoundTripAliceTest(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = sop.encrypt()
byte[] ciphertext = assumeSupported(sop::encrypt)
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.plaintext(message)
.toByteArrayAndResult()
.getBytes();
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
ByteArrayAndResult<DecryptionResult> bytesAndResult = assumeSupported(sop::decrypt)
.withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.ciphertext(ciphertext)
.toByteArrayAndResult();
byte[] plaintext = bytesAndResult.getBytes();
assertArrayEquals(message, plaintext);
assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext.");
DecryptionResult result = bytesAndResult.getResult();
assertNotNull(result.getSessionKey().get());
if (result.getSessionKey().isPresent()) {
assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null.");
}
}
@ParameterizedTest
@MethodSource("provideInstances")
public void encryptDecryptRoundTripBobTest(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = sop.encrypt()
byte[] ciphertext = assumeSupported(sop::encrypt)
.withCert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8))
.plaintext(message)
.toByteArrayAndResult()
.getBytes();
byte[] plaintext = sop.decrypt()
byte[] plaintext = assumeSupported(sop::decrypt)
.withKey(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8))
.ciphertext(ciphertext)
.toByteArrayAndResult()
.getBytes();
assertArrayEquals(message, plaintext);
assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext.");
}
@ParameterizedTest
@MethodSource("provideInstances")
public void encryptDecryptRoundTripCarolTest(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = sop.encrypt()
byte[] ciphertext = assumeSupported(sop::encrypt)
.withCert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8))
.plaintext(message)
.toByteArrayAndResult()
.getBytes();
byte[] plaintext = sop.decrypt()
byte[] plaintext = assumeSupported(sop::decrypt)
.withKey(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8))
.ciphertext(ciphertext)
.toByteArrayAndResult()
.getBytes();
assertArrayEquals(message, plaintext);
assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext.");
}
@ParameterizedTest
@MethodSource("provideInstances")
public void encryptNoArmorThenArmorThenDecryptRoundTrip(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = sop.encrypt()
byte[] ciphertext = assumeSupported(sop::encrypt)
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.noArmor()
.plaintext(message)
.toByteArrayAndResult()
.getBytes();
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(ciphertext)
.getBytes();
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
ByteArrayAndResult<DecryptionResult> bytesAndResult = assumeSupported(sop::decrypt)
.withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.ciphertext(armored)
.toByteArrayAndResult();
byte[] plaintext = bytesAndResult.getBytes();
assertArrayEquals(message, plaintext);
assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext.");
}
@ParameterizedTest
@MethodSource("provideInstances")
public void encryptSignDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = sop.encrypt()
byte[] ciphertext = assumeSupported(sop::encrypt)
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(EncryptAs.binary)
@ -163,17 +170,19 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.toByteArrayAndResult()
.getBytes();
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
ByteArrayAndResult<DecryptionResult> bytesAndResult = assumeSupported(sop::decrypt)
.withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.ciphertext(ciphertext)
.toByteArrayAndResult();
byte[] plaintext = bytesAndResult.getBytes();
assertArrayEquals(message, plaintext);
assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext.");
DecryptionResult result = bytesAndResult.getResult();
assertNotNull(result.getSessionKey().get());
if (result.getSessionKey().isPresent()) {
assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null.");
}
List<Verification> verificationList = result.getVerifications();
VerificationListAssert.assertThatVerificationList(verificationList)
@ -187,7 +196,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void encryptSignAsTextDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = sop.encrypt()
byte[] ciphertext = assumeSupported(sop::encrypt)
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(EncryptAs.text)
@ -195,14 +204,14 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.toByteArrayAndResult()
.getBytes();
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
ByteArrayAndResult<DecryptionResult> bytesAndResult = assumeSupported(sop::decrypt)
.withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.ciphertext(ciphertext)
.toByteArrayAndResult();
byte[] plaintext = bytesAndResult.getBytes();
assertArrayEquals(message, plaintext);
assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext.");
DecryptionResult result = bytesAndResult.getResult();
assertNotNull(result.getSessionKey().get());
@ -218,17 +227,17 @@ public class EncryptDecryptTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void encryptSignDecryptVerifyRoundTripWithFreshEncryptedKeyTest(SOP sop) throws IOException {
byte[] keyPassword = "sw0rdf1sh".getBytes(StandardCharsets.UTF_8);
byte[] key = sop.generateKey()
byte[] key = assumeSupported(sop::generateKey)
.withKeyPassword(keyPassword)
.userId("Alice <alice@openpgp.org>")
.generate()
.getBytes();
byte[] cert = sop.extractCert()
byte[] cert = assumeSupported(sop::extractCert)
.key(key)
.getBytes();
byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = sop.encrypt()
byte[] ciphertext = assumeSupported(sop::encrypt)
.withCert(cert)
.signWith(key)
.withKeyPassword(keyPassword)
@ -236,7 +245,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.toByteArrayAndResult()
.getBytes();
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
ByteArrayAndResult<DecryptionResult> bytesAndResult = assumeSupported(sop::decrypt)
.withKey(key)
.withKeyPassword(keyPassword)
.verifyWithCert(cert)
@ -269,7 +278,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
Date beforeSignature = new Date(signatureDate.getTime() - 1000); // 1 sec before signing date
assertThrows(SOPGPException.NoSignature.class, () -> {
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
ByteArrayAndResult<DecryptionResult> bytesAndResult = assumeSupported(sop::decrypt)
.withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.verifyNotAfter(beforeSignature)
@ -303,7 +312,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
Date afterSignature = new Date(signatureDate.getTime() + 1000); // 1 sec after signing date
assertThrows(SOPGPException.NoSignature.class, () -> {
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
ByteArrayAndResult<DecryptionResult> bytesAndResult = assumeSupported(sop::decrypt)
.withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.verifyNotBefore(afterSignature)
@ -322,7 +331,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
public void missingArgsTest(SOP sop) {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
assertThrows(SOPGPException.MissingArg.class, () -> sop.encrypt()
assertThrows(SOPGPException.MissingArg.class, () -> assumeSupported(sop::encrypt)
.plaintext(message)
.toByteArrayAndResult()
.getBytes());
@ -332,10 +341,61 @@ public class EncryptDecryptTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void passingSecretKeysForPublicKeysFails(SOP sop) {
assertThrows(SOPGPException.BadData.class, () ->
sop.encrypt()
assumeSupported(sop::encrypt)
.withCert(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))
.toByteArrayAndResult()
.getBytes());
}
@ParameterizedTest
@MethodSource("provideInstances")
public void encryptDecryptWithAllSupportedKeyGenerationProfiles(SOP sop) throws IOException {
List<Profile> profiles = assumeSupported(sop::listProfiles).generateKey();
List<byte[]> keys = new ArrayList<>();
List<byte[]> certs = new ArrayList<>();
for (Profile p : profiles) {
byte[] k = assumeSupported(sop::generateKey)
.profile(p)
.userId(p.getName())
.generate()
.getBytes();
keys.add(k);
byte[] c = assumeSupported(sop::extractCert)
.key(k)
.getBytes();
certs.add(c);
}
byte[] plaintext = "Hello, World!\n".getBytes();
Encrypt encrypt = assumeSupported(sop::encrypt);
for (byte[] c : certs) {
encrypt.withCert(c);
}
for (byte[] k : keys) {
encrypt.signWith(k);
}
ByteArrayAndResult<EncryptionResult> encRes = encrypt.plaintext(plaintext)
.toByteArrayAndResult();
EncryptionResult eResult = encRes.getResult();
byte[] ciphertext = encRes.getBytes();
for (byte[] k : keys) {
Decrypt decrypt = assumeSupported(sop::decrypt)
.withKey(k);
for (byte[] c : certs) {
decrypt.verifyWithCert(c);
}
ByteArrayAndResult<DecryptionResult> decRes = decrypt.ciphertext(ciphertext)
.toByteArrayAndResult();
DecryptionResult dResult = decRes.getResult();
byte[] decPlaintext = decRes.getBytes();
assertArrayEquals(plaintext, decPlaintext, "Decrypted plaintext does not match original plaintext.");
assertEquals(certs.size(), dResult.getVerifications().size());
}
}
}

View file

@ -28,12 +28,12 @@ public class ExtractCertTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void extractArmoredCertFromArmoredKeyTest(SOP sop) throws IOException {
InputStream keyIn = sop.generateKey()
InputStream keyIn = assumeSupported(sop::generateKey)
.userId("Alice <alice@openpgp.org>")
.generate()
.getInputStream();
byte[] cert = sop.extractCert().key(keyIn).getBytes();
byte[] cert = assumeSupported(sop::extractCert).key(keyIn).getBytes();
JUtils.assertArrayStartsWith(cert, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK);
JUtils.assertArrayEndsWithIgnoreNewlines(cert, TestData.END_PGP_PUBLIC_KEY_BLOCK);
}
@ -41,7 +41,7 @@ public class ExtractCertTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void extractAliceCertFromAliceKeyTest(SOP sop) throws IOException {
byte[] armoredCert = sop.extractCert()
byte[] armoredCert = assumeSupported(sop::extractCert)
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.getBytes();
JUtils.assertAsciiArmorEquals(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8), armoredCert);
@ -50,7 +50,7 @@ public class ExtractCertTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void extractBobsCertFromBobsKeyTest(SOP sop) throws IOException {
byte[] armoredCert = sop.extractCert()
byte[] armoredCert = assumeSupported(sop::extractCert)
.key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8))
.getBytes();
JUtils.assertAsciiArmorEquals(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8), armoredCert);
@ -59,7 +59,7 @@ public class ExtractCertTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void extractCarolsCertFromCarolsKeyTest(SOP sop) throws IOException {
byte[] armoredCert = sop.extractCert()
byte[] armoredCert = assumeSupported(sop::extractCert)
.key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8))
.getBytes();
JUtils.assertAsciiArmorEquals(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8), armoredCert);
@ -68,12 +68,12 @@ public class ExtractCertTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void extractUnarmoredCertFromArmoredKeyTest(SOP sop) throws IOException {
InputStream keyIn = sop.generateKey()
InputStream keyIn = assumeSupported(sop::generateKey)
.userId("Alice <alice@openpgp.org>")
.generate()
.getInputStream();
byte[] cert = sop.extractCert()
byte[] cert = assumeSupported(sop::extractCert)
.noArmor()
.key(keyIn)
.getBytes();
@ -84,13 +84,13 @@ public class ExtractCertTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void extractArmoredCertFromUnarmoredKeyTest(SOP sop) throws IOException {
InputStream keyIn = sop.generateKey()
InputStream keyIn = assumeSupported(sop::generateKey)
.userId("Alice <alice@openpgp.org>")
.noArmor()
.generate()
.getInputStream();
byte[] cert = sop.extractCert()
byte[] cert = assumeSupported(sop::extractCert)
.key(keyIn)
.getBytes();
@ -101,13 +101,13 @@ public class ExtractCertTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void extractUnarmoredCertFromUnarmoredKeyTest(SOP sop) throws IOException {
InputStream keyIn = sop.generateKey()
InputStream keyIn = assumeSupported(sop::generateKey)
.noArmor()
.userId("Alice <alice@openpgp.org>")
.generate()
.getInputStream();
byte[] cert = sop.extractCert()
byte[] cert = assumeSupported(sop::extractCert)
.noArmor()
.key(keyIn)
.getBytes();

View file

@ -9,6 +9,7 @@ import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.params.ParameterizedTest;
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 sop.testsuite.JUtils;
@ -16,9 +17,11 @@ import sop.testsuite.TestData;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class GenerateKeyTest extends AbstractSOPTest {
@ -30,7 +33,7 @@ public class GenerateKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void generateKeyTest(SOP sop) throws IOException {
byte[] key = sop.generateKey()
byte[] key = assumeSupported(sop::generateKey)
.userId("Alice <alice@openpgp.org>")
.generate()
.getBytes();
@ -42,7 +45,7 @@ public class GenerateKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void generateKeyNoArmor(SOP sop) throws IOException {
byte[] key = sop.generateKey()
byte[] key = assumeSupported(sop::generateKey)
.userId("Alice <alice@openpgp.org>")
.noArmor()
.generate()
@ -54,7 +57,7 @@ public class GenerateKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void generateKeyWithMultipleUserIdsTest(SOP sop) throws IOException {
byte[] key = sop.generateKey()
byte[] key = assumeSupported(sop::generateKey)
.userId("Alice <alice@openpgp.org>")
.userId("Bob <bob@openpgp.org>")
.generate()
@ -67,7 +70,7 @@ public class GenerateKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void generateKeyWithoutUserIdTest(SOP sop) throws IOException {
byte[] key = sop.generateKey()
byte[] key = assumeSupported(sop::generateKey)
.generate()
.getBytes();
@ -78,7 +81,7 @@ public class GenerateKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void generateKeyWithPasswordTest(SOP sop) throws IOException {
byte[] key = sop.generateKey()
byte[] key = assumeSupported(sop::generateKey)
.userId("Alice <alice@openpgp.org>")
.withKeyPassword("sw0rdf1sh")
.generate()
@ -91,7 +94,7 @@ public class GenerateKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void generateKeyWithMultipleUserIdsAndPassword(SOP sop) throws IOException {
byte[] key = sop.generateKey()
byte[] key = assumeSupported(sop::generateKey)
.userId("Alice <alice@openpgp.org>")
.userId("Bob <bob@openpgp.org>")
.withKeyPassword("sw0rdf1sh")
@ -105,17 +108,45 @@ public class GenerateKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void generateSigningOnlyKey(SOP sop) throws IOException {
byte[] signingOnlyKey = sop.generateKey()
byte[] signingOnlyKey = assumeSupported(sop::generateKey)
.signingOnly()
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] signingOnlyCert = sop.extractCert()
byte[] signingOnlyCert = assumeSupported(sop::extractCert)
.key(signingOnlyKey)
.getBytes();
assertThrows(SOPGPException.CertCannotEncrypt.class, () ->
sop.encrypt().withCert(signingOnlyCert)
.plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)));
assumeSupported(sop::encrypt).withCert(signingOnlyCert)
.plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))
.toByteArrayAndResult()
.getBytes());
}
@ParameterizedTest
@MethodSource("provideInstances")
public void generateKeyWithSupportedProfiles(SOP sop) throws IOException {
List<Profile> profiles = assumeSupported(sop::listProfiles)
.generateKey();
for (Profile profile : profiles) {
generateKeyWithProfile(sop, profile.getName());
}
}
private void generateKeyWithProfile(SOP sop, String profile) throws IOException {
byte[] key;
try {
key = assumeSupported(sop::generateKey)
.profile(profile)
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
} catch (SOPGPException.UnsupportedProfile e) {
key = null;
}
assumeTrue(key != null, "'generate-key' does not support profile '" + profile + "'.");
JUtils.assertArrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK);
}
}

View file

@ -36,12 +36,12 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest {
public void inlineSignThenDetachThenDetachedVerifyTest(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] inlineSigned = sop.inlineSign()
byte[] inlineSigned = assumeSupported(sop::inlineSign)
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.getBytes();
ByteArrayAndResult<Signatures> bytesAndResult = sop.inlineDetach()
ByteArrayAndResult<Signatures> bytesAndResult = assumeSupported(sop::inlineDetach)
.message(inlineSigned)
.toByteArrayAndResult();
@ -51,7 +51,7 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest {
byte[] signatures = bytesAndResult.getResult()
.getBytes();
List<Verification> verifications = sop.detachedVerify()
List<Verification> verifications = assumeSupported(sop::detachedVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(signatures)
.data(plaintext);
@ -64,12 +64,12 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest {
public void inlineSignThenDetachNoArmorThenArmorThenDetachedVerifyTest(SOP sop) throws IOException {
byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8);
byte[] inlineSigned = sop.inlineSign()
byte[] inlineSigned = assumeSupported(sop::inlineSign)
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.getBytes();
ByteArrayAndResult<Signatures> bytesAndResult = sop.inlineDetach()
ByteArrayAndResult<Signatures> bytesAndResult = assumeSupported(sop::inlineDetach)
.noArmor()
.message(inlineSigned)
.toByteArrayAndResult();
@ -81,12 +81,12 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest {
.getBytes();
Assertions.assertFalse(JUtils.arrayStartsWith(signatures, TestData.BEGIN_PGP_SIGNATURE));
byte[] armored = sop.armor()
byte[] armored = assumeSupported(sop::armor)
.data(signatures)
.getBytes();
JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_SIGNATURE);
List<Verification> verifications = sop.detachedVerify()
List<Verification> verifications = assumeSupported(sop::detachedVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signatures(armored)
.data(plaintext);

View file

@ -40,14 +40,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
public void inlineSignVerifyAlice(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] inlineSigned = sop.inlineSign()
byte[] inlineSigned = assumeSupported(sop::inlineSign)
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.getBytes();
JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE);
ByteArrayAndResult<List<Verification>> bytesAndResult = sop.inlineVerify()
ByteArrayAndResult<List<Verification>> bytesAndResult = assumeSupported(sop::inlineVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.data(inlineSigned)
.toByteArrayAndResult();
@ -66,7 +66,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
public void inlineSignVerifyAliceNoArmor(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] inlineSigned = sop.inlineSign()
byte[] inlineSigned = assumeSupported(sop::inlineSign)
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.noArmor()
.data(message)
@ -74,7 +74,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
Assertions.assertFalse(JUtils.arrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE));
ByteArrayAndResult<List<Verification>> bytesAndResult = sop.inlineVerify()
ByteArrayAndResult<List<Verification>> bytesAndResult = assumeSupported(sop::inlineVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.data(inlineSigned)
.toByteArrayAndResult();
@ -93,7 +93,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
public void clearsignVerifyAlice(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] clearsigned = sop.inlineSign()
byte[] clearsigned = assumeSupported(sop::inlineSign)
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(InlineSignAs.clearsigned)
.data(message)
@ -101,12 +101,13 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
JUtils.assertArrayStartsWith(clearsigned, TestData.BEGIN_PGP_SIGNED_MESSAGE);
ByteArrayAndResult<List<Verification>> bytesAndResult = sop.inlineVerify()
ByteArrayAndResult<List<Verification>> bytesAndResult = assumeSupported(sop::inlineVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.data(clearsigned)
.toByteArrayAndResult();
assertArrayEquals(message, bytesAndResult.getBytes());
assertArrayEquals(message, bytesAndResult.getBytes(),
"ASCII armored message does not appear to start with the 'BEGIN PGP SIGNED MESSAGE' header.");
List<Verification> verificationList = bytesAndResult.getResult();
VerificationListAssert.assertThatVerificationList(verificationList)
@ -121,7 +122,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
byte[] message = TestData.ALICE_INLINE_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8);
Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE;
ByteArrayAndResult<List<Verification>> bytesAndResult = sop.inlineVerify()
ByteArrayAndResult<List<Verification>> bytesAndResult = assumeSupported(sop::inlineVerify)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.data(message)
.toByteArrayAndResult();
@ -141,7 +142,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE;
Date afterSignature = new Date(signatureDate.getTime() + 1000); // 1 sec before sig
assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify()
assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify)
.notBefore(afterSignature)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.data(message)
@ -155,7 +156,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE;
Date beforeSignature = new Date(signatureDate.getTime() - 1000); // 1 sec before sig
assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify()
assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify)
.notAfter(beforeSignature)
.cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.data(message)
@ -167,14 +168,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
public void inlineSignVerifyBob(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] inlineSigned = sop.inlineSign()
byte[] inlineSigned = assumeSupported(sop::inlineSign)
.key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.getBytes();
JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE);
ByteArrayAndResult<List<Verification>> bytesAndResult = sop.inlineVerify()
ByteArrayAndResult<List<Verification>> bytesAndResult = assumeSupported(sop::inlineVerify)
.cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8))
.data(inlineSigned)
.toByteArrayAndResult();
@ -193,14 +194,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
public void inlineSignVerifyCarol(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] inlineSigned = sop.inlineSign()
byte[] inlineSigned = assumeSupported(sop::inlineSign)
.key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8))
.data(message)
.getBytes();
JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE);
ByteArrayAndResult<List<Verification>> bytesAndResult = sop.inlineVerify()
ByteArrayAndResult<List<Verification>> bytesAndResult = assumeSupported(sop::inlineVerify)
.cert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8))
.data(inlineSigned)
.toByteArrayAndResult();
@ -219,14 +220,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest {
public void inlineSignVerifyProtectedKey(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] inlineSigned = sop.inlineSign()
byte[] inlineSigned = assumeSupported(sop::inlineSign)
.withKeyPassword(TestData.PASSWORD)
.key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8))
.mode(InlineSignAs.binary)
.data(message)
.getBytes();
ByteArrayAndResult<List<Verification>> bytesAndResult = sop.inlineVerify()
ByteArrayAndResult<List<Verification>> bytesAndResult = assumeSupported(sop::inlineVerify)
.cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8))
.data(inlineSigned)
.toByteArrayAndResult();

View file

@ -26,8 +26,7 @@ public class ListProfilesTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void listGenerateKeyProfiles(SOP sop) {
List<Profile> profiles = sop
.listProfiles()
List<Profile> profiles = assumeSupported(sop::listProfiles)
.generateKey();
assertFalse(profiles.isEmpty());
@ -36,8 +35,7 @@ public class ListProfilesTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void listEncryptProfiles(SOP sop) {
List<Profile> profiles = sop
.listProfiles()
List<Profile> profiles = assumeSupported(sop::listProfiles)
.encrypt();
assertFalse(profiles.isEmpty());
@ -46,8 +44,7 @@ public class ListProfilesTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void listUnsupportedProfiles(SOP sop) {
assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop
.listProfiles()
assertThrows(SOPGPException.UnsupportedProfile.class, () -> assumeSupported(sop::listProfiles)
.subcommand("invalid"));
}
}

View file

@ -0,0 +1,164 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.operation;
import kotlin.collections.ArraysKt;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import sop.SOP;
import java.io.IOException;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
public class MergeCertsTest extends AbstractSOPTest {
static Stream<Arguments> provideInstances() {
return provideBackends();
}
@ParameterizedTest
@MethodSource("provideInstances")
public void testMergeWithItself(SOP sop) throws IOException {
byte[] key = assumeSupported(sop::generateKey)
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] cert = assumeSupported(sop::extractCert)
.key(key)
.getBytes();
byte[] merged = assumeSupported(sop::mergeCerts)
.updates(cert)
.baseCertificates(cert)
.getBytes();
assertArrayEquals(cert, merged);
}
@ParameterizedTest
@MethodSource("provideInstances")
public void testMergeWithItselfArmored(SOP sop) throws IOException {
byte[] key = assumeSupported(sop::generateKey)
.noArmor()
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] cert = assumeSupported(sop::extractCert)
.key(key)
.getBytes();
byte[] merged = assumeSupported(sop::mergeCerts)
.updates(cert)
.baseCertificates(cert)
.getBytes();
assertArrayEquals(cert, merged);
}
@ParameterizedTest
@MethodSource("provideInstances")
public void testMergeWithItselfViaBase(SOP sop) throws IOException {
byte[] key = assumeSupported(sop::generateKey)
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] cert = assumeSupported(sop::extractCert)
.key(key)
.getBytes();
byte[] certs = ArraysKt.plus(cert, cert);
byte[] merged = assumeSupported(sop::mergeCerts)
.updates(cert)
.baseCertificates(certs)
.getBytes();
assertArrayEquals(cert, merged);
}
@ParameterizedTest
@MethodSource("provideInstances")
public void testApplyBaseToUpdate(SOP sop) throws IOException {
byte[] key = assumeSupported(sop::generateKey)
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] cert = assumeSupported(sop::extractCert)
.key(key)
.getBytes();
byte[] update = assumeSupported(sop::revokeKey)
.keys(key)
.getBytes();
byte[] merged = assumeSupported(sop::mergeCerts)
.updates(cert)
.baseCertificates(update)
.getBytes();
assertArrayEquals(update, merged);
}
@ParameterizedTest
@MethodSource("provideInstances")
public void testApplyUpdateToBase(SOP sop) throws IOException {
byte[] key = assumeSupported(sop::generateKey)
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] cert = assumeSupported(sop::extractCert)
.key(key)
.getBytes();
byte[] update = assumeSupported(sop::revokeKey)
.keys(key)
.getBytes();
byte[] merged = assumeSupported(sop::mergeCerts)
.updates(update)
.baseCertificates(cert)
.getBytes();
assertArrayEquals(update, merged);
}
@ParameterizedTest
@MethodSource("provideInstances")
public void testApplyUpdateToMissingBaseDoesNothing(SOP sop) throws IOException {
byte[] aliceKey = assumeSupported(sop::generateKey)
.userId("Alice <alice@pgpainless.org>")
.generate()
.getBytes();
byte[] aliceCert = assumeSupported(sop::extractCert)
.key(aliceKey)
.getBytes();
byte[] bobKey = assumeSupported(sop::generateKey)
.userId("Bob <bob@pgpainless.org>")
.generate()
.getBytes();
byte[] bobCert = assumeSupported(sop::extractCert)
.key(bobKey)
.getBytes();
byte[] merged = assumeSupported(sop::mergeCerts)
.updates(bobCert)
.baseCertificates(aliceCert)
.getBytes();
assertArrayEquals(aliceCert, merged);
}
}

View file

@ -36,8 +36,8 @@ public class RevokeKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void revokeUnprotectedKey(SOP sop) throws IOException {
byte[] secretKey = sop.generateKey().userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] revocation = sop.revokeKey().keys(secretKey).getBytes();
byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] revocation = assumeSupported(sop::revokeKey).keys(secretKey).getBytes();
assertTrue(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK));
assertFalse(Arrays.equals(secretKey, revocation));
@ -46,8 +46,8 @@ public class RevokeKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void revokeUnprotectedKeyNoArmor(SOP sop) throws IOException {
byte[] secretKey = sop.generateKey().userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] revocation = sop.revokeKey().noArmor().keys(secretKey).getBytes();
byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] revocation = assumeSupported(sop::revokeKey).noArmor().keys(secretKey).getBytes();
assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK));
}
@ -55,8 +55,8 @@ public class RevokeKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void revokeUnprotectedKeyUnarmored(SOP sop) throws IOException {
byte[] secretKey = sop.generateKey().userId("Alice <alice@pgpainless.org>").noArmor().generate().getBytes();
byte[] revocation = sop.revokeKey().noArmor().keys(secretKey).getBytes();
byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice <alice@pgpainless.org>").noArmor().generate().getBytes();
byte[] revocation = assumeSupported(sop::revokeKey).noArmor().keys(secretKey).getBytes();
assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK));
assertFalse(Arrays.equals(secretKey, revocation));
@ -65,18 +65,18 @@ public class RevokeKeyTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void revokeCertificateFails(SOP sop) throws IOException {
byte[] secretKey = sop.generateKey().generate().getBytes();
byte[] certificate = sop.extractCert().key(secretKey).getBytes();
byte[] secretKey = assumeSupported(sop::generateKey).generate().getBytes();
byte[] certificate = assumeSupported(sop::extractCert).key(secretKey).getBytes();
assertThrows(SOPGPException.BadData.class, () -> sop.revokeKey().keys(certificate).getBytes());
assertThrows(SOPGPException.BadData.class, () -> assumeSupported(sop::revokeKey).keys(certificate).getBytes());
}
@ParameterizedTest
@MethodSource("provideInstances")
public void revokeProtectedKey(SOP sop) throws IOException {
byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8);
byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] revocation = sop.revokeKey().withKeyPassword(password).keys(secretKey).getBytes();
byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(password).keys(secretKey).getBytes();
assertFalse(Arrays.equals(secretKey, revocation));
}
@ -86,8 +86,8 @@ public class RevokeKeyTest extends AbstractSOPTest {
public void revokeProtectedKeyWithMultiplePasswordOptions(SOP sop) throws IOException {
byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8);
byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8);
byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] revocation = sop.revokeKey().withKeyPassword(wrongPassword).withKeyPassword(password).keys(secretKey).getBytes();
byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).withKeyPassword(password).keys(secretKey).getBytes();
assertFalse(Arrays.equals(secretKey, revocation));
}
@ -96,9 +96,9 @@ public class RevokeKeyTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void revokeProtectedKeyWithMissingPassphraseFails(SOP sop) throws IOException {
byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8);
byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice <alice@pgpainless.org>").generate().getBytes();
assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().keys(secretKey).getBytes());
assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).keys(secretKey).getBytes());
}
@ParameterizedTest
@ -106,27 +106,27 @@ public class RevokeKeyTest extends AbstractSOPTest {
public void revokeProtectedKeyWithWrongPassphraseFails(SOP sop) throws IOException {
byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8);
String wrongPassword = "or4ng3";
byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice <alice@pgpainless.org>").generate().getBytes();
byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice <alice@pgpainless.org>").generate().getBytes();
assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().withKeyPassword(wrongPassword).keys(secretKey).getBytes());
assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).keys(secretKey).getBytes());
}
@ParameterizedTest
@MethodSource("provideInstances")
public void revokeKeyIsNowHardRevoked(SOP sop) throws IOException {
byte[] key = sop.generateKey().generate().getBytes();
byte[] cert = sop.extractCert().key(key).getBytes();
byte[] key = assumeSupported(sop::generateKey).generate().getBytes();
byte[] cert = assumeSupported(sop::extractCert).key(key).getBytes();
// Sign a message with the key
byte[] msg = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] signedMsg = sop.inlineSign().key(key).data(msg).getBytes();
byte[] signedMsg = assumeSupported(sop::inlineSign).key(key).data(msg).getBytes();
// Verifying the message with the valid cert works
List<Verification> result = sop.inlineVerify().cert(cert).data(signedMsg).toByteArrayAndResult().getResult();
List<Verification> result = assumeSupported(sop::inlineVerify).cert(cert).data(signedMsg).toByteArrayAndResult().getResult();
VerificationListAssert.assertThatVerificationList(result).hasSingleItem();
// Now hard revoke the key and re-check signature, expecting no valid certification
byte[] revokedCert = sop.revokeKey().keys(key).getBytes();
assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify().cert(revokedCert).data(signedMsg).toByteArrayAndResult());
byte[] revokedCert = assumeSupported(sop::revokeKey).keys(key).getBytes();
assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify).cert(revokedCert).data(signedMsg).toByteArrayAndResult());
}
}

View file

@ -28,7 +28,7 @@ public class VersionTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void versionNameTest(SOP sop) {
String name = sop.version().getName();
String name = assumeSupported(sop::version).getName();
assertNotNull(name);
assertFalse(name.isEmpty());
}
@ -36,21 +36,21 @@ public class VersionTest extends AbstractSOPTest {
@ParameterizedTest
@MethodSource("provideInstances")
public void versionVersionTest(SOP sop) {
String version = sop.version().getVersion();
String version = assumeSupported(sop::version).getVersion();
assertFalse(version.isEmpty());
}
@ParameterizedTest
@MethodSource("provideInstances")
public void backendVersionTest(SOP sop) {
String backend = sop.version().getBackendVersion();
String backend = assumeSupported(sop::version).getBackendVersion();
assertFalse(backend.isEmpty());
}
@ParameterizedTest
@MethodSource("provideInstances")
public void extendedVersionTest(SOP sop) {
String extended = sop.version().getExtendedVersion();
String extended = assumeSupported(sop::version).getExtendedVersion();
assertFalse(extended.isEmpty());
}
@ -58,27 +58,27 @@ public class VersionTest extends AbstractSOPTest {
@MethodSource("provideInstances")
public void sopSpecVersionTest(SOP sop) {
try {
sop.version().getSopSpecVersion();
assumeSupported(sop::version).getSopSpecVersion();
} catch (RuntimeException e) {
throw new TestAbortedException("SOP backend does not support 'version --sop-spec' yet.");
}
String sopSpec = sop.version().getSopSpecVersion();
if (sop.version().isSopSpecImplementationIncomplete()) {
String sopSpec = assumeSupported(sop::version).getSopSpecVersion();
if (assumeSupported(sop::version).isSopSpecImplementationIncomplete()) {
assertTrue(sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-"));
} else {
assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-"));
}
int sopRevision = sop.version().getSopSpecRevisionNumber();
assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision));
int sopRevision = assumeSupported(sop::version).getSopSpecRevisionNumber();
assertTrue(assumeSupported(sop::version).getSopSpecRevisionName().endsWith("" + sopRevision));
}
@ParameterizedTest
@MethodSource("provideInstances")
public void sopVVersionTest(SOP sop) {
try {
sop.version().getSopVVersion();
assumeSupported(sop::version).getSopVVersion();
} catch (SOPGPException.UnsupportedOption e) {
throw new TestAbortedException(
"Implementation does (gracefully) not provide coverage for any sopv interface version.");
@ -86,4 +86,10 @@ public class VersionTest extends AbstractSOPTest {
throw new TestAbortedException("Implementation does not provide coverage for any sopv interface version.");
}
}
@ParameterizedTest
@MethodSource("provideInstances")
public void sopJavaVersionTest(SOP sop) {
assertNotNull(assumeSupported(sop::version).getSopJavaVersion());
}
}

View file

@ -20,17 +20,22 @@ import sop.util.UTF8Util
* in the IETF namespace that begins with the string `draft-` should have semantics that hew as
* closely as possible to the referenced Internet Draft.
* @param description a free-form description of the profile.
* @see <a
* href="https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html#name-profile">
* SOP Spec - Profile</a>
* @param aliases list of optional profile alias names
* @see
* [SOP Spec - Profile](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html#name-profile)
*/
data class Profile(val name: String, val description: Optional<String>) {
data class Profile(
val name: String,
val description: Optional<String>,
val aliases: List<String> = listOf()
) {
@JvmOverloads
constructor(
name: String,
description: String? = null
) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null }))
description: String? = null,
aliases: List<String> = listOf()
) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null }), aliases)
init {
require(name.trim().isNotBlank()) { "Name cannot be empty." }
@ -45,13 +50,33 @@ data class Profile(val name: String, val description: Optional<String>) {
fun hasDescription() = description.isPresent
/**
* Return a copy of this [Profile] with the aliases set to the given strings.
*
* @param alias one or more alias names
* @return profile with aliases
*/
fun withAliases(vararg alias: String): Profile {
return Profile(name, description, alias.toList())
}
/**
* Convert the profile into a String for displaying.
*
* @return string
*/
override fun toString(): String =
if (description.isEmpty) name else "$name: ${description.get()}"
override fun toString(): String = buildString {
append(name)
if (!description.isEmpty || !aliases.isEmpty()) {
append(":")
}
if (!description.isEmpty) {
append(" ${description.get()}")
}
if (!aliases.isEmpty()) {
append(" (aliases: ${aliases.joinToString(separator = ", ")})")
}
}
companion object {
@ -64,9 +89,21 @@ data class Profile(val name: String, val description: Optional<String>) {
@JvmStatic
fun parse(string: String): Profile {
return if (string.contains(": ")) {
Profile(
string.substring(0, string.indexOf(": ")),
string.substring(string.indexOf(": ") + 2).trim())
val name = string.substring(0, string.indexOf(": "))
var description = string.substring(string.indexOf(": ") + 2).trim()
if (description.contains("(aliases: ")) {
val aliases =
description.substring(
description.indexOf("(aliases: ") + 10, description.indexOf(")"))
description = description.substring(0, description.indexOf("(aliases: ")).trim()
Profile(name, description, aliases.split(", ").toList())
} else {
if (description.isNotBlank()) {
Profile(name, description)
} else {
Profile(name)
}
}
} else if (string.endsWith(":")) {
Profile(string.substring(0, string.length - 1))
} else {

View file

@ -4,18 +4,7 @@
package sop
import sop.operation.Armor
import sop.operation.ChangeKeyPassword
import sop.operation.Dearmor
import sop.operation.Decrypt
import sop.operation.DetachedSign
import sop.operation.Encrypt
import sop.operation.ExtractCert
import sop.operation.GenerateKey
import sop.operation.InlineDetach
import sop.operation.InlineSign
import sop.operation.ListProfiles
import sop.operation.RevokeKey
import sop.operation.*
/**
* Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related
@ -26,48 +15,57 @@ import sop.operation.RevokeKey
interface SOP : SOPV {
/** Generate a secret key. */
fun generateKey(): GenerateKey
fun generateKey(): GenerateKey?
/** Extract a certificate (public key) from a secret key. */
fun extractCert(): ExtractCert
fun extractCert(): ExtractCert?
/**
* Create detached signatures. If you want to sign a message inline, use [inlineSign] instead.
*/
fun sign(): DetachedSign = detachedSign()
fun sign(): DetachedSign? = detachedSign()
/**
* Create detached signatures. If you want to sign a message inline, use [inlineSign] instead.
*/
fun detachedSign(): DetachedSign
fun detachedSign(): DetachedSign?
/**
* Sign a message using inline signatures. If you need to create detached signatures, use
* [detachedSign] instead.
*/
fun inlineSign(): InlineSign
fun inlineSign(): InlineSign?
/** Detach signatures from an inline signed message. */
fun inlineDetach(): InlineDetach
fun inlineDetach(): InlineDetach?
/** Encrypt a message. */
fun encrypt(): Encrypt
fun encrypt(): Encrypt?
/** Decrypt a message. */
fun decrypt(): Decrypt
fun decrypt(): Decrypt?
/** Convert binary OpenPGP data to ASCII. */
fun armor(): Armor
fun armor(): Armor?
/** Converts ASCII armored OpenPGP data to binary. */
fun dearmor(): Dearmor
fun dearmor(): Dearmor?
/** List supported [Profiles][Profile] of a subcommand. */
fun listProfiles(): ListProfiles
fun listProfiles(): ListProfiles?
/** Revoke one or more secret keys. */
fun revokeKey(): RevokeKey
fun revokeKey(): RevokeKey?
/** Update a key's password. */
fun changeKeyPassword(): ChangeKeyPassword
fun changeKeyPassword(): ChangeKeyPassword?
/** Keep a secret key up-to-date. */
fun updateKey(): UpdateKey?
/** Merge OpenPGP certificates. */
fun mergeCerts(): MergeCerts?
/** Certify OpenPGP Certificate User-IDs. */
fun certifyUserId(): CertifyUserId?
}

View file

@ -6,29 +6,47 @@ package sop
import sop.operation.DetachedVerify
import sop.operation.InlineVerify
import sop.operation.ValidateUserId
import sop.operation.Version
/** Subset of [SOP] implementing only OpenPGP signature verification. */
interface SOPV {
/** Get information about the implementations name and version. */
fun version(): Version
/**
* Get information about the implementations name and version.
*
* @since sopv 1.0
*/
fun version(): Version?
/**
* Verify detached signatures. If you need to verify an inline-signed message, use
* [inlineVerify] instead.
*
* @since sopv 1.0
*/
fun verify(): DetachedVerify = detachedVerify()
fun verify(): DetachedVerify? = detachedVerify()
/**
* Verify detached signatures. If you need to verify an inline-signed message, use
* [inlineVerify] instead.
*
* @since sopv 1.0
*/
fun detachedVerify(): DetachedVerify
fun detachedVerify(): DetachedVerify?
/**
* Verify signatures of an inline-signed message. If you need to verify detached signatures over
* a message, use [detachedVerify] instead.
*
* @since sopv 1.0
*/
fun inlineVerify(): InlineVerify
fun inlineVerify(): InlineVerify?
/**
* Validate a UserID in an OpenPGP certificate.
*
* @since sopv 1.2
*/
fun validateUserId(): ValidateUserId?
}

View file

@ -10,13 +10,23 @@ import sop.enums.SignatureMode
import sop.util.Optional
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(
val creationTime: Date,
val signingKeyFingerprint: String,
val signingCertFingerprint: String,
val signatureMode: Optional<SignatureMode>,
val description: Optional<String>
val jsonOrDescription: Optional<String>
) {
@JvmOverloads
constructor(
creationTime: Date,
@ -31,10 +41,49 @@ data class Verification(
Optional.ofNullable(signatureMode),
Optional.ofNullable(description?.trim()))
@JvmOverloads
constructor(
creationTime: Date,
signingKeyFingerprint: String,
signingCertFingerprint: String,
signatureMode: SignatureMode? = null,
json: JSON,
jsonSerializer: JSONSerializer
) : this(
creationTime,
signingKeyFingerprint,
signingCertFingerprint,
Optional.ofNullable(signatureMode),
Optional.of(jsonSerializer.serialize(json)))
@Deprecated("Replaced by jsonOrDescription", replaceWith = ReplaceWith("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. This method returns `null` if parsing fails.
*
* @param parser [JSONParser] implementation
* @return successfully parsed [JSON] POJO or `null`.
*/
fun getJson(parser: JSONParser): JSON? {
return jsonOrDescription.get()?.let {
try {
parser.parse(it)
} catch (e: ParseException) {
null
}
}
}
override fun toString(): String =
"${UTCUtil.formatUTCDate(creationTime)} $signingKeyFingerprint $signingCertFingerprint" +
(if (signatureMode.isPresent) " mode:${signatureMode.get()}" else "") +
(if (description.isPresent) " ${description.get()}" else "")
(if (jsonOrDescription.isPresent) " ${jsonOrDescription.get()}" else "")
companion object {
@JvmStatic
@ -73,4 +122,50 @@ data class Verification(
}
}
}
/**
* POJO data class representing JSON metadata.
*
* @param signers list of supplied CERTS objects that could have issued the signature,
* identified by the name given on the command line.
* @param comment a freeform UTF-8 encoded text describing the verification
* @param ext an extension object containing arbitrary, implementation-specific data
*/
data class JSON(val signers: List<String>, val comment: String?, val ext: Any?) {
/** Create a JSON object with only a list of signers. */
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 {
/**
* Parse a [JSON] POJO from the given single-line [string]. If the string does not represent
* a JSON object matching the [JSON] definition, this method throws a [ParseException].
*
* @param string [String] representation of the [JSON] object.
* @return parsed [JSON] POJO
* @throws ParseException if the [string] is not a JSON string representing the [JSON]
* object.
*/
@Throws(ParseException::class) fun parse(string: String): JSON
}
/**
* Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON
* strings.
*/
fun interface JSONSerializer {
/**
* Serialize the given [JSON] object into a single-line JSON string.
*
* @param json JSON POJO
* @return JSON string
*/
fun serialize(json: JSON): String
}
}

Some files were not shown because too many files have changed in this diff Show more