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

Update yubikit to 3.0.0, fix key generation, refactor code

This commit is contained in:
Paul Schaub 2025-12-04 23:12:08 +01:00
parent bb796a6819
commit 84c30f9629
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
9 changed files with 101 additions and 28 deletions

View file

@ -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"

View file

@ -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)
}
}

View file

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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)
}

View file

@ -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))
}
}
}

View file

@ -22,10 +22,14 @@ 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) }
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 {

View file

@ -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)

View file

@ -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())

View file

@ -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) }
}
}
}
}

View file

@ -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'
}
}