1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-09 02:09:38 +02:00

Implement update-key command properly

This commit is contained in:
Paul Schaub 2025-06-12 13:35:24 +02:00
parent 6fe4a6c9c6
commit db0d753867
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
3 changed files with 297 additions and 2 deletions

View file

@ -0,0 +1,221 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key.modification.secretkeyring
import java.util.*
import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites.Combination
import org.bouncycastle.openpgp.api.KeyPairGeneratorCallback
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
import org.bouncycastle.openpgp.api.OpenPGPKey
import org.bouncycastle.openpgp.api.OpenPGPKeyEditor
import org.bouncycastle.openpgp.api.SignatureParameters
import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.Feature
import org.pgpainless.bouncycastle.PolicyAdapter
import org.pgpainless.bouncycastle.extensions.getKeyVersion
import org.pgpainless.bouncycastle.extensions.toAEADCipherModes
import org.pgpainless.bouncycastle.extensions.toCompressionAlgorithms
import org.pgpainless.bouncycastle.extensions.toHashAlgorithms
import org.pgpainless.bouncycastle.extensions.toSymmetricKeyAlgorithms
import org.pgpainless.key.protection.SecretKeyRingProtector
import org.pgpainless.policy.Policy
class OpenPGPKeyUpdater(
private var key: OpenPGPKey,
private val protector: SecretKeyRingProtector,
private val api: PGPainless = PGPainless.getInstance(),
private val policy: Policy = api.algorithmPolicy,
private val referenceTime: Date = Date()
) {
init {
key =
OpenPGPKey(
key.pgpSecretKeyRing, api.implementation, PolicyAdapter(Policy.wildcardPolicy()))
}
private val keyEditor = OpenPGPKeyEditor(key, protector)
fun extendExpirationIfExpiresBefore(
expiresBeforeSeconds: Long,
newExpirationTimeSecondsFromNow: Long? = _5YEARS
) = apply {
require(expiresBeforeSeconds > 0) {
"Time period to check expiration within MUST be positive."
}
require(newExpirationTimeSecondsFromNow == null || newExpirationTimeSecondsFromNow > 0) {
"New expiration period MUST be null or positive."
}
}
fun replaceRejectedAlgorithmPreferencesAndFeatures(addNewAlgorithms: Boolean = false) = apply {
val features = key.primaryKey.getFeatures(referenceTime)?.features ?: 0
val newFeatures =
Feature.fromBitmask(features.toInt())
// Filter out unsupported features
.filter { policy.featurePolicy.isAcceptable(it) }
.toSet()
// Optionally add in new capabilities
.plus(
if (addNewAlgorithms) policy.keyGenerationAlgorithmSuite.features ?: listOf()
else listOf())
.toTypedArray()
.let { Feature.toBitmask(*it) }
// Hash Algs
val hashAlgs = key.primaryKey.hashAlgorithmPreferences.toHashAlgorithms()
val newHashAlgs =
hashAlgs
// Filter out unsupported hash algorithms
.filter { policy.dataSignatureHashAlgorithmPolicy.isAcceptable(it) }
// Optionally add in new hash algorithms
.plus(
if (addNewAlgorithms)
policy.keyGenerationAlgorithmSuite.hashAlgorithms ?: listOf()
else listOf())
.toSet()
// Sym Algs
val symAlgs = key.primaryKey.symmetricCipherPreferences.toSymmetricKeyAlgorithms()
val newSymAlgs =
symAlgs
.filter {
policy.messageEncryptionAlgorithmPolicy.symmetricAlgorithmPolicy.isAcceptable(
it)
}
.plus(
if (addNewAlgorithms)
policy.keyGenerationAlgorithmSuite.symmetricKeyAlgorithms ?: listOf()
else listOf())
.toSet()
// Comp Algs
val compAlgs = key.primaryKey.compressionAlgorithmPreferences.toCompressionAlgorithms()
val newCompAlgs =
compAlgs
.filter { policy.compressionAlgorithmPolicy.isAcceptable(it) }
.plus(
if (addNewAlgorithms)
policy.keyGenerationAlgorithmSuite.compressionAlgorithms ?: listOf()
else listOf())
.toSet()
// AEAD Prefs
val aeadAlgs = key.primaryKey.aeadCipherSuitePreferences.toAEADCipherModes()
val newAeadAlgs =
aeadAlgs
.filter {
policy.messageEncryptionAlgorithmPolicy.isAcceptable(
MessageEncryptionMechanism.aead(
it.ciphermode.algorithmId, it.aeadAlgorithm.algorithmId))
}
.plus(policy.keyGenerationAlgorithmSuite.aeadAlgorithms ?: listOf())
.toSet()
if (features != newFeatures ||
hashAlgs != newHashAlgs ||
symAlgs != newSymAlgs ||
compAlgs != newCompAlgs ||
aeadAlgs != newAeadAlgs) {
keyEditor.addDirectKeySignature(
SignatureParameters.Callback.modifyHashedSubpackets { sigGen ->
sigGen.apply {
setKeyFlags(key.primaryKey.keyFlags?.flags ?: 0)
setFeature(true, newFeatures)
setPreferredHashAlgorithms(
true, newHashAlgs.map { it.algorithmId }.toIntArray())
setPreferredSymmetricAlgorithms(
true, newSymAlgs.map { it.algorithmId }.toIntArray())
setPreferredCompressionAlgorithms(
true, newCompAlgs.map { it.algorithmId }.toIntArray())
setPreferredAEADCiphersuites(
true,
newAeadAlgs
.map {
Combination(
it.ciphermode.algorithmId, it.aeadAlgorithm.algorithmId)
}
.toTypedArray())
}
})
}
}
fun replaceWeakSubkeys(
revokeWeakKeys: Boolean = true,
signingKeysOnly: Boolean
): OpenPGPKeyUpdater = apply {
replaceWeakSigningSubkeys(revokeWeakKeys)
if (!signingKeysOnly) {
replaceWeakEncryptionSubkeys(revokeWeakKeys)
}
}
fun replaceWeakEncryptionSubkeys(
revokeWeakKeys: Boolean,
keyPairGeneratorCallback: KeyPairGeneratorCallback =
KeyPairGeneratorCallback.encryptionKey()
) {
val weakEncryptionKeys =
key.getEncryptionKeys(referenceTime).filterNot {
policy.publicKeyAlgorithmPolicy.isAcceptable(
it.algorithm, it.pgpPublicKey.bitStrength)
}
if (weakEncryptionKeys.isNotEmpty()) {
keyEditor.addEncryptionSubkey(keyPairGeneratorCallback)
}
if (revokeWeakKeys) {
weakEncryptionKeys
.filterNot { it.keyIdentifier.matches(key.primaryKey.keyIdentifier) }
.forEach { keyEditor.revokeComponentKey(it) }
}
}
fun replaceWeakSigningSubkeys(
revokeWeakKeys: Boolean,
keyPairGenerator: PGPKeyPairGenerator = provideKeyPairGenerator(),
keyPairGeneratorCallback: KeyPairGeneratorCallback = KeyPairGeneratorCallback.signingKey()
) {
val weakSigningKeys =
key.getSigningKeys(referenceTime).filterNot {
policy.publicKeyAlgorithmPolicy.isAcceptable(
it.algorithm, it.pgpPublicKey.bitStrength)
}
if (weakSigningKeys.isNotEmpty()) {
keyEditor.addSigningSubkey(keyPairGeneratorCallback)
}
if (revokeWeakKeys) {
weakSigningKeys
.filterNot { it.keyIdentifier.matches(key.primaryKey.keyIdentifier) }
.forEach { keyEditor.revokeComponentKey(it) }
}
keyPairGeneratorCallback.generateFrom(keyPairGenerator)
}
private fun provideKeyPairGenerator(): PGPKeyPairGenerator {
return api.implementation
.pgpKeyPairGeneratorProvider()
.get(key.primaryKey.getKeyVersion().numeric, referenceTime)
}
fun finish(): OpenPGPKey {
return keyEditor.done()
}
companion object {
const val SECOND: Long = 1000
const val MINUTE: Long = 60 * SECOND
const val HOUR: Long = 60 * MINUTE
const val DAY: Long = 24 * HOUR
const val YEAR: Long = 365 * DAY
const val _5YEARS: Long = 5 * YEAR
}
}

View file

@ -21,6 +21,7 @@ class Policy {
val messageDecryptionAlgorithmPolicy: MessageEncryptionMechanismPolicy
val compressionAlgorithmPolicy: CompressionAlgorithmPolicy
val publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy
val featurePolicy: FeaturePolicy = FeaturePolicy.defaultFeaturePolicy()
val keyProtectionSettings: KeyRingProtectionSettings
val notationRegistry: NotationRegistry
val keyGenerationAlgorithmSuite: AlgorithmSuite
@ -717,4 +718,65 @@ class Policy {
keyGenerationAlgorithmSuite)
.apply { enableKeyParameterValidation = origin.enableKeyParameterValidation }
}
abstract class FeaturePolicy {
fun isAcceptable(feature: Byte): Boolean =
Feature.fromId(feature)?.let { isAcceptable(it) } ?: false
abstract fun isAcceptable(feature: Feature): Boolean
companion object {
@JvmStatic
fun defaultFeaturePolicy(): FeaturePolicy {
return whiteList(
listOf(Feature.MODIFICATION_DETECTION, Feature.MODIFICATION_DETECTION_2))
}
@JvmStatic
fun whiteList(whitelistedFeatures: List<Feature>): FeaturePolicy {
return object : FeaturePolicy() {
override fun isAcceptable(feature: Feature): Boolean {
return whitelistedFeatures.contains(feature)
}
}
}
}
}
companion object {
fun wildcardPolicy(): Policy =
Policy(
HashAlgorithmPolicy(HashAlgorithm.SHA512, HashAlgorithm.entries),
HashAlgorithmPolicy(HashAlgorithm.SHA512, HashAlgorithm.entries),
HashAlgorithmPolicy(HashAlgorithm.SHA512, HashAlgorithm.entries),
object :
MessageEncryptionMechanismPolicy(
SymmetricKeyAlgorithmPolicy(
SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.entries),
MessageEncryptionMechanism.integrityProtected(
SymmetricKeyAlgorithm.AES_256.algorithmId)) {
override fun isAcceptable(
encryptionMechanism: MessageEncryptionMechanism
): Boolean {
return true
}
},
object :
MessageEncryptionMechanismPolicy(
SymmetricKeyAlgorithmPolicy(
SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.entries),
MessageEncryptionMechanism.integrityProtected(
SymmetricKeyAlgorithm.AES_256.algorithmId)) {
override fun isAcceptable(
encryptionMechanism: MessageEncryptionMechanism
): Boolean {
return true
}
},
CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(),
PublicKeyAlgorithmPolicy(PublicKeyAlgorithm.entries.associateWith { 0 }.toMap()),
KeyRingProtectionSettings.secureDefaultSettings(),
NotationRegistry(setOf()),
AlgorithmSuite.defaultAlgorithmSuite)
}
}

View file

@ -1,15 +1,17 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
//
// SPDX-License-Identifier: CC0-1.0
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.sop
import java.io.InputStream
import java.io.OutputStream
import java.util.*
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.bouncycastle.openpgp.api.OpenPGPCertificate
import org.pgpainless.PGPainless
import org.pgpainless.key.modification.secretkeyring.OpenPGPKeyUpdater
import org.pgpainless.util.OpenPGPCertificateUtil
import org.pgpainless.util.Passphrase
import sop.Ready
@ -27,8 +29,9 @@ class UpdateKeyImpl(private val api: PGPainless) : UpdateKey {
override fun key(key: InputStream): Ready {
return object : Ready() {
override fun writeTo(outputStream: OutputStream) {
val keyList =
var keyList =
api.readKey().parseKeys(key).map {
// Merge keys
if (mergeCerts[it.keyIdentifier] == null) {
it
} else {
@ -41,6 +44,15 @@ class UpdateKeyImpl(private val api: PGPainless) : UpdateKey {
}
}
// Update keys
keyList =
keyList.map {
OpenPGPKeyUpdater(it, protector, api)
.replaceRejectedAlgorithmPreferencesAndFeatures(addCapabilities)
.replaceWeakSubkeys(true, signingOnly)
.finish()
}
if (armor) {
OpenPGPCertificateUtil.armor(keyList, outputStream)
} else {