mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2025-09-08 09:49:44 +02:00
Compare commits
9 commits
1084cf6128
...
ced207382c
Author | SHA1 | Date | |
---|---|---|---|
ced207382c | |||
4325bfb2ee | |||
6796ec95e7 | |||
a705bb4c3d | |||
051499974d | |||
cbdcfe54cb | |||
f7e6de4aaa | |||
a906028ce7 | |||
3700c60fde |
12 changed files with 280 additions and 42 deletions
29
.reuse/dep5
29
.reuse/dep5
|
@ -1,29 +0,0 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: SOP-Java
|
||||
Upstream-Contact: Paul Schaub <info@pgpainless.org>
|
||||
Source: https://pgpainless.org
|
||||
|
||||
# Sample paragraph, commented out:
|
||||
#
|
||||
# Files: src/*
|
||||
# Copyright: $YEAR $NAME <$CONTACT>
|
||||
# License: ...
|
||||
|
||||
# Gradle build tool
|
||||
Files: gradle*
|
||||
Copyright: 2015 the original author or authors.
|
||||
License: Apache-2.0
|
||||
|
||||
# Woodpecker build files
|
||||
Files: .woodpecker/*
|
||||
Copyright: 2022 the original author or authors.
|
||||
License: Apache-2.0
|
||||
|
||||
Files: external-sop/src/main/resources/sop/testsuite/external/*
|
||||
Copyright: 2023 the original author or authors
|
||||
License: Apache-2.0
|
||||
|
||||
# Github Issue Templates
|
||||
Files: .github/ISSUE_TEMPLATE/*
|
||||
Copyright: 2024 the original author or authors
|
||||
License: Apache-2.0
|
32
REUSE.toml
Normal file
32
REUSE.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
# SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
version = 1
|
||||
SPDX-PackageName = "SOP-Java"
|
||||
SPDX-PackageSupplier = "Paul Schaub <info@pgpainless.org>"
|
||||
SPDX-PackageDownloadLocation = "https://pgpainless.org"
|
||||
|
||||
[[annotations]]
|
||||
path = "gradle**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2015 the original author or authors."
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
[[annotations]]
|
||||
path = ".woodpecker/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2022 the original author or authors."
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
[[annotations]]
|
||||
path = "external-sop/src/main/resources/sop/testsuite/external/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2023 the original author or authors"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
[[annotations]]
|
||||
path = ".github/ISSUE_TEMPLATE/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2024 the original author or authors"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
|
@ -21,7 +21,7 @@ class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey {
|
|||
|
||||
override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") }
|
||||
|
||||
override fun noNewMechanisms(): UpdateKey = apply { commandList.add("--no-new-mechanisms") }
|
||||
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")
|
||||
|
|
|
@ -88,7 +88,10 @@ class SopCLI {
|
|||
// 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
|
||||
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
|
||||
|
@ -96,7 +99,8 @@ class SopCLI {
|
|||
executionExceptionHandler = SOPExecutionExceptionHandler()
|
||||
exitCodeExceptionMapper = SOPExceptionExitCodeMapper()
|
||||
isCaseInsensitiveEnumValuesAllowed = true
|
||||
}.execute(*args)
|
||||
}
|
||||
.execute(*args)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import sop.exception.SOPGPException
|
|||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||
class MergeCertsCmd : AbstractSopCmd() {
|
||||
|
||||
@CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = false
|
||||
@CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = true
|
||||
|
||||
@CommandLine.Parameters(paramLabel = "CERTS") var updates: List<String> = listOf()
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class UpdateKeyCmd : AbstractSopCmd() {
|
|||
|
||||
@Option(names = ["--signing-only"]) var signingOnly = false
|
||||
|
||||
@Option(names = ["--no-new-mechanisms"]) var noNewMechanisms = false
|
||||
@Option(names = ["--no-added-capabilities"]) var noAddedCapabilities = false
|
||||
|
||||
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
|
||||
var withKeyPassword: List<String> = listOf()
|
||||
|
@ -38,8 +38,8 @@ class UpdateKeyCmd : AbstractSopCmd() {
|
|||
updateKey.signingOnly()
|
||||
}
|
||||
|
||||
if (noNewMechanisms) {
|
||||
updateKey.noNewMechanisms()
|
||||
if (noAddedCapabilities) {
|
||||
updateKey.noAddedCapabilities()
|
||||
}
|
||||
|
||||
for (passwordFileName in withKeyPassword) {
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
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
|
||||
import java.util.*
|
||||
|
||||
@Command(
|
||||
name = "validate-userid",
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
// 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.assertFalse;
|
||||
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
|
||||
assertFalse(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 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");
|
||||
|
||||
assertFalse(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());
|
||||
}
|
||||
}
|
|
@ -9,9 +9,10 @@ package sop
|
|||
*
|
||||
* @param micAlg string identifying the digest mechanism used to create the signed message. This is
|
||||
* useful for setting the `micalg=` parameter for the multipart/signed content-type of a PGP/MIME
|
||||
* object as described in section 5 of [RFC3156](https://www.rfc-editor.org/rfc/rfc3156#section-5).
|
||||
* If more than one signature was generated and different digest mechanisms were used, the value
|
||||
* of the micalg object is an empty string.
|
||||
* object as described in section 5 of
|
||||
* [RFC3156](https://www.rfc-editor.org/rfc/rfc3156#section-5). If more than one signature was
|
||||
* generated and different digest mechanisms were used, the value of the micalg object is an empty
|
||||
* string.
|
||||
*/
|
||||
data class SigningResult(val micAlg: MicAlg) {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -378,4 +394,23 @@ abstract class SOPGPException : RuntimeException {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,25 +19,61 @@ interface UpdateKey {
|
|||
*/
|
||||
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
|
||||
|
||||
@Throws(SOPGPException.UnsupportedOption::class) fun noNewMechanisms(): 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,
|
||||
|
@ -45,6 +81,12 @@ interface UpdateKey {
|
|||
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,
|
||||
|
|
|
@ -6,8 +6,8 @@ package sop.operation
|
|||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import sop.exception.SOPGPException
|
||||
import java.util.*
|
||||
import sop.exception.SOPGPException
|
||||
|
||||
/** Subcommand to validate UserIDs on certificates. */
|
||||
interface ValidateUserId {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue