Per-key protection and default binding signatures

This commit is contained in:
Paul Schaub 2024-02-29 16:16:16 +01:00
parent b4240ac9f7
commit f1aa910431
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
3 changed files with 242 additions and 25 deletions

View File

@ -5,6 +5,7 @@
package org.pgpainless.key
import java.nio.charset.Charset
import org.bouncycastle.openpgp.PGPKeyPair
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSecretKey
@ -108,6 +109,8 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable<OpenPgpFingerprint>
*/
@JvmStatic fun of(key: PGPPublicKey): OpenPgpFingerprint = of(key.version, key.fingerprint)
@JvmStatic fun of(key: PGPKeyPair): OpenPgpFingerprint = of(key.publicKey)
@JvmStatic
fun of(keyVersion: Int, binaryFingerprint: ByteArray): OpenPgpFingerprint =
when (keyVersion) {

View File

@ -22,6 +22,7 @@ import org.pgpainless.algorithm.KeyFlag
import org.pgpainless.algorithm.PublicKeyAlgorithm
import org.pgpainless.bouncycastle.extensions.plusCertification
import org.pgpainless.implementation.ImplementationFactory
import org.pgpainless.key.OpenPgpFingerprint
import org.pgpainless.key.generation.DefinePrimaryKey.PrimaryKeyBuilder
import org.pgpainless.key.generation.DefineSubkeys.SubkeyBuilder
import org.pgpainless.key.generation.OpenPgpKeyTemplates.Companion.v4
@ -99,6 +100,9 @@ internal constructor(val policy: Policy, val creationTime: Date, val preferences
// signatures with the intended key flags.
protected var keyFlags: List<KeyFlag>? = null
// Dedicated protector for the primary key.
protected var primaryKeyProtector: SecretKeyRingProtector? = null
/**
* Callback to set preferences on the key and user-ids, such as algorithm preferences, features
* etc. This callback will be used to modify direct-key signatures and bindings for user-ids.
@ -398,6 +402,25 @@ internal constructor(val policy: Policy, val creationTime: Date, val preferences
fun skipDefaultSignature() {
builder.skipDefaultDirectKeySignature = true
}
/**
* Set a dedicated [Passphrase] for the primary key. This is useful, if each (sub-) key of
* this OpenPGP key is intended to use a different passphrase.
*
* @param passphrase passphrase to protect the primary key with
*/
fun setPrimaryKeyPassphrase(passphrase: Passphrase) =
setPrimaryKeyProtector(SecretKeyRingProtector.unlockAnyKeyWith(passphrase))
/**
* Set a dedicated [SecretKeyRingProtector] for the primary key. This is useful, if each
* (sub-) key of this OpenPGP key is intended to use a different passphrase.
*
* @param protector protector to protect the primary key with
*/
fun setPrimaryKeyProtector(protector: SecretKeyRingProtector) {
builder.primaryKeyProtector = protector
}
}
}
@ -412,14 +435,19 @@ internal constructor(val policy: Policy, val creationTime: Date, val preferences
*/
abstract class DefineSubkeys<B : DefineSubkeys<B>>
internal constructor(
internal var primaryKey: PGPKeyPair,
internal val primaryKey: PGPKeyPair,
internal val primaryKeyProtector: SecretKeyRingProtector?,
internal val policy: Policy,
internal val creationTime: Date,
internal val subkeys: MutableList<PGPKeyPair> = mutableListOf()
internal val subkeys: MutableList<PGPKeyPair> = mutableListOf(),
internal val subkeyProtectors: MutableMap<OpenPgpFingerprint, SecretKeyRingProtector> =
mutableMapOf(),
internal val skipDefaultBindingSignatureFor: MutableList<OpenPgpFingerprint> = mutableListOf()
) {
/**
* Add a subkey to the OpenPGP key.
* Add a subkey to the OpenPGP key. If no explicit binding signature is set inside [block], the
* key will be bound using a default binding signature containing the given [flags].
*
* @param type subkey type
* @param creationTime creation time of the subkey
@ -439,14 +467,23 @@ internal constructor(
sanitizeSubkeyCreationTime(creationTime, primaryKey)
var subkey = generateSubkey(type, creationTime)
val subkeyBlock =
block
?: {
addBindingSignature(
SelfSignatureSubpackets.applyHashed { flags?.let { setKeyFlags(it) } },
bindingTime = creationTime)
}
// Default function block will only set appropriate key flags
val defaultBlock: SubkeyBlock = {
addBindingSignature(
SelfSignatureSubpackets.applyHashed { flags?.let { setKeyFlags(it) } },
bindingTime = creationTime)
}
// if no custom function block is given, simply set key flags
val subkeyBlock = block ?: defaultBlock
subkey = invokeOnSubkey(subkey, subkeyBlock)
// If no binding signature was added yet, add a default binding sig using the default
// block
if (!skipDefaultBindingSignatureFor.contains(OpenPgpFingerprint.of(subkey))) {
subkey = invokeOnSubkey(subkey, defaultBlock)
}
subkeys.add(subkey)
}
as B
@ -472,7 +509,9 @@ internal constructor(
): PGPKeyPair
/**
* Finish the key generation and return the OpenPGP [PGPSecretKeyRing].
* Finish the key generation and return the OpenPGP [PGPSecretKeyRing]. The [protector] is used
* as a catch-all to protect any keys where the user did not specify protection explicitly
* otherwise.
*
* @param protector protector to protect the OpenPGP key's secret components with
* @return finished [PGPSecretKeyRing]
@ -481,7 +520,8 @@ internal constructor(
/**
* Finish the key generation and return the OpenPGP [PGPSecretKeyRing] protected with the given
* [passphrase].
* [passphrase]. The [passphrase] is used as a catch-all to protect any keys where the user did
* not specify protection explicitly otherwise.
*
* @param passphrase passphrase to protect the OpenPGP key's secret components with
* @return finished [PGPSecretKeyRing]
@ -543,6 +583,10 @@ internal constructor(
/**
* Add a binding signature to the subkey.
*
* If this method is not explicitly called at least once while adding a subkey, the subkey
* will be bound using a default binding signature. To prevent adding this default
* signature, call [skipDefaultBindingSignatureFor].
*
* @param subpacketsCallback callback to modify the binding signatures subpackets
* @param hashAlgorithm hash algorithm to be used during signature calculation
* @param bindingTime creation time of the binding signature
@ -559,6 +603,16 @@ internal constructor(
builder.sanitizeBindingTime(bindingTime, subkey)
doAddBindingSignature(subpacketsCallback, hashAlgorithm, bindingTime)
skipDefaultBindingSignature()
}
/**
* Do not bind the key using a default binding signature, even if the user did not add an
* explicit binding signature. This method is useful mostly for testing to generate keys
* with unbound subkeys.
*/
fun skipDefaultBindingSignature() {
builder.skipDefaultBindingSignatureFor.add(OpenPgpFingerprint.of(subkey))
}
abstract fun doAddBindingSignature(
@ -566,6 +620,13 @@ internal constructor(
hashAlgorithm: HashAlgorithm,
bindingTime: Date
)
fun setSubkeyPassphrase(passphrase: Passphrase) =
setSubkeyProtector(SecretKeyRingProtector.unlockAnyKeyWith(passphrase))
fun setSubkeyProtector(protector: SecretKeyRingProtector) {
builder.subkeyProtectors[OpenPgpFingerprint.of(subkey)] = protector
}
}
}
@ -646,10 +707,13 @@ internal constructor(primaryKey: PGPKeyPair, subkey: PGPKeyPair, builder: Define
*/
abstract class DefineSubkeysV4<O : DefineSubkeys<O>>(
primaryKey: PGPKeyPair,
primaryKeyProtector: SecretKeyRingProtector?,
policy: Policy,
creationTime: Date,
subkeys: List<PGPKeyPair>
) : DefineSubkeys<O>(primaryKey, policy, creationTime, subkeys.toMutableList()) {
) :
DefineSubkeys<O>(
primaryKey, primaryKeyProtector, policy, creationTime, subkeys.toMutableList()) {
override fun generateSubkey(type: KeyType, creationTime: Date): PGPKeyPair {
return OpenPgpKeyPairGenerator.V4().generateSubkey(type, creationTime)
@ -674,7 +738,7 @@ internal constructor(primaryKey: PGPKeyPair, subkey: PGPKeyPair, builder: Define
primaryKey.publicKey,
ImplementationFactory.getInstance().v4FingerprintCalculator,
true,
protector.getEncryptor(primaryKey.keyID)))
(primaryKeyProtector ?: protector).getEncryptor(primaryKey.keyID)))
// Subkeys
subkeys.forEach {
@ -684,7 +748,9 @@ internal constructor(primaryKey: PGPKeyPair, subkey: PGPKeyPair, builder: Define
it.publicKey,
ImplementationFactory.getInstance().v4FingerprintCalculator,
false,
protector.getEncryptor(it.keyID)))
subkeyProtectors
.getOrDefault(OpenPgpFingerprint.of(it), protector)
.getEncryptor(it.keyID)))
}
})
}
@ -782,7 +848,7 @@ internal constructor(policy: Policy, creationTime: Date, preferences: AlgorithmS
invokeOnPrimaryKey(primaryKey) { addDirectKeySignature(preferencesSubpackets()) }
}
return OpinionatedDefineSubkeysV4(primaryKey, policy, creationTime)
return OpinionatedDefineSubkeysV4(primaryKey, primaryKeyProtector, policy, creationTime)
}
override fun sanitizeHashAlgorithm(algorithm: HashAlgorithm) {
@ -866,7 +932,7 @@ internal constructor(policy: Policy, creationTime: Date, preferences: AlgorithmS
primaryKey = invokeOnPrimaryKey(primaryKey, block)
// return builder for adding subkeys
return UnopinionatedDefineSubkeysV4(primaryKey, policy, creationTime)
return UnopinionatedDefineSubkeysV4(primaryKey, primaryKeyProtector, policy, creationTime)
}
}
@ -878,9 +944,14 @@ internal constructor(policy: Policy, creationTime: Date, preferences: AlgorithmS
* @param creationTime creation time of the OpenPGP key
*/
class OpinionatedDefineSubkeysV4
internal constructor(primaryKey: PGPKeyPair, policy: Policy, creationTime: Date) :
internal constructor(
primaryKey: PGPKeyPair,
primaryKeyProtector: SecretKeyRingProtector?,
policy: Policy,
creationTime: Date
) :
SubkeyBuilderV4.DefineSubkeysV4<OpinionatedDefineSubkeysV4>(
primaryKey, policy, creationTime, listOf()) {
primaryKey, primaryKeyProtector, policy, creationTime, listOf()) {
/**
* Return an unopinionated implementation of this builder.
@ -890,7 +961,9 @@ internal constructor(primaryKey: PGPKeyPair, policy: Policy, creationTime: Date)
fun unopinionated() = UnopinionatedDefineSubkeysV4(this)
/**
* Add a subkey for signing messages to the OpenPGP key.
* Add a subkey for signing messages to the OpenPGP key. If no explicit binding signature is set
* inside [block], the key will be bound using a default binding signature marking the key as
* signing capable.
*
* @param type signing key type
* @param creationTime creation time of the signing subkey
@ -906,7 +979,9 @@ internal constructor(primaryKey: PGPKeyPair, policy: Policy, creationTime: Date)
}
/**
* Add a subkey for signing messages to the OpenPGP key.
* Add a subkey for signing messages to the OpenPGP key. If no explicit binding signature is set
* inside [block], the key will be bound using a default binding signature marking the key as
* signing capable.
*
* @param type signing key type
* @param block function block to add binding signatures to the subkey
@ -915,7 +990,9 @@ internal constructor(primaryKey: PGPKeyPair, policy: Policy, creationTime: Date)
addSigningSubkey(type, this.creationTime, block)
/**
* Add a subkey for message encryption to the OpenPGP key.
* Add a subkey for message encryption to the OpenPGP key. If no explicit binding signature is
* set inside [block], the key will be bound using a default binding signature marking the key
* as encryption capable.
*
* @param type encryption key type
* @param creationTime creation time of the encryption key
@ -932,7 +1009,9 @@ internal constructor(primaryKey: PGPKeyPair, policy: Policy, creationTime: Date)
}
/**
* Add a subkey for message encryption to the OpenPGP key.
* Add a subkey for message encryption to the OpenPGP key. If no explicit binding signature is
* set inside [block], the key will be bound using a default binding signature marking the key
* as encryption capable.
*
* @param type encryption key type
* @param block function block to add binding signatures to the subkey
@ -996,12 +1075,13 @@ internal constructor(primaryKey: PGPKeyPair, policy: Policy, creationTime: Date)
class UnopinionatedDefineSubkeysV4
internal constructor(
primaryKey: PGPKeyPair,
primaryKeyProtector: SecretKeyRingProtector?,
policy: Policy,
creationTime: Date,
subkeys: List<PGPKeyPair> = mutableListOf()
) :
SubkeyBuilderV4.DefineSubkeysV4<UnopinionatedDefineSubkeysV4>(
primaryKey, policy, creationTime, subkeys) {
primaryKey, primaryKeyProtector, policy, creationTime, subkeys) {
/**
* Constructor to build an unopinionated variant of the given [OpinionatedDefineSubkeysV4].
@ -1011,7 +1091,11 @@ internal constructor(
internal constructor(
opinionated: OpinionatedDefineSubkeysV4
) : this(
opinionated.primaryKey, opinionated.policy, opinionated.creationTime, opinionated.subkeys)
opinionated.primaryKey,
opinionated.primaryKeyProtector,
opinionated.policy,
opinionated.creationTime,
opinionated.subkeys)
override fun generateSubkey(type: KeyType, creationTime: Date): PGPKeyPair {
return OpenPgpKeyPairGenerator.V4().generateSubkey(type, creationTime)

View File

@ -17,6 +17,7 @@ import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.fail
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.HashAlgorithm
import org.pgpainless.algorithm.KeyFlag
@ -28,10 +29,13 @@ import org.pgpainless.key.generation.type.KeyType
import org.pgpainless.key.generation.type.eddsa.EdDSACurve
import org.pgpainless.key.generation.type.rsa.RsaLength
import org.pgpainless.key.generation.type.xdh.XDHSpec
import org.pgpainless.key.protection.SecretKeyRingProtector
import org.pgpainless.key.protection.UnlockSecretKey
import org.pgpainless.policy.Policy
import org.pgpainless.signature.subpackets.SelfSignatureSubpackets
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil
import org.pgpainless.util.DateUtil
import org.pgpainless.util.Passphrase
class OpenPgpKeyGeneratorTest {
@ -664,4 +668,130 @@ class OpenPgpKeyGeneratorTest {
.build()
.let { println(it.toAsciiArmor()) }
}
@Test
fun `generate key with unbound subkey`() {
val policy = Policy()
val key =
PGPainless.generateOpenPgpKey(policy)
.buildV4Key()
.setPrimaryKey(KeyType.EDDSA(EdDSACurve._Ed25519))
.addSigningSubkey(KeyType.EDDSA(EdDSACurve._Ed25519)) {
skipDefaultBindingSignature()
}
.build()
val subkey = key.secretKeys.asSequence().last()
assertFalse(PGPainless.inspectKeyRing(key).isKeyValidlyBound(subkey.keyID))
}
@Test
fun `generate key with a single passphrase for all subkeys`() {
val policy = Policy()
val key =
PGPainless.generateOpenPgpKey(policy)
.buildV4Key()
.setPrimaryKey(KeyType.EDDSA(EdDSACurve._Ed25519))
.addSigningSubkey(KeyType.EDDSA(EdDSACurve._Ed25519))
.addEncryptionSubkey(KeyType.XDH(XDHSpec._X25519))
.build(Passphrase.fromPassword("sw0rdf1sh"))
assertTrue(PGPainless.inspectKeyRing(key).isFullyEncrypted)
for (subkey in key.secretKeys) {
UnlockSecretKey.unlockSecretKey(subkey, Passphrase.fromPassword("sw0rdf1sh"))
}
}
@Test
fun `generate key with dedicated passphrases for all subkeys`() {
val policy = Policy()
val key =
PGPainless.generateOpenPgpKey(policy)
.buildV4Key()
.setPrimaryKey(KeyType.EDDSA(EdDSACurve._Ed25519)) {
setPrimaryKeyPassphrase(Passphrase.fromPassword("MyColorIsRed"))
}
.addSigningSubkey(KeyType.EDDSA(EdDSACurve._Ed25519)) {
setSubkeyPassphrase(Passphrase.fromPassword("MyColorIsGreen"))
}
.addEncryptionSubkey(KeyType.XDH(XDHSpec._X25519)) {
setSubkeyPassphrase(Passphrase.fromPassword("MyColorIsBlue"))
}
.build()
assertTrue(PGPainless.inspectKeyRing(key).isFullyEncrypted)
key.secretKeys.asSequence().forEachIndexed { index, subkey ->
UnlockSecretKey.unlockSecretKey(
subkey,
when (index) {
0 -> Passphrase.fromPassword("MyColorIsRed")
1 -> Passphrase.fromPassword("MyColorIsGreen")
2 -> Passphrase.fromPassword("MyColorIsBlue")
else -> fail { "Unexpected secret key at index $index" }
})
}
}
@Test
fun `generate key with dedicated subkey passphrase and catch-all primary key protection`() {
val policy = Policy()
val key =
PGPainless.generateOpenPgpKey(policy)
.buildV4Key()
.setPrimaryKey(KeyType.EDDSA(EdDSACurve._Ed25519))
.addSigningSubkey(KeyType.EDDSA(EdDSACurve._Ed25519)) {
setSubkeyPassphrase(Passphrase.fromPassword("Yin"))
}
.build(Passphrase.fromPassword("Yang"))
assertTrue(PGPainless.inspectKeyRing(key).isFullyEncrypted)
key.secretKeys.asSequence().forEachIndexed { index, subkey ->
UnlockSecretKey.unlockSecretKey(
subkey,
when (index) {
0 -> Passphrase.fromPassword("Yang")
1 -> Passphrase.fromPassword("Yin")
else -> fail { "Unexpected secret key at index $index" }
})
}
}
@Test
fun `generate key with unprotected subkey and other keys protected through catch-all`() {
val policy = Policy()
val key =
PGPainless.generateOpenPgpKey(policy)
.buildV4Key()
.setPrimaryKey(KeyType.EDDSA(EdDSACurve._Ed25519))
.addSigningSubkey(KeyType.EDDSA(EdDSACurve._Ed25519)) {
// Only the signing subkey is unprotected
setSubkeyProtector(SecretKeyRingProtector.unprotectedKeys())
}
.addEncryptionSubkey(KeyType.XDH(XDHSpec._X25519))
// All other keys are protected
.build(Passphrase.fromPassword("sw0rdf1sh"))
val info = PGPainless.inspectKeyRing(key)
assertFalse(info.isFullyEncrypted)
assertFalse(info.isFullyDecrypted)
key.secretKeys.asSequence().forEachIndexed { index, subkey ->
UnlockSecretKey.unlockSecretKey(
subkey,
when (index) {
0,
2 -> Passphrase.fromPassword("sw0rdf1sh")
1 -> Passphrase.emptyPassphrase()
else -> fail { "Unexpected subkey at index $index" }
})
}
}
}