Implement validate-userid command

This commit is contained in:
Paul Schaub 2024-09-19 16:56:25 +02:00
parent 1945411712
commit 0ec2961cbe
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
8 changed files with 216 additions and 17 deletions

View file

@ -30,6 +30,7 @@ import sop.exception.SOPGPException
UpdateKeyCmd::class, UpdateKeyCmd::class,
MergeCertsCmd::class, MergeCertsCmd::class,
CertifyUserIdCmd::class, CertifyUserIdCmd::class,
ValidateUserIdCmd::class,
// Messaging subcommands // Messaging subcommands
SignCmd::class, SignCmd::class,
VerifyCmd::class, VerifyCmd::class,

View file

@ -0,0 +1,74 @@
// 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
import sop.util.HexUtil.Companion.bytesToHex
@Command(
name = "validate-userid",
resourceBundle = "msg_validate-userid",
exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE)
class ValidateUserIdCmd : AbstractSopCmd() {
@Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false
@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()
}
validateUserId.userId(userId)
for (authority in authorities) {
try {
getInput(authority).use { validateUserId.authorities(it) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (b: SOPGPException.BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", authority)
throw SOPGPException.BadData(errorMsg, b)
}
}
try {
val valid = validateUserId.subjects(System.`in`)
if (!valid) {
val errorMsg = getMsg("sop.error.runtime.any_cert_user_id_no_match", userId)
throw SOPGPException.CertUserIdNoMatch(errorMsg)
}
} catch (e: SOPGPException.CertUserIdNoMatch) {
val errorMsg =
if (e.fingerprint != null) {
getMsg(
"sop.error.runtime.cert_user_id_no_match",
bytesToHex(e.fingerprint!!),
userId)
} else {
getMsg("sop.error.runtime.any_cert_user_id_no_match", userId)
}
throw SOPGPException.CertUserIdNoMatch(errorMsg, e)
} catch (e: SOPGPException.BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", "STDIN")
throw SOPGPException.BadData(errorMsg, e)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -80,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_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.no_verifiable_signature_found=No verifiable signature found.
sop.error.runtime.cannot_decrypt_message=Message could not be decrypted. 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 ## Usage errors
sop.error.usage.password_or_cert_required=At least one password file or cert file required for encryption. 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. sop.error.usage.argument_required=Argument '%s' is required.

View file

@ -0,0 +1,18 @@
# 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
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -0,0 +1,18 @@
# 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
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -60,18 +60,15 @@ interface SOP : SOPV {
/** Update a key's password. */ /** Update a key's password. */
fun changeKeyPassword(): ChangeKeyPassword fun changeKeyPassword(): ChangeKeyPassword
/** /** Keep a secret key up-to-date. */
* Keep a secret key up-to-date.
*/
fun updateKey(): UpdateKey fun updateKey(): UpdateKey
/** /** Merge OpenPGP certificates. */
* Merge OpenPGP certificates.
*/
fun mergeCerts(): MergeCerts fun mergeCerts(): MergeCerts
/** /** Certify OpenPGP Certificate User-IDs. */
* Certify OpenPGP Certificate User-IDs.
*/
fun certifyUserId(): CertifyUserId fun certifyUserId(): CertifyUserId
/** Validate a UserID in an OpenPGP certificate. */
fun validateUserId(): ValidateUserId
} }

View file

@ -338,9 +338,7 @@ abstract class SOPGPException : RuntimeException {
} }
} }
/** /** The primary key of a KEYS object is too weak or revoked. */
* The primary key of a KEYS object is too weak or revoked.
*/
class PrimaryKeyBad : SOPGPException { class PrimaryKeyBad : SOPGPException {
constructor() : super() constructor() : super()
@ -353,13 +351,26 @@ abstract class SOPGPException : RuntimeException {
} }
} }
/** /** The CERTS object has no matching User ID. */
* The CERTS object has no matching User ID.
*/
class CertUserIdNoMatch : SOPGPException { class CertUserIdNoMatch : SOPGPException {
constructor() : super()
constructor(errorMsg: String) : super(errorMsg) 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 override fun getExitCode(): Int = EXIT_CODE

View file

@ -0,0 +1,78 @@
// 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.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())
}