mirror of
https://github.com/pgpainless/pgpainless.git
synced 2025-09-09 18:29:39 +02:00
Implement update-key command properly
This commit is contained in:
parent
d18158ac83
commit
19534fef09
3 changed files with 297 additions and 2 deletions
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue