From de47a683d92e92081900ab098f0512000ba7d2c0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 1 Dec 2025 23:42:44 +0100 Subject: [PATCH] WIP: Transparent decryption --- .../ConsumerOptions.kt | 6 ++ .../OpenPgpMessageInputStream.kt | 47 +++++++++++++- .../hardware/HardwareTokenBackend.kt | 14 ++++ .../kotlin/org/pgpainless/yubikey/Yubikey.kt | 5 +- .../yubikey/YubikeyHardwareTokenBackend.kt | 29 ++++++++- .../org/pgpainless/yubikey/YubikeyHelper.kt | 2 +- .../pgpainless/yubikey/YubikeyKeyGenerator.kt | 64 ++++++++++--------- .../yubikey/YubikeyDecryptionTest.kt | 43 +++++++------ .../YubikeyHardwareTokenBackendTest.kt | 15 ++++- .../yubikey/YubikeyKeyGeneratorTest.kt | 12 ++-- .../pgpainless/yubikey/YubikeySigningTest.kt | 58 ++--------------- .../org/pgpainless/yubikey/YubikeyTest.kt | 37 +++++++++++ .../src/test/resources/yubikey.properties | 7 ++ 13 files changed, 223 insertions(+), 116 deletions(-) create mode 100644 pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt create mode 100644 pgpainless-yubikey/src/test/resources/yubikey.properties diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt index d8433f25..d9d8add8 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -17,6 +17,7 @@ import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.pgpainless.PGPainless import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy +import org.pgpainless.hardware.HardwareTokenBackend import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.signature.SignatureUtils @@ -44,6 +45,7 @@ class ConsumerOptions(private val api: PGPainless) { private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy() private var allowDecryptionWithNonEncryptionKey: Boolean = false + val hardwareTokenBackends: List = mutableListOf() /** * 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) } + fun addHardwareTokenBackend(backend: HardwareTokenBackend) = apply { + (hardwareTokenBackends as MutableList).add(backend) + } + /** * Add a custom [PublicKeyDataDecryptorFactory] which enable decryption of messages, e.g. using * hardware-backed secret keys. (See e.g. diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 8425181a..88d5ca59 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -38,6 +38,7 @@ import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.bouncycastle.openpgp.api.exception.MalformedOpenPGPSignatureException import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory +import org.bouncycastle.openpgp.operator.PGPDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.bouncycastle.util.io.TeeInputStream import org.pgpainless.PGPainless @@ -66,7 +67,9 @@ import org.pgpainless.exception.MissingPassphraseException import org.pgpainless.exception.SignatureValidationException import org.pgpainless.exception.UnacceptableAlgorithmException import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.hardware.HardwareTokenBackend import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey import org.pgpainless.signature.consumer.OnePassSignatureCheck import org.pgpainless.util.ArmoredInputStreamFactory @@ -434,9 +437,6 @@ class OpenPgpMessageInputStream( "Message is encrypted for ${secretKey.keyIdentifier}, but the key is not encryption capable.") continue } - if (hasUnsupportedS2KSpecifier(secretKey)) { - continue - } LOGGER.debug("Attempt decryption using secret key ${decryptionKeys.keyIdentifier}") val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue @@ -447,6 +447,28 @@ class OpenPgpMessageInputStream( 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 = try { unlockSecretKey(secretKey, protector) @@ -527,6 +549,25 @@ class OpenPgpMessageInputStream( 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( esks: ESKsAndData, privateKey: PGPKeyPair, diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/hardware/HardwareTokenBackend.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/hardware/HardwareTokenBackend.kt index 4e60f7c6..07f44826 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/hardware/HardwareTokenBackend.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/hardware/HardwareTokenBackend.kt @@ -1,6 +1,20 @@ 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 { + + fun getBackendName(): String + + fun provideDecryptorsFor( + secKey: OpenPGPKey.OpenPGPSecretKey, + protector: SecretKeyRingProtector, + pkesk: PGPPublicKeyEncryptedData + ): Iterator + fun listDeviceSerials(): List fun listKeyFingerprints(): Map> diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt index 32a91e43..4bb64479 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt @@ -12,6 +12,7 @@ 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 @@ -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) } diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt index 51c36732..f20e6b1a 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt @@ -3,11 +3,36 @@ 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.bouncycastle.openpgp.PGPPublicKeyEncryptedData +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.gnupg.GnuPGDummyKeyUtil import org.pgpainless.hardware.HardwareTokenBackend +import org.pgpainless.key.protection.SecretKeyRingProtector class YubikeyHardwareTokenBackend : HardwareTokenBackend { + override fun getBackendName(): String { + return "PGPainless-Yubikey" + } + + override fun provideDecryptorsFor( + secKey: OpenPGPKey.OpenPGPSecretKey, + protector: SecretKeyRingProtector, + pkesk: PGPPublicKeyEncryptedData + ): Iterator { + 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 { return YubikeyHelper().listDevices() .mapNotNull { yk -> yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } } @@ -16,9 +41,9 @@ class YubikeyHardwareTokenBackend : HardwareTokenBackend { override fun listKeyFingerprints(): Map> { return YubikeyHelper().listDevices() .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) - //6session.getData(KeyRef.DEC.fingerprint) + //session.getData(KeyRef.DEC.fingerprint) session.getData(KeyRef.SIG.fingerprint) diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt index 467992d5..1656eacc 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt @@ -29,7 +29,7 @@ class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) { } } - fun moveKeyToCard(componentKey: OpenPGPPrivateKey, + fun moveToYubikey(componentKey: OpenPGPPrivateKey, yubikey: Yubikey, adminPin: CharArray, keyRef: KeyRef = keyRefForKey(componentKey.publicKey) diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt index 69d723e6..e3a5aa7b 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt @@ -32,39 +32,27 @@ class YubikeyKeyGenerator(private val api: PGPainless) { adminPin: CharArray, keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4, 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) session.verifyAdminPin(adminPin) - val pkVal = session.generateEcKey(KeyRef.ATT, OpenPgpCurve.SECP521R1) - val pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA) + var pkVal = session.generateEcKey(KeyRef.ATT, OpenPgpCurve.SECP521R1) + 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, @@ -79,7 +67,21 @@ class YubikeyKeyGenerator(private val api: PGPainless) { 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( SecretKeyPacket( pubKey.publicKeyPacket, @@ -108,8 +110,8 @@ class YubikeyKeyGenerator(private val api: PGPainless) { SecretSubkeyPacket( pubSubKey.publicKeyPacket, 0, - SecretKeyPacket.USAGE_SHA1, - S2K.gnuDummyS2K(S2K.GNUDummyParams.divertToCard()), + 0xfc, + null, null, GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)), pubSubKey diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyDecryptionTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyDecryptionTest.kt index 6b55f524..369f8e95 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyDecryptionTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyDecryptionTest.kt @@ -2,18 +2,15 @@ package org.pgpainless.yubikey import com.yubico.yubikit.core.smartcard.SmartCardConnection 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.Assumptions.assumeTrue import org.junit.jupiter.api.Test -import org.pgpainless.PGPainless import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.util.Passphrase -class YubikeyDecryptionTest { - - val USER_PIN: CharArray = "123456".toCharArray() - val ADMIN_PIN: CharArray = "12345678".toCharArray() +class YubikeyDecryptionTest : YubikeyTest() { + // Complete software key private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + "Comment: Alice \n" + @@ -51,6 +48,8 @@ class YubikeyDecryptionTest { "1POPHzF3cMIReYhZfiJUEBV19suL\n" + "=dA6G\n" + "-----END PGP PRIVATE KEY BLOCK-----" + + // Software certificate private val CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + "Comment: Alice \n" + @@ -103,35 +102,43 @@ class YubikeyDecryptionTest { "-----END PGP MESSAGE-----" @Test - fun decryptMessageUsingYubikey() { - val api = PGPainless(BcOpenPGPImplementation()) - api.hardwareTokenBackends.add(YubikeyHardwareTokenBackend()) + fun decryptMessageWithYubikey() { 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() - // Write key - val divertToCard = yubikey.storeKeyInSlot(decKey.unlock(), KeyRef.DEC, ADMIN_PIN) + // Write key to card + val hardwareBasedKey = helper.moveToYubikey(decKey.unlock(), yubikey, adminPin, KeyRef.DEC) // 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 { val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection(it, decKey.pgpPublicKey) val decIn = api.processMessage() .onInputStream(msgIn) .withOptions( ConsumerOptions.get(api) - //.addDecryptionKey(api.readKey().parseKey(KEY)) .addCustomDecryptorFactory(decFac) ) val msg = decIn.readAllBytes() decIn.close() assertEquals("Hello, World!\n", String(msg)) } + + */ } } diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt index 5d3ecf28..96d2e924 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt @@ -1,17 +1,26 @@ package org.pgpainless.yubikey +import org.gnupg.GnuPGDummyKeyUtil 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.Test +import java.util.Arrays -class YubikeyHardwareTokenBackendTest { +class YubikeyHardwareTokenBackendTest : YubikeyTest() { val backend = YubikeyHardwareTokenBackend() @Test fun testListDeviceSerials() { - assertNotNull(backend.listDeviceSerials()) - assumeTrue(backend.listDeviceSerials().isNotEmpty()) + val serials = backend.listDeviceSerials() + assertTrue(serials.any { + it.contentEquals( + GnuPGDummyKeyUtil.serialToBytes( + allowedSerialNumber + ) + ) + }) } @Test diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt index 223b6fa7..a125e1d5 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt @@ -1,18 +1,22 @@ package org.pgpainless.yubikey +import org.junit.jupiter.api.Assertions.assertTrue 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() +class YubikeyKeyGeneratorTest : YubikeyTest() { @Test fun generateKey() { - val helper = YubikeyHelper() 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()) + for (subkey in key.secretKeys) { + assertTrue(subkey.value.hasExternalSecretKey()) + } } } diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeySigningTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeySigningTest.kt index 7b3c4f09..4b8d41e1 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeySigningTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeySigningTest.kt @@ -1,14 +1,9 @@ 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.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider 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.decryption_verification.ConsumerOptions import org.pgpainless.encryption_signing.ProducerOptions @@ -17,10 +12,7 @@ import org.pgpainless.signature.PGPContentSignerBuilderProviderFactory import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream -class YubikeySigningTest { - - val USER_PIN: CharArray = "123456".toCharArray() - val ADMIN_PIN: CharArray = "12345678".toCharArray() +class YubikeySigningTest : YubikeyTest() { private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + @@ -59,56 +51,16 @@ class YubikeySigningTest { "1POPHzF3cMIReYhZfiJUEBV19suL\n" + "=dA6G\n" + "-----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 \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 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 signingKeyOnCard = helper.moveKeyToCard(signingKey.unlock(), yubikey, ADMIN_PIN) - println(signingKeyOnCard.toAsciiArmoredString()) + val hardwareBasedSigningKey = helper.moveToYubikey(signingKey.unlock(), yubikey, adminPin) + println(hardwareBasedSigningKey.toAsciiArmoredString()) val msgOut = ByteArrayOutputStream() device.openConnection(SmartCardConnection::class.java).use { @@ -123,7 +75,7 @@ class YubikeySigningTest { .generateMessage() .onOutputStream(msgOut) .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.close() @@ -133,7 +85,7 @@ class YubikeySigningTest { api.processMessage() .onInputStream(ByteArrayInputStream(msgOut.toByteArray())) .withOptions(ConsumerOptions.get() - .addVerificationCert(signingKeyOnCard.toCertificate()) + .addVerificationCert(hardwareBasedSigningKey.toCertificate()) ).use { it.readAllBytes() it.close() diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt new file mode 100644 index 00000000..0853ca8b --- /dev/null +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt @@ -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") + } +} diff --git a/pgpainless-yubikey/src/test/resources/yubikey.properties b/pgpainless-yubikey/src/test/resources/yubikey.properties new file mode 100644 index 00000000..6c036a45 --- /dev/null +++ b/pgpainless-yubikey/src/test/resources/yubikey.properties @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +ADMIN_PIN=12345678 +USER_PIN=123456 +ALLOWED_DEVICE_SERIAL=15472425 \ No newline at end of file