1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-12-07 12:51:07 +01:00

WIP: Transparent decryption

This commit is contained in:
Paul Schaub 2025-12-01 23:42:44 +01:00
parent 510f8276e7
commit de47a683d9
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
13 changed files with 223 additions and 116 deletions

View file

@ -17,6 +17,7 @@ import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
import org.pgpainless.PGPainless import org.pgpainless.PGPainless
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy
import org.pgpainless.hardware.HardwareTokenBackend
import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.SecretKeyRingProtector
import org.pgpainless.signature.SignatureUtils import org.pgpainless.signature.SignatureUtils
@ -44,6 +45,7 @@ class ConsumerOptions(private val api: PGPainless) {
private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE
private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy() private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy()
private var allowDecryptionWithNonEncryptionKey: Boolean = false private var allowDecryptionWithNonEncryptionKey: Boolean = false
val hardwareTokenBackends: List<HardwareTokenBackend> = mutableListOf()
/** /**
* Consider signatures on the message made before the given timestamp invalid. Null means no * Consider signatures on the message made before the given timestamp invalid. Null means no
@ -236,6 +238,10 @@ class ConsumerOptions(private val api: PGPainless) {
decryptionPassphrases.add(passphrase) decryptionPassphrases.add(passphrase)
} }
fun addHardwareTokenBackend(backend: HardwareTokenBackend) = apply {
(hardwareTokenBackends as MutableList).add(backend)
}
/** /**
* Add a custom [PublicKeyDataDecryptorFactory] which enable decryption of messages, e.g. using * Add a custom [PublicKeyDataDecryptorFactory] which enable decryption of messages, e.g. using
* hardware-backed secret keys. (See e.g. * hardware-backed secret keys. (See e.g.

View file

@ -38,6 +38,7 @@ import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
import org.bouncycastle.openpgp.api.exception.MalformedOpenPGPSignatureException import org.bouncycastle.openpgp.api.exception.MalformedOpenPGPSignatureException
import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory
import org.bouncycastle.openpgp.operator.PGPDataDecryptorFactory
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
import org.bouncycastle.util.io.TeeInputStream import org.bouncycastle.util.io.TeeInputStream
import org.pgpainless.PGPainless import org.pgpainless.PGPainless
@ -66,7 +67,9 @@ import org.pgpainless.exception.MissingPassphraseException
import org.pgpainless.exception.SignatureValidationException import org.pgpainless.exception.SignatureValidationException
import org.pgpainless.exception.UnacceptableAlgorithmException import org.pgpainless.exception.UnacceptableAlgorithmException
import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.exception.WrongPassphraseException
import org.pgpainless.hardware.HardwareTokenBackend
import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.key.protection.SecretKeyRingProtector
import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey
import org.pgpainless.signature.consumer.OnePassSignatureCheck import org.pgpainless.signature.consumer.OnePassSignatureCheck
import org.pgpainless.util.ArmoredInputStreamFactory import org.pgpainless.util.ArmoredInputStreamFactory
@ -434,9 +437,6 @@ class OpenPgpMessageInputStream(
"Message is encrypted for ${secretKey.keyIdentifier}, but the key is not encryption capable.") "Message is encrypted for ${secretKey.keyIdentifier}, but the key is not encryption capable.")
continue continue
} }
if (hasUnsupportedS2KSpecifier(secretKey)) {
continue
}
LOGGER.debug("Attempt decryption using secret key ${decryptionKeys.keyIdentifier}") LOGGER.debug("Attempt decryption using secret key ${decryptionKeys.keyIdentifier}")
val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue
@ -447,6 +447,28 @@ class OpenPgpMessageInputStream(
continue continue
} }
if (secretKey.hasExternalSecretKey()) {
LOGGER.debug("Decryption key ${secretKey.keyIdentifier} is located on an external device, e.g. a smartcard.")
for (hardwareTokenBackend in options.hardwareTokenBackends) {
LOGGER.debug("Attempt decryption with ${hardwareTokenBackend.getBackendName()} backend.")
if (decryptWithHardwareKey(
hardwareTokenBackend,
esks,
secretKey,
protector,
SubkeyIdentifier(secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier),
pkesk)) {
return true
}
}
}
/*
if (hasUnsupportedS2KSpecifier(secretKey)) {
continue
}
*/
val privateKey = val privateKey =
try { try {
unlockSecretKey(secretKey, protector) unlockSecretKey(secretKey, protector)
@ -527,6 +549,25 @@ class OpenPgpMessageInputStream(
return false return false
} }
private fun decryptWithHardwareKey(
hardwareTokenBackend: HardwareTokenBackend,
esks: ESKsAndData,
secretKey: OpenPGPSecretKey,
protector: SecretKeyRingProtector,
subkeyIdentifier: SubkeyIdentifier,
pkesk: PGPPublicKeyEncryptedData
): Boolean {
val decryptors = hardwareTokenBackend.provideDecryptorsFor(secretKey, protector, pkesk)
while (decryptors.hasNext()) {
val decryptor = decryptors.next()
val success = decryptPKESKAndStream(esks, subkeyIdentifier, decryptor, pkesk)
if (success) {
return true
}
}
return false
}
private fun decryptWithPrivateKey( private fun decryptWithPrivateKey(
esks: ESKsAndData, esks: ESKsAndData,
privateKey: PGPKeyPair, privateKey: PGPKeyPair,

View file

@ -1,6 +1,20 @@
package org.pgpainless.hardware package org.pgpainless.hardware
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData
import org.bouncycastle.openpgp.api.OpenPGPKey
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
import org.pgpainless.key.protection.SecretKeyRingProtector
interface HardwareTokenBackend { interface HardwareTokenBackend {
fun getBackendName(): String
fun provideDecryptorsFor(
secKey: OpenPGPKey.OpenPGPSecretKey,
protector: SecretKeyRingProtector,
pkesk: PGPPublicKeyEncryptedData
): Iterator<PublicKeyDataDecryptorFactory>
fun listDeviceSerials(): List<ByteArray> fun listDeviceSerials(): List<ByteArray>
fun listKeyFingerprints(): Map<ByteArray, List<ByteArray>> fun listKeyFingerprints(): Map<ByteArray, List<ByteArray>>

View file

@ -12,6 +12,7 @@ import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter
import org.gnupg.GnuPGDummyKeyUtil import org.gnupg.GnuPGDummyKeyUtil
data class Yubikey(val info: DeviceInfo, val device: YubiKeyDevice) { data class Yubikey(val info: DeviceInfo, val device: YubiKeyDevice) {
fun storeKeyInSlot(key: OpenPGPPrivateKey, keyRef: KeyRef, adminPin: CharArray) { fun storeKeyInSlot(key: OpenPGPPrivateKey, keyRef: KeyRef, adminPin: CharArray) {
device.openConnection(SmartCardConnection::class.java).use { device.openConnection(SmartCardConnection::class.java).use {
// Extract private key // Extract private key
@ -31,5 +32,7 @@ data class Yubikey(val info: DeviceInfo, val device: YubiKeyDevice) {
} }
} }
val encodedSerial = GnuPGDummyKeyUtil.serialToBytes(info.serialNumber!!) val serialNumber: Int = info.serialNumber!!
val encodedSerialNumber = GnuPGDummyKeyUtil.serialToBytes(serialNumber)
} }

View file

@ -3,11 +3,36 @@ package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.openpgp.KeyRef import com.yubico.yubikit.openpgp.KeyRef
import com.yubico.yubikit.openpgp.OpenPgpSession import com.yubico.yubikit.openpgp.OpenPgpSession
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData
import org.bouncycastle.openpgp.api.OpenPGPKey
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
import org.gnupg.GnuPGDummyKeyUtil import org.gnupg.GnuPGDummyKeyUtil
import org.pgpainless.hardware.HardwareTokenBackend import org.pgpainless.hardware.HardwareTokenBackend
import org.pgpainless.key.protection.SecretKeyRingProtector
class YubikeyHardwareTokenBackend : HardwareTokenBackend { class YubikeyHardwareTokenBackend : HardwareTokenBackend {
override fun getBackendName(): String {
return "PGPainless-Yubikey"
}
override fun provideDecryptorsFor(
secKey: OpenPGPKey.OpenPGPSecretKey,
protector: SecretKeyRingProtector,
pkesk: PGPPublicKeyEncryptedData
): Iterator<PublicKeyDataDecryptorFactory> {
val devices = YubikeyHelper().listDevices()
return devices.map { yubikey ->
yubikey.device.openConnection(SmartCardConnection::class.java).use {
val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection(
it,
secKey.pgpPublicKey
)
decFac as PublicKeyDataDecryptorFactory
}
}.iterator()
}
override fun listDeviceSerials(): List<ByteArray> { override fun listDeviceSerials(): List<ByteArray> {
return YubikeyHelper().listDevices() return YubikeyHelper().listDevices()
.mapNotNull { yk -> yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } } .mapNotNull { yk -> yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } }
@ -16,9 +41,9 @@ class YubikeyHardwareTokenBackend : HardwareTokenBackend {
override fun listKeyFingerprints(): Map<ByteArray, List<ByteArray>> { override fun listKeyFingerprints(): Map<ByteArray, List<ByteArray>> {
return YubikeyHelper().listDevices() return YubikeyHelper().listDevices()
.associate { yk -> .associate { yk ->
yk.encodedSerial to yk.device.openConnection(SmartCardConnection::class.java).use { yk.encodedSerialNumber to yk.device.openConnection(SmartCardConnection::class.java).use {
val session = OpenPgpSession(it) val session = OpenPgpSession(it)
//6session.getData(KeyRef.DEC.fingerprint) //session.getData(KeyRef.DEC.fingerprint)
session.getData(KeyRef.SIG.fingerprint) session.getData(KeyRef.SIG.fingerprint)

View file

@ -29,7 +29,7 @@ class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) {
} }
} }
fun moveKeyToCard(componentKey: OpenPGPPrivateKey, fun moveToYubikey(componentKey: OpenPGPPrivateKey,
yubikey: Yubikey, yubikey: Yubikey,
adminPin: CharArray, adminPin: CharArray,
keyRef: KeyRef = keyRefForKey(componentKey.publicKey) keyRef: KeyRef = keyRefForKey(componentKey.publicKey)

View file

@ -32,39 +32,27 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
adminPin: CharArray, adminPin: CharArray,
keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4, keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
creationTime: Date = Date()): OpenPGPKey { creationTime: Date = Date()): OpenPGPKey {
val primaryKey: PGPSecretKey = yubikey.device.openConnection(SmartCardConnection::class.java).use { yubikey.device.openConnection(SmartCardConnection::class.java).use {
val session = OpenPgpSession(it) val session = OpenPgpSession(it)
session.verifyAdminPin(adminPin) session.verifyAdminPin(adminPin)
val pkVal = session.generateEcKey(KeyRef.ATT, OpenPgpCurve.SECP521R1) var pkVal = session.generateEcKey(KeyRef.ATT, OpenPgpCurve.SECP521R1)
val pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA) var pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA)
toStubbedSecretKey(pubKey, yubikey.info) val primarykey = toExternalSecretKey(pubKey, yubikey.info)
pkVal = session.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP521R1)
pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime,PublicKeyAlgorithm.ECDSA)
val signingKey = toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info)
pkVal = session.generateEcKey(KeyRef.DEC, OpenPgpCurve.SECP521R1)
pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDH)
val encryptionKey = toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info)
return OpenPGPKey(PGPSecretKeyRing(listOf(primarykey, signingKey, encryptionKey)))
} }
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, private fun toPGPPublicKey(pkVal: PublicKeyValues,
@ -79,7 +67,21 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
creationTime.toSecondsPrecision()) creationTime.toSecondsPrecision())
} }
private fun toStubbedSecretKey(pubKey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey { private fun toExternalSecretKey(pubkey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey {
return PGPSecretKey(
SecretKeyPacket(
pubkey.publicKeyPacket,
0,
0xfc,
null,
null,
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)
),
pubkey
)
}
private fun toGnuStubbedSecretKey(pubKey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey {
return PGPSecretKey( return PGPSecretKey(
SecretKeyPacket( SecretKeyPacket(
pubKey.publicKeyPacket, pubKey.publicKeyPacket,
@ -108,8 +110,8 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
SecretSubkeyPacket( SecretSubkeyPacket(
pubSubKey.publicKeyPacket, pubSubKey.publicKeyPacket,
0, 0,
SecretKeyPacket.USAGE_SHA1, 0xfc,
S2K.gnuDummyS2K(S2K.GNUDummyParams.divertToCard()), null,
null, null,
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)), GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)),
pubSubKey pubSubKey

View file

@ -2,18 +2,15 @@ package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.openpgp.KeyRef import com.yubico.yubikit.openpgp.KeyRef
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.pgpainless.PGPainless
import org.pgpainless.decryption_verification.ConsumerOptions import org.pgpainless.decryption_verification.ConsumerOptions
import org.pgpainless.key.protection.SecretKeyRingProtector
import org.pgpainless.util.Passphrase
class YubikeyDecryptionTest { class YubikeyDecryptionTest : YubikeyTest() {
val USER_PIN: CharArray = "123456".toCharArray()
val ADMIN_PIN: CharArray = "12345678".toCharArray()
// Complete software key
private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
"Comment: Alice <alice@pgpainless.org>\n" + "Comment: Alice <alice@pgpainless.org>\n" +
@ -51,6 +48,8 @@ class YubikeyDecryptionTest {
"1POPHzF3cMIReYhZfiJUEBV19suL\n" + "1POPHzF3cMIReYhZfiJUEBV19suL\n" +
"=dA6G\n" + "=dA6G\n" +
"-----END PGP PRIVATE KEY BLOCK-----" "-----END PGP PRIVATE KEY BLOCK-----"
// Software certificate
private val CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + private val CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
"Comment: Alice <alice@pgpainless.org>\n" + "Comment: Alice <alice@pgpainless.org>\n" +
@ -103,35 +102,43 @@ class YubikeyDecryptionTest {
"-----END PGP MESSAGE-----" "-----END PGP MESSAGE-----"
@Test @Test
fun decryptMessageUsingYubikey() { fun decryptMessageWithYubikey() {
val api = PGPainless(BcOpenPGPImplementation())
api.hardwareTokenBackends.add(YubikeyHardwareTokenBackend())
val key = api.readKey().parseKey(KEY) 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 decKey = key.secretKeys[key.encryptionKeys[0].keyIdentifier]!!
val msgIn = MSG.byteInputStream() val msgIn = MSG.byteInputStream()
// Write key // Write key to card
val divertToCard = yubikey.storeKeyInSlot(decKey.unlock(), KeyRef.DEC, ADMIN_PIN) val hardwareBasedKey = helper.moveToYubikey(decKey.unlock(), yubikey, adminPin, KeyRef.DEC)
// Decrypt // Decrypt
// TODO: Make hardware decryption transparent as shown below!
val decIn = api.processMessage()
.onInputStream(msgIn)
.withOptions(ConsumerOptions.get(api)
.addHardwareTokenBackend(YubikeyHardwareTokenBackend())
.addDecryptionKey(hardwareBasedKey,
SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(String(userPin)))))
val msg = decIn.readAllBytes()
decIn.close()
assertEquals("Hello, World!\n", String(msg))
/*
yubikey.device.openConnection(SmartCardConnection::class.java).use { yubikey.device.openConnection(SmartCardConnection::class.java).use {
val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection(it, decKey.pgpPublicKey) val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection(it, decKey.pgpPublicKey)
val decIn = api.processMessage() val decIn = api.processMessage()
.onInputStream(msgIn) .onInputStream(msgIn)
.withOptions( .withOptions(
ConsumerOptions.get(api) ConsumerOptions.get(api)
//.addDecryptionKey(api.readKey().parseKey(KEY))
.addCustomDecryptorFactory(decFac) .addCustomDecryptorFactory(decFac)
) )
val msg = decIn.readAllBytes() val msg = decIn.readAllBytes()
decIn.close() decIn.close()
assertEquals("Hello, World!\n", String(msg)) assertEquals("Hello, World!\n", String(msg))
} }
*/
} }
} }

View file

@ -1,17 +1,26 @@
package org.pgpainless.yubikey package org.pgpainless.yubikey
import org.gnupg.GnuPGDummyKeyUtil
import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.util.Arrays
class YubikeyHardwareTokenBackendTest { class YubikeyHardwareTokenBackendTest : YubikeyTest() {
val backend = YubikeyHardwareTokenBackend() val backend = YubikeyHardwareTokenBackend()
@Test @Test
fun testListDeviceSerials() { fun testListDeviceSerials() {
assertNotNull(backend.listDeviceSerials()) val serials = backend.listDeviceSerials()
assumeTrue(backend.listDeviceSerials().isNotEmpty()) assertTrue(serials.any {
it.contentEquals(
GnuPGDummyKeyUtil.serialToBytes(
allowedSerialNumber
)
)
})
} }
@Test @Test

View file

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

View file

@ -1,14 +1,9 @@
package org.pgpainless.yubikey package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.openpgp.KeyRef
import com.yubico.yubikit.openpgp.OpenPgpSession
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.HashAlgorithm
import org.pgpainless.decryption_verification.ConsumerOptions import org.pgpainless.decryption_verification.ConsumerOptions
import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.ProducerOptions
@ -17,10 +12,7 @@ import org.pgpainless.signature.PGPContentSignerBuilderProviderFactory
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
class YubikeySigningTest { class YubikeySigningTest : YubikeyTest() {
val USER_PIN: CharArray = "123456".toCharArray()
val ADMIN_PIN: CharArray = "12345678".toCharArray()
private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
@ -59,56 +51,16 @@ class YubikeySigningTest {
"1POPHzF3cMIReYhZfiJUEBV19suL\n" + "1POPHzF3cMIReYhZfiJUEBV19suL\n" +
"=dA6G\n" + "=dA6G\n" +
"-----END PGP PRIVATE KEY BLOCK-----" "-----END PGP PRIVATE KEY BLOCK-----"
private val CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
"Comment: Alice <alice@pgpainless.org>\n" +
"\n" +
"mJMEaNQmhRMFK4EEACMEIwQBgF429XlvPyJdpfXDxVjVEOJc04wcpfkIoX1CzIjm\n" +
"daRyv+mz2jfFZlQsCkhw2GsrPRJuKz++1JkspKU+4Vot9dIBD94Y+MoZUgHM4m0t\n" +
"ItqAdaRcxZWXDpSB0eZH3/lC+VkMUjiqK1Po4qOdZttgpLz+uHcox3gxanjyndAQ\n" +
"gVQf36u0HEFsaWNlIDxhbGljZUBwZ3BhaW5sZXNzLm9yZz7CwCEEExMKAFEFgmjU\n" +
"JoUJEOsuVw2e4iiRFqEEuyrD4eWVzQXPpc/m6y5XDZ7iKJECmwEFFQoJCAsFFgID\n" +
"AQAECwkIBwknCQEJAgkDCAECngkFiQlmAYACmQEAAEjjAgQOfqnx03DV4MUGGytd\n" +
"G02k5KbyeHbuAooyAcn8LItbaAlwzMn9Pu2uwLFEeqi0yhfF2QILwnh4QPHkhC6U\n" +
"wn5nbwIED8I1+3lbVifmpZm+1xwyU8JldGHvGDd0nkJ+wZsB22rr8dTIOgju2Z2a\n" +
"UOOBRlaCnRfPwYjlMaaacn+T2RZBQhK4lwRo1CaFEgUrgQQAIwQjBAE/KkBQQfj0\n" +
"4fzk0LWTlrbbdh95ZK4SyTLoXVY8lueyRPlTu399uPBZUxVBkJNaStvv+TgzEXXO\n" +
"1cHXOccEI9b04ACpUWbTq1ArP162V7OUuSGIAoxMw3B/jMWlNgPy/ktvv1Tidj66\n" +
"Q3QKtx6Iir340AWuMszisM/9sJRazsJxVFZIvgMBCgnCugQYEwoAKgWCaNQmhQkQ\n" +
"3dKuQkQmRdEWoQQCHpcKMNUN0N4jKkrd0q5CRCZF0QKbDAAAYPsCCOFKIUwIzOrc\n" +
"Hguz8+xc90UQRnfKpmZ61Ex8fkVN2sWRTnA10+dlb8zIDEXeUApSU8NB3W3ktTQM\n" +
"ATfKt/0xluzRAgjN1uXdxss0e0WVUuetJDkeVxBJCSuauqNqJsGQ70/5G8kQY9Wt\n" +
"Cn+5WmQDsjjThmlAlpF0LjLmbBeL7LrUz0ibariTBGjUJoUTBSuBBAAjBCMEAXJu\n" +
"E0+qkMbYx9+TmpzsD5L/8XYCyurEGj1YqTjFwoHLXQKCulsHP/UipGu1ulw1pjS9\n" +
"yXlhzMEb/k+0sxM4xdB0AHmdObt8e2xzeO8qkNm4NdvHANQb+tiQch0gIZ2nR1x+\n" +
"ZjhcRy+Dr/TCZvUk2l98YFcK0jS2qKvnd2HKxp4NCWU9wsCdBBgTCgDMBYJo1CaF\n" +
"CRBLTd5YEMyfuxahBDYBgrodBZ7KXs6saUtN3lgQzJ+7ApsCoSAEGRMKAAYFgmjU\n" +
"JoUACgkQS03eWBDMn7uLJgIInpyhe/J0Fy+likj7uzAt1XrCY44hh0Agdr8wVfpD\n" +
"haGScktFOEf8x9UOjT7eVyRVG1MmPsQqViiIKp8t+Jz3B34CBAkVYweWbPmVQyuv\n" +
"WGHaoTZY4xmbboF/VV6zIT6C1Rg8hPvSzaN70ZPMVLNqtp1AEgGxtWqL542Z49B+\n" +
"OJRoCqnxAACt9QIJAfwhrKW+CB/YI34UIVXC5cqTwvqZkSEwd5F9r0k4wob3AXD0\n" +
"SQ2bWAHY7SBZZlGK1J5UA8SVDp7xvxmQjKpweUHsAgdinLvX6C9DtLelq18l0MJI\n" +
"pjM8eSnn8ruFW7I9JvMleWkuHmzxJscbVmjru9Tzjx8xd3DCEXmIWX4iVBAVdfbL\n" +
"iw==\n" +
"=Oq+Y\n" +
"-----END PGP PUBLIC KEY BLOCK-----"
@Test @Test
fun signMessageWithYubikey() { 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 device = yubikey.device
val key = api.readKey().parseKey(KEY) val key = api.readKey().parseKey(KEY)
val signingKey = key.secretKeys[key.signingKeys[0].keyIdentifier]!! val signingKey = key.secretKeys[key.signingKeys[0].keyIdentifier]!!
val signingKeyOnCard = helper.moveKeyToCard(signingKey.unlock(), yubikey, ADMIN_PIN) val hardwareBasedSigningKey = helper.moveToYubikey(signingKey.unlock(), yubikey, adminPin)
println(signingKeyOnCard.toAsciiArmoredString()) println(hardwareBasedSigningKey.toAsciiArmoredString())
val msgOut = ByteArrayOutputStream() val msgOut = ByteArrayOutputStream()
device.openConnection(SmartCardConnection::class.java).use { device.openConnection(SmartCardConnection::class.java).use {
@ -123,7 +75,7 @@ class YubikeySigningTest {
.generateMessage() .generateMessage()
.onOutputStream(msgOut) .onOutputStream(msgOut)
.withOptions(ProducerOptions.sign(SigningOptions.get() .withOptions(ProducerOptions.sign(SigningOptions.get()
.addInlineSignature(signingKeyOnCard.signingKeys[0], factory, HashAlgorithm.SHA512))) .addInlineSignature(hardwareBasedSigningKey.signingKeys[0], factory, HashAlgorithm.SHA512)))
sigOut.write("Hello, World!".toByteArray()) sigOut.write("Hello, World!".toByteArray())
sigOut.close() sigOut.close()
@ -133,7 +85,7 @@ class YubikeySigningTest {
api.processMessage() api.processMessage()
.onInputStream(ByteArrayInputStream(msgOut.toByteArray())) .onInputStream(ByteArrayInputStream(msgOut.toByteArray()))
.withOptions(ConsumerOptions.get() .withOptions(ConsumerOptions.get()
.addVerificationCert(signingKeyOnCard.toCertificate()) .addVerificationCert(hardwareBasedSigningKey.toCertificate())
).use { ).use {
it.readAllBytes() it.readAllBytes()
it.close() it.close()

View file

@ -0,0 +1,37 @@
package org.pgpainless.yubikey
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
import org.opentest4j.TestAbortedException
import org.pgpainless.PGPainless
import java.util.Properties
abstract class YubikeyTest() {
val adminPin: CharArray
val userPin: CharArray
val allowedSerialNumber: Int
init {
javaClass.classLoader.getResourceAsStream("yubikey.properties").use {
val props = Properties().apply { load(it) }
adminPin = getProperty(props, "ADMIN_PIN").toCharArray()
userPin = getProperty(props, "USER_PIN").toCharArray()
allowedSerialNumber = getProperty(props, "ALLOWED_DEVICE_SERIAL").toInt()
}
}
open val api: PGPainless = PGPainless(BcOpenPGPImplementation()).apply {
hardwareTokenBackends.add(YubikeyHardwareTokenBackend())
}
open val helper: YubikeyHelper = YubikeyHelper(api)
val yubikey: Yubikey = YubikeyHelper().listDevices().find { it.serialNumber == allowedSerialNumber }
?: throw TestAbortedException("No allowed device found.")
private fun getProperty(properties: Properties, key: String): String {
return properties.getProperty(key)
?: throw TestAbortedException("Could not find property $key in pgpainless-yubikey/src/test/resources/yubikey.properties")
}
}

View file

@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
ADMIN_PIN=12345678
USER_PIN=123456
ALLOWED_DEVICE_SERIAL=15472425