mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2025-09-08 09:49:44 +02:00
Compare commits
64 commits
ad137d6351
...
b3223372c6
Author | SHA1 | Date | |
---|---|---|---|
b3223372c6 | |||
9762f1f043 | |||
191ec8c07d | |||
07d0aa6941 | |||
12835bfb8e | |||
04d154f63d | |||
01be696f75 | |||
86718a2690 | |||
e72e5a15c0 | |||
ac17000ff1 | |||
2a22cea29b | |||
8a7fd5cb58 | |||
21766a1f39 | |||
cdcbae7e5f | |||
ebfde35422 | |||
bff4423f93 | |||
5a7a8ae901 | |||
79aece6f04 | |||
28d06c330d | |||
ab13cc1de1 | |||
e1d048225b | |||
61206dde53 | |||
589884672a | |||
47a6db8702 | |||
0df80470c6 | |||
00a02686c8 | |||
c5d9e57f69 | |||
e481717421 | |||
9677f1fd0b | |||
e5cb58468b | |||
4ef5444e78 | |||
77106942d1 | |||
6d23d3771d | |||
9360b0e8ce | |||
38c5a947dd | |||
be460fabab | |||
138e275bb6 | |||
091b5f9a5e | |||
dea7e905a9 | |||
8c077a9c13 | |||
a8cfb8fbf4 | |||
a8497617d5 | |||
68bab9cbb4 | |||
65aa0afd4e | |||
a72545e3b9 | |||
082cbde869 | |||
b300be42a4 | |||
5105b6f4ad | |||
7f1c1b1aae | |||
4cf410a9f9 | |||
f1bdce99cb | |||
f7cc9ab816 | |||
40ccb8cc99 | |||
0bb50952c5 | |||
69fbfc09a7 | |||
dd12e28926 | |||
4ed326a142 | |||
122cd016a1 | |||
2b6a5dd651 | |||
d6c1330874 | |||
ada77be955 | |||
84404d629f | |||
4115a5041d | |||
1dcf13244d |
85 changed files with 2191 additions and 239 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -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.
|
||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0
|
|||
# SOP for Java
|
||||
|
||||
[](https://ci.codeberg.org/PGPainless/sop-java)
|
||||
[](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/)
|
||||
[](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/14/)
|
||||
[](https://coveralls.io/github/pgpainless/sop-java?branch=main)
|
||||
[](https://api.reuse.software/info/github.com/pgpainless/sop-java)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
13
build.gradle
13
build.gradle
|
@ -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 {
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
|
48
external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt
vendored
Normal file
48
external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt
vendored
Normal 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)
|
||||
}
|
30
external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt
vendored
Normal file
30
external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt
vendored
Normal 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)
|
||||
}
|
43
external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt
vendored
Normal file
43
external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt
vendored
Normal 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)
|
||||
}
|
43
external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt
vendored
Normal file
43
external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt
vendored
Normal 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)}")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// SPDX-FileCopyrightText: 2025 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.CertifyValidateUserIdTest;
|
||||
|
||||
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
|
||||
public class ExternalCertifyValidateUserIdTest extends CertifyValidateUserIdTest {
|
||||
|
||||
}
|
13
external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java
vendored
Normal file
13
external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
// SPDX-FileCopyrightText: 2025 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.ChangeKeyPasswordTest;
|
||||
|
||||
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
|
||||
public class ExternalChangeKeyPasswordTest extends ChangeKeyPasswordTest {
|
||||
|
||||
}
|
13
external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java
vendored
Normal file
13
external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
// SPDX-FileCopyrightText: 2025 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.MergeCertsTest;
|
||||
|
||||
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
|
||||
public class ExternalMergeCertsTest extends MergeCertsTest {
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -45,12 +45,12 @@ 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
static Stream<Arguments> provideInstances() {
|
||||
return AbstractSOPTest.provideBackends();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideInstances")
|
||||
public void certifyUserId(SOP sop) throws IOException {
|
||||
byte[] aliceKey = sop.generateKey()
|
||||
.withKeyPassword("sw0rdf1sh")
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
byte[] aliceCert = sop.extractCert()
|
||||
.key(aliceKey)
|
||||
.getBytes();
|
||||
|
||||
byte[] bobKey = sop.generateKey()
|
||||
.userId("Bob <bob@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
byte[] bobCert = sop.extractCert()
|
||||
.key(bobKey)
|
||||
.getBytes();
|
||||
|
||||
// Alice has her own user-id self-certified
|
||||
assertTrue(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, () ->
|
||||
sop.validateUserId()
|
||||
.authorities(aliceCert)
|
||||
.userId("Bob <bob@pgpainless.org>")
|
||||
.subjects(bobCert),
|
||||
"Alice has not yet certified Bobs user-id");
|
||||
|
||||
byte[] bobCertifiedByAlice = sop.certifyUserId()
|
||||
.userId("Bob <bob@pgpainless.org>")
|
||||
.withKeyPassword("sw0rdf1sh")
|
||||
.keys(aliceKey)
|
||||
.certs(bobCert)
|
||||
.getBytes();
|
||||
|
||||
assertTrue(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 = sop.generateKey()
|
||||
.noArmor()
|
||||
.withKeyPassword("sw0rdf1sh")
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
byte[] aliceCert = sop.extractCert()
|
||||
.noArmor()
|
||||
.key(aliceKey)
|
||||
.getBytes();
|
||||
|
||||
byte[] bobKey = sop.generateKey()
|
||||
.noArmor()
|
||||
.userId("Bob <bob@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
byte[] bobCert = sop.extractCert()
|
||||
.noArmor()
|
||||
.key(bobKey)
|
||||
.getBytes();
|
||||
|
||||
byte[] bobCertifiedByAlice = sop.certifyUserId()
|
||||
.noArmor()
|
||||
.userId("Bob <bob@pgpainless.org>")
|
||||
.withKeyPassword("sw0rdf1sh")
|
||||
.keys(aliceKey)
|
||||
.certs(bobCert)
|
||||
.getBytes();
|
||||
|
||||
assertTrue(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 = sop.generateKey()
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
byte[] aliceCert = sop.extractCert()
|
||||
.key(aliceKey)
|
||||
.getBytes();
|
||||
|
||||
byte[] bobKey = sop.generateKey()
|
||||
.userId("Bob <bob@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
byte[] bobCert = sop.extractCert()
|
||||
.key(bobKey)
|
||||
.getBytes();
|
||||
|
||||
assertThrows(SOPGPException.CertUserIdNoMatch.class, () ->
|
||||
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 = sop.certifyUserId()
|
||||
.userId("Bobby")
|
||||
.noRequireSelfSig()
|
||||
.keys(aliceKey)
|
||||
.certs(bobCert)
|
||||
.getBytes();
|
||||
|
||||
assertTrue(sop.validateUserId()
|
||||
.userId("Bobby")
|
||||
.authorities(aliceCert)
|
||||
.subjects(bobWithPetName),
|
||||
"Alice accepts the pet-name she gave to Bob");
|
||||
|
||||
assertThrows(SOPGPException.CertUserIdNoMatch.class, () ->
|
||||
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 = sop.generateKey()
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
byte[] aliceRevokedCert = sop.revokeKey()
|
||||
.keys(aliceKey)
|
||||
.getBytes();
|
||||
byte[] aliceRevokedKey = sop.updateKey()
|
||||
.mergeCerts(aliceRevokedCert)
|
||||
.key(aliceKey)
|
||||
.getBytes();
|
||||
|
||||
byte[] bobKey = sop.generateKey()
|
||||
.userId("Bob <bob@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
byte[] bobCert = sop.extractCert()
|
||||
.key(bobKey)
|
||||
.getBytes();
|
||||
|
||||
assertThrows(SOPGPException.KeyCannotCertify.class, () ->
|
||||
sop.certifyUserId()
|
||||
.userId("Bob <bob@pgpainless.org>")
|
||||
.keys(aliceRevokedKey)
|
||||
.certs(bobCert)
|
||||
.getBytes());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -338,4 +342,55 @@ public class EncryptDecryptTest extends AbstractSOPTest {
|
|||
.toByteArrayAndResult()
|
||||
.getBytes());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideInstances")
|
||||
public void encryptDecryptWithAllSupportedKeyGenerationProfiles(SOP sop) throws IOException {
|
||||
List<Profile> profiles = sop.listProfiles().generateKey();
|
||||
|
||||
List<byte[]> keys = new ArrayList<>();
|
||||
List<byte[]> certs = new ArrayList<>();
|
||||
for (Profile p : profiles) {
|
||||
byte[] k = sop.generateKey()
|
||||
.profile(p)
|
||||
.userId(p.getName())
|
||||
.generate()
|
||||
.getBytes();
|
||||
keys.add(k);
|
||||
|
||||
byte[] c = sop.extractCert()
|
||||
.key(k)
|
||||
.getBytes();
|
||||
certs.add(c);
|
||||
}
|
||||
|
||||
byte[] plaintext = "Hello, World!\n".getBytes();
|
||||
|
||||
Encrypt encrypt = 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 = 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);
|
||||
assertEquals(certs.size(), dResult.getVerifications().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -116,6 +119,34 @@ public class GenerateKeyTest extends AbstractSOPTest {
|
|||
|
||||
assertThrows(SOPGPException.CertCannotEncrypt.class, () ->
|
||||
sop.encrypt().withCert(signingOnlyCert)
|
||||
.plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)));
|
||||
.plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))
|
||||
.toByteArrayAndResult()
|
||||
.getBytes());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideInstances")
|
||||
public void generateKeyWithSupportedProfiles(SOP sop) throws IOException {
|
||||
List<Profile> profiles = sop.listProfiles()
|
||||
.generateKey();
|
||||
|
||||
for (Profile profile : profiles) {
|
||||
generateKeyWithProfile(sop, profile.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void generateKeyWithProfile(SOP sop, String profile) throws IOException {
|
||||
byte[] key;
|
||||
try {
|
||||
key = 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = sop.generateKey()
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
|
||||
byte[] cert = sop.extractCert()
|
||||
.key(key)
|
||||
.getBytes();
|
||||
|
||||
byte[] merged = sop.mergeCerts()
|
||||
.updates(cert)
|
||||
.baseCertificates(cert)
|
||||
.getBytes();
|
||||
|
||||
assertArrayEquals(cert, merged);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideInstances")
|
||||
public void testMergeWithItselfArmored(SOP sop) throws IOException {
|
||||
byte[] key = sop.generateKey()
|
||||
.noArmor()
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
|
||||
byte[] cert = sop.extractCert()
|
||||
.key(key)
|
||||
.getBytes();
|
||||
|
||||
byte[] merged = sop.mergeCerts()
|
||||
.updates(cert)
|
||||
.baseCertificates(cert)
|
||||
.getBytes();
|
||||
|
||||
assertArrayEquals(cert, merged);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideInstances")
|
||||
public void testMergeWithItselfViaBase(SOP sop) throws IOException {
|
||||
byte[] key = sop.generateKey()
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
|
||||
byte[] cert = sop.extractCert()
|
||||
.key(key)
|
||||
.getBytes();
|
||||
|
||||
byte[] certs = ArraysKt.plus(cert, cert);
|
||||
|
||||
byte[] merged = sop.mergeCerts()
|
||||
.updates(cert)
|
||||
.baseCertificates(certs)
|
||||
.getBytes();
|
||||
|
||||
assertArrayEquals(cert, merged);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideInstances")
|
||||
public void testApplyBaseToUpdate(SOP sop) throws IOException {
|
||||
byte[] key = sop.generateKey()
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
|
||||
byte[] cert = sop.extractCert()
|
||||
.key(key)
|
||||
.getBytes();
|
||||
|
||||
byte[] update = sop.revokeKey()
|
||||
.keys(key)
|
||||
.getBytes();
|
||||
|
||||
byte[] merged = sop.mergeCerts()
|
||||
.updates(cert)
|
||||
.baseCertificates(update)
|
||||
.getBytes();
|
||||
|
||||
assertArrayEquals(update, merged);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideInstances")
|
||||
public void testApplyUpdateToBase(SOP sop) throws IOException {
|
||||
byte[] key = sop.generateKey()
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
|
||||
byte[] cert = sop.extractCert()
|
||||
.key(key)
|
||||
.getBytes();
|
||||
|
||||
byte[] update = sop.revokeKey()
|
||||
.keys(key)
|
||||
.getBytes();
|
||||
|
||||
byte[] merged = sop.mergeCerts()
|
||||
.updates(update)
|
||||
.baseCertificates(cert)
|
||||
.getBytes();
|
||||
|
||||
assertArrayEquals(update, merged);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideInstances")
|
||||
public void testApplyUpdateToMissingBaseDoesNothing(SOP sop) throws IOException {
|
||||
byte[] aliceKey = sop.generateKey()
|
||||
.userId("Alice <alice@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
|
||||
byte[] aliceCert = sop.extractCert()
|
||||
.key(aliceKey)
|
||||
.getBytes();
|
||||
|
||||
byte[] bobKey = sop.generateKey()
|
||||
.userId("Bob <bob@pgpainless.org>")
|
||||
.generate()
|
||||
.getBytes();
|
||||
|
||||
byte[] bobCert = sop.extractCert()
|
||||
.key(bobKey)
|
||||
.getBytes();
|
||||
|
||||
byte[] merged = sop.mergeCerts()
|
||||
.updates(bobCert)
|
||||
.baseCertificates(aliceCert)
|
||||
.getBytes();
|
||||
|
||||
assertArrayEquals(aliceCert, merged);
|
||||
}
|
||||
|
||||
}
|
|
@ -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(sop.version().getSopJavaVersion());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
|
|
|
@ -6,29 +6,33 @@ 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
|
||||
fun version(): Version?
|
||||
|
||||
/**
|
||||
* Verify detached signatures. If you need to verify an inline-signed message, use
|
||||
* [inlineVerify] instead.
|
||||
*/
|
||||
fun verify(): DetachedVerify = detachedVerify()
|
||||
fun verify(): DetachedVerify? = detachedVerify()
|
||||
|
||||
/**
|
||||
* Verify detached signatures. If you need to verify an inline-signed message, use
|
||||
* [inlineVerify] instead.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
fun inlineVerify(): InlineVerify
|
||||
fun inlineVerify(): InlineVerify?
|
||||
|
||||
/** Validate a UserID in an OpenPGP certificate. */
|
||||
fun validateUserId(): ValidateUserId?
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,22 @@ abstract class SOPGPException : RuntimeException {
|
|||
|
||||
abstract fun getExitCode(): Int
|
||||
|
||||
/** An otherwise unspecified failure occurred */
|
||||
class UnspecificFailure : SOPGPException {
|
||||
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
constructor(message: String, e: Throwable) : super(message, e)
|
||||
|
||||
constructor(e: Throwable) : super(e)
|
||||
|
||||
override fun getExitCode(): Int = EXIT_CODE
|
||||
|
||||
companion object {
|
||||
const val EXIT_CODE = 1
|
||||
}
|
||||
}
|
||||
|
||||
/** No acceptable signatures found (sop verify, inline-verify). */
|
||||
class NoSignature : SOPGPException {
|
||||
@JvmOverloads
|
||||
|
@ -337,4 +353,64 @@ abstract class SOPGPException : RuntimeException {
|
|||
const val EXIT_CODE = 101
|
||||
}
|
||||
}
|
||||
|
||||
/** The primary key of a KEYS object is too weak or revoked. */
|
||||
class PrimaryKeyBad : SOPGPException {
|
||||
constructor() : super()
|
||||
|
||||
constructor(errorMsg: String) : super(errorMsg)
|
||||
|
||||
override fun getExitCode(): Int = EXIT_CODE
|
||||
|
||||
companion object {
|
||||
const val EXIT_CODE = 103
|
||||
}
|
||||
}
|
||||
|
||||
/** The CERTS object has no matching User ID. */
|
||||
class CertUserIdNoMatch : SOPGPException {
|
||||
|
||||
val fingerprint: ByteArray?
|
||||
|
||||
constructor() : super() {
|
||||
fingerprint = null
|
||||
}
|
||||
|
||||
constructor(fingerprint: ByteArray) : super() {
|
||||
this.fingerprint = fingerprint
|
||||
}
|
||||
|
||||
constructor(errorMsg: String) : super(errorMsg) {
|
||||
fingerprint = null
|
||||
}
|
||||
|
||||
constructor(errorMsg: String, cause: Throwable) : super(errorMsg, cause) {
|
||||
fingerprint = null
|
||||
}
|
||||
|
||||
override fun getExitCode(): Int = EXIT_CODE
|
||||
|
||||
companion object {
|
||||
const val EXIT_CODE = 107
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Key not certification-capable (e.g., expired, revoked, unacceptable usage flags) (sop
|
||||
* certify-userid)
|
||||
*/
|
||||
class KeyCannotCertify : SOPGPException {
|
||||
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
constructor(message: String, e: Throwable) : super(message, e)
|
||||
|
||||
constructor(e: Throwable) : super(e)
|
||||
|
||||
override fun getExitCode(): Int = EXIT_CODE
|
||||
|
||||
companion object {
|
||||
const val EXIT_CODE = 109
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
41
sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt
Normal file
41
sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt
Normal file
|
@ -0,0 +1,41 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.operation
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import sop.Ready
|
||||
import sop.exception.SOPGPException
|
||||
import sop.util.UTF8Util
|
||||
|
||||
interface CertifyUserId {
|
||||
|
||||
@Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): CertifyUserId
|
||||
|
||||
@Throws(SOPGPException.UnsupportedOption::class) fun userId(userId: String): CertifyUserId
|
||||
|
||||
@Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class)
|
||||
fun withKeyPassword(password: String): CertifyUserId =
|
||||
withKeyPassword(password.toByteArray(UTF8Util.UTF8))
|
||||
|
||||
@Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class)
|
||||
fun withKeyPassword(password: ByteArray): CertifyUserId
|
||||
|
||||
@Throws(SOPGPException.UnsupportedOption::class) fun noRequireSelfSig(): CertifyUserId
|
||||
|
||||
@Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class)
|
||||
fun keys(keys: InputStream): CertifyUserId
|
||||
|
||||
@Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class)
|
||||
fun keys(keys: ByteArray): CertifyUserId = keys(keys.inputStream())
|
||||
|
||||
@Throws(
|
||||
SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class)
|
||||
fun certs(certs: InputStream): Ready
|
||||
|
||||
@Throws(
|
||||
SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class)
|
||||
fun certs(certs: ByteArray): Ready = certs(certs.inputStream())
|
||||
}
|
27
sop-java/src/main/kotlin/sop/operation/MergeCerts.kt
Normal file
27
sop-java/src/main/kotlin/sop/operation/MergeCerts.kt
Normal file
|
@ -0,0 +1,27 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.operation
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import sop.Ready
|
||||
import sop.exception.SOPGPException
|
||||
|
||||
interface MergeCerts {
|
||||
|
||||
@Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): MergeCerts
|
||||
|
||||
@Throws(SOPGPException.BadData::class, IOException::class)
|
||||
fun updates(updateCerts: InputStream): MergeCerts
|
||||
|
||||
@Throws(SOPGPException.BadData::class, IOException::class)
|
||||
fun updates(updateCerts: ByteArray): MergeCerts = updates(updateCerts.inputStream())
|
||||
|
||||
@Throws(SOPGPException.BadData::class, IOException::class)
|
||||
fun baseCertificates(certs: InputStream): Ready
|
||||
|
||||
@Throws(SOPGPException.BadData::class, IOException::class)
|
||||
fun baseCertificates(certs: ByteArray): Ready = baseCertificates(certs.inputStream())
|
||||
}
|
96
sop-java/src/main/kotlin/sop/operation/UpdateKey.kt
Normal file
96
sop-java/src/main/kotlin/sop/operation/UpdateKey.kt
Normal file
|
@ -0,0 +1,96 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.operation
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import sop.Ready
|
||||
import sop.exception.SOPGPException
|
||||
import sop.util.UTF8Util
|
||||
|
||||
interface UpdateKey {
|
||||
|
||||
/**
|
||||
* Disable ASCII armor encoding of the output.
|
||||
*
|
||||
* @return builder instance
|
||||
*/
|
||||
fun noArmor(): UpdateKey
|
||||
|
||||
/**
|
||||
* Allow key to be used for signing only. If this option is not present, the operation may add a
|
||||
* new, encryption-capable component key.
|
||||
*/
|
||||
@Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey
|
||||
|
||||
/**
|
||||
* Do not allow adding new capabilities to the key. If this option is not present, the operation
|
||||
* may add support for new capabilities to the key.
|
||||
*/
|
||||
@Throws(SOPGPException.UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey
|
||||
|
||||
/**
|
||||
* Provide a passphrase for unlocking the secret key.
|
||||
*
|
||||
* @param password password
|
||||
*/
|
||||
@Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class)
|
||||
fun withKeyPassword(password: String): UpdateKey =
|
||||
withKeyPassword(password.toByteArray(UTF8Util.UTF8))
|
||||
|
||||
/**
|
||||
* Provide a passphrase for unlocking the secret key.
|
||||
*
|
||||
* @param password password
|
||||
*/
|
||||
@Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class)
|
||||
fun withKeyPassword(password: ByteArray): UpdateKey
|
||||
|
||||
/**
|
||||
* Provide certificates that might contain updated signatures or third-party certifications.
|
||||
* These certificates will be merged into the key.
|
||||
*
|
||||
* @param certs input stream of certificates
|
||||
*/
|
||||
@Throws(
|
||||
SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class)
|
||||
fun mergeCerts(certs: InputStream): UpdateKey
|
||||
|
||||
/**
|
||||
* Provide certificates that might contain updated signatures or third-party certifications.
|
||||
* These certificates will be merged into the key.
|
||||
*
|
||||
* @param certs binary certificates
|
||||
*/
|
||||
@Throws(
|
||||
SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class)
|
||||
fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream())
|
||||
|
||||
/**
|
||||
* Provide the OpenPGP key to update.
|
||||
*
|
||||
* @param key input stream containing the key
|
||||
* @return handle to acquire the updated OpenPGP key from
|
||||
*/
|
||||
@Throws(
|
||||
SOPGPException.BadData::class,
|
||||
IOException::class,
|
||||
SOPGPException.KeyIsProtected::class,
|
||||
SOPGPException.PrimaryKeyBad::class)
|
||||
fun key(key: InputStream): Ready
|
||||
|
||||
/**
|
||||
* Provide the OpenPGP key to update.
|
||||
*
|
||||
* @param key binary OpenPGP key
|
||||
* @return handle to acquire the updated OpenPGP key from
|
||||
*/
|
||||
@Throws(
|
||||
SOPGPException.BadData::class,
|
||||
IOException::class,
|
||||
SOPGPException.KeyIsProtected::class,
|
||||
SOPGPException.PrimaryKeyBad::class)
|
||||
fun key(key: ByteArray): Ready = key(key.inputStream())
|
||||
}
|
81
sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt
Normal file
81
sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt
Normal file
|
@ -0,0 +1,81 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.operation
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import sop.exception.SOPGPException
|
||||
|
||||
/** Subcommand to validate UserIDs on certificates. */
|
||||
interface ValidateUserId {
|
||||
|
||||
/**
|
||||
* If this is set, then the USERID is treated as an e-mail address, and matched only against the
|
||||
* e-mail address part of each correctly bound User ID. The rest of each correctly bound User ID
|
||||
* is ignored.
|
||||
*
|
||||
* @return this
|
||||
*/
|
||||
@Throws(SOPGPException.UnsupportedOption::class) fun addrSpecOnly(): ValidateUserId
|
||||
|
||||
/**
|
||||
* Set the UserID to validate. To match only the email address, call [addrSpecOnly].
|
||||
*
|
||||
* @param userId UserID or email address
|
||||
* @return this
|
||||
*/
|
||||
fun userId(userId: String): ValidateUserId
|
||||
|
||||
/**
|
||||
* Add certificates, which act as authorities. The [userId] is only considered correctly bound,
|
||||
* if it was bound by an authoritative certificate.
|
||||
*
|
||||
* @param certs authoritative certificates
|
||||
* @return this
|
||||
*/
|
||||
@Throws(SOPGPException.BadData::class, IOException::class)
|
||||
fun authorities(certs: InputStream): ValidateUserId
|
||||
|
||||
/**
|
||||
* Add certificates, which act as authorities. The [userId] is only considered correctly bound,
|
||||
* if it was bound by an authoritative certificate.
|
||||
*
|
||||
* @param certs authoritative certificates
|
||||
* @return this
|
||||
*/
|
||||
@Throws(SOPGPException.BadData::class, IOException::class)
|
||||
fun authorities(certs: ByteArray): ValidateUserId = authorities(certs.inputStream())
|
||||
|
||||
/**
|
||||
* Add subject certificates, on which UserID bindings are validated.
|
||||
*
|
||||
* @param certs subject certificates
|
||||
* @return true if all subject certificates have a correct binding to the UserID.
|
||||
* @throws SOPGPException.BadData if the subject certificates are malformed
|
||||
* @throws IOException if a parser exception happens
|
||||
* @throws SOPGPException.CertUserIdNoMatch if any subject certificate does not have a correctly
|
||||
* bound UserID that matches [userId].
|
||||
*/
|
||||
@Throws(
|
||||
SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class)
|
||||
fun subjects(certs: InputStream): Boolean
|
||||
|
||||
/**
|
||||
* Add subject certificates, on which UserID bindings are validated.
|
||||
*
|
||||
* @param certs subject certificates
|
||||
* @return true if all subject certificates have a correct binding to the UserID.
|
||||
* @throws SOPGPException.BadData if the subject certificates are malformed
|
||||
* @throws IOException if a parser exception happens
|
||||
* @throws SOPGPException.CertUserIdNoMatch if any subject certificate does not have a correctly
|
||||
* bound UserID that matches [userId].
|
||||
*/
|
||||
@Throws(
|
||||
SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class)
|
||||
fun subjects(certs: ByteArray): Boolean = subjects(certs.inputStream())
|
||||
|
||||
fun validateAt(date: Date): ValidateUserId
|
||||
}
|
|
@ -115,12 +115,12 @@ interface Version {
|
|||
fun getSopJavaVersion(): String? {
|
||||
return try {
|
||||
val resourceIn: InputStream =
|
||||
javaClass.getResourceAsStream("/sop-java-version.properties")
|
||||
Version::class.java.getResourceAsStream("/sop-java-version.properties")
|
||||
?: throw IOException("File sop-java-version.properties not found.")
|
||||
val properties = Properties().apply { load(resourceIn) }
|
||||
properties.getProperty("sop-java-version")
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
"DEVELOPMENT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.util
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* [OutputStream] that buffers data being written into it, until its underlying output stream is
|
||||
* being replaced. At that point, first all the buffered data is being written to the underlying
|
||||
* stream, followed by any successive data that may get written to the [ProxyOutputStream]. This
|
||||
* class is useful if we need to provide an [OutputStream] at one point in time when the final
|
||||
* target output stream is not yet known.
|
||||
*/
|
||||
@Deprecated("Marked for removal.")
|
||||
// TODO: Remove in 11.X
|
||||
class ProxyOutputStream : OutputStream() {
|
||||
private val buffer = ByteArrayOutputStream()
|
||||
private var swapped: OutputStream? = null
|
||||
|
||||
@Synchronized
|
||||
fun replaceOutputStream(underlying: OutputStream) {
|
||||
this.swapped = underlying
|
||||
swapped!!.write(buffer.toByteArray())
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
override fun write(b: ByteArray) {
|
||||
if (swapped == null) {
|
||||
buffer.write(b)
|
||||
} else {
|
||||
swapped!!.write(b)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
if (swapped == null) {
|
||||
buffer.write(b, off, len)
|
||||
} else {
|
||||
swapped!!.write(b, off, len)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
override fun flush() {
|
||||
buffer.flush()
|
||||
if (swapped != null) {
|
||||
swapped!!.flush()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
buffer.close()
|
||||
if (swapped != null) {
|
||||
swapped!!.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
override fun write(i: Int) {
|
||||
if (swapped == null) {
|
||||
buffer.write(i)
|
||||
} else {
|
||||
swapped!!.write(i)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,9 @@
|
|||
package sop;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sop.util.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
@ -19,6 +22,37 @@ public class ProfileTest {
|
|||
assertEquals("default: Use the implementers recommendations.", profile.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withAliasesToString() {
|
||||
Profile profile = new Profile(
|
||||
"Foo",
|
||||
Optional.of("Something something"),
|
||||
Arrays.asList("Bar", "Baz"));
|
||||
assertEquals("Foo: Something something (aliases: Bar, Baz)", profile.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWithAliases() {
|
||||
Profile profile = Profile.parse("Foo: Something something (aliases: Bar, Baz)");
|
||||
assertEquals("Foo", profile.getName());
|
||||
assertEquals("Something something", profile.getDescription().get());
|
||||
assertEquals(Arrays.asList("Bar", "Baz"), profile.getAliases());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changeAliasesWithWithAliases() {
|
||||
Profile p = new Profile("Foo", "Bar any Baz", Arrays.asList("tinitus", "particle"));
|
||||
p = p.withAliases("fnord", "qbit");
|
||||
|
||||
assertEquals("Foo", p.getName());
|
||||
assertEquals("Bar any Baz", p.getDescription().get());
|
||||
|
||||
assertTrue(p.getAliases().contains("fnord"));
|
||||
assertTrue(p.getAliases().contains("qbit"));
|
||||
assertFalse(p.getAliases().contains("tinitus"));
|
||||
assertFalse(p.getAliases().contains("particle"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringNameOnly() {
|
||||
Profile profile = new Profile("default");
|
||||
|
|
163
sop-java/src/test/java/sop/VerificationJSONTest.java
Normal file
163
sop-java/src/test/java/sop/VerificationJSONTest.java
Normal file
|
@ -0,0 +1,163 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sop.enums.SignatureMode;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class VerificationJSONTest {
|
||||
|
||||
// A hacky self-made "JSON parser" stand-in.
|
||||
// Only used for testing, do not use in production!
|
||||
private Verification.JSONParser dummyParser = new Verification.JSONParser() {
|
||||
@NotNull
|
||||
@Override
|
||||
public Verification.JSON parse(@NotNull String string) throws ParseException {
|
||||
if (!string.startsWith("{")) {
|
||||
throw new ParseException("Alleged JSON String does not begin with '{'", 0);
|
||||
}
|
||||
if (!string.endsWith("}")) {
|
||||
throw new ParseException("Alleged JSON String does not end with '}'", string.length() - 1);
|
||||
}
|
||||
|
||||
List<String> signersList = new ArrayList<>();
|
||||
Matcher signersMat = Pattern.compile("\"signers\": \\[(.*?)\\]").matcher(string);
|
||||
if (signersMat.find()) {
|
||||
String signersCat = signersMat.group(1);
|
||||
String[] split = signersCat.split(",");
|
||||
for (String s : split) {
|
||||
s = s.trim();
|
||||
signersList.add(s.substring(1, s.length() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
String comment = null;
|
||||
Matcher commentMat = Pattern.compile("\"comment\": \"(.*?)\"").matcher(string);
|
||||
if (commentMat.find()) {
|
||||
comment = commentMat.group(1);
|
||||
}
|
||||
|
||||
String ext = null;
|
||||
Matcher extMat = Pattern.compile("\"ext\": (.*?})}").matcher(string);
|
||||
if (extMat.find()) {
|
||||
ext = extMat.group(1);
|
||||
}
|
||||
|
||||
return new Verification.JSON(signersList, comment, ext);
|
||||
}
|
||||
};
|
||||
|
||||
// A just as hacky "JSON Serializer" lookalike.
|
||||
// Also don't use in production, for testing only!
|
||||
private Verification.JSONSerializer dummySerializer = new Verification.JSONSerializer() {
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull Verification.JSON json) {
|
||||
if (json.getSigners().isEmpty() && json.getComment() == null && json.getExt() == null) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
boolean comma = false;
|
||||
|
||||
if (!json.getSigners().isEmpty()) {
|
||||
comma = true;
|
||||
sb.append("\"signers\": [");
|
||||
for (Iterator<String> iterator = json.getSigners().iterator(); iterator.hasNext(); ) {
|
||||
String signer = iterator.next();
|
||||
sb.append('\"').append(signer).append('\"');
|
||||
if (iterator.hasNext()) {
|
||||
sb.append(", ");
|
||||
}
|
||||
}
|
||||
sb.append(']');
|
||||
}
|
||||
|
||||
if (json.getComment() != null) {
|
||||
if (comma) {
|
||||
sb.append(", ");
|
||||
}
|
||||
comma = true;
|
||||
sb.append("\"comment\": \"").append(json.getComment()).append('\"');
|
||||
}
|
||||
|
||||
if (json.getExt() != null) {
|
||||
if (comma) {
|
||||
sb.append(", ");
|
||||
}
|
||||
comma = true;
|
||||
sb.append("\"ext\": ").append(json.getExt().toString());
|
||||
}
|
||||
return sb.append('}').toString();
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testSimpleSerializeParse() throws ParseException {
|
||||
String signer = "alice.pub";
|
||||
Verification.JSON json = new Verification.JSON(signer);
|
||||
|
||||
String string = dummySerializer.serialize(json);
|
||||
assertEquals("{\"signers\": [\"alice.pub\"]}", string);
|
||||
|
||||
Verification.JSON parsed = dummyParser.parse(string);
|
||||
assertEquals(signer, parsed.getSigners().get(0));
|
||||
assertEquals(1, parsed.getSigners().size());
|
||||
assertNull(parsed.getComment());
|
||||
assertNull(parsed.getExt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdvancedSerializeParse() throws ParseException {
|
||||
Verification.JSON json = new Verification.JSON(
|
||||
Arrays.asList("../certs/alice.pub", "/etc/pgp/certs.pgp"),
|
||||
"This is a comment",
|
||||
"{\"Foo\": \"Bar\"}");
|
||||
|
||||
String serialized = dummySerializer.serialize(json);
|
||||
assertEquals("{\"signers\": [\"../certs/alice.pub\", \"/etc/pgp/certs.pgp\"], \"comment\": \"This is a comment\", \"ext\": {\"Foo\": \"Bar\"}}",
|
||||
serialized);
|
||||
|
||||
Verification.JSON parsed = dummyParser.parse(serialized);
|
||||
assertEquals(json.getSigners(), parsed.getSigners());
|
||||
assertEquals(json.getComment(), parsed.getComment());
|
||||
assertEquals(json.getExt(), parsed.getExt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerificationWithSimpleJson() {
|
||||
String string = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E mode:text {\"signers\": [\"alice.pgp\"]}";
|
||||
Verification verification = Verification.fromString(string);
|
||||
|
||||
assertTrue(verification.getContainsJson());
|
||||
assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningKeyFingerprint());
|
||||
assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningCertFingerprint());
|
||||
assertEquals(SignatureMode.text, verification.getSignatureMode().get());
|
||||
|
||||
Verification.JSON json = verification.getJson(dummyParser);
|
||||
assertNotNull(json, "The verification string MUST contain valid extension json");
|
||||
|
||||
assertEquals(Collections.singletonList("alice.pgp"), json.getSigners());
|
||||
assertNull(json.getComment());
|
||||
assertNull(json.getExt());
|
||||
|
||||
verification = new Verification(verification.getCreationTime(), verification.getSigningKeyFingerprint(), verification.getSigningCertFingerprint(), verification.getSignatureMode().get(), json, dummySerializer);
|
||||
assertEquals(string, verification.toString());
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import java.text.ParseException;
|
|||
import java.util.Date;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class VerificationTest {
|
||||
|
@ -25,6 +26,8 @@ public class VerificationTest {
|
|||
Verification verification = new Verification(signDate, keyFP, certFP);
|
||||
assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString());
|
||||
|
||||
assertFalse(verification.getContainsJson());
|
||||
|
||||
VerificationAssert.assertThatVerification(verification)
|
||||
.issuedBy(certFP)
|
||||
.isBySigningKey(keyFP)
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ProxyOutputStreamTest {
|
||||
|
||||
@Test
|
||||
public void replaceOutputStreamThrowsNPEForNull() {
|
||||
ProxyOutputStream proxy = new ProxyOutputStream();
|
||||
assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSwappingStreamPreservesWrittenBytes() throws IOException {
|
||||
byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
ProxyOutputStream proxy = new ProxyOutputStream();
|
||||
proxy.write(firstSection);
|
||||
|
||||
ByteArrayOutputStream swappedStream = new ByteArrayOutputStream();
|
||||
proxy.replaceOutputStream(swappedStream);
|
||||
|
||||
proxy.write(secondSection);
|
||||
proxy.close();
|
||||
|
||||
assertEquals("Foo\nBar\nBaz\n", swappedStream.toString());
|
||||
}
|
||||
}
|
|
@ -4,15 +4,13 @@
|
|||
|
||||
allprojects {
|
||||
ext {
|
||||
shortVersion = '10.1.2'
|
||||
shortVersion = '14.0.1'
|
||||
isSnapshot = true
|
||||
minAndroidSdk = 10
|
||||
javaSourceCompatibility = 11
|
||||
gsonVersion = '2.10.1'
|
||||
jsrVersion = '3.0.2'
|
||||
junitVersion = '5.8.2'
|
||||
junitSysExitVersion = '1.1.2'
|
||||
logbackVersion = '1.2.13' // 1.4+ cause CLI spam
|
||||
logbackVersion = '1.5.13'
|
||||
mockitoVersion = '4.5.1'
|
||||
picocliVersion = '4.6.3'
|
||||
slf4jVersion = '1.7.36'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue