diff --git a/pgpainless-yubikey/build.gradle b/pgpainless-yubikey/build.gradle index 4986228f..1f219b96 100644 --- a/pgpainless-yubikey/build.gradle +++ b/pgpainless-yubikey/build.gradle @@ -23,8 +23,12 @@ dependencies { implementation(project(":pgpainless-core")) // api "org.bouncycastle:bcpkix-jdk18on:$bouncyCastleVersion" - api "com.yubico.yubikit:openpgp:$yubikitVersion" - api "com.yubico.yubikit:desktop:$yubikitVersion" + api ("com.yubico.yubikit:openpgp:$yubikitVersion") { + exclude group: "org.junit", module: "junit-bom" + } + api ("com.yubico.yubikit:desktop:$yubikitVersion") { + exclude group: "org.junit", module: "junit-bom" + } // api "com.yubico.yubikit:piv:$yubikitVersion" implementation "com.google.code.findbugs:jsr305:3.0.2" 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 8eeaf2e2..fb79e822 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt @@ -5,36 +5,25 @@ 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) + session.writePrivateKey(key, keyRef) + session.writeFingerprint(key.publicKey, keyRef) + session.writeGenerationTime(key.publicKey, keyRef) } } diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyExtensions.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyExtensions.kt new file mode 100644 index 00000000..0dc7e271 --- /dev/null +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyExtensions.kt @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.yubikey + +import com.yubico.yubikit.core.keys.PrivateKeyValues +import com.yubico.yubikit.openpgp.KeyRef +import com.yubico.yubikit.openpgp.OpenPgpSession +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter + +/** + * Writes the private key bytes of an [OpenPGPPrivateKey] to the slot identified by the provided + * [KeyRef]. This method requires the session to have a verified admin pin. + */ +internal fun OpenPgpSession.writePrivateKey(key: OpenPGPPrivateKey, keyRef: KeyRef) { + writePrivateKey(key.keyPair.privateKey, keyRef) +} + +/** + * Writes the private key bytes of a [PGPPrivateKey] to the slot identified by the provided + * [KeyRef]. This method requires the session to have a verified admin pin. + */ +internal fun OpenPgpSession.writePrivateKey(key: PGPPrivateKey, keyRef: KeyRef) { + val privateKey = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()).getPrivateKey(key) + putKey(keyRef, PrivateKeyValues.fromPrivateKey(privateKey)) +} + +/** + * Writes the 20-octet fingerprint of an OpenPGP key to the slot identified by the provided + * [KeyRef]. This method requires the session to have a verified admin pin. + */ +internal fun OpenPgpSession.writeFingerprint( + key: OpenPGPCertificate.OpenPGPComponentKey, + keyRef: KeyRef +) = writeFingerprint(key.pgpPublicKey, keyRef) + +/** + * Writes the 20-octet fingerprint of an OpenPGP key to the slot identified by the provided + * [KeyRef]. This method requires the session to have a verified admin pin. + */ +internal fun OpenPgpSession.writeFingerprint(key: PGPPublicKey, keyRef: KeyRef) { + setFingerprint(keyRef, key.fingerprint) +} + +/** + * Writes the key generation time of an OpenPGP key to the slot identified by the provided [KeyRef]. + * This method requires the session to have a verified admin pin. + */ +internal fun OpenPgpSession.writeGenerationTime( + key: OpenPGPCertificate.OpenPGPComponentKey, + keyRef: KeyRef +) = writeGenerationTime(key.pgpPublicKey, keyRef) + +/** + * Writes the key generation time of an OpenPGP key to the slot identified by the provided [KeyRef]. + * This method requires the session to have a verified admin pin. + */ +internal fun OpenPgpSession.writeGenerationTime(key: PGPPublicKey, keyRef: KeyRef) { + val time = (key.publicKeyPacket.time.time / 1000).toInt() + setGenerationTime(keyRef, time) +} 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 7c987c74..fc1edac0 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt @@ -55,13 +55,13 @@ class YubikeyHardwareTokenBackend : HardwareTokenBackend { yk.device.openConnection(SmartCardConnection::class.java).use { val session = OpenPgpSession(it) // session.getData(KeyRef.DEC.fingerprint) - session.getData(KeyRef.SIG.fingerprint) + val ddo = session.applicationRelatedData.discretionary listOfNotNull( - session.getData(KeyRef.ATT.fingerprint), - session.getData(KeyRef.SIG.fingerprint), - session.getData(KeyRef.DEC.fingerprint), - session.getData(KeyRef.AUT.fingerprint)) + ddo.getFingerprint(KeyRef.ATT), + ddo.getFingerprint(KeyRef.SIG), + ddo.getFingerprint(KeyRef.DEC), + ddo.getFingerprint(KeyRef.AUT)) } } } 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 978ca668..00c984c3 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt @@ -22,10 +22,14 @@ import org.pgpainless.key.OpenPgpFingerprint class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) { fun listDevices(manager: YubiKitManager = YubiKitManager()): List = - manager - .listAllDevices() - .filter { it.key is CompositeDevice } - .map { Yubikey(it.value, it.key) } + try { + manager + .listAllDevices() + .filter { it.key is CompositeDevice } + .map { Yubikey(it.value, it.key) } + } catch (e: RuntimeException) { + emptyList() + } fun factoryReset(yubikey: Yubikey) { yubikey.device.openConnection(SmartCardConnection::class.java).use { 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 deed2b67..77453c3b 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt @@ -44,16 +44,22 @@ class YubikeyKeyGenerator(private val api: PGPainless) { var pkVal = session.generateEcKey(KeyRef.ATT, OpenPgpCurve.SECP521R1) var pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA) + session.writeFingerprint(pubKey, KeyRef.ATT) + session.writeGenerationTime(pubKey, KeyRef.ATT) val primarykey = toExternalSecretKey(pubKey, yubikey.info) pkVal = session.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP521R1) pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA) + session.writeFingerprint(pubKey, KeyRef.SIG) + session.writeGenerationTime(pubKey, KeyRef.SIG) val signingKey = toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info) pkVal = session.generateEcKey(KeyRef.DEC, OpenPgpCurve.SECP521R1) pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDH) + session.writeFingerprint(pubKey, KeyRef.DEC) + session.writeGenerationTime(pubKey, KeyRef.DEC) val encryptionKey = toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info) 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 b5ebfbe5..efd3151b 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt @@ -7,7 +7,6 @@ package org.pgpainless.yubikey import org.gnupg.GnuPGDummyKeyUtil import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assumptions.assumeTrue -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test class YubikeyHardwareTokenBackendTest : YubikeyTest() { @@ -22,7 +21,6 @@ class YubikeyHardwareTokenBackendTest : YubikeyTest() { } @Test - @Disabled("because yubikit-android 2.9.0 cannot extract fingerprints") fun testListKeys() { val keys = backend.listKeyFingerprints() assumeTrue(keys.isNotEmpty()) 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 3722bdea..fd78e4d2 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt @@ -14,14 +14,19 @@ class YubikeyKeyGeneratorTest : YubikeyTest() { @Test fun generateKey() { + val backend = YubikeyHardwareTokenBackend() val keyGen = YubikeyKeyGenerator(PGPainless.getInstance()) val key = keyGen.generateModernKey(yubikey, adminPin, OpenPGPKeyVersion.v4, Date()) println(key.toAsciiArmoredString()) // TODO: More thorough checking once key generation is implemented with binding signatures, // userids and other metadata + val fingerprints = backend.listKeyFingerprints().entries.first().value for (subkey in key.secretKeys) { assertTrue(subkey.value.hasExternalSecretKey()) + assertTrue { + fingerprints.any { it.contentEquals(subkey.value.pgpPublicKey.fingerprint) } + } } } } diff --git a/version.gradle b/version.gradle index fdc3745e..a1e9c05d 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' sopJavaVersion = '14.0.3' - yubikitVersion = '2.9.0' + yubikitVersion = '3.0.0' } }