mirror of
https://github.com/pgpainless/pgpainless.git
synced 2025-12-10 06:11:08 +01:00
Transparent decryption
This commit is contained in:
parent
de47a683d9
commit
a1af39a4f7
18 changed files with 461 additions and 357 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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" +
|
||||||
"lNoEaNQmhRMFK4EEACMEIwQBgF429XlvPyJdpfXDxVjVEOJc04wcpfkIoX1CzIjm\n" +
|
"\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" +
|
||||||
"mJMEaNQmhRMFK4EEACMEIwQBgF429XlvPyJdpfXDxVjVEOJc04wcpfkIoX1CzIjm\n" +
|
"\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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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" +
|
||||||
"lNoEaNQmhRMFK4EEACMEIwQBgF429XlvPyJdpfXDxVjVEOJc04wcpfkIoX1CzIjm\n" +
|
"\n" +
|
||||||
"daRyv+mz2jfFZlQsCkhw2GsrPRJuKz++1JkspKU+4Vot9dIBD94Y+MoZUgHM4m0t\n" +
|
"lNoEaNQmhRMFK4EEACMEIwQBgF429XlvPyJdpfXDxVjVEOJc04wcpfkIoX1CzIjm\n" +
|
||||||
"ItqAdaRcxZWXDpSB0eZH3/lC+VkMUjiqK1Po4qOdZttgpLz+uHcox3gxanjyndAQ\n" +
|
"daRyv+mz2jfFZlQsCkhw2GsrPRJuKz++1JkspKU+4Vot9dIBD94Y+MoZUgHM4m0t\n" +
|
||||||
"gVQf36sAAgkB/ZgECBHkzUUXyxLBEv9FO4lK02Fo9b2yk4Gu3O7iG84KYEuBWelT\n" +
|
"ItqAdaRcxZWXDpSB0eZH3/lC+VkMUjiqK1Po4qOdZttgpLz+uHcox3gxanjyndAQ\n" +
|
||||||
"+1VXcmExh1pLvHvZ6nKO4fuyAf9yEB6vh8Ah5LQcQWxpY2UgPGFsaWNlQHBncGFp\n" +
|
"gVQf36sAAgkB/ZgECBHkzUUXyxLBEv9FO4lK02Fo9b2yk4Gu3O7iG84KYEuBWelT\n" +
|
||||||
"bmxlc3Mub3JnPsLAIQQTEwoAUQWCaNQmhQkQ6y5XDZ7iKJEWoQS7KsPh5ZXNBc+l\n" +
|
"+1VXcmExh1pLvHvZ6nKO4fuyAf9yEB6vh8Ah5LQcQWxpY2UgPGFsaWNlQHBncGFp\n" +
|
||||||
"z+brLlcNnuIokQKbAQUVCgkICwUWAgMBAAQLCQgHCScJAQkCCQMIAQKeCQWJCWYB\n" +
|
"bmxlc3Mub3JnPsLAIQQTEwoAUQWCaNQmhQkQ6y5XDZ7iKJEWoQS7KsPh5ZXNBc+l\n" +
|
||||||
"gAKZAQAASOMCBA5+qfHTcNXgxQYbK10bTaTkpvJ4du4CijIByfwsi1toCXDMyf0+\n" +
|
"z+brLlcNnuIokQKbAQUVCgkICwUWAgMBAAQLCQgHCScJAQkCCQMIAQKeCQWJCWYB\n" +
|
||||||
"7a7AsUR6qLTKF8XZAgvCeHhA8eSELpTCfmdvAgQPwjX7eVtWJ+almb7XHDJTwmV0\n" +
|
"gAKZAQAASOMCBA5+qfHTcNXgxQYbK10bTaTkpvJ4du4CijIByfwsi1toCXDMyf0+\n" +
|
||||||
"Ye8YN3SeQn7BmwHbauvx1Mg6CO7ZnZpQ44FGVoKdF8/BiOUxpppyf5PZFkFCEpze\n" +
|
"7a7AsUR6qLTKF8XZAgvCeHhA8eSELpTCfmdvAgQPwjX7eVtWJ+almb7XHDJTwmV0\n" +
|
||||||
"BGjUJoUSBSuBBAAjBCMEAT8qQFBB+PTh/OTQtZOWttt2H3lkrhLJMuhdVjyW57JE\n" +
|
"Ye8YN3SeQn7BmwHbauvx1Mg6CO7ZnZpQ44FGVoKdF8/BiOUxpppyf5PZFkFCEpze\n" +
|
||||||
"+VO7f3248FlTFUGQk1pK2+/5ODMRdc7Vwdc5xwQj1vTgAKlRZtOrUCs/XrZXs5S5\n" +
|
"BGjUJoUSBSuBBAAjBCMEAT8qQFBB+PTh/OTQtZOWttt2H3lkrhLJMuhdVjyW57JE\n" +
|
||||||
"IYgCjEzDcH+MxaU2A/L+S2+/VOJ2PrpDdAq3HoiKvfjQBa4yzOKwz/2wlFrOwnFU\n" +
|
"+VO7f3248FlTFUGQk1pK2+/5ODMRdc7Vwdc5xwQj1vTgAKlRZtOrUCs/XrZXs5S5\n" +
|
||||||
"Vki+AwEKCQACCQH2m8vDn53COUmwjoaCMKMP5xZcR2dRhqCpK3oQtg+kkQ+wOzJV\n" +
|
"IYgCjEzDcH+MxaU2A/L+S2+/VOJ2PrpDdAq3HoiKvfjQBa4yzOKwz/2wlFrOwnFU\n" +
|
||||||
"ygcT8Dg7Yl0Z7zLMhRnHOcwTZDFQk52GUQNbfhx7wroEGBMKACoFgmjUJoUJEN3S\n" +
|
"Vki+AwEKCQACCQH2m8vDn53COUmwjoaCMKMP5xZcR2dRhqCpK3oQtg+kkQ+wOzJV\n" +
|
||||||
"rkJEJkXRFqEEAh6XCjDVDdDeIypK3dKuQkQmRdECmwwAAGD7AgjhSiFMCMzq3B4L\n" +
|
"ygcT8Dg7Yl0Z7zLMhRnHOcwTZDFQk52GUQNbfhx7wroEGBMKACoFgmjUJoUJEN3S\n" +
|
||||||
"s/PsXPdFEEZ3yqZmetRMfH5FTdrFkU5wNdPnZW/MyAxF3lAKUlPDQd1t5LU0DAE3\n" +
|
"rkJEJkXRFqEEAh6XCjDVDdDeIypK3dKuQkQmRdECmwwAAGD7AgjhSiFMCMzq3B4L\n" +
|
||||||
"yrf9MZbs0QIIzdbl3cbLNHtFlVLnrSQ5HlcQSQkrmrqjaibBkO9P+RvJEGPVrQp/\n" +
|
"s/PsXPdFEEZ3yqZmetRMfH5FTdrFkU5wNdPnZW/MyAxF3lAKUlPDQd1t5LU0DAE3\n" +
|
||||||
"uVpkA7I404ZpQJaRdC4y5mwXi+y61M9Im2qc2QRo1CaFEwUrgQQAIwQjBAFybhNP\n" +
|
"yrf9MZbs0QIIzdbl3cbLNHtFlVLnrSQ5HlcQSQkrmrqjaibBkO9P+RvJEGPVrQp/\n" +
|
||||||
"qpDG2Mffk5qc7A+S//F2AsrqxBo9WKk4xcKBy10CgrpbBz/1IqRrtbpcNaY0vcl5\n" +
|
"uVpkA7I404ZpQJaRdC4y5mwXi+y61M9Im2qc2QRo1CaFEwUrgQQAIwQjBAFybhNP\n" +
|
||||||
"YczBG/5PtLMTOMXQdAB5nTm7fHtsc3jvKpDZuDXbxwDUG/rYkHIdICGdp0dcfmY4\n" +
|
"qpDG2Mffk5qc7A+S//F2AsrqxBo9WKk4xcKBy10CgrpbBz/1IqRrtbpcNaY0vcl5\n" +
|
||||||
"XEcvg6/0wmb1JNpffGBXCtI0tqir53dhysaeDQllPQACCOiZvj9ozIpvGgCSRbkP\n" +
|
"YczBG/5PtLMTOMXQdAB5nTm7fHtsc3jvKpDZuDXbxwDUG/rYkHIdICGdp0dcfmY4\n" +
|
||||||
"zjQZuLEVEPLQ608ABZFSZJCL7l1Ycj6VSYsG/deoAocukMD36G+obEjhYcGpFp7k\n" +
|
"XEcvg6/0wmb1JNpffGBXCtI0tqir53dhysaeDQllPQACCOiZvj9ozIpvGgCSRbkP\n" +
|
||||||
"sq9fIG3CwJ0EGBMKAMwFgmjUJoUJEEtN3lgQzJ+7FqEENgGCuh0FnspezqxpS03e\n" +
|
"zjQZuLEVEPLQ608ABZFSZJCL7l1Ycj6VSYsG/deoAocukMD36G+obEjhYcGpFp7k\n" +
|
||||||
"WBDMn7sCmwKhIAQZEwoABgWCaNQmhQAKCRBLTd5YEMyfu4smAgienKF78nQXL6WK\n" +
|
"sq9fIG3CwJ0EGBMKAMwFgmjUJoUJEEtN3lgQzJ+7FqEENgGCuh0FnspezqxpS03e\n" +
|
||||||
"SPu7MC3VesJjjiGHQCB2vzBV+kOFoZJyS0U4R/zH1Q6NPt5XJFUbUyY+xCpWKIgq\n" +
|
"WBDMn7sCmwKhIAQZEwoABgWCaNQmhQAKCRBLTd5YEMyfu4smAgienKF78nQXL6WK\n" +
|
||||||
"ny34nPcHfgIECRVjB5Zs+ZVDK69YYdqhNljjGZtugX9VXrMhPoLVGDyE+9LNo3vR\n" +
|
"SPu7MC3VesJjjiGHQCB2vzBV+kOFoZJyS0U4R/zH1Q6NPt5XJFUbUyY+xCpWKIgq\n" +
|
||||||
"k8xUs2q2nUASAbG1aovnjZnj0H44lGgKqfEAAK31AgkB/CGspb4IH9gjfhQhVcLl\n" +
|
"ny34nPcHfgIECRVjB5Zs+ZVDK69YYdqhNljjGZtugX9VXrMhPoLVGDyE+9LNo3vR\n" +
|
||||||
"ypPC+pmRITB3kX2vSTjChvcBcPRJDZtYAdjtIFlmUYrUnlQDxJUOnvG/GZCMqnB5\n" +
|
"k8xUs2q2nUASAbG1aovnjZnj0H44lGgKqfEAAK31AgkB/CGspb4IH9gjfhQhVcLl\n" +
|
||||||
"QewCB2Kcu9foL0O0t6WrXyXQwkimMzx5Kefyu4Vbsj0m8yV5aS4ebPEmxxtWaOu7\n" +
|
"ypPC+pmRITB3kX2vSTjChvcBcPRJDZtYAdjtIFlmUYrUnlQDxJUOnvG/GZCMqnB5\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())
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue