1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-12-05 03:41:07 +01:00
This commit is contained in:
Paul Schaub 2025-11-28 12:46:36 +01:00
parent 6f3f988707
commit 510f8276e7
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
13 changed files with 333 additions and 81 deletions

View file

@ -56,6 +56,12 @@ class GnuPGDummyKeyUtil private constructor() {
* @return builder
*/
@JvmStatic fun modify(secretKeys: PGPSecretKeyRing) = Builder(secretKeys)
@JvmStatic fun serialToBytes(sn: Int) = byteArrayOf(
(sn shr 24).toByte(),
(sn shr(16)).toByte(),
(sn shr(8)).toByte(),
sn.toByte())
}
class Builder(private val keys: PGPSecretKeyRing) {

View file

@ -27,6 +27,7 @@ import org.pgpainless.bouncycastle.PolicyAdapter
import org.pgpainless.bouncycastle.extensions.setAlgorithmSuite
import org.pgpainless.decryption_verification.DecryptionBuilder
import org.pgpainless.encryption_signing.EncryptionBuilder
import org.pgpainless.hardware.HardwareTokenBackend
import org.pgpainless.key.certification.CertifyCertificate
import org.pgpainless.key.generation.KeyRingBuilder
import org.pgpainless.key.generation.KeyRingTemplates
@ -52,6 +53,8 @@ class PGPainless(
val algorithmPolicy: Policy = Policy()
) {
val hardwareTokenBackends = mutableListOf<HardwareTokenBackend>()
constructor(
algorithmPolicy: Policy
) : this(OpenPGPImplementation.getInstance(), algorithmPolicy)

View file

@ -209,4 +209,8 @@ $algorithm of size $bitSize is not acceptable.""",
}
}
}
class GeneralKeyException(message: String,
fingerprint: OpenPgpFingerprint
) : KeyException(message, fingerprint)
}

View file

@ -0,0 +1,7 @@
package org.pgpainless.hardware
interface HardwareTokenBackend {
fun listDeviceSerials(): List<ByteArray>
fun listKeyFingerprints(): Map<ByteArray, List<ByteArray>>
}

View file

@ -0,0 +1,35 @@
package org.pgpainless.yubikey
import com.yubico.yubikit.core.YubiKeyDevice
import com.yubico.yubikit.core.keys.PrivateKeyValues
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.management.DeviceInfo
import com.yubico.yubikit.openpgp.KeyRef
import com.yubico.yubikit.openpgp.OpenPgpSession
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter
import org.gnupg.GnuPGDummyKeyUtil
data class Yubikey(val info: DeviceInfo, val device: YubiKeyDevice) {
fun storeKeyInSlot(key: OpenPGPPrivateKey, keyRef: KeyRef, adminPin: CharArray) {
device.openConnection(SmartCardConnection::class.java).use {
// Extract private key
val privateKey = JcaPGPKeyConverter().setProvider(BouncyCastleProvider())
.getPrivateKey(key.keyPair.privateKey)
val session = OpenPgpSession(it as SmartCardConnection)
// Storing keys requires admin pin
session.verifyAdminPin(adminPin)
session.putKey(keyRef, PrivateKeyValues.fromPrivateKey(privateKey))
val fp = key.publicKey.pgpPublicKey.fingerprint
session.setFingerprint(keyRef, fp)
val time = (key.publicKey.pgpPublicKey.publicKeyPacket.time.time / 1000).toInt()
session.setGenerationTime(keyRef, time)
}
}
val encodedSerial = GnuPGDummyKeyUtil.serialToBytes(info.serialNumber!!)
}

View file

@ -0,0 +1,34 @@
package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.openpgp.KeyRef
import com.yubico.yubikit.openpgp.OpenPgpSession
import org.gnupg.GnuPGDummyKeyUtil
import org.pgpainless.hardware.HardwareTokenBackend
class YubikeyHardwareTokenBackend : HardwareTokenBackend {
override fun listDeviceSerials(): List<ByteArray> {
return YubikeyHelper().listDevices()
.mapNotNull { yk -> yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } }
}
override fun listKeyFingerprints(): Map<ByteArray, List<ByteArray>> {
return YubikeyHelper().listDevices()
.associate { yk ->
yk.encodedSerial to yk.device.openConnection(SmartCardConnection::class.java).use {
val session = OpenPgpSession(it)
//6session.getData(KeyRef.DEC.fingerprint)
session.getData(KeyRef.SIG.fingerprint)
listOfNotNull(
session.getData(KeyRef.ATT.fingerprint),
session.getData(KeyRef.SIG.fingerprint),
session.getData(KeyRef.DEC.fingerprint),
session.getData(KeyRef.AUT.fingerprint)
)
}
}
}
}

View file

@ -0,0 +1,61 @@
package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.desktop.CompositeDevice
import com.yubico.yubikit.desktop.YubiKitManager
import com.yubico.yubikit.openpgp.KeyRef
import com.yubico.yubikit.openpgp.OpenPgpSession
import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey
import org.bouncycastle.openpgp.api.OpenPGPKey
import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey
import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey
import org.gnupg.GnuPGDummyKeyUtil
import org.pgpainless.PGPainless
import org.pgpainless.bouncycastle.extensions.toOpenPGPKey
import org.pgpainless.exception.KeyException
import org.pgpainless.key.OpenPgpFingerprint
class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) {
fun listDevices(
manager: YubiKitManager = YubiKitManager()
): List<Yubikey> = manager.listAllDevices()
.filter { it.key is CompositeDevice }
.map { Yubikey(it.value, it.key) }
fun factoryReset(yubikey: Yubikey) {
yubikey.device.openConnection(SmartCardConnection::class.java).use {
OpenPgpSession(it).reset()
}
}
fun moveKeyToCard(componentKey: OpenPGPPrivateKey,
yubikey: Yubikey,
adminPin: CharArray,
keyRef: KeyRef = keyRefForKey(componentKey.publicKey)
): OpenPGPKey {
// Move private key to hardware token
yubikey.storeKeyInSlot(componentKey, keyRef, adminPin)
// Modify software key to indicate key has been diverted to card
return indicateMovedToCard(componentKey.secretKey, yubikey)
}
private fun indicateMovedToCard(key: OpenPGPSecretKey, yubikey: Yubikey): OpenPGPKey {
return GnuPGDummyKeyUtil.modify(key.openPGPKey)
.divertPrivateKeysToCard(
{ it.matchesExplicit(key.keyIdentifier) },
GnuPGDummyKeyUtil.serialToBytes(yubikey.info.serialNumber!!))
.toOpenPGPKey(api.implementation)
}
private fun keyRefForKey(key: OpenPGPComponentKey): KeyRef {
return when {
key.isSigningKey -> KeyRef.SIG
key.isEncryptionKey -> KeyRef.DEC
key.isCertificationKey -> KeyRef.ATT
else -> throw KeyException.GeneralKeyException(
"Cannot determine usage for the key.", OpenPgpFingerprint.of(key))
}
}
}

View file

@ -0,0 +1,118 @@
package org.pgpainless.yubikey
import com.yubico.yubikit.core.keys.PublicKeyValues
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.management.DeviceInfo
import com.yubico.yubikit.openpgp.KeyRef
import com.yubico.yubikit.openpgp.OpenPgpCurve
import com.yubico.yubikit.openpgp.OpenPgpSession
import openpgp.toSecondsPrecision
import org.bouncycastle.bcpg.PublicSubkeyPacket
import org.bouncycastle.bcpg.S2K
import org.bouncycastle.bcpg.SecretKeyPacket
import org.bouncycastle.bcpg.SecretSubkeyPacket
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSecretKey
import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.bouncycastle.openpgp.api.OpenPGPKey
import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter
import org.gnupg.GnuPGDummyKeyUtil
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.OpenPGPKeyVersion
import org.pgpainless.algorithm.PublicKeyAlgorithm
import java.util.*
class YubikeyKeyGenerator(private val api: PGPainless) {
private val converter = JcaPGPKeyConverter().setProvider(BouncyCastleProvider())
fun generateModernKey(yubikey: Yubikey,
adminPin: CharArray,
keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
creationTime: Date = Date()): OpenPGPKey {
val primaryKey: PGPSecretKey = yubikey.device.openConnection(SmartCardConnection::class.java).use {
val session = OpenPgpSession(it)
session.verifyAdminPin(adminPin)
val pkVal = session.generateEcKey(KeyRef.ATT, OpenPgpCurve.SECP521R1)
val pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA)
toStubbedSecretKey(pubKey, yubikey.info)
}
val signingKey: PGPSecretKey = yubikey.device.openConnection(SmartCardConnection::class.java).use {
val session = OpenPgpSession(it)
session.verifyAdminPin(adminPin)
val pkVal = session.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP521R1)
val pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime,PublicKeyAlgorithm.ECDSA)
toSecretSubKey(toStubbedSecretKey(pubKey, yubikey.info), yubikey.info)
}
val encryptionKey: PGPSecretKey = yubikey.device.openConnection(SmartCardConnection::class.java).use {
val session = OpenPgpSession(it)
session.verifyAdminPin(adminPin)
val pkVal = session.generateEcKey(KeyRef.DEC, OpenPgpCurve.X25519)
val pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime,
if (keyVersion == OpenPGPKeyVersion.v6) PublicKeyAlgorithm.X25519
else PublicKeyAlgorithm.ECDH)
toSecretSubKey(toStubbedSecretKey(pubKey, yubikey.info), yubikey.info)
}
return OpenPGPKey(PGPSecretKeyRing(listOf(primaryKey, signingKey, encryptionKey)))
}
private fun toPGPPublicKey(pkVal: PublicKeyValues,
version: OpenPGPKeyVersion,
creationTime: Date,
algorithm: PublicKeyAlgorithm
): PGPPublicKey {
return converter.getPGPPublicKey(version.numeric,
algorithm.algorithmId,
null,
pkVal.toPublicKey(),
creationTime.toSecondsPrecision())
}
private fun toStubbedSecretKey(pubKey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey {
return PGPSecretKey(
SecretKeyPacket(
pubKey.publicKeyPacket,
0,
SecretKeyPacket.USAGE_SHA1,
S2K.gnuDummyS2K(S2K.GNUDummyParams.divertToCard()),
null,
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)
),
pubKey)
}
private fun toSecretSubKey(
key: PGPSecretKey,
deviceInfo: DeviceInfo,
fingerPrintCalculator: KeyFingerPrintCalculator = api.implementation.keyFingerPrintCalculator()
): PGPSecretKey {
val pubSubKey = PGPPublicKey(
PublicSubkeyPacket(
key.publicKey.version,
key.publicKey.algorithm,
key.publicKey.creationTime,
key.publicKey.publicKeyPacket.key),
fingerPrintCalculator)
return PGPSecretKey(
SecretSubkeyPacket(
pubSubKey.publicKeyPacket,
0,
SecretKeyPacket.USAGE_SHA1,
S2K.gnuDummyS2K(S2K.GNUDummyParams.divertToCard()),
null,
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)),
pubSubKey
)
}
}

View file

@ -1,18 +1,12 @@
package org.pgpainless.yubikey
import com.yubico.yubikit.core.keys.PrivateKeyValues
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.desktop.CompositeDevice
import com.yubico.yubikit.desktop.YubiKitManager
import com.yubico.yubikit.openpgp.OpenPgpSession
import org.bouncycastle.jce.provider.BouncyCastleProvider
import com.yubico.yubikit.openpgp.KeyRef
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter
import org.gnupg.GnuPGDummyKeyUtil
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test
import org.pgpainless.PGPainless
import org.pgpainless.bouncycastle.extensions.toOpenPGPKey
import org.pgpainless.decryption_verification.ConsumerOptions
class YubikeyDecryptionTest {
@ -111,43 +105,22 @@ class YubikeyDecryptionTest {
@Test
fun decryptMessageUsingYubikey() {
val api = PGPainless(BcOpenPGPImplementation())
api.hardwareTokenBackends.add(YubikeyHardwareTokenBackend())
val key = api.readKey().parseKey(KEY)
val helper = YubikeyHelper(api)
val devices = helper.listDevices()
assumeTrue(devices.isNotEmpty())
val yubikey = devices.first()
val decKey = key.secretKeys[key.encryptionKeys[0].keyIdentifier]!!
val msgIn = MSG.byteInputStream()
val privKey = decKey.pgpSecretKey.extractPrivateKey(null)
val k = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()).getPrivateKey(privKey)
val sn = 15472425
val movedToCard = GnuPGDummyKeyUtil.modify(key)
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter { it.matchesExplicit(decKey.keyIdentifier) }, byteArrayOf(
(sn shr 24).toByte(), (sn shr(16)).toByte(), (sn shr(8)).toByte(), sn.toByte()
)).toOpenPGPKey(api.implementation)
println(MSG)
val manager = YubiKitManager()
val device = manager.listAllDevices().entries.find { it.key is CompositeDevice }?.key
?: throw IllegalStateException("No Yubikey attached.")
// Write key
device.openConnection(SmartCardConnection::class.java).use {
val connection = it
val openpgp = OpenPgpSession(connection as SmartCardConnection)
openpgp.reset()
val divertToCard = yubikey.storeKeyInSlot(decKey.unlock(), KeyRef.DEC, ADMIN_PIN)
openpgp.verifyAdminPin(ADMIN_PIN)
openpgp.putKey(
com.yubico.yubikit.openpgp.KeyRef.DEC,
PrivateKeyValues.fromPrivateKey(k)
)
val fp = decKey.pgpPublicKey.fingerprint
openpgp.setFingerprint(com.yubico.yubikit.openpgp.KeyRef.DEC, fp)
openpgp.setGenerationTime(
com.yubico.yubikit.openpgp.KeyRef.DEC,
(decKey.pgpPublicKey.publicKeyPacket.time.time / 1000).toInt()
)
}
device.openConnection(SmartCardConnection::class.java).use {
// Decrypt
yubikey.device.openConnection(SmartCardConnection::class.java).use {
val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection(it, decKey.pgpPublicKey)
val decIn = api.processMessage()
.onInputStream(msgIn)

View file

@ -0,0 +1,22 @@
package org.pgpainless.yubikey
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test
class YubikeyHardwareTokenBackendTest {
val backend = YubikeyHardwareTokenBackend()
@Test
fun testListDeviceSerials() {
assertNotNull(backend.listDeviceSerials())
assumeTrue(backend.listDeviceSerials().isNotEmpty())
}
@Test
fun testListKeys() {
val keys = backend.listKeyFingerprints()
assumeTrue(keys.isNotEmpty())
}
}

View file

@ -0,0 +1,18 @@
package org.pgpainless.yubikey
import org.junit.jupiter.api.Test
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.OpenPGPKeyVersion
import java.util.*
class YubikeyKeyGeneratorTest {
val ADMIN_PIN: CharArray = "12345678".toCharArray()
@Test
fun generateKey() {
val helper = YubikeyHelper()
val keyGen = YubikeyKeyGenerator(PGPainless.getInstance())
val key = keyGen.generateModernKey(helper.listDevices().first(), ADMIN_PIN, OpenPGPKeyVersion.v4, Date())
println(key.toAsciiArmoredString())
}
}

View file

@ -1,20 +1,15 @@
package org.pgpainless.yubikey
import com.yubico.yubikit.core.keys.PrivateKeyValues
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.desktop.CompositeDevice
import com.yubico.yubikit.desktop.YubiKitManager
import com.yubico.yubikit.openpgp.KeyRef
import com.yubico.yubikit.openpgp.OpenPgpSession
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter
import org.gnupg.GnuPGDummyKeyUtil
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.HashAlgorithm
import org.pgpainless.bouncycastle.extensions.toOpenPGPKey
import org.pgpainless.decryption_verification.ConsumerOptions
import org.pgpainless.encryption_signing.ProducerOptions
import org.pgpainless.encryption_signing.SigningOptions
@ -101,43 +96,19 @@ class YubikeySigningTest {
@Test
fun signMessageWithYubikey() {
val api = PGPainless(BcOpenPGPImplementation())
val helper = YubikeyHelper(api)
val devices = helper.listDevices()
assumeTrue(devices.isNotEmpty())
val yubikey = devices.first()
val device = yubikey.device
val key = api.readKey().parseKey(KEY)
val signingKey = key.secretKeys[key.signingKeys[0].keyIdentifier]!!
val privKey = signingKey.pgpSecretKey.extractPrivateKey(null)
val k = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()).getPrivateKey(privKey)
val sn = 15472425
val movedToCard = GnuPGDummyKeyUtil.modify(key)
.divertPrivateKeysToCard(
GnuPGDummyKeyUtil.KeyFilter { it.matchesExplicit(signingKey.keyIdentifier) }, byteArrayOf(
(sn shr 24).toByte(), (sn shr(16)).toByte(), (sn shr(8)).toByte(), sn.toByte()
)).toOpenPGPKey(api.implementation)
println(movedToCard.toAsciiArmoredString())
val manager = YubiKitManager()
val device = manager.listAllDevices().entries.find { it.key is CompositeDevice }?.key
?: throw IllegalStateException("No Yubikey attached.")
// Write key
device.openConnection(SmartCardConnection::class.java).use {
val connection = it
val openpgp = OpenPgpSession(connection as SmartCardConnection)
openpgp.reset()
openpgp.verifyAdminPin(ADMIN_PIN)
openpgp.putKey(
com.yubico.yubikit.openpgp.KeyRef.SIG,
PrivateKeyValues.fromPrivateKey(k)
)
val fp = signingKey.pgpPublicKey.fingerprint
openpgp.setFingerprint(com.yubico.yubikit.openpgp.KeyRef.SIG, fp)
openpgp.setGenerationTime(
com.yubico.yubikit.openpgp.KeyRef.SIG,
(signingKey.pgpPublicKey.publicKeyPacket.time.time / 1000).toInt()
)
}
val signingKeyOnCard = helper.moveKeyToCard(signingKey.unlock(), yubikey, ADMIN_PIN)
println(signingKeyOnCard.toAsciiArmoredString())
val msgOut = ByteArrayOutputStream()
device.openConnection(SmartCardConnection::class.java).use {
@ -152,7 +123,7 @@ class YubikeySigningTest {
.generateMessage()
.onOutputStream(msgOut)
.withOptions(ProducerOptions.sign(SigningOptions.get()
.addInlineSignature(movedToCard.signingKeys[0], factory, HashAlgorithm.SHA512)))
.addInlineSignature(signingKeyOnCard.signingKeys[0], factory, HashAlgorithm.SHA512)))
sigOut.write("Hello, World!".toByteArray())
sigOut.close()
@ -162,7 +133,7 @@ class YubikeySigningTest {
api.processMessage()
.onInputStream(ByteArrayInputStream(msgOut.toByteArray()))
.withOptions(ConsumerOptions.get()
.addVerificationCert(key.toCertificate())
.addVerificationCert(signingKeyOnCard.toCertificate())
).use {
it.readAllBytes()
it.close()

View file

@ -14,6 +14,6 @@ allprojects {
mockitoVersion = '4.5.1'
slf4jVersion = '1.7.36'
sopJavaVersion = '14.0.3'
yubikitVersion = '2.8.2'
yubikitVersion = '2.9.0'
}
}