1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-12-09 22:01:10 +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-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]]
path = ".github/ISSUE_TEMPLATE/**"
precedence = "aggregate"

View file

@ -57,11 +57,10 @@ class GnuPGDummyKeyUtil private constructor() {
*/
@JvmStatic fun modify(secretKeys: PGPSecretKeyRing) = Builder(secretKeys)
@JvmStatic fun serialToBytes(sn: Int) = byteArrayOf(
(sn shr 24).toByte(),
(sn shr(16)).toByte(),
(sn shr(8)).toByte(),
sn.toByte())
@JvmStatic
fun serialToBytes(sn: Int) =
byteArrayOf(
(sn shr 24).toByte(), (sn shr (16)).toByte(), (sn shr (8)).toByte(), sn.toByte())
}
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.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
@ -448,15 +447,18 @@ class OpenPgpMessageInputStream(
}
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) {
LOGGER.debug("Attempt decryption with ${hardwareTokenBackend.getBackendName()} backend.")
LOGGER.debug(
"Attempt decryption with ${hardwareTokenBackend.getBackendName()} backend.")
if (decryptWithHardwareKey(
hardwareTokenBackend,
esks,
secretKey,
protector,
SubkeyIdentifier(secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier),
SubkeyIdentifier(
secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier),
pkesk)) {
return true
}
@ -624,16 +626,22 @@ class OpenPgpMessageInputStream(
pkesk: PGPPublicKeyEncryptedData
): Boolean {
try {
val decrypted = pkesk.getDataStream(decryptorFactory)
val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory))
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)
encryptedData.decryptionKey = decryptionKeyId
encryptedData.sessionKey = sessionKey
encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyIdentifier })
LOGGER.debug("Successfully decrypted data with key $decryptionKeyId")
val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options)
val integrityProtected =
IntegrityProtectedInputStream(decrypted, sessionKeyEncData, options)
nestedInputStream =
OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api)
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 {
return when (EncryptedDataPacketType.of(esks)!!) {
EncryptedDataPacketType.SED ->

View file

@ -313,12 +313,14 @@ class SigningOptions(private val api: PGPainless) {
subpacketsCallback)
}
fun addInlineSignature(hardwareBackedKey: OpenPGPComponentKey,
fun addInlineSignature(
hardwareBackedKey: OpenPGPComponentKey,
hardwareContentSignerBuilderProviderFactory: PGPContentSignerBuilderProviderFactory,
hashAlgorithm: HashAlgorithm,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
subpacketsCallback: Callback? = null
) = addHardwareSigningMethod(
) =
addHardwareSigningMethod(
hardwareBackedKey,
hardwareContentSignerBuilderProviderFactory,
hashAlgorithm,
@ -532,7 +534,14 @@ class SigningOptions(private val api: PGPainless) {
hashAlgorithm: HashAlgorithm,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
subpacketsCallback: Callback? = null
) = addHardwareSigningMethod(hardwareBackedKey, hardwareContentSignerBuilderProviderFactory, hashAlgorithm, signatureType, true, subpacketsCallback)
) =
addHardwareSigningMethod(
hardwareBackedKey,
hardwareContentSignerBuilderProviderFactory,
hashAlgorithm,
signatureType,
true,
subpacketsCallback)
private fun addHardwareSigningMethod(
hardwareBackedKey: OpenPGPComponentKey,
@ -540,15 +549,15 @@ class SigningOptions(private val api: PGPainless) {
hashAlgorithm: HashAlgorithm,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
detached: Boolean,
subpacketsCallback: Callback? = null) = apply {
subpacketsCallback: Callback? = null
) = apply {
rejectWeakKeys(hardwareBackedKey)
val pubkey = hardwareBackedKey.pgpPublicKey
val pgpContentSignerBuilder = hardwareContentSignerBuilderProviderFactory.create(hashAlgorithm)
.get(pubkey)
val pgpContentSignerBuilder =
hardwareContentSignerBuilderProviderFactory.create(hashAlgorithm).get(pubkey)
val generator = PGPSignatureGenerator(
pgpContentSignerBuilder, pubkey)
.apply {
val generator =
PGPSignatureGenerator(pgpContentSignerBuilder, pubkey).apply {
init(signatureType.signatureType.code, pubkey)
}
@ -584,15 +593,17 @@ class SigningOptions(private val api: PGPainless) {
if (!api.algorithmPolicy.publicKeyAlgorithmPolicy.isAcceptable(
publicKeyAlgorithm, bitStrength)) {
throw UnacceptableSigningKeyException(
PublicKeyAlgorithmPolicyException(
signingKey, publicKeyAlgorithm, bitStrength))
PublicKeyAlgorithmPolicyException(signingKey, publicKeyAlgorithm, bitStrength))
}
}
private fun prepareSignatureGenerator(generator: PGPSignatureGenerator, signingKey: PGPPublicKey, subpacketCallback: Callback?) {
private fun prepareSignatureGenerator(
generator: PGPSignatureGenerator,
signingKey: PGPPublicKey,
subpacketCallback: Callback?
) {
// Subpackets
val hashedSubpackets =
SignatureSubpackets.createHashedSubpackets(signingKey)
val hashedSubpackets = SignatureSubpackets.createHashedSubpackets(signingKey)
val unhashedSubpackets = SignatureSubpackets.createEmptySubpackets()
if (subpacketCallback != null) {
subpacketCallback.modifyHashedSubpackets(hashedSubpackets)

View file

@ -210,7 +210,6 @@ $algorithm of size $bitSize is not acceptable.""",
}
}
class GeneralKeyException(message: String,
fingerprint: OpenPgpFingerprint
) : KeyException(message, fingerprint)
class GeneralKeyException(message: String, fingerprint: OpenPgpFingerprint) :
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
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
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
import com.yubico.yubikit.core.YubiKeyDevice
@ -16,7 +20,9 @@ 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())
val privateKey =
JcaPGPKeyConverter()
.setProvider(BouncyCastleProvider())
.getPrivateKey(key.keyPair.privateKey)
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.smartcard.SmartCardConnection
import com.yubico.yubikit.openpgp.OpenPgpSession
import java.util.*
import org.bouncycastle.bcpg.ECDHPublicBCPGKey
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags
@ -27,7 +28,6 @@ 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(
callback: HardwareSecurity.DecryptionCallback,
@ -36,13 +36,11 @@ class YubikeyDataDecryptorFactory(
companion object {
@JvmStatic
val LOGGER = LoggerFactory.getLogger(YubikeyDataDecryptorFactory::class.java)
@JvmStatic val LOGGER = LoggerFactory.getLogger(YubikeyDataDecryptorFactory::class.java)
val ADMIN_PIN: CharArray = "12345678".toCharArray()
val USER_PIN: CharArray = "123456".toCharArray()
@JvmStatic
fun createDecryptorFromConnection(
smartCardConnection: SmartCardConnection,
@ -51,11 +49,13 @@ class YubikeyDataDecryptorFactory(
val openpgpSession = OpenPgpSession(smartCardConnection)
val decKeyIdentifier = SubkeyIdentifier(OpenPgpV4Fingerprint(pubkey))
val isRSAKey = pubkey.algorithm == PublicKeyAlgorithmTags.RSA_GENERAL
|| pubkey.algorithm == PublicKeyAlgorithmTags.RSA_SIGN
|| pubkey.algorithm == PublicKeyAlgorithmTags.RSA_ENCRYPT
val isRSAKey =
pubkey.algorithm == PublicKeyAlgorithmTags.RSA_GENERAL ||
pubkey.algorithm == PublicKeyAlgorithmTags.RSA_SIGN ||
pubkey.algorithm == PublicKeyAlgorithmTags.RSA_ENCRYPT
val callback = object : HardwareSecurity.DecryptionCallback {
val callback =
object : HardwareSecurity.DecryptionCallback {
override fun decryptSessionKey(
keyIdentifier: KeyIdentifier,
keyAlgorithm: Int,
@ -63,26 +63,28 @@ class YubikeyDataDecryptorFactory(
pkeskVersion: Int
): ByteArray {
// TODO: Move user pin verification somewhere else
openpgpSession.verifyAdminPin(ADMIN_PIN)
openpgpSession.verifyUserPin(USER_PIN, true)
LOGGER.debug("Attempt decryption with key {}", keyIdentifier)
if(isRSAKey) {
if (isRSAKey) {
// easy
LOGGER.debug("Key is RSA key of length {}", pubkey.bitStrength)
val decryptedSessionKey = openpgpSession.decrypt(sessionKeyData)
smartCardConnection.close()
return decryptedSessionKey
} else {
// meh...
val curveName = pubkey.getCurveName()
val ecPubKey: ECDHPublicBCPGKey = pubkey.publicKeyPacket.key as ECDHPublicBCPGKey
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
val pLen =
((((sessionKeyData[0].toInt() and 0xff) shl 8) + (sessionKeyData[1].toInt() and 0xff)) + 7) / 8
((((sessionKeyData[0].toInt() and 0xff) shl 8) +
(sessionKeyData[1].toInt() and 0xff)) + 7) / 8
checkRange(2 + pLen + 1, sessionKeyData)
val pEnc = ByteArray(pLen)
System.arraycopy(sessionKeyData, 2, pEnc, 0, pLen)
@ -96,11 +98,15 @@ class YubikeyDataDecryptorFactory(
// perform ECDH key agreement via the Yubikey
val params = ECNamedCurveTable.getParameterSpec(curveName)
val publicPoint = params.curve.decodePoint(pEnc)
val peerKey = JcaPGPKeyConverter().setProvider(BouncyCastleProvider())
val peerKey =
JcaPGPKeyConverter()
.setProvider(BouncyCastleProvider())
.getPublicKey(
PGPPublicKey(
PublicKeyPacket(
pubkey.version, PublicKeyAlgorithmTags.ECDH, Date(),
pubkey.version,
PublicKeyAlgorithmTags.ECDH,
Date(),
ECDHPublicBCPGKey(
ecPubKey.curveOID,
publicPoint,
@ -112,12 +118,15 @@ class YubikeyDataDecryptorFactory(
),
)
val secret = openpgpSession.decrypt(PublicKeyValues.fromPublicKey(peerKey))
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(
val userKeyingMaterial =
RFC6637Utils.createUserKeyingMaterial(
pubkey.publicKeyPacket,
BcKeyFingerprintCalculator(),
)
@ -127,7 +136,8 @@ class YubikeyDataDecryptorFactory(
symmetricKeyAlgorithm,
)
val key =
KeyParameter(rfc6637KDFCalculator.createKey(secret, userKeyingMaterial))
KeyParameter(
rfc6637KDFCalculator.createKey(secret, userKeyingMaterial))
return PGPPad.unpadSessionData(
BcPublicKeyDataDecryptorFactory.unwrapSessionData(

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.yubikey
import com.yubico.yubikit.core.smartcard.SmartCardConnection
@ -21,38 +25,43 @@ class YubikeyHardwareTokenBackend : HardwareTokenBackend {
protector: SecretKeyRingProtector,
pkesk: PGPPublicKeyEncryptedData
): Iterator<PublicKeyDataDecryptorFactory> {
val devices = YubikeyHelper().listDevices()
return devices.map { yubikey ->
yubikey.device.openConnection(SmartCardConnection::class.java).use {
val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection(
it,
secKey.pgpPublicKey
)
return object : Iterator<PublicKeyDataDecryptorFactory> {
val devices = YubikeyHelper().listDevices().iterator()
override fun hasNext(): Boolean {
return devices.hasNext()
}
override fun next(): PublicKeyDataDecryptorFactory {
return devices.next().device.openConnection(SmartCardConnection::class.java).let {
val decFac =
YubikeyDataDecryptorFactory.createDecryptorFromConnection(
it, secKey.pgpPublicKey)
decFac as PublicKeyDataDecryptorFactory
}
}.iterator()
}
}
}
override fun listDeviceSerials(): List<ByteArray> {
return YubikeyHelper().listDevices()
.mapNotNull { yk -> yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } }
return YubikeyHelper().listDevices().mapNotNull { yk ->
yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) }
}
}
override fun listKeyFingerprints(): Map<ByteArray, List<ByteArray>> {
return YubikeyHelper().listDevices()
.associate { yk ->
yk.encodedSerialNumber to yk.device.openConnection(SmartCardConnection::class.java).use {
return YubikeyHelper().listDevices().associate { yk ->
yk.encodedSerialNumber to
yk.device.openConnection(SmartCardConnection::class.java).use {
val session = OpenPgpSession(it)
//session.getData(KeyRef.DEC.fingerprint)
// session.getData(KeyRef.DEC.fingerprint)
session.getData(KeyRef.SIG.fingerprint)
listOfNotNull(
session.getData(KeyRef.ATT.fingerprint),
session.getData(KeyRef.SIG.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
import com.yubico.yubikit.core.smartcard.SmartCardConnection
@ -17,9 +21,9 @@ import org.pgpainless.key.OpenPgpFingerprint
class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) {
fun listDevices(
manager: YubiKitManager = YubiKitManager()
): List<Yubikey> = manager.listAllDevices()
fun listDevices(manager: YubiKitManager = YubiKitManager()): List<Yubikey> =
manager
.listAllDevices()
.filter { it.key is CompositeDevice }
.map { Yubikey(it.value, it.key) }
@ -29,7 +33,8 @@ class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) {
}
}
fun moveToYubikey(componentKey: OpenPGPPrivateKey,
fun moveToYubikey(
componentKey: OpenPGPPrivateKey,
yubikey: Yubikey,
adminPin: CharArray,
keyRef: KeyRef = keyRefForKey(componentKey.publicKey)
@ -54,7 +59,8 @@ class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) {
key.isSigningKey -> KeyRef.SIG
key.isEncryptionKey -> KeyRef.DEC
key.isCertificationKey -> KeyRef.ATT
else -> throw KeyException.GeneralKeyException(
else ->
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
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.OpenPgpCurve
import com.yubico.yubikit.openpgp.OpenPgpSession
import java.util.*
import openpgp.toSecondsPrecision
import org.bouncycastle.bcpg.PublicSubkeyPacket
import org.bouncycastle.bcpg.S2K
@ -22,16 +27,17 @@ import org.gnupg.GnuPGDummyKeyUtil
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.OpenPGPKeyVersion
import org.pgpainless.algorithm.PublicKeyAlgorithm
import java.util.*
class YubikeyKeyGenerator(private val api: PGPainless) {
private val converter = JcaPGPKeyConverter().setProvider(BouncyCastleProvider())
fun generateModernKey(yubikey: Yubikey,
fun generateModernKey(
yubikey: Yubikey,
adminPin: CharArray,
keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
creationTime: Date = Date()): OpenPGPKey {
creationTime: Date = Date()
): OpenPGPKey {
yubikey.device.openConnection(SmartCardConnection::class.java).use {
val session = OpenPgpSession(it)
session.verifyAdminPin(adminPin)
@ -42,25 +48,28 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
val primarykey = toExternalSecretKey(pubKey, yubikey.info)
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)
pkVal = session.generateEcKey(KeyRef.DEC, OpenPgpCurve.SECP521R1)
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)))
}
}
private fun toPGPPublicKey(pkVal: PublicKeyValues,
private fun toPGPPublicKey(
pkVal: PublicKeyValues,
version: OpenPGPKeyVersion,
creationTime: Date,
algorithm: PublicKeyAlgorithm
): PGPPublicKey {
return converter.getPGPPublicKey(version.numeric,
return converter.getPGPPublicKey(
version.numeric,
algorithm.algorithmId,
null,
pkVal.toPublicKey(),
@ -75,10 +84,8 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
0xfc,
null,
null,
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)
),
pubkey
)
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)),
pubkey)
}
private fun toGnuStubbedSecretKey(pubKey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey {
@ -89,17 +96,18 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
SecretKeyPacket.USAGE_SHA1,
S2K.gnuDummyS2K(S2K.GNUDummyParams.divertToCard()),
null,
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)
),
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)),
pubKey)
}
private fun toSecretSubKey(
key: PGPSecretKey,
deviceInfo: DeviceInfo,
fingerPrintCalculator: KeyFingerPrintCalculator = api.implementation.keyFingerPrintCalculator()
fingerPrintCalculator: KeyFingerPrintCalculator =
api.implementation.keyFingerPrintCalculator()
): PGPSecretKey {
val pubSubKey = PGPPublicKey(
val pubSubKey =
PGPPublicKey(
PublicSubkeyPacket(
key.publicKey.version,
key.publicKey.algorithm,
@ -114,7 +122,6 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
null,
null,
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
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.openpgp.OpenPgpSession
import java.io.OutputStream
import org.bouncycastle.openpgp.PGPPrivateKey
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.api.OpenPGPImplementation
@ -10,7 +15,6 @@ import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider
import org.pgpainless.algorithm.HashAlgorithm
import org.pgpainless.yubikey.YubikeyDataDecryptorFactory.Companion.USER_PIN
import java.io.OutputStream
class YubikeyPGPContentSignerBuilderProvider(
val hashAlgorithm: HashAlgorithm,
@ -18,23 +22,23 @@ class YubikeyPGPContentSignerBuilderProvider(
private val implementation: OpenPGPImplementation = OpenPGPImplementation.getInstance()
) : PGPContentSignerBuilderProvider(hashAlgorithm.algorithmId) {
private val softwareSignerBuilderProvider = implementation.pgpContentSignerBuilderProvider(hashAlgorithm.algorithmId)
private val softwareSignerBuilderProvider =
implementation.pgpContentSignerBuilderProvider(hashAlgorithm.algorithmId)
override fun get(publicSigningKey: PGPPublicKey): PGPContentSignerBuilder {
return object : PGPContentSignerBuilder {
override fun build(signatureType: Int,
privateKey: PGPPrivateKey?
): PGPContentSigner {
override fun build(signatureType: Int, privateKey: PGPPrivateKey?): PGPContentSigner {
// Delegate software-based signing keys to the implementations default
// content signer builder provider
return softwareSignerBuilderProvider.get(publicSigningKey)
return softwareSignerBuilderProvider
.get(publicSigningKey)
.build(signatureType, privateKey)
}
override fun build(signatureType: Int
): PGPContentSigner {
val digestCalculator = implementation.pgpDigestCalculatorProvider().get(hashAlgorithmId)
override fun build(signatureType: Int): PGPContentSigner {
val digestCalculator =
implementation.pgpDigestCalculatorProvider().get(hashAlgorithmId)
val openPgpSession = OpenPgpSession(smartcardConnection)
// 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
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.openpgp.KeyRef
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@ -11,7 +14,8 @@ import org.pgpainless.util.Passphrase
class YubikeyDecryptionTest : YubikeyTest() {
// Complete software key
private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
private val KEY =
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
"Comment: Alice <alice@pgpainless.org>\n" +
"\n" +
@ -50,7 +54,8 @@ class YubikeyDecryptionTest : YubikeyTest() {
"-----END PGP PRIVATE KEY BLOCK-----"
// Software certificate
private val CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
private val CERT =
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
"Comment: Alice <alice@pgpainless.org>\n" +
"\n" +
@ -84,7 +89,8 @@ class YubikeyDecryptionTest : YubikeyTest() {
"=Oq+Y\n" +
"-----END PGP PUBLIC KEY BLOCK-----"
private val MSG = "-----BEGIN PGP MESSAGE-----\n" +
private val MSG =
"-----BEGIN PGP MESSAGE-----\n" +
"Version: PGPainless\n" +
"\n" +
"wcAQBhUEAh6XCjDVDdDeIypK3dKuQkQmRdESBCMEAV9Lm/I5jEe9t8Mdd7Pmk7S0\n" +
@ -115,12 +121,16 @@ class YubikeyDecryptionTest : YubikeyTest() {
// TODO: Make hardware decryption transparent as shown below!
val decIn = api.processMessage()
val decIn =
api.processMessage()
.onInputStream(msgIn)
.withOptions(ConsumerOptions.get(api)
.withOptions(
ConsumerOptions.get(api)
.addHardwareTokenBackend(YubikeyHardwareTokenBackend())
.addDecryptionKey(hardwareBasedKey,
SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(String(userPin)))))
.addDecryptionKey(
hardwareBasedKey,
SecretKeyRingProtector.unlockAnyKeyWith(
Passphrase.fromPassword(String(userPin)))))
val msg = decIn.readAllBytes()
decIn.close()
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
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 : YubikeyTest() {
@ -14,13 +16,8 @@ class YubikeyHardwareTokenBackendTest : YubikeyTest() {
@Test
fun testListDeviceSerials() {
val serials = backend.listDeviceSerials()
assertTrue(serials.any {
it.contentEquals(
GnuPGDummyKeyUtil.serialToBytes(
allowedSerialNumber
)
)
})
assertTrue(
serials.any { it.contentEquals(GnuPGDummyKeyUtil.serialToBytes(allowedSerialNumber)) })
}
@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
import java.util.*
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 : YubikeyTest() {
@Test
fun generateKey() {
val keyGen = YubikeyKeyGenerator(PGPainless.getInstance())
val key = keyGen.generateModernKey(
yubikey, adminPin, OpenPGPKeyVersion.v4, Date())
val key = keyGen.generateModernKey(yubikey, adminPin, OpenPGPKeyVersion.v4, Date())
println(key.toAsciiArmoredString())
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
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@ -9,12 +15,11 @@ import org.pgpainless.decryption_verification.ConsumerOptions
import org.pgpainless.encryption_signing.ProducerOptions
import org.pgpainless.encryption_signing.SigningOptions
import org.pgpainless.signature.PGPContentSignerBuilderProviderFactory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
class YubikeySigningTest : YubikeyTest() {
private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
private val KEY =
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
"Comment: Alice <alice@pgpainless.org>\n" +
"\n" +
@ -65,17 +70,25 @@ class YubikeySigningTest : YubikeyTest() {
val msgOut = ByteArrayOutputStream()
device.openConnection(SmartCardConnection::class.java).use {
val connection = it
val factory = object : PGPContentSignerBuilderProviderFactory {
override fun create(hashAlgorithm: HashAlgorithm): PGPContentSignerBuilderProvider {
val factory =
object : PGPContentSignerBuilderProviderFactory {
override fun create(
hashAlgorithm: HashAlgorithm
): PGPContentSignerBuilderProvider {
return YubikeyPGPContentSignerBuilderProvider(hashAlgorithm, connection)
}
}
val sigOut = api
.generateMessage()
val sigOut =
api.generateMessage()
.onOutputStream(msgOut)
.withOptions(ProducerOptions.sign(SigningOptions.get()
.addInlineSignature(hardwareBasedSigningKey.signingKeys[0], factory, HashAlgorithm.SHA512)))
.withOptions(
ProducerOptions.sign(
SigningOptions.get()
.addInlineSignature(
hardwareBasedSigningKey.signingKeys[0],
factory,
HashAlgorithm.SHA512)))
sigOut.write("Hello, World!".toByteArray())
sigOut.close()
@ -84,9 +97,9 @@ class YubikeySigningTest : YubikeyTest() {
api.processMessage()
.onInputStream(ByteArrayInputStream(msgOut.toByteArray()))
.withOptions(ConsumerOptions.get()
.addVerificationCert(hardwareBasedSigningKey.toCertificate())
).use {
.withOptions(
ConsumerOptions.get().addVerificationCert(hardwareBasedSigningKey.toCertificate()))
.use {
it.readAllBytes()
it.close()
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
import java.util.Properties
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
import org.opentest4j.TestAbortedException
import org.pgpainless.PGPainless
import java.util.Properties
abstract class YubikeyTest() {
@ -21,17 +25,20 @@ abstract class YubikeyTest() {
}
}
open val api: PGPainless = PGPainless(BcOpenPGPImplementation()).apply {
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 }
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")
?: throw TestAbortedException(
"Could not find property $key in pgpainless-yubikey/src/test/resources/yubikey.properties")
}
}