1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-12-10 06:11:08 +01:00

Transparent decryption

This commit is contained in:
Paul Schaub 2025-12-02 13:10:47 +01:00
parent de47a683d9
commit a1af39a4f7
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
18 changed files with 461 additions and 357 deletions

View file

@ -111,6 +111,13 @@ precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>" SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "Apache-2.0" SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "pgpainless-yubikey/src/test/resources/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]] [[annotations]]
path = ".github/ISSUE_TEMPLATE/**" path = ".github/ISSUE_TEMPLATE/**"
precedence = "aggregate" precedence = "aggregate"

View file

@ -57,11 +57,10 @@ class GnuPGDummyKeyUtil private constructor() {
*/ */
@JvmStatic fun modify(secretKeys: PGPSecretKeyRing) = Builder(secretKeys) @JvmStatic fun modify(secretKeys: PGPSecretKeyRing) = Builder(secretKeys)
@JvmStatic fun serialToBytes(sn: Int) = byteArrayOf( @JvmStatic
(sn shr 24).toByte(), fun serialToBytes(sn: Int) =
(sn shr(16)).toByte(), byteArrayOf(
(sn shr(8)).toByte(), (sn shr 24).toByte(), (sn shr (16)).toByte(), (sn shr (8)).toByte(), sn.toByte())
sn.toByte())
} }
class Builder(private val keys: PGPSecretKeyRing) { class Builder(private val keys: PGPSecretKeyRing) {

View file

@ -38,7 +38,6 @@ 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
@ -448,16 +447,19 @@ class OpenPgpMessageInputStream(
} }
if (secretKey.hasExternalSecretKey()) { if (secretKey.hasExternalSecretKey()) {
LOGGER.debug("Decryption key ${secretKey.keyIdentifier} is located on an external device, e.g. a smartcard.") LOGGER.debug(
"Decryption key ${secretKey.keyIdentifier} is located on an external device, e.g. a smartcard.")
for (hardwareTokenBackend in options.hardwareTokenBackends) { for (hardwareTokenBackend in options.hardwareTokenBackends) {
LOGGER.debug("Attempt decryption with ${hardwareTokenBackend.getBackendName()} backend.") LOGGER.debug(
"Attempt decryption with ${hardwareTokenBackend.getBackendName()} backend.")
if (decryptWithHardwareKey( if (decryptWithHardwareKey(
hardwareTokenBackend, hardwareTokenBackend,
esks, esks,
secretKey, secretKey,
protector, protector,
SubkeyIdentifier(secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier), SubkeyIdentifier(
pkesk)) { secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier),
pkesk)) {
return true return true
} }
} }
@ -624,16 +626,22 @@ class OpenPgpMessageInputStream(
pkesk: PGPPublicKeyEncryptedData pkesk: PGPPublicKeyEncryptedData
): Boolean { ): Boolean {
try { try {
val decrypted = pkesk.getDataStream(decryptorFactory)
val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory)) val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory))
throwIfUnacceptable(sessionKey.algorithm) throwIfUnacceptable(sessionKey.algorithm)
val pgpSessionKey = PGPSessionKey(sessionKey.algorithm.algorithmId, sessionKey.key)
val sessionKeyEncData = esks.esks.extractSessionKeyEncryptedData()
val decrypted =
sessionKeyEncData.getDataStream(
api.implementation.sessionKeyDataDecryptorFactory(pgpSessionKey))
val encryptedData = esks.toEncryptedData(sessionKey, layerMetadata.depth) val encryptedData = esks.toEncryptedData(sessionKey, layerMetadata.depth)
encryptedData.decryptionKey = decryptionKeyId encryptedData.decryptionKey = decryptionKeyId
encryptedData.sessionKey = sessionKey encryptedData.sessionKey = sessionKey
encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyIdentifier }) encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyIdentifier })
LOGGER.debug("Successfully decrypted data with key $decryptionKeyId") LOGGER.debug("Successfully decrypted data with key $decryptionKeyId")
val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options) val integrityProtected =
IntegrityProtectedInputStream(decrypted, sessionKeyEncData, options)
nestedInputStream = nestedInputStream =
OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api) OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api)
return true return true
@ -790,7 +798,7 @@ class OpenPgpMessageInputStream(
} }
} }
private class ESKsAndData(private val esks: PGPEncryptedDataList) { private class ESKsAndData(val esks: PGPEncryptedDataList) {
fun toEncryptedData(sk: SessionKey, depth: Int): EncryptedData { fun toEncryptedData(sk: SessionKey, depth: Int): EncryptedData {
return when (EncryptedDataPacketType.of(esks)!!) { return when (EncryptedDataPacketType.of(esks)!!) {
EncryptedDataPacketType.SED -> EncryptedDataPacketType.SED ->

View file

@ -313,18 +313,20 @@ class SigningOptions(private val api: PGPainless) {
subpacketsCallback) subpacketsCallback)
} }
fun addInlineSignature(hardwareBackedKey: OpenPGPComponentKey, fun addInlineSignature(
hardwareContentSignerBuilderProviderFactory: PGPContentSignerBuilderProviderFactory, hardwareBackedKey: OpenPGPComponentKey,
hashAlgorithm: HashAlgorithm, hardwareContentSignerBuilderProviderFactory: PGPContentSignerBuilderProviderFactory,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, hashAlgorithm: HashAlgorithm,
subpacketsCallback: Callback? = null signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
) = addHardwareSigningMethod( subpacketsCallback: Callback? = null
hardwareBackedKey, ) =
hardwareContentSignerBuilderProviderFactory, addHardwareSigningMethod(
hashAlgorithm, hardwareBackedKey,
signatureType, hardwareContentSignerBuilderProviderFactory,
false, hashAlgorithm,
subpacketsCallback) signatureType,
false,
subpacketsCallback)
/** /**
* Add detached signatures with all key rings from the provided secret key ring collection. * Add detached signatures with all key rings from the provided secret key ring collection.
@ -532,7 +534,14 @@ class SigningOptions(private val api: PGPainless) {
hashAlgorithm: HashAlgorithm, hashAlgorithm: HashAlgorithm,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
subpacketsCallback: Callback? = null subpacketsCallback: Callback? = null
) = addHardwareSigningMethod(hardwareBackedKey, hardwareContentSignerBuilderProviderFactory, hashAlgorithm, signatureType, true, subpacketsCallback) ) =
addHardwareSigningMethod(
hardwareBackedKey,
hardwareContentSignerBuilderProviderFactory,
hashAlgorithm,
signatureType,
true,
subpacketsCallback)
private fun addHardwareSigningMethod( private fun addHardwareSigningMethod(
hardwareBackedKey: OpenPGPComponentKey, hardwareBackedKey: OpenPGPComponentKey,
@ -540,15 +549,15 @@ class SigningOptions(private val api: PGPainless) {
hashAlgorithm: HashAlgorithm, hashAlgorithm: HashAlgorithm,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
detached: Boolean, detached: Boolean,
subpacketsCallback: Callback? = null) = apply { subpacketsCallback: Callback? = null
) = apply {
rejectWeakKeys(hardwareBackedKey) rejectWeakKeys(hardwareBackedKey)
val pubkey = hardwareBackedKey.pgpPublicKey val pubkey = hardwareBackedKey.pgpPublicKey
val pgpContentSignerBuilder = hardwareContentSignerBuilderProviderFactory.create(hashAlgorithm) val pgpContentSignerBuilder =
.get(pubkey) hardwareContentSignerBuilderProviderFactory.create(hashAlgorithm).get(pubkey)
val generator = PGPSignatureGenerator( val generator =
pgpContentSignerBuilder, pubkey) PGPSignatureGenerator(pgpContentSignerBuilder, pubkey).apply {
.apply {
init(signatureType.signatureType.code, pubkey) init(signatureType.signatureType.code, pubkey)
} }
@ -582,17 +591,19 @@ class SigningOptions(private val api: PGPainless) {
val publicKeyAlgorithm = requireFromId(signingKey.pgpPublicKey.algorithm) val publicKeyAlgorithm = requireFromId(signingKey.pgpPublicKey.algorithm)
val bitStrength = signingKey.pgpPublicKey.bitStrength val bitStrength = signingKey.pgpPublicKey.bitStrength
if (!api.algorithmPolicy.publicKeyAlgorithmPolicy.isAcceptable( if (!api.algorithmPolicy.publicKeyAlgorithmPolicy.isAcceptable(
publicKeyAlgorithm, bitStrength)) { publicKeyAlgorithm, bitStrength)) {
throw UnacceptableSigningKeyException( throw UnacceptableSigningKeyException(
PublicKeyAlgorithmPolicyException( PublicKeyAlgorithmPolicyException(signingKey, publicKeyAlgorithm, bitStrength))
signingKey, publicKeyAlgorithm, bitStrength))
} }
} }
private fun prepareSignatureGenerator(generator: PGPSignatureGenerator, signingKey: PGPPublicKey, subpacketCallback: Callback?) { private fun prepareSignatureGenerator(
generator: PGPSignatureGenerator,
signingKey: PGPPublicKey,
subpacketCallback: Callback?
) {
// Subpackets // Subpackets
val hashedSubpackets = val hashedSubpackets = SignatureSubpackets.createHashedSubpackets(signingKey)
SignatureSubpackets.createHashedSubpackets(signingKey)
val unhashedSubpackets = SignatureSubpackets.createEmptySubpackets() val unhashedSubpackets = SignatureSubpackets.createEmptySubpackets()
if (subpacketCallback != null) { if (subpacketCallback != null) {
subpacketCallback.modifyHashedSubpackets(hashedSubpackets) subpacketCallback.modifyHashedSubpackets(hashedSubpackets)

View file

@ -210,7 +210,6 @@ $algorithm of size $bitSize is not acceptable.""",
} }
} }
class GeneralKeyException(message: String, class GeneralKeyException(message: String, fingerprint: OpenPgpFingerprint) :
fingerprint: OpenPgpFingerprint KeyException(message, fingerprint)
) : KeyException(message, fingerprint)
} }

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.hardware package org.pgpainless.hardware
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.signature package org.pgpainless.signature
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey package org.pgpainless.yubikey
import com.yubico.yubikit.core.YubiKeyDevice import com.yubico.yubikit.core.YubiKeyDevice
@ -16,8 +20,10 @@ 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
val privateKey = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()) val privateKey =
.getPrivateKey(key.keyPair.privateKey) JcaPGPKeyConverter()
.setProvider(BouncyCastleProvider())
.getPrivateKey(key.keyPair.privateKey)
val session = OpenPgpSession(it as SmartCardConnection) val session = OpenPgpSession(it as SmartCardConnection)

View file

@ -7,6 +7,7 @@ package org.pgpainless.yubikey
import com.yubico.yubikit.core.keys.PublicKeyValues import com.yubico.yubikit.core.keys.PublicKeyValues
import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.openpgp.OpenPgpSession import com.yubico.yubikit.openpgp.OpenPgpSession
import java.util.*
import org.bouncycastle.bcpg.ECDHPublicBCPGKey import org.bouncycastle.bcpg.ECDHPublicBCPGKey
import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags import org.bouncycastle.bcpg.PublicKeyAlgorithmTags
@ -27,7 +28,6 @@ import org.pgpainless.decryption_verification.HardwareSecurity
import org.pgpainless.key.OpenPgpV4Fingerprint import org.pgpainless.key.OpenPgpV4Fingerprint
import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.SubkeyIdentifier
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.util.*
class YubikeyDataDecryptorFactory( class YubikeyDataDecryptorFactory(
callback: HardwareSecurity.DecryptionCallback, callback: HardwareSecurity.DecryptionCallback,
@ -36,13 +36,11 @@ class YubikeyDataDecryptorFactory(
companion object { companion object {
@JvmStatic @JvmStatic val LOGGER = LoggerFactory.getLogger(YubikeyDataDecryptorFactory::class.java)
val LOGGER = LoggerFactory.getLogger(YubikeyDataDecryptorFactory::class.java)
val ADMIN_PIN: CharArray = "12345678".toCharArray() val ADMIN_PIN: CharArray = "12345678".toCharArray()
val USER_PIN: CharArray = "123456".toCharArray() val USER_PIN: CharArray = "123456".toCharArray()
@JvmStatic @JvmStatic
fun createDecryptorFromConnection( fun createDecryptorFromConnection(
smartCardConnection: SmartCardConnection, smartCardConnection: SmartCardConnection,
@ -51,94 +49,106 @@ class YubikeyDataDecryptorFactory(
val openpgpSession = OpenPgpSession(smartCardConnection) val openpgpSession = OpenPgpSession(smartCardConnection)
val decKeyIdentifier = SubkeyIdentifier(OpenPgpV4Fingerprint(pubkey)) val decKeyIdentifier = SubkeyIdentifier(OpenPgpV4Fingerprint(pubkey))
val isRSAKey = pubkey.algorithm == PublicKeyAlgorithmTags.RSA_GENERAL val isRSAKey =
|| pubkey.algorithm == PublicKeyAlgorithmTags.RSA_SIGN pubkey.algorithm == PublicKeyAlgorithmTags.RSA_GENERAL ||
|| pubkey.algorithm == PublicKeyAlgorithmTags.RSA_ENCRYPT pubkey.algorithm == PublicKeyAlgorithmTags.RSA_SIGN ||
pubkey.algorithm == PublicKeyAlgorithmTags.RSA_ENCRYPT
val callback = object : HardwareSecurity.DecryptionCallback { val callback =
override fun decryptSessionKey( object : HardwareSecurity.DecryptionCallback {
keyIdentifier: KeyIdentifier, override fun decryptSessionKey(
keyAlgorithm: Int, keyIdentifier: KeyIdentifier,
sessionKeyData: ByteArray, keyAlgorithm: Int,
pkeskVersion: Int sessionKeyData: ByteArray,
): ByteArray { pkeskVersion: Int
// TODO: Move user pin verification somewhere else ): ByteArray {
openpgpSession.verifyAdminPin(ADMIN_PIN) // TODO: Move user pin verification somewhere else
openpgpSession.verifyUserPin(USER_PIN, true) openpgpSession.verifyUserPin(USER_PIN, true)
LOGGER.debug("Attempt decryption with key {}", keyIdentifier) LOGGER.debug("Attempt decryption with key {}", keyIdentifier)
if(isRSAKey) { if (isRSAKey) {
// easy // easy
LOGGER.debug("Key is RSA key of length {}", pubkey.bitStrength) LOGGER.debug("Key is RSA key of length {}", pubkey.bitStrength)
val decryptedSessionKey = openpgpSession.decrypt(sessionKeyData) val decryptedSessionKey = openpgpSession.decrypt(sessionKeyData)
return decryptedSessionKey smartCardConnection.close()
} else { return decryptedSessionKey
// meh... } else {
val curveName = pubkey.getCurveName() // meh...
val ecPubKey: ECDHPublicBCPGKey = pubkey.publicKeyPacket.key as ECDHPublicBCPGKey val curveName = pubkey.getCurveName()
LOGGER.debug("Key is ECDH key over curve $curveName") val ecPubKey: ECDHPublicBCPGKey =
// split session data into peer key and encrypted session key 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 // peer key
val pLen = val pLen =
((((sessionKeyData[0].toInt() and 0xff) shl 8) + (sessionKeyData[1].toInt() and 0xff)) + 7) / 8 ((((sessionKeyData[0].toInt() and 0xff) shl 8) +
checkRange(2 + pLen + 1, sessionKeyData) (sessionKeyData[1].toInt() and 0xff)) + 7) / 8
val pEnc = ByteArray(pLen) checkRange(2 + pLen + 1, sessionKeyData)
System.arraycopy(sessionKeyData, 2, pEnc, 0, pLen) val pEnc = ByteArray(pLen)
System.arraycopy(sessionKeyData, 2, pEnc, 0, pLen)
// encrypted session key // encrypted session key
val keyLen = sessionKeyData[pLen + 2].toInt() and 0xff val keyLen = sessionKeyData[pLen + 2].toInt() and 0xff
checkRange(2 + pLen + 1 + keyLen, sessionKeyData) checkRange(2 + pLen + 1 + keyLen, sessionKeyData)
val keyEnc = ByteArray(keyLen) val keyEnc = ByteArray(keyLen)
System.arraycopy(sessionKeyData, 2 + pLen + 1, keyEnc, 0, keyLen) System.arraycopy(sessionKeyData, 2 + pLen + 1, keyEnc, 0, keyLen)
// perform ECDH key agreement via the Yubikey // perform ECDH key agreement via the Yubikey
val params = ECNamedCurveTable.getParameterSpec(curveName) val params = ECNamedCurveTable.getParameterSpec(curveName)
val publicPoint = params.curve.decodePoint(pEnc) val publicPoint = params.curve.decodePoint(pEnc)
val peerKey = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()) val peerKey =
.getPublicKey( JcaPGPKeyConverter()
PGPPublicKey( .setProvider(BouncyCastleProvider())
PublicKeyPacket( .getPublicKey(
pubkey.version, PublicKeyAlgorithmTags.ECDH, Date(), PGPPublicKey(
ECDHPublicBCPGKey( PublicKeyPacket(
ecPubKey.curveOID, pubkey.version,
publicPoint, PublicKeyAlgorithmTags.ECDH,
ecPubKey.hashAlgorithm.toInt(), Date(),
ecPubKey.symmetricKeyAlgorithm.toInt(), ECDHPublicBCPGKey(
ecPubKey.curveOID,
publicPoint,
ecPubKey.hashAlgorithm.toInt(),
ecPubKey.symmetricKeyAlgorithm.toInt(),
),
),
BcKeyFingerprintCalculator(),
), ),
), )
val secret =
openpgpSession.decrypt(PublicKeyValues.fromPublicKey(peerKey))
smartCardConnection.close()
// Use the shared key to decrypt the session key
val hashAlgorithm: Int = ecPubKey.hashAlgorithm.toInt()
val symmetricKeyAlgorithm: Int = ecPubKey.symmetricKeyAlgorithm.toInt()
val userKeyingMaterial =
RFC6637Utils.createUserKeyingMaterial(
pubkey.publicKeyPacket,
BcKeyFingerprintCalculator(), BcKeyFingerprintCalculator(),
)
val rfc6637KDFCalculator =
RFC6637KDFCalculator(
BcPGPDigestCalculatorProvider()[hashAlgorithm],
symmetricKeyAlgorithm,
)
val key =
KeyParameter(
rfc6637KDFCalculator.createKey(secret, userKeyingMaterial))
return PGPPad.unpadSessionData(
BcPublicKeyDataDecryptorFactory.unwrapSessionData(
keyEnc,
symmetricKeyAlgorithm,
key,
), ),
) )
}
val secret = openpgpSession.decrypt(PublicKeyValues.fromPublicKey(peerKey))
// Use the shared key to decrypt the session key
val hashAlgorithm: Int = ecPubKey.hashAlgorithm.toInt()
val symmetricKeyAlgorithm: Int = ecPubKey.symmetricKeyAlgorithm.toInt()
val userKeyingMaterial = RFC6637Utils.createUserKeyingMaterial(
pubkey.publicKeyPacket,
BcKeyFingerprintCalculator(),
)
val rfc6637KDFCalculator =
RFC6637KDFCalculator(
BcPGPDigestCalculatorProvider()[hashAlgorithm],
symmetricKeyAlgorithm,
)
val key =
KeyParameter(rfc6637KDFCalculator.createKey(secret, userKeyingMaterial))
return PGPPad.unpadSessionData(
BcPublicKeyDataDecryptorFactory.unwrapSessionData(
keyEnc,
symmetricKeyAlgorithm,
key,
),
)
} }
} }
}
return YubikeyDataDecryptorFactory(callback, decKeyIdentifier) return YubikeyDataDecryptorFactory(callback, decKeyIdentifier)
} }

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.smartcard.SmartCardConnection
@ -21,39 +25,44 @@ class YubikeyHardwareTokenBackend : HardwareTokenBackend {
protector: SecretKeyRingProtector, protector: SecretKeyRingProtector,
pkesk: PGPPublicKeyEncryptedData pkesk: PGPPublicKeyEncryptedData
): Iterator<PublicKeyDataDecryptorFactory> { ): Iterator<PublicKeyDataDecryptorFactory> {
val devices = YubikeyHelper().listDevices() return object : Iterator<PublicKeyDataDecryptorFactory> {
return devices.map { yubikey -> val devices = YubikeyHelper().listDevices().iterator()
yubikey.device.openConnection(SmartCardConnection::class.java).use {
val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection( override fun hasNext(): Boolean {
it, return devices.hasNext()
secKey.pgpPublicKey
)
decFac as PublicKeyDataDecryptorFactory
} }
}.iterator()
override fun next(): PublicKeyDataDecryptorFactory {
return devices.next().device.openConnection(SmartCardConnection::class.java).let {
val decFac =
YubikeyDataDecryptorFactory.createDecryptorFromConnection(
it, secKey.pgpPublicKey)
decFac as PublicKeyDataDecryptorFactory
}
}
}
} }
override fun listDeviceSerials(): List<ByteArray> { override fun listDeviceSerials(): List<ByteArray> {
return YubikeyHelper().listDevices() return YubikeyHelper().listDevices().mapNotNull { yk ->
.mapNotNull { yk -> yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } } yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) }
}
} }
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.encodedSerialNumber to
yk.encodedSerialNumber to yk.device.openConnection(SmartCardConnection::class.java).use { yk.device.openConnection(SmartCardConnection::class.java).use {
val session = OpenPgpSession(it) val session = OpenPgpSession(it)
//session.getData(KeyRef.DEC.fingerprint) // session.getData(KeyRef.DEC.fingerprint)
session.getData(KeyRef.SIG.fingerprint) session.getData(KeyRef.SIG.fingerprint)
listOfNotNull( listOfNotNull(
session.getData(KeyRef.ATT.fingerprint), session.getData(KeyRef.ATT.fingerprint),
session.getData(KeyRef.SIG.fingerprint), session.getData(KeyRef.SIG.fingerprint),
session.getData(KeyRef.DEC.fingerprint), session.getData(KeyRef.DEC.fingerprint),
session.getData(KeyRef.AUT.fingerprint) session.getData(KeyRef.AUT.fingerprint))
)
} }
} }
} }
} }

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.smartcard.SmartCardConnection
@ -17,11 +21,11 @@ import org.pgpainless.key.OpenPgpFingerprint
class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) { class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) {
fun listDevices( fun listDevices(manager: YubiKitManager = YubiKitManager()): List<Yubikey> =
manager: YubiKitManager = YubiKitManager() manager
): List<Yubikey> = manager.listAllDevices() .listAllDevices()
.filter { it.key is CompositeDevice } .filter { it.key is CompositeDevice }
.map { Yubikey(it.value, it.key) } .map { Yubikey(it.value, it.key) }
fun factoryReset(yubikey: Yubikey) { fun factoryReset(yubikey: Yubikey) {
yubikey.device.openConnection(SmartCardConnection::class.java).use { yubikey.device.openConnection(SmartCardConnection::class.java).use {
@ -29,10 +33,11 @@ class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) {
} }
} }
fun moveToYubikey(componentKey: OpenPGPPrivateKey, fun moveToYubikey(
yubikey: Yubikey, componentKey: OpenPGPPrivateKey,
adminPin: CharArray, yubikey: Yubikey,
keyRef: KeyRef = keyRefForKey(componentKey.publicKey) adminPin: CharArray,
keyRef: KeyRef = keyRefForKey(componentKey.publicKey)
): OpenPGPKey { ): OpenPGPKey {
// Move private key to hardware token // Move private key to hardware token
yubikey.storeKeyInSlot(componentKey, keyRef, adminPin) yubikey.storeKeyInSlot(componentKey, keyRef, adminPin)
@ -54,8 +59,9 @@ class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) {
key.isSigningKey -> KeyRef.SIG key.isSigningKey -> KeyRef.SIG
key.isEncryptionKey -> KeyRef.DEC key.isEncryptionKey -> KeyRef.DEC
key.isCertificationKey -> KeyRef.ATT key.isCertificationKey -> KeyRef.ATT
else -> throw KeyException.GeneralKeyException( else ->
"Cannot determine usage for the key.", OpenPgpFingerprint.of(key)) throw KeyException.GeneralKeyException(
"Cannot determine usage for the key.", OpenPgpFingerprint.of(key))
} }
} }
} }

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey package org.pgpainless.yubikey
import com.yubico.yubikit.core.keys.PublicKeyValues import com.yubico.yubikit.core.keys.PublicKeyValues
@ -6,6 +10,7 @@ import com.yubico.yubikit.management.DeviceInfo
import com.yubico.yubikit.openpgp.KeyRef import com.yubico.yubikit.openpgp.KeyRef
import com.yubico.yubikit.openpgp.OpenPgpCurve import com.yubico.yubikit.openpgp.OpenPgpCurve
import com.yubico.yubikit.openpgp.OpenPgpSession import com.yubico.yubikit.openpgp.OpenPgpSession
import java.util.*
import openpgp.toSecondsPrecision import openpgp.toSecondsPrecision
import org.bouncycastle.bcpg.PublicSubkeyPacket import org.bouncycastle.bcpg.PublicSubkeyPacket
import org.bouncycastle.bcpg.S2K import org.bouncycastle.bcpg.S2K
@ -22,16 +27,17 @@ import org.gnupg.GnuPGDummyKeyUtil
import org.pgpainless.PGPainless import org.pgpainless.PGPainless
import org.pgpainless.algorithm.OpenPGPKeyVersion import org.pgpainless.algorithm.OpenPGPKeyVersion
import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm
import java.util.*
class YubikeyKeyGenerator(private val api: PGPainless) { class YubikeyKeyGenerator(private val api: PGPainless) {
private val converter = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()) private val converter = JcaPGPKeyConverter().setProvider(BouncyCastleProvider())
fun generateModernKey(yubikey: Yubikey, fun generateModernKey(
adminPin: CharArray, yubikey: Yubikey,
keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4, adminPin: CharArray,
creationTime: Date = Date()): OpenPGPKey { keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
creationTime: Date = Date()
): OpenPGPKey {
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)
@ -42,25 +48,28 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
val primarykey = toExternalSecretKey(pubKey, yubikey.info) val primarykey = toExternalSecretKey(pubKey, yubikey.info)
pkVal = session.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP521R1) pkVal = session.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP521R1)
pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime,PublicKeyAlgorithm.ECDSA) pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA)
val signingKey = toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info) val signingKey = toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info)
pkVal = session.generateEcKey(KeyRef.DEC, OpenPgpCurve.SECP521R1) pkVal = session.generateEcKey(KeyRef.DEC, OpenPgpCurve.SECP521R1)
pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDH) pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDH)
val encryptionKey = toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info) val encryptionKey =
toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info)
return OpenPGPKey(PGPSecretKeyRing(listOf(primarykey, signingKey, encryptionKey))) return OpenPGPKey(PGPSecretKeyRing(listOf(primarykey, signingKey, encryptionKey)))
} }
} }
private fun toPGPPublicKey(pkVal: PublicKeyValues, private fun toPGPPublicKey(
version: OpenPGPKeyVersion, pkVal: PublicKeyValues,
creationTime: Date, version: OpenPGPKeyVersion,
algorithm: PublicKeyAlgorithm creationTime: Date,
algorithm: PublicKeyAlgorithm
): PGPPublicKey { ): PGPPublicKey {
return converter.getPGPPublicKey(version.numeric, return converter.getPGPPublicKey(
version.numeric,
algorithm.algorithmId, algorithm.algorithmId,
null, null,
pkVal.toPublicKey(), pkVal.toPublicKey(),
@ -75,10 +84,8 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
0xfc, 0xfc,
null, null,
null, null,
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!) GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)),
), pubkey)
pubkey
)
} }
private fun toGnuStubbedSecretKey(pubKey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey { private fun toGnuStubbedSecretKey(pubKey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey {
@ -89,23 +96,24 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
SecretKeyPacket.USAGE_SHA1, SecretKeyPacket.USAGE_SHA1,
S2K.gnuDummyS2K(S2K.GNUDummyParams.divertToCard()), S2K.gnuDummyS2K(S2K.GNUDummyParams.divertToCard()),
null, null,
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!) GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)),
),
pubKey) pubKey)
} }
private fun toSecretSubKey( private fun toSecretSubKey(
key: PGPSecretKey, key: PGPSecretKey,
deviceInfo: DeviceInfo, deviceInfo: DeviceInfo,
fingerPrintCalculator: KeyFingerPrintCalculator = api.implementation.keyFingerPrintCalculator() fingerPrintCalculator: KeyFingerPrintCalculator =
api.implementation.keyFingerPrintCalculator()
): PGPSecretKey { ): PGPSecretKey {
val pubSubKey = PGPPublicKey( val pubSubKey =
PublicSubkeyPacket( PGPPublicKey(
key.publicKey.version, PublicSubkeyPacket(
key.publicKey.algorithm, key.publicKey.version,
key.publicKey.creationTime, key.publicKey.algorithm,
key.publicKey.publicKeyPacket.key), key.publicKey.creationTime,
fingerPrintCalculator) key.publicKey.publicKeyPacket.key),
fingerPrintCalculator)
return PGPSecretKey( return PGPSecretKey(
SecretSubkeyPacket( SecretSubkeyPacket(
pubSubKey.publicKeyPacket, pubSubKey.publicKeyPacket,
@ -114,7 +122,6 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
null, null,
null, null,
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)), GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)),
pubSubKey pubSubKey)
)
} }
} }

View file

@ -1,7 +1,12 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
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.OpenPgpSession import com.yubico.yubikit.openpgp.OpenPgpSession
import java.io.OutputStream
import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.PGPPrivateKey
import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.api.OpenPGPImplementation import org.bouncycastle.openpgp.api.OpenPGPImplementation
@ -10,7 +15,6 @@ import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider
import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.HashAlgorithm
import org.pgpainless.yubikey.YubikeyDataDecryptorFactory.Companion.USER_PIN import org.pgpainless.yubikey.YubikeyDataDecryptorFactory.Companion.USER_PIN
import java.io.OutputStream
class YubikeyPGPContentSignerBuilderProvider( class YubikeyPGPContentSignerBuilderProvider(
val hashAlgorithm: HashAlgorithm, val hashAlgorithm: HashAlgorithm,
@ -18,23 +22,23 @@ class YubikeyPGPContentSignerBuilderProvider(
private val implementation: OpenPGPImplementation = OpenPGPImplementation.getInstance() private val implementation: OpenPGPImplementation = OpenPGPImplementation.getInstance()
) : PGPContentSignerBuilderProvider(hashAlgorithm.algorithmId) { ) : PGPContentSignerBuilderProvider(hashAlgorithm.algorithmId) {
private val softwareSignerBuilderProvider = implementation.pgpContentSignerBuilderProvider(hashAlgorithm.algorithmId) private val softwareSignerBuilderProvider =
implementation.pgpContentSignerBuilderProvider(hashAlgorithm.algorithmId)
override fun get(publicSigningKey: PGPPublicKey): PGPContentSignerBuilder { override fun get(publicSigningKey: PGPPublicKey): PGPContentSignerBuilder {
return object : PGPContentSignerBuilder { return object : PGPContentSignerBuilder {
override fun build(signatureType: Int, override fun build(signatureType: Int, privateKey: PGPPrivateKey?): PGPContentSigner {
privateKey: PGPPrivateKey?
): PGPContentSigner {
// Delegate software-based signing keys to the implementations default // Delegate software-based signing keys to the implementations default
// content signer builder provider // content signer builder provider
return softwareSignerBuilderProvider.get(publicSigningKey) return softwareSignerBuilderProvider
.get(publicSigningKey)
.build(signatureType, privateKey) .build(signatureType, privateKey)
} }
override fun build(signatureType: Int override fun build(signatureType: Int): PGPContentSigner {
): PGPContentSigner { val digestCalculator =
val digestCalculator = implementation.pgpDigestCalculatorProvider().get(hashAlgorithmId) implementation.pgpDigestCalculatorProvider().get(hashAlgorithmId)
val openPgpSession = OpenPgpSession(smartcardConnection) val openPgpSession = OpenPgpSession(smartcardConnection)
// TODO: Move pin authorization somewhere else // TODO: Move pin authorization somewhere else

View file

@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.openpgp.KeyRef import com.yubico.yubikit.openpgp.KeyRef
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -11,95 +14,98 @@ import org.pgpainless.util.Passphrase
class YubikeyDecryptionTest : YubikeyTest() { class YubikeyDecryptionTest : YubikeyTest() {
// Complete software key // Complete software key
private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + private val KEY =
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: Alice <alice@pgpainless.org>\n" + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
"\n" + "Comment: Alice <alice@pgpainless.org>\n" +
"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\n" +
"1POPHzF3cMIReYhZfiJUEBV19suL\n" + "QewCB2Kcu9foL0O0t6WrXyXQwkimMzx5Kefyu4Vbsj0m8yV5aS4ebPEmxxtWaOu7\n" +
"=dA6G\n" + "1POPHzF3cMIReYhZfiJUEBV19suL\n" +
"-----END PGP PRIVATE KEY BLOCK-----" "=dA6G\n" +
"-----END PGP PRIVATE KEY BLOCK-----"
// Software certificate // Software certificate
private val CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + private val CERT =
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"Comment: Alice <alice@pgpainless.org>\n" + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
"\n" + "Comment: Alice <alice@pgpainless.org>\n" +
"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\n" +
"iw==\n" + "pjM8eSnn8ruFW7I9JvMleWkuHmzxJscbVmjru9Tzjx8xd3DCEXmIWX4iVBAVdfbL\n" +
"=Oq+Y\n" + "iw==\n" +
"-----END PGP PUBLIC KEY BLOCK-----" "=Oq+Y\n" +
"-----END PGP PUBLIC KEY BLOCK-----"
private val MSG = "-----BEGIN PGP MESSAGE-----\n" + private val MSG =
"Version: PGPainless\n" + "-----BEGIN PGP MESSAGE-----\n" +
"\n" + "Version: PGPainless\n" +
"wcAQBhUEAh6XCjDVDdDeIypK3dKuQkQmRdESBCMEAV9Lm/I5jEe9t8Mdd7Pmk7S0\n" + "\n" +
"3q308GnSq640CbhgORysK4+dnRYMzZFphil7dDsKWe2X7RMz7TDiPQhaoro6z0JP\n" + "wcAQBhUEAh6XCjDVDdDeIypK3dKuQkQmRdESBCMEAV9Lm/I5jEe9t8Mdd7Pmk7S0\n" +
"AZMx5eFiL0irdC9qV+0LvSnGJ8CyW3K15mKUomX82unAhquEhLtuPBufAN4bf2ia\n" + "3q308GnSq640CbhgORysK4+dnRYMzZFphil7dDsKWe2X7RMz7TDiPQhaoro6z0JP\n" +
"EiM85oz2U8CZ2Un48QLldDoHMKOdeAqX1xqFeBrD+ObgNsNfCLoYg4SM/EOUc06x\n" + "AZMx5eFiL0irdC9qV+0LvSnGJ8CyW3K15mKUomX82unAhquEhLtuPBufAN4bf2ia\n" +
"U78DC23EfOI428Nfvzq1GiqVhtLAYgIJAQMNZthd/Qa2vPy8EaMLXn/NV35v4PzO\n" + "EiM85oz2U8CZ2Un48QLldDoHMKOdeAqX1xqFeBrD+ObgNsNfCLoYg4SM/EOUc06x\n" +
"39OYkdHRTO6g6OTI4Qf6fpXWoC8GdHIMOHGPMh2hKCXIXPEV0bncfnrUIXk9+miX\n" + "U78DC23EfOI428Nfvzq1GiqVhtLAYgIJAQMNZthd/Qa2vPy8EaMLXn/NV35v4PzO\n" +
"7pFaM7kn/YGO48QUtY5ZxJdJAcjZA+vHBws8eDKC5Ajl5VYZrX187MQ+x/JID642\n" + "39OYkdHRTO6g6OTI4Qf6fpXWoC8GdHIMOHGPMh2hKCXIXPEV0bncfnrUIXk9+miX\n" +
"QNsxUocyYwvRZenRQCuUV0vee08iLia/olzVjYQvsPYg6F/wa0KZRat2WMi/ofy9\n" + "7pFaM7kn/YGO48QUtY5ZxJdJAcjZA+vHBws8eDKC5Ajl5VYZrX187MQ+x/JID642\n" +
"8C0tMUo31K4v2/z9T58DAR0P8AmLH/+196ijRbJ61U8HdiqYYPz7pevKdRB3N/0b\n" + "QNsxUocyYwvRZenRQCuUV0vee08iLia/olzVjYQvsPYg6F/wa0KZRat2WMi/ofy9\n" +
"dKkwF/chL+a/fSaxfAtJF2Zua4iW1OyrsbgIyXADUoS12K056A24yYE6dbMGVdhS\n" + "8C0tMUo31K4v2/z9T58DAR0P8AmLH/+196ijRbJ61U8HdiqYYPz7pevKdRB3N/0b\n" +
"+kQi5FkaOBCe1HVuETfNZ9XYV1312Dlj\n" + "dKkwF/chL+a/fSaxfAtJF2Zua4iW1OyrsbgIyXADUoS12K056A24yYE6dbMGVdhS\n" +
"=XVu4\n" + "+kQi5FkaOBCe1HVuETfNZ9XYV1312Dlj\n" +
"-----END PGP MESSAGE-----" "=XVu4\n" +
"-----END PGP MESSAGE-----"
@Test @Test
fun decryptMessageWithYubikey() { fun decryptMessageWithYubikey() {
@ -115,12 +121,16 @@ class YubikeyDecryptionTest : YubikeyTest() {
// TODO: Make hardware decryption transparent as shown below! // TODO: Make hardware decryption transparent as shown below!
val decIn = api.processMessage() val decIn =
.onInputStream(msgIn) api.processMessage()
.withOptions(ConsumerOptions.get(api) .onInputStream(msgIn)
.addHardwareTokenBackend(YubikeyHardwareTokenBackend()) .withOptions(
.addDecryptionKey(hardwareBasedKey, ConsumerOptions.get(api)
SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(String(userPin))))) .addHardwareTokenBackend(YubikeyHardwareTokenBackend())
.addDecryptionKey(
hardwareBasedKey,
SecretKeyRingProtector.unlockAnyKeyWith(
Passphrase.fromPassword(String(userPin)))))
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,11 +1,13 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey package org.pgpainless.yubikey
import org.gnupg.GnuPGDummyKeyUtil import org.gnupg.GnuPGDummyKeyUtil
import org.junit.jupiter.api.Assertions.assertNotNull
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.Assumptions.assumeTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.util.Arrays
class YubikeyHardwareTokenBackendTest : YubikeyTest() { class YubikeyHardwareTokenBackendTest : YubikeyTest() {
@ -14,13 +16,8 @@ class YubikeyHardwareTokenBackendTest : YubikeyTest() {
@Test @Test
fun testListDeviceSerials() { fun testListDeviceSerials() {
val serials = backend.listDeviceSerials() val serials = backend.listDeviceSerials()
assertTrue(serials.any { assertTrue(
it.contentEquals( serials.any { it.contentEquals(GnuPGDummyKeyUtil.serialToBytes(allowedSerialNumber)) })
GnuPGDummyKeyUtil.serialToBytes(
allowedSerialNumber
)
)
})
} }
@Test @Test

View file

@ -1,18 +1,21 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey package org.pgpainless.yubikey
import java.util.*
import org.junit.jupiter.api.Assertions.assertTrue 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.*
class YubikeyKeyGeneratorTest : YubikeyTest() { class YubikeyKeyGeneratorTest : YubikeyTest() {
@Test @Test
fun generateKey() { fun generateKey() {
val keyGen = YubikeyKeyGenerator(PGPainless.getInstance()) val keyGen = YubikeyKeyGenerator(PGPainless.getInstance())
val key = keyGen.generateModernKey( val key = keyGen.generateModernKey(yubikey, adminPin, OpenPGPKeyVersion.v4, Date())
yubikey, adminPin, OpenPGPKeyVersion.v4, Date())
println(key.toAsciiArmoredString()) println(key.toAsciiArmoredString())
for (subkey in key.secretKeys) { for (subkey in key.secretKeys) {

View file

@ -1,6 +1,12 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.smartcard.SmartCardConnection
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
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.Test import org.junit.jupiter.api.Test
@ -9,48 +15,47 @@ import org.pgpainless.decryption_verification.ConsumerOptions
import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.ProducerOptions
import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.encryption_signing.SigningOptions
import org.pgpainless.signature.PGPContentSignerBuilderProviderFactory import org.pgpainless.signature.PGPContentSignerBuilderProviderFactory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
class YubikeySigningTest : YubikeyTest() { class YubikeySigningTest : YubikeyTest() {
private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + private val KEY =
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: Alice <alice@pgpainless.org>\n" + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
"\n" + "Comment: Alice <alice@pgpainless.org>\n" +
"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\n" +
"1POPHzF3cMIReYhZfiJUEBV19suL\n" + "QewCB2Kcu9foL0O0t6WrXyXQwkimMzx5Kefyu4Vbsj0m8yV5aS4ebPEmxxtWaOu7\n" +
"=dA6G\n" + "1POPHzF3cMIReYhZfiJUEBV19suL\n" +
"-----END PGP PRIVATE KEY BLOCK-----" "=dA6G\n" +
"-----END PGP PRIVATE KEY BLOCK-----"
@Test @Test
fun signMessageWithYubikey() { fun signMessageWithYubikey() {
@ -65,17 +70,25 @@ class YubikeySigningTest : YubikeyTest() {
val msgOut = ByteArrayOutputStream() val msgOut = ByteArrayOutputStream()
device.openConnection(SmartCardConnection::class.java).use { device.openConnection(SmartCardConnection::class.java).use {
val connection = it val connection = it
val factory = object : PGPContentSignerBuilderProviderFactory { val factory =
override fun create(hashAlgorithm: HashAlgorithm): PGPContentSignerBuilderProvider { object : PGPContentSignerBuilderProviderFactory {
return YubikeyPGPContentSignerBuilderProvider(hashAlgorithm, connection) override fun create(
hashAlgorithm: HashAlgorithm
): PGPContentSignerBuilderProvider {
return YubikeyPGPContentSignerBuilderProvider(hashAlgorithm, connection)
}
} }
}
val sigOut = api val sigOut =
.generateMessage() api.generateMessage()
.onOutputStream(msgOut) .onOutputStream(msgOut)
.withOptions(ProducerOptions.sign(SigningOptions.get() .withOptions(
.addInlineSignature(hardwareBasedSigningKey.signingKeys[0], factory, HashAlgorithm.SHA512))) ProducerOptions.sign(
SigningOptions.get()
.addInlineSignature(
hardwareBasedSigningKey.signingKeys[0],
factory,
HashAlgorithm.SHA512)))
sigOut.write("Hello, World!".toByteArray()) sigOut.write("Hello, World!".toByteArray())
sigOut.close() sigOut.close()
@ -84,9 +97,9 @@ class YubikeySigningTest : YubikeyTest() {
api.processMessage() api.processMessage()
.onInputStream(ByteArrayInputStream(msgOut.toByteArray())) .onInputStream(ByteArrayInputStream(msgOut.toByteArray()))
.withOptions(ConsumerOptions.get() .withOptions(
.addVerificationCert(hardwareBasedSigningKey.toCertificate()) ConsumerOptions.get().addVerificationCert(hardwareBasedSigningKey.toCertificate()))
).use { .use {
it.readAllBytes() it.readAllBytes()
it.close() it.close()
assertTrue(it.metadata.isVerifiedSigned()) assertTrue(it.metadata.isVerifiedSigned())

View file

@ -1,9 +1,13 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey package org.pgpainless.yubikey
import java.util.Properties
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
import org.opentest4j.TestAbortedException import org.opentest4j.TestAbortedException
import org.pgpainless.PGPainless import org.pgpainless.PGPainless
import java.util.Properties
abstract class YubikeyTest() { abstract class YubikeyTest() {
@ -21,17 +25,20 @@ abstract class YubikeyTest() {
} }
} }
open val api: PGPainless = PGPainless(BcOpenPGPImplementation()).apply { open val api: PGPainless =
hardwareTokenBackends.add(YubikeyHardwareTokenBackend()) PGPainless(BcOpenPGPImplementation()).apply {
} hardwareTokenBackends.add(YubikeyHardwareTokenBackend())
}
open val helper: YubikeyHelper = YubikeyHelper(api) open val helper: YubikeyHelper = YubikeyHelper(api)
val yubikey: Yubikey = YubikeyHelper().listDevices().find { it.serialNumber == allowedSerialNumber } val yubikey: Yubikey =
?: throw TestAbortedException("No allowed device found.") YubikeyHelper().listDevices().find { it.serialNumber == allowedSerialNumber }
?: throw TestAbortedException("No allowed device found.")
private fun getProperty(properties: Properties, key: String): String { private fun getProperty(properties: Properties, key: String): String {
return properties.getProperty(key) return properties.getProperty(key)
?: throw TestAbortedException("Could not find property $key in pgpainless-yubikey/src/test/resources/yubikey.properties") ?: throw TestAbortedException(
"Could not find property $key in pgpainless-yubikey/src/test/resources/yubikey.properties")
} }
} }