From 89bce1ca14d7b0bbf0b50714a76ba16a1939374d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Nov 2025 15:41:09 +0100 Subject: [PATCH] YubikeyDataDecryptorFactory: WIP with SECP521r1 keys --- .../yubikey/YubikeyDataDecryptorFactory.kt | 20 ++++++-- .../org/pgpainless/yubikey/YubikeyTest.kt | 48 +++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt index ade9a1b5..50d724bf 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt @@ -12,6 +12,7 @@ import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.bcpg.PublicKeyAlgorithmTags import org.bouncycastle.bcpg.PublicKeyPacket import org.bouncycastle.crypto.params.KeyParameter +import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.operator.PGPPad @@ -21,9 +22,11 @@ import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory import org.bouncycastle.openpgp.operator.bc.RFC6637KDFCalculator import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter +import org.pgpainless.bouncycastle.extensions.getCurveName import org.pgpainless.decryption_verification.HardwareSecurity import org.pgpainless.key.OpenPgpV4Fingerprint import org.pgpainless.key.SubkeyIdentifier +import org.slf4j.LoggerFactory import java.util.* class YubikeyDataDecryptorFactory( @@ -32,6 +35,10 @@ class YubikeyDataDecryptorFactory( ) : HardwareSecurity.HardwareDataDecryptorFactory(subkeyIdentifier, callback) { companion object { + + @JvmStatic + val LOGGER = LoggerFactory.getLogger(YubikeyDataDecryptorFactory::class.java) + val ADMIN_PIN: CharArray = "12345678".toCharArray() val USER_PIN: CharArray = "123456".toCharArray() @@ -42,8 +49,7 @@ class YubikeyDataDecryptorFactory( pubkey: PGPPublicKey ): HardwareSecurity.HardwareDataDecryptorFactory { val openpgpSession = OpenPgpSession(smartCardConnection) - // openpgpSession.verifyAdminPin(ADMIN_PIN) - val decKeyIdentifier: SubkeyIdentifier = SubkeyIdentifier(OpenPgpV4Fingerprint(pubkey)) + val decKeyIdentifier = SubkeyIdentifier(OpenPgpV4Fingerprint(pubkey)) val isRSAKey = pubkey.algorithm == PublicKeyAlgorithmTags.RSA_GENERAL || pubkey.algorithm == PublicKeyAlgorithmTags.RSA_SIGN @@ -60,13 +66,18 @@ class YubikeyDataDecryptorFactory( openpgpSession.verifyAdminPin(ADMIN_PIN) openpgpSession.verifyUserPin(USER_PIN, true) + LOGGER.debug("Attempt decryption with key {}", keyIdentifier) + if(isRSAKey) { // easy + LOGGER.debug("Key is RSA key of length {}", pubkey.bitStrength) val decryptedSessionKey = openpgpSession.decrypt(sessionKeyData) return decryptedSessionKey } else { // meh... + val curveName = pubkey.getCurveName() val ecPubKey: ECDHPublicBCPGKey = pubkey.publicKeyPacket.key as ECDHPublicBCPGKey + LOGGER.debug("Key is ECDH key over curve $curveName") // split session data into peer key and encrypted session key // peer key @@ -83,9 +94,8 @@ class YubikeyDataDecryptorFactory( System.arraycopy(sessionKeyData, 2 + pLen + 1, keyEnc, 0, keyLen) // perform ECDH key agreement via the Yubikey - val x9Params = - org.bouncycastle.asn1.x9.ECNamedCurveTable.getByOIDLazy(ecPubKey.curveOID) - val publicPoint = x9Params.curve.decodePoint(pEnc) + val params = ECNamedCurveTable.getParameterSpec(curveName) + val publicPoint = params.curve.decodePoint(pEnc) val peerKey = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()) .getPublicKey( PGPPublicKey( diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt index 126e6864..8aed562d 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt @@ -5,15 +5,20 @@ 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.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openpgp.PGPUtil 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.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.pgpainless.PGPainless import org.pgpainless.algorithm.KeyFlag import org.pgpainless.algorithm.OpenPGPKeyVersion +import org.pgpainless.bouncycastle.extensions.getCurveName import org.pgpainless.bouncycastle.extensions.toOpenPGPKey import org.pgpainless.decryption_verification.ConsumerOptions import org.pgpainless.encryption_signing.EncryptionOptions @@ -119,6 +124,7 @@ class YubikeyTest { @Test fun test() { + println(CERT) val api = PGPainless(BcOpenPGPImplementation()) val key = api.readKey().parseKey(KEY) @@ -171,4 +177,46 @@ class YubikeyTest { assertEquals("Hello, World!\n", String(msg)) } } + + val pubKeyAscii = // "modernKeyRing" + """ + -----BEGIN PGP PUBLIC KEY BLOCK----- + Comment: C68A 9140 9A00 3C55 5D8A 62A5 D3ED 03F9 FF75 68D4 + Comment: john doe + + mDMEaR7iehYJKwYBBAHaRw8BAQdA5XMRsc8HUXiJkjtOSCj86v+OeemU41U08Lmi + 2PQ3Tnm0HGpvaG4gZG9lIDxqLmRvZUBleGFtcGxlLmNvbT7CnwQTFgoAUQkQ0+0D + +f91aNQWoQTGipFAmgA8VV2KYqXT7QP5/3Vo1AWCaR7iegKbAQUVCgkICwUWAgMB + AAQLCQgHCScJAQkCCQMIAQKeCQWJCWYBgAKZAQAAFecBALy6FxELczpihvkVJPa2 + iaV7zDcfFBvX4KyTr506hxufAP4ixT59d/QRMWmuRN6QRkRcgduLaw2l/Hs/zBuV + tQjHDsKfBBMWCgBRApsBBRUKCQgLBRYCAwEABAsJCAcJJwkBCQIJAwgBAp4JCRDT + 7QP5/3Vo1BahBMaKkUCaADxVXYpipdPtA/n/dWjUBYJpHuJ7BYkAAAAAApkBAAAK + SQD9FpbJAinkmaeHluaKmiCp0HggoGF8aji9rDqSvUDtnWsA/i5I1eZ0rPvxZc6z + pIbfRHdbdgOTmTZEOOz82GQVmsMLuDgEaR7iehIKKwYBBAGXVQEFAQEHQHc9W+J1 + IPl7nekdLrx5SLdvYnNNocULlqqLoDgN3fV4AwEIB8J4BBgWCgAqCRDT7QP5/3Vo + 1BahBMaKkUCaADxVXYpipdPtA/n/dWjUBYJpHuJ6ApsMAACh/AEA1r+JB8uhMX7N + l4B3QOF9zLmUXhihRvE0tyY3cCCwUrYA/2yqF1mA8dHsDuDnWEUYxgX+ZpYBXr+P + j9ZKl/HoNeIOuDMEaR7iehYJKwYBBAHaRw8BAQdAQPTWzF21MpuSRjclxeAS+lZH + ulTwm/HsOaVpur8vSZ/CwC8EGBYKAKEJENPtA/n/dWjUFqEExoqRQJoAPFVdimKl + 0+0D+f91aNQFgmke4noCmwJ2IAQZFgoAHQWCaR7iehYhBMqKZfP+EDi1iRE58a14 + HgK5jFyDAAoJEK14HgK5jFyDoRUA/0GmLpBUVFSEdbSh+o7tz6xncAIjkm20LWIy + PF81ilR9AP0a/MVoE9ivY7HK9uu79cc2Y5IratjiXRpamYqODQutAQAA848A/Ro1 + SfAfFAmMDfcbuKvpQEK/d4T3455End3ohd5TXb7VAP9/wMxzLJ1K5mE6LTQ5Hw4b + m9XtYRYVHugI27XFacaFAg== + =3yy3 + -----END PGP PUBLIC KEY BLOCK----- + """.trimIndent() + + @Test + fun getx9ParamsTest() { + val certificate = org.bouncycastle.openpgp.api.OpenPGPKeyReader().parseCertificate(pubKeyAscii.toByteArray()) + val pubKey = certificate.getEncryptionKeys().first().getPGPPublicKey() + + assertTrue(pubKey.isEncryptionKey()) + + val ecPubKey = pubKey.getPublicKeyPacket().getKey() as org.bouncycastle.bcpg.ECDHPublicBCPGKey + val params = ECNamedCurveTable.getParameterSpec(pubKey.getCurveName()) + + assertNotNull(params) // fails + } }