From ed9b2f5fef7c9d8436c41d373e59555265abcfef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:45:14 +0100 Subject: [PATCH] Move signature verification operations to sopv interface subset --- .../main/kotlin/sop/external/ExternalSOPV.kt | 53 ++++++++++ .../sop/external/operation/VersionExternal.kt | 4 + .../main/kotlin/sop/cli/picocli/SopVCLI.kt | 98 +++++++++++++++++++ .../sop/cli/picocli/commands/VersionCmd.kt | 6 ++ .../src/main/resources/msg_sop.properties | 2 + .../src/main/resources/msg_sop_de.properties | 2 + sop-java/src/main/kotlin/sop/SOP.kt | 26 +---- sop-java/src/main/kotlin/sop/SOPV.kt | 34 +++++++ .../src/main/kotlin/sop/operation/Version.kt | 10 ++ .../sop/testsuite/operation/VersionTest.java | 14 +++ 10 files changed, 224 insertions(+), 25 deletions(-) create mode 100644 external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt create mode 100644 sop-java/src/main/kotlin/sop/SOPV.kt diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt new file mode 100644 index 0000000..f22f947 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external + +import java.nio.file.Files +import java.util.* +import sop.SOPV +import sop.external.ExternalSOP.TempDirProvider +import sop.external.operation.DetachedVerifyExternal +import sop.external.operation.InlineVerifyExternal +import sop.external.operation.VersionExternal +import sop.operation.DetachedVerify +import sop.operation.InlineVerify +import sop.operation.Version + +/** + * Implementation of the [SOPV] API subset using an external sopv/sop binary. + * + * Instantiate an [ExternalSOPV] object for the given binary and the given [TempDirProvider] using + * empty environment variables. + * + * @param binaryName name / path of the sopv binary + * @param tempDirProvider custom tempDirProvider + */ +class ExternalSOPV( + private val binaryName: String, + private val properties: Properties = Properties(), + private val tempDirProvider: TempDirProvider = defaultTempDirProvider() +) : SOPV { + + override fun version(): Version = VersionExternal(binaryName, properties) + + override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties) + + override fun inlineVerify(): InlineVerify = + InlineVerifyExternal(binaryName, properties, tempDirProvider) + + companion object { + + /** + * Default implementation of the [TempDirProvider] which stores temporary files in the + * systems temp dir ([Files.createTempDirectory]). + * + * @return default implementation + */ + @JvmStatic + fun defaultTempDirProvider(): TempDirProvider { + return TempDirProvider { Files.createTempDirectory("ext-sopv").toFile() } + } + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt index 7e13fc1..728f3b6 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt @@ -68,6 +68,10 @@ class VersionExternal(binary: String, environment: Properties) : Version { return null } + override fun getSopVVersion(): String { + return executeForLines(commandList.plus("--sopv")) + } + override fun getSopSpecVersion(): String { return executeForLines(commandList.plus("--sop-spec")) } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt new file mode 100644 index 0000000..9a8b4b4 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import java.util.* +import kotlin.system.exitProcess +import picocli.AutoComplete +import picocli.CommandLine +import sop.SOPV +import sop.cli.picocli.commands.* +import sop.exception.SOPGPException + +@CommandLine.Command( + name = "sopv", + resourceBundle = "msg_sop", + exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, + subcommands = + [ + // Meta subcommands + VersionCmd::class, + // signature verification subcommands + VerifyCmd::class, + InlineVerifyCmd::class, + // misc + CommandLine.HelpCommand::class, + AutoComplete.GenerateCompletion::class]) +class SopVCLI { + + companion object { + @JvmStatic private var sopvInstance: SOPV? = null + + @JvmStatic + fun getSopV(): SOPV = + checkNotNull(sopvInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") } + + @JvmStatic + fun setSopVInstance(sopv: SOPV?) { + sopvInstance = sopv + } + + @JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop") + + @JvmField var EXECUTABLE_NAME = "sopv" + + @JvmField + @CommandLine.Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) + var stacktrace = false + + @JvmStatic + fun main(vararg args: String) { + val exitCode = execute(*args) + if (exitCode != 0) { + exitProcess(exitCode) + } + } + + @JvmStatic + fun execute(vararg args: String): Int { + // Set locale + CommandLine(InitLocale()).parseArgs(*args) + + // Re-set bundle with updated locale + cliMsg = ResourceBundle.getBundle("msg_sop") + + return CommandLine(SopVCLI::class.java) + .apply { + // explicitly set help command resource bundle + subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) + // Hide generate-completion command + subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) + // overwrite executable name + commandName = EXECUTABLE_NAME + // setup exception handling + executionExceptionHandler = SOPExecutionExceptionHandler() + exitCodeExceptionMapper = SOPExceptionExitCodeMapper() + isCaseInsensitiveEnumValuesAllowed = true + } + .execute(*args) + } + } + + /** + * Control the locale. + * + * @see Picocli Readme + */ + @CommandLine.Command + class InitLocale { + @CommandLine.Option(names = ["-l", "--locale"], descriptionKey = "sop.locale") + fun setLocale(locale: String) = Locale.setDefault(Locale(locale)) + + @CommandLine.Unmatched + var remainder: MutableList = + mutableListOf() // ignore any other parameters and options in the first parsing phase + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt index 75197fe..8b1936a 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt @@ -22,6 +22,7 @@ class VersionCmd : AbstractSopCmd() { @Option(names = ["--extended"]) var extended: Boolean = false @Option(names = ["--backend"]) var backend: Boolean = false @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false + @Option(names = ["--sopv"]) var sopv: Boolean = false } override fun run() { @@ -47,5 +48,10 @@ class VersionCmd : AbstractSopCmd() { println(version.getSopSpecVersion()) return } + + if (exclusive!!.sopv) { + println(version.getSopVVersion()) + return + } } } diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 52c5368..7979eb3 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 sop.name=sop +sopv.name=sopv usage.header=Stateless OpenPGP Protocol +sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset locale=Locale for description texts # Generic diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 5900f39..40a316d 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 sop.name=sop +sopv.name=sopv usage.header=Stateless OpenPGP Protocol +sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset locale=Gebietsschema für Beschreibungstexte # Generic diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index e01763a..7fdd414 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -9,16 +9,13 @@ import sop.operation.ChangeKeyPassword import sop.operation.Dearmor import sop.operation.Decrypt import sop.operation.DetachedSign -import sop.operation.DetachedVerify import sop.operation.Encrypt import sop.operation.ExtractCert import sop.operation.GenerateKey import sop.operation.InlineDetach import sop.operation.InlineSign -import sop.operation.InlineVerify import sop.operation.ListProfiles import sop.operation.RevokeKey -import sop.operation.Version /** * Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related @@ -26,10 +23,7 @@ import sop.operation.Version * intended for reuse. If you for example need to generate multiple keys, make a dedicated call to * [generateKey] once per key generation. */ -interface SOP { - - /** Get information about the implementations name and version. */ - fun version(): Version +interface SOP : SOPV { /** Generate a secret key. */ fun generateKey(): GenerateKey @@ -53,24 +47,6 @@ interface SOP { */ fun inlineSign(): InlineSign - /** - * Verify detached signatures. If you need to verify an inline-signed message, use - * [inlineVerify] instead. - */ - fun verify(): DetachedVerify = detachedVerify() - - /** - * Verify detached signatures. If you need to verify an inline-signed message, use - * [inlineVerify] instead. - */ - 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 - /** Detach signatures from an inline signed message. */ fun inlineDetach(): InlineDetach diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt new file mode 100644 index 0000000..d331559 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.operation.DetachedVerify +import sop.operation.InlineVerify +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 + + /** + * Verify detached signatures. If you need to verify an inline-signed message, use + * [inlineVerify] instead. + */ + fun verify(): DetachedVerify = detachedVerify() + + /** + * Verify detached signatures. If you need to verify an inline-signed message, use + * [inlineVerify] instead. + */ + 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 +} diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index 9b3bd8a..5f26491 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -4,6 +4,9 @@ package sop.operation +import kotlin.jvm.Throws +import sop.exception.SOPGPException + interface Version { /** @@ -97,4 +100,11 @@ interface Version { * @return remarks or null */ fun getSopSpecImplementationRemarks(): String? + + /** + * Return the single-line SEMVER version of the sopv interface subset it provides complete + * coverage of. If the implementation does not provide complete coverage for any sopv interface, + * this method throws an [SOPGPException.UnsupportedOption] instead. + */ + @Throws(SOPGPException.UnsupportedOption::class) fun getSopVVersion(): String } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java index 0b19d20..f836935 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opentest4j.TestAbortedException; import sop.SOP; +import sop.exception.SOPGPException; import java.util.stream.Stream; @@ -72,4 +73,17 @@ public class VersionTest extends AbstractSOPTest { int sopRevision = sop.version().getSopSpecRevisionNumber(); assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void sopVVersionTest(SOP sop) { + try { + sop.version().getSopVVersion(); + } catch (SOPGPException.UnsupportedOption e) { + throw new TestAbortedException( + "Implementation does (gracefully) not provide coverage for any sopv interface version."); + } catch (RuntimeException e) { + throw new TestAbortedException("Implementation does not provide coverage for any sopv interface version."); + } + } }