Move signature verification operations to sopv interface subset

This commit is contained in:
Paul Schaub 2024-03-17 15:45:14 +01:00
parent cd208c8942
commit ed9b2f5fef
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
10 changed files with 224 additions and 25 deletions

View file

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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() }
}
}
}

View file

@ -68,6 +68,10 @@ class VersionExternal(binary: String, environment: Properties) : Version {
return null return null
} }
override fun getSopVVersion(): String {
return executeForLines(commandList.plus("--sopv"))
}
override fun getSopSpecVersion(): String { override fun getSopSpecVersion(): String {
return executeForLines(commandList.plus("--sop-spec")) return executeForLines(commandList.plus("--sop-spec"))
} }

View file

@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <a href="https://picocli.info/#_controlling_the_locale">Picocli Readme</a>
*/
@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<String> =
mutableListOf() // ignore any other parameters and options in the first parsing phase
}
}

View file

@ -22,6 +22,7 @@ class VersionCmd : AbstractSopCmd() {
@Option(names = ["--extended"]) var extended: Boolean = false @Option(names = ["--extended"]) var extended: Boolean = false
@Option(names = ["--backend"]) var backend: Boolean = false @Option(names = ["--backend"]) var backend: Boolean = false
@Option(names = ["--sop-spec"]) var sopSpec: Boolean = false @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false
@Option(names = ["--sopv"]) var sopv: Boolean = false
} }
override fun run() { override fun run() {
@ -47,5 +48,10 @@ class VersionCmd : AbstractSopCmd() {
println(version.getSopSpecVersion()) println(version.getSopSpecVersion())
return return
} }
if (exclusive!!.sopv) {
println(version.getSopVVersion())
return
}
} }
} }

View file

@ -2,7 +2,9 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
sop.name=sop sop.name=sop
sopv.name=sopv
usage.header=Stateless OpenPGP Protocol usage.header=Stateless OpenPGP Protocol
sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset
locale=Locale for description texts locale=Locale for description texts
# Generic # Generic

View file

@ -2,7 +2,9 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
sop.name=sop sop.name=sop
sopv.name=sopv
usage.header=Stateless OpenPGP Protocol usage.header=Stateless OpenPGP Protocol
sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset
locale=Gebietsschema für Beschreibungstexte locale=Gebietsschema für Beschreibungstexte
# Generic # Generic

View file

@ -9,16 +9,13 @@ import sop.operation.ChangeKeyPassword
import sop.operation.Dearmor import sop.operation.Dearmor
import sop.operation.Decrypt import sop.operation.Decrypt
import sop.operation.DetachedSign import sop.operation.DetachedSign
import sop.operation.DetachedVerify
import sop.operation.Encrypt import sop.operation.Encrypt
import sop.operation.ExtractCert import sop.operation.ExtractCert
import sop.operation.GenerateKey import sop.operation.GenerateKey
import sop.operation.InlineDetach import sop.operation.InlineDetach
import sop.operation.InlineSign import sop.operation.InlineSign
import sop.operation.InlineVerify
import sop.operation.ListProfiles import sop.operation.ListProfiles
import sop.operation.RevokeKey import sop.operation.RevokeKey
import sop.operation.Version
/** /**
* Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related * 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 * intended for reuse. If you for example need to generate multiple keys, make a dedicated call to
* [generateKey] once per key generation. * [generateKey] once per key generation.
*/ */
interface SOP { interface SOP : SOPV {
/** Get information about the implementations name and version. */
fun version(): Version
/** Generate a secret key. */ /** Generate a secret key. */
fun generateKey(): GenerateKey fun generateKey(): GenerateKey
@ -53,24 +47,6 @@ interface SOP {
*/ */
fun inlineSign(): InlineSign 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. */ /** Detach signatures from an inline signed message. */
fun inlineDetach(): InlineDetach fun inlineDetach(): InlineDetach

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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
}

View file

@ -4,6 +4,9 @@
package sop.operation package sop.operation
import kotlin.jvm.Throws
import sop.exception.SOPGPException
interface Version { interface Version {
/** /**
@ -97,4 +100,11 @@ interface Version {
* @return remarks or null * @return remarks or null
*/ */
fun getSopSpecImplementationRemarks(): String? 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
} }

View file

@ -10,6 +10,7 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.opentest4j.TestAbortedException; import org.opentest4j.TestAbortedException;
import sop.SOP; import sop.SOP;
import sop.exception.SOPGPException;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -72,4 +73,17 @@ public class VersionTest extends AbstractSOPTest {
int sopRevision = sop.version().getSopSpecRevisionNumber(); int sopRevision = sop.version().getSopSpecRevisionNumber();
assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); 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.");
}
}
} }