mirror of
https://github.com/pgpainless/pgpainless.git
synced 2025-12-07 12:51:07 +01:00
WIP: Transparent decryption
This commit is contained in:
parent
510f8276e7
commit
de47a683d9
13 changed files with 223 additions and 116 deletions
|
|
@ -17,6 +17,7 @@ import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
|
||||||
import org.pgpainless.PGPainless
|
import org.pgpainless.PGPainless
|
||||||
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy
|
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy
|
||||||
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy
|
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy
|
||||||
|
import org.pgpainless.hardware.HardwareTokenBackend
|
||||||
import org.pgpainless.key.SubkeyIdentifier
|
import org.pgpainless.key.SubkeyIdentifier
|
||||||
import org.pgpainless.key.protection.SecretKeyRingProtector
|
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||||
import org.pgpainless.signature.SignatureUtils
|
import org.pgpainless.signature.SignatureUtils
|
||||||
|
|
@ -44,6 +45,7 @@ class ConsumerOptions(private val api: PGPainless) {
|
||||||
private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE
|
private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE
|
||||||
private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy()
|
private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy()
|
||||||
private var allowDecryptionWithNonEncryptionKey: Boolean = false
|
private var allowDecryptionWithNonEncryptionKey: Boolean = false
|
||||||
|
val hardwareTokenBackends: List<HardwareTokenBackend> = mutableListOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consider signatures on the message made before the given timestamp invalid. Null means no
|
* Consider signatures on the message made before the given timestamp invalid. Null means no
|
||||||
|
|
@ -236,6 +238,10 @@ class ConsumerOptions(private val api: PGPainless) {
|
||||||
decryptionPassphrases.add(passphrase)
|
decryptionPassphrases.add(passphrase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addHardwareTokenBackend(backend: HardwareTokenBackend) = apply {
|
||||||
|
(hardwareTokenBackends as MutableList).add(backend)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a custom [PublicKeyDataDecryptorFactory] which enable decryption of messages, e.g. using
|
* Add a custom [PublicKeyDataDecryptorFactory] which enable decryption of messages, e.g. using
|
||||||
* hardware-backed secret keys. (See e.g.
|
* hardware-backed secret keys. (See e.g.
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ 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
|
||||||
|
|
@ -66,7 +67,9 @@ import org.pgpainless.exception.MissingPassphraseException
|
||||||
import org.pgpainless.exception.SignatureValidationException
|
import org.pgpainless.exception.SignatureValidationException
|
||||||
import org.pgpainless.exception.UnacceptableAlgorithmException
|
import org.pgpainless.exception.UnacceptableAlgorithmException
|
||||||
import org.pgpainless.exception.WrongPassphraseException
|
import org.pgpainless.exception.WrongPassphraseException
|
||||||
|
import org.pgpainless.hardware.HardwareTokenBackend
|
||||||
import org.pgpainless.key.SubkeyIdentifier
|
import org.pgpainless.key.SubkeyIdentifier
|
||||||
|
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||||
import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey
|
import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey
|
||||||
import org.pgpainless.signature.consumer.OnePassSignatureCheck
|
import org.pgpainless.signature.consumer.OnePassSignatureCheck
|
||||||
import org.pgpainless.util.ArmoredInputStreamFactory
|
import org.pgpainless.util.ArmoredInputStreamFactory
|
||||||
|
|
@ -434,9 +437,6 @@ class OpenPgpMessageInputStream(
|
||||||
"Message is encrypted for ${secretKey.keyIdentifier}, but the key is not encryption capable.")
|
"Message is encrypted for ${secretKey.keyIdentifier}, but the key is not encryption capable.")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (hasUnsupportedS2KSpecifier(secretKey)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.debug("Attempt decryption using secret key ${decryptionKeys.keyIdentifier}")
|
LOGGER.debug("Attempt decryption using secret key ${decryptionKeys.keyIdentifier}")
|
||||||
val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue
|
val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue
|
||||||
|
|
@ -447,6 +447,28 @@ class OpenPgpMessageInputStream(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (secretKey.hasExternalSecretKey()) {
|
||||||
|
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.")
|
||||||
|
if (decryptWithHardwareKey(
|
||||||
|
hardwareTokenBackend,
|
||||||
|
esks,
|
||||||
|
secretKey,
|
||||||
|
protector,
|
||||||
|
SubkeyIdentifier(secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier),
|
||||||
|
pkesk)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if (hasUnsupportedS2KSpecifier(secretKey)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
val privateKey =
|
val privateKey =
|
||||||
try {
|
try {
|
||||||
unlockSecretKey(secretKey, protector)
|
unlockSecretKey(secretKey, protector)
|
||||||
|
|
@ -527,6 +549,25 @@ class OpenPgpMessageInputStream(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decryptWithHardwareKey(
|
||||||
|
hardwareTokenBackend: HardwareTokenBackend,
|
||||||
|
esks: ESKsAndData,
|
||||||
|
secretKey: OpenPGPSecretKey,
|
||||||
|
protector: SecretKeyRingProtector,
|
||||||
|
subkeyIdentifier: SubkeyIdentifier,
|
||||||
|
pkesk: PGPPublicKeyEncryptedData
|
||||||
|
): Boolean {
|
||||||
|
val decryptors = hardwareTokenBackend.provideDecryptorsFor(secretKey, protector, pkesk)
|
||||||
|
while (decryptors.hasNext()) {
|
||||||
|
val decryptor = decryptors.next()
|
||||||
|
val success = decryptPKESKAndStream(esks, subkeyIdentifier, decryptor, pkesk)
|
||||||
|
if (success) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun decryptWithPrivateKey(
|
private fun decryptWithPrivateKey(
|
||||||
esks: ESKsAndData,
|
esks: ESKsAndData,
|
||||||
privateKey: PGPKeyPair,
|
privateKey: PGPKeyPair,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,20 @@
|
||||||
package org.pgpainless.hardware
|
package org.pgpainless.hardware
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData
|
||||||
|
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||||
|
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
|
||||||
|
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||||
|
|
||||||
interface HardwareTokenBackend {
|
interface HardwareTokenBackend {
|
||||||
|
|
||||||
|
fun getBackendName(): String
|
||||||
|
|
||||||
|
fun provideDecryptorsFor(
|
||||||
|
secKey: OpenPGPKey.OpenPGPSecretKey,
|
||||||
|
protector: SecretKeyRingProtector,
|
||||||
|
pkesk: PGPPublicKeyEncryptedData
|
||||||
|
): Iterator<PublicKeyDataDecryptorFactory>
|
||||||
|
|
||||||
fun listDeviceSerials(): List<ByteArray>
|
fun listDeviceSerials(): List<ByteArray>
|
||||||
|
|
||||||
fun listKeyFingerprints(): Map<ByteArray, List<ByteArray>>
|
fun listKeyFingerprints(): Map<ByteArray, List<ByteArray>>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyConverter
|
||||||
import org.gnupg.GnuPGDummyKeyUtil
|
import org.gnupg.GnuPGDummyKeyUtil
|
||||||
|
|
||||||
data class Yubikey(val info: DeviceInfo, val device: YubiKeyDevice) {
|
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
|
||||||
|
|
@ -31,5 +32,7 @@ data class Yubikey(val info: DeviceInfo, val device: YubiKeyDevice) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val encodedSerial = GnuPGDummyKeyUtil.serialToBytes(info.serialNumber!!)
|
val serialNumber: Int = info.serialNumber!!
|
||||||
|
|
||||||
|
val encodedSerialNumber = GnuPGDummyKeyUtil.serialToBytes(serialNumber)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,36 @@ package org.pgpainless.yubikey
|
||||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||||
import com.yubico.yubikit.openpgp.KeyRef
|
import com.yubico.yubikit.openpgp.KeyRef
|
||||||
import com.yubico.yubikit.openpgp.OpenPgpSession
|
import com.yubico.yubikit.openpgp.OpenPgpSession
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData
|
||||||
|
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||||
|
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
|
||||||
import org.gnupg.GnuPGDummyKeyUtil
|
import org.gnupg.GnuPGDummyKeyUtil
|
||||||
import org.pgpainless.hardware.HardwareTokenBackend
|
import org.pgpainless.hardware.HardwareTokenBackend
|
||||||
|
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||||
|
|
||||||
class YubikeyHardwareTokenBackend : HardwareTokenBackend {
|
class YubikeyHardwareTokenBackend : HardwareTokenBackend {
|
||||||
|
|
||||||
|
override fun getBackendName(): String {
|
||||||
|
return "PGPainless-Yubikey"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun provideDecryptorsFor(
|
||||||
|
secKey: OpenPGPKey.OpenPGPSecretKey,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
decFac as PublicKeyDataDecryptorFactory
|
||||||
|
}
|
||||||
|
}.iterator()
|
||||||
|
}
|
||||||
|
|
||||||
override fun listDeviceSerials(): List<ByteArray> {
|
override fun listDeviceSerials(): List<ByteArray> {
|
||||||
return YubikeyHelper().listDevices()
|
return YubikeyHelper().listDevices()
|
||||||
.mapNotNull { yk -> yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } }
|
.mapNotNull { yk -> yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } }
|
||||||
|
|
@ -16,9 +41,9 @@ class YubikeyHardwareTokenBackend : HardwareTokenBackend {
|
||||||
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.encodedSerial to yk.device.openConnection(SmartCardConnection::class.java).use {
|
yk.encodedSerialNumber to yk.device.openConnection(SmartCardConnection::class.java).use {
|
||||||
val session = OpenPgpSession(it)
|
val session = OpenPgpSession(it)
|
||||||
//6session.getData(KeyRef.DEC.fingerprint)
|
//session.getData(KeyRef.DEC.fingerprint)
|
||||||
session.getData(KeyRef.SIG.fingerprint)
|
session.getData(KeyRef.SIG.fingerprint)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveKeyToCard(componentKey: OpenPGPPrivateKey,
|
fun moveToYubikey(componentKey: OpenPGPPrivateKey,
|
||||||
yubikey: Yubikey,
|
yubikey: Yubikey,
|
||||||
adminPin: CharArray,
|
adminPin: CharArray,
|
||||||
keyRef: KeyRef = keyRefForKey(componentKey.publicKey)
|
keyRef: KeyRef = keyRefForKey(componentKey.publicKey)
|
||||||
|
|
|
||||||
|
|
@ -32,39 +32,27 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
|
||||||
adminPin: CharArray,
|
adminPin: CharArray,
|
||||||
keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
|
keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
|
||||||
creationTime: Date = Date()): OpenPGPKey {
|
creationTime: Date = Date()): OpenPGPKey {
|
||||||
val primaryKey: PGPSecretKey = 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)
|
||||||
|
|
||||||
val pkVal = session.generateEcKey(KeyRef.ATT, OpenPgpCurve.SECP521R1)
|
var pkVal = session.generateEcKey(KeyRef.ATT, OpenPgpCurve.SECP521R1)
|
||||||
val pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA)
|
var pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA)
|
||||||
|
|
||||||
toStubbedSecretKey(pubKey, yubikey.info)
|
val primarykey = toExternalSecretKey(pubKey, yubikey.info)
|
||||||
|
|
||||||
|
pkVal = session.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP521R1)
|
||||||
|
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)
|
||||||
|
|
||||||
|
return OpenPGPKey(PGPSecretKeyRing(listOf(primarykey, signingKey, encryptionKey)))
|
||||||
}
|
}
|
||||||
|
|
||||||
val signingKey: PGPSecretKey = yubikey.device.openConnection(SmartCardConnection::class.java).use {
|
|
||||||
val session = OpenPgpSession(it)
|
|
||||||
session.verifyAdminPin(adminPin)
|
|
||||||
|
|
||||||
val pkVal = session.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP521R1)
|
|
||||||
val pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime,PublicKeyAlgorithm.ECDSA)
|
|
||||||
|
|
||||||
toSecretSubKey(toStubbedSecretKey(pubKey, yubikey.info), yubikey.info)
|
|
||||||
}
|
|
||||||
|
|
||||||
val encryptionKey: PGPSecretKey = yubikey.device.openConnection(SmartCardConnection::class.java).use {
|
|
||||||
val session = OpenPgpSession(it)
|
|
||||||
session.verifyAdminPin(adminPin)
|
|
||||||
|
|
||||||
val pkVal = session.generateEcKey(KeyRef.DEC, OpenPgpCurve.X25519)
|
|
||||||
val pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime,
|
|
||||||
if (keyVersion == OpenPGPKeyVersion.v6) PublicKeyAlgorithm.X25519
|
|
||||||
else PublicKeyAlgorithm.ECDH)
|
|
||||||
|
|
||||||
toSecretSubKey(toStubbedSecretKey(pubKey, yubikey.info), yubikey.info)
|
|
||||||
}
|
|
||||||
|
|
||||||
return OpenPGPKey(PGPSecretKeyRing(listOf(primaryKey, signingKey, encryptionKey)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toPGPPublicKey(pkVal: PublicKeyValues,
|
private fun toPGPPublicKey(pkVal: PublicKeyValues,
|
||||||
|
|
@ -79,7 +67,21 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
|
||||||
creationTime.toSecondsPrecision())
|
creationTime.toSecondsPrecision())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toStubbedSecretKey(pubKey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey {
|
private fun toExternalSecretKey(pubkey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey {
|
||||||
|
return PGPSecretKey(
|
||||||
|
SecretKeyPacket(
|
||||||
|
pubkey.publicKeyPacket,
|
||||||
|
0,
|
||||||
|
0xfc,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)
|
||||||
|
),
|
||||||
|
pubkey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toGnuStubbedSecretKey(pubKey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey {
|
||||||
return PGPSecretKey(
|
return PGPSecretKey(
|
||||||
SecretKeyPacket(
|
SecretKeyPacket(
|
||||||
pubKey.publicKeyPacket,
|
pubKey.publicKeyPacket,
|
||||||
|
|
@ -108,8 +110,8 @@ class YubikeyKeyGenerator(private val api: PGPainless) {
|
||||||
SecretSubkeyPacket(
|
SecretSubkeyPacket(
|
||||||
pubSubKey.publicKeyPacket,
|
pubSubKey.publicKeyPacket,
|
||||||
0,
|
0,
|
||||||
SecretKeyPacket.USAGE_SHA1,
|
0xfc,
|
||||||
S2K.gnuDummyS2K(S2K.GNUDummyParams.divertToCard()),
|
null,
|
||||||
null,
|
null,
|
||||||
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)),
|
GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)),
|
||||||
pubSubKey
|
pubSubKey
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,15 @@ package org.pgpainless.yubikey
|
||||||
|
|
||||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||||
import com.yubico.yubikit.openpgp.KeyRef
|
import com.yubico.yubikit.openpgp.KeyRef
|
||||||
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
|
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assumptions.assumeTrue
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.pgpainless.PGPainless
|
|
||||||
import org.pgpainless.decryption_verification.ConsumerOptions
|
import org.pgpainless.decryption_verification.ConsumerOptions
|
||||||
|
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
|
||||||
class YubikeyDecryptionTest {
|
class YubikeyDecryptionTest : YubikeyTest() {
|
||||||
|
|
||||||
val USER_PIN: CharArray = "123456".toCharArray()
|
|
||||||
val ADMIN_PIN: CharArray = "12345678".toCharArray()
|
|
||||||
|
|
||||||
|
// 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: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
|
||||||
"Comment: Alice <alice@pgpainless.org>\n" +
|
"Comment: Alice <alice@pgpainless.org>\n" +
|
||||||
|
|
@ -51,6 +48,8 @@ class YubikeyDecryptionTest {
|
||||||
"1POPHzF3cMIReYhZfiJUEBV19suL\n" +
|
"1POPHzF3cMIReYhZfiJUEBV19suL\n" +
|
||||||
"=dA6G\n" +
|
"=dA6G\n" +
|
||||||
"-----END PGP PRIVATE KEY BLOCK-----"
|
"-----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: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
|
||||||
"Comment: Alice <alice@pgpainless.org>\n" +
|
"Comment: Alice <alice@pgpainless.org>\n" +
|
||||||
|
|
@ -103,35 +102,43 @@ class YubikeyDecryptionTest {
|
||||||
"-----END PGP MESSAGE-----"
|
"-----END PGP MESSAGE-----"
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun decryptMessageUsingYubikey() {
|
fun decryptMessageWithYubikey() {
|
||||||
val api = PGPainless(BcOpenPGPImplementation())
|
|
||||||
api.hardwareTokenBackends.add(YubikeyHardwareTokenBackend())
|
|
||||||
val key = api.readKey().parseKey(KEY)
|
val key = api.readKey().parseKey(KEY)
|
||||||
|
|
||||||
val helper = YubikeyHelper(api)
|
|
||||||
val devices = helper.listDevices()
|
|
||||||
assumeTrue(devices.isNotEmpty())
|
|
||||||
val yubikey = devices.first()
|
|
||||||
|
|
||||||
val decKey = key.secretKeys[key.encryptionKeys[0].keyIdentifier]!!
|
val decKey = key.secretKeys[key.encryptionKeys[0].keyIdentifier]!!
|
||||||
val msgIn = MSG.byteInputStream()
|
val msgIn = MSG.byteInputStream()
|
||||||
|
|
||||||
// Write key
|
// Write key to card
|
||||||
val divertToCard = yubikey.storeKeyInSlot(decKey.unlock(), KeyRef.DEC, ADMIN_PIN)
|
val hardwareBasedKey = helper.moveToYubikey(decKey.unlock(), yubikey, adminPin, KeyRef.DEC)
|
||||||
|
|
||||||
// Decrypt
|
// Decrypt
|
||||||
|
|
||||||
|
// TODO: Make hardware decryption transparent as shown below!
|
||||||
|
|
||||||
|
val decIn = api.processMessage()
|
||||||
|
.onInputStream(msgIn)
|
||||||
|
.withOptions(ConsumerOptions.get(api)
|
||||||
|
.addHardwareTokenBackend(YubikeyHardwareTokenBackend())
|
||||||
|
.addDecryptionKey(hardwareBasedKey,
|
||||||
|
SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(String(userPin)))))
|
||||||
|
val msg = decIn.readAllBytes()
|
||||||
|
decIn.close()
|
||||||
|
assertEquals("Hello, World!\n", String(msg))
|
||||||
|
/*
|
||||||
|
|
||||||
yubikey.device.openConnection(SmartCardConnection::class.java).use {
|
yubikey.device.openConnection(SmartCardConnection::class.java).use {
|
||||||
val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection(it, decKey.pgpPublicKey)
|
val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection(it, decKey.pgpPublicKey)
|
||||||
val decIn = api.processMessage()
|
val decIn = api.processMessage()
|
||||||
.onInputStream(msgIn)
|
.onInputStream(msgIn)
|
||||||
.withOptions(
|
.withOptions(
|
||||||
ConsumerOptions.get(api)
|
ConsumerOptions.get(api)
|
||||||
//.addDecryptionKey(api.readKey().parseKey(KEY))
|
|
||||||
.addCustomDecryptorFactory(decFac)
|
.addCustomDecryptorFactory(decFac)
|
||||||
)
|
)
|
||||||
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,17 +1,26 @@
|
||||||
package org.pgpainless.yubikey
|
package org.pgpainless.yubikey
|
||||||
|
|
||||||
|
import org.gnupg.GnuPGDummyKeyUtil
|
||||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
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.Assumptions.assumeTrue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.util.Arrays
|
||||||
|
|
||||||
class YubikeyHardwareTokenBackendTest {
|
class YubikeyHardwareTokenBackendTest : YubikeyTest() {
|
||||||
|
|
||||||
val backend = YubikeyHardwareTokenBackend()
|
val backend = YubikeyHardwareTokenBackend()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testListDeviceSerials() {
|
fun testListDeviceSerials() {
|
||||||
assertNotNull(backend.listDeviceSerials())
|
val serials = backend.listDeviceSerials()
|
||||||
assumeTrue(backend.listDeviceSerials().isNotEmpty())
|
assertTrue(serials.any {
|
||||||
|
it.contentEquals(
|
||||||
|
GnuPGDummyKeyUtil.serialToBytes(
|
||||||
|
allowedSerialNumber
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
package org.pgpainless.yubikey
|
package org.pgpainless.yubikey
|
||||||
|
|
||||||
|
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.*
|
import java.util.*
|
||||||
|
|
||||||
class YubikeyKeyGeneratorTest {
|
class YubikeyKeyGeneratorTest : YubikeyTest() {
|
||||||
val ADMIN_PIN: CharArray = "12345678".toCharArray()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun generateKey() {
|
fun generateKey() {
|
||||||
val helper = YubikeyHelper()
|
|
||||||
val keyGen = YubikeyKeyGenerator(PGPainless.getInstance())
|
val keyGen = YubikeyKeyGenerator(PGPainless.getInstance())
|
||||||
val key = keyGen.generateModernKey(helper.listDevices().first(), ADMIN_PIN, OpenPGPKeyVersion.v4, Date())
|
val key = keyGen.generateModernKey(
|
||||||
|
yubikey, adminPin, OpenPGPKeyVersion.v4, Date())
|
||||||
|
|
||||||
println(key.toAsciiArmoredString())
|
println(key.toAsciiArmoredString())
|
||||||
|
for (subkey in key.secretKeys) {
|
||||||
|
assertTrue(subkey.value.hasExternalSecretKey())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
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.KeyRef
|
|
||||||
import com.yubico.yubikit.openpgp.OpenPgpSession
|
|
||||||
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
|
|
||||||
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.Assumptions.assumeTrue
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.pgpainless.PGPainless
|
|
||||||
import org.pgpainless.algorithm.HashAlgorithm
|
import org.pgpainless.algorithm.HashAlgorithm
|
||||||
import org.pgpainless.decryption_verification.ConsumerOptions
|
import org.pgpainless.decryption_verification.ConsumerOptions
|
||||||
import org.pgpainless.encryption_signing.ProducerOptions
|
import org.pgpainless.encryption_signing.ProducerOptions
|
||||||
|
|
@ -17,10 +12,7 @@ import org.pgpainless.signature.PGPContentSignerBuilderProviderFactory
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class YubikeySigningTest {
|
class YubikeySigningTest : YubikeyTest() {
|
||||||
|
|
||||||
val USER_PIN: CharArray = "123456".toCharArray()
|
|
||||||
val ADMIN_PIN: CharArray = "12345678".toCharArray()
|
|
||||||
|
|
||||||
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: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" +
|
||||||
|
|
@ -59,56 +51,16 @@ class YubikeySigningTest {
|
||||||
"1POPHzF3cMIReYhZfiJUEBV19suL\n" +
|
"1POPHzF3cMIReYhZfiJUEBV19suL\n" +
|
||||||
"=dA6G\n" +
|
"=dA6G\n" +
|
||||||
"-----END PGP PRIVATE KEY BLOCK-----"
|
"-----END PGP PRIVATE KEY BLOCK-----"
|
||||||
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" +
|
|
||||||
"mJMEaNQmhRMFK4EEACMEIwQBgF429XlvPyJdpfXDxVjVEOJc04wcpfkIoX1CzIjm\n" +
|
|
||||||
"daRyv+mz2jfFZlQsCkhw2GsrPRJuKz++1JkspKU+4Vot9dIBD94Y+MoZUgHM4m0t\n" +
|
|
||||||
"ItqAdaRcxZWXDpSB0eZH3/lC+VkMUjiqK1Po4qOdZttgpLz+uHcox3gxanjyndAQ\n" +
|
|
||||||
"gVQf36u0HEFsaWNlIDxhbGljZUBwZ3BhaW5sZXNzLm9yZz7CwCEEExMKAFEFgmjU\n" +
|
|
||||||
"JoUJEOsuVw2e4iiRFqEEuyrD4eWVzQXPpc/m6y5XDZ7iKJECmwEFFQoJCAsFFgID\n" +
|
|
||||||
"AQAECwkIBwknCQEJAgkDCAECngkFiQlmAYACmQEAAEjjAgQOfqnx03DV4MUGGytd\n" +
|
|
||||||
"G02k5KbyeHbuAooyAcn8LItbaAlwzMn9Pu2uwLFEeqi0yhfF2QILwnh4QPHkhC6U\n" +
|
|
||||||
"wn5nbwIED8I1+3lbVifmpZm+1xwyU8JldGHvGDd0nkJ+wZsB22rr8dTIOgju2Z2a\n" +
|
|
||||||
"UOOBRlaCnRfPwYjlMaaacn+T2RZBQhK4lwRo1CaFEgUrgQQAIwQjBAE/KkBQQfj0\n" +
|
|
||||||
"4fzk0LWTlrbbdh95ZK4SyTLoXVY8lueyRPlTu399uPBZUxVBkJNaStvv+TgzEXXO\n" +
|
|
||||||
"1cHXOccEI9b04ACpUWbTq1ArP162V7OUuSGIAoxMw3B/jMWlNgPy/ktvv1Tidj66\n" +
|
|
||||||
"Q3QKtx6Iir340AWuMszisM/9sJRazsJxVFZIvgMBCgnCugQYEwoAKgWCaNQmhQkQ\n" +
|
|
||||||
"3dKuQkQmRdEWoQQCHpcKMNUN0N4jKkrd0q5CRCZF0QKbDAAAYPsCCOFKIUwIzOrc\n" +
|
|
||||||
"Hguz8+xc90UQRnfKpmZ61Ex8fkVN2sWRTnA10+dlb8zIDEXeUApSU8NB3W3ktTQM\n" +
|
|
||||||
"ATfKt/0xluzRAgjN1uXdxss0e0WVUuetJDkeVxBJCSuauqNqJsGQ70/5G8kQY9Wt\n" +
|
|
||||||
"Cn+5WmQDsjjThmlAlpF0LjLmbBeL7LrUz0ibariTBGjUJoUTBSuBBAAjBCMEAXJu\n" +
|
|
||||||
"E0+qkMbYx9+TmpzsD5L/8XYCyurEGj1YqTjFwoHLXQKCulsHP/UipGu1ulw1pjS9\n" +
|
|
||||||
"yXlhzMEb/k+0sxM4xdB0AHmdObt8e2xzeO8qkNm4NdvHANQb+tiQch0gIZ2nR1x+\n" +
|
|
||||||
"ZjhcRy+Dr/TCZvUk2l98YFcK0jS2qKvnd2HKxp4NCWU9wsCdBBgTCgDMBYJo1CaF\n" +
|
|
||||||
"CRBLTd5YEMyfuxahBDYBgrodBZ7KXs6saUtN3lgQzJ+7ApsCoSAEGRMKAAYFgmjU\n" +
|
|
||||||
"JoUACgkQS03eWBDMn7uLJgIInpyhe/J0Fy+likj7uzAt1XrCY44hh0Agdr8wVfpD\n" +
|
|
||||||
"haGScktFOEf8x9UOjT7eVyRVG1MmPsQqViiIKp8t+Jz3B34CBAkVYweWbPmVQyuv\n" +
|
|
||||||
"WGHaoTZY4xmbboF/VV6zIT6C1Rg8hPvSzaN70ZPMVLNqtp1AEgGxtWqL542Z49B+\n" +
|
|
||||||
"OJRoCqnxAACt9QIJAfwhrKW+CB/YI34UIVXC5cqTwvqZkSEwd5F9r0k4wob3AXD0\n" +
|
|
||||||
"SQ2bWAHY7SBZZlGK1J5UA8SVDp7xvxmQjKpweUHsAgdinLvX6C9DtLelq18l0MJI\n" +
|
|
||||||
"pjM8eSnn8ruFW7I9JvMleWkuHmzxJscbVmjru9Tzjx8xd3DCEXmIWX4iVBAVdfbL\n" +
|
|
||||||
"iw==\n" +
|
|
||||||
"=Oq+Y\n" +
|
|
||||||
"-----END PGP PUBLIC KEY BLOCK-----"
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun signMessageWithYubikey() {
|
fun signMessageWithYubikey() {
|
||||||
val api = PGPainless(BcOpenPGPImplementation())
|
|
||||||
val helper = YubikeyHelper(api)
|
|
||||||
|
|
||||||
val devices = helper.listDevices()
|
|
||||||
assumeTrue(devices.isNotEmpty())
|
|
||||||
|
|
||||||
val yubikey = devices.first()
|
|
||||||
val device = yubikey.device
|
val device = yubikey.device
|
||||||
val key = api.readKey().parseKey(KEY)
|
val key = api.readKey().parseKey(KEY)
|
||||||
|
|
||||||
val signingKey = key.secretKeys[key.signingKeys[0].keyIdentifier]!!
|
val signingKey = key.secretKeys[key.signingKeys[0].keyIdentifier]!!
|
||||||
|
|
||||||
val signingKeyOnCard = helper.moveKeyToCard(signingKey.unlock(), yubikey, ADMIN_PIN)
|
val hardwareBasedSigningKey = helper.moveToYubikey(signingKey.unlock(), yubikey, adminPin)
|
||||||
println(signingKeyOnCard.toAsciiArmoredString())
|
println(hardwareBasedSigningKey.toAsciiArmoredString())
|
||||||
|
|
||||||
val msgOut = ByteArrayOutputStream()
|
val msgOut = ByteArrayOutputStream()
|
||||||
device.openConnection(SmartCardConnection::class.java).use {
|
device.openConnection(SmartCardConnection::class.java).use {
|
||||||
|
|
@ -123,7 +75,7 @@ class YubikeySigningTest {
|
||||||
.generateMessage()
|
.generateMessage()
|
||||||
.onOutputStream(msgOut)
|
.onOutputStream(msgOut)
|
||||||
.withOptions(ProducerOptions.sign(SigningOptions.get()
|
.withOptions(ProducerOptions.sign(SigningOptions.get()
|
||||||
.addInlineSignature(signingKeyOnCard.signingKeys[0], factory, HashAlgorithm.SHA512)))
|
.addInlineSignature(hardwareBasedSigningKey.signingKeys[0], factory, HashAlgorithm.SHA512)))
|
||||||
|
|
||||||
sigOut.write("Hello, World!".toByteArray())
|
sigOut.write("Hello, World!".toByteArray())
|
||||||
sigOut.close()
|
sigOut.close()
|
||||||
|
|
@ -133,7 +85,7 @@ class YubikeySigningTest {
|
||||||
api.processMessage()
|
api.processMessage()
|
||||||
.onInputStream(ByteArrayInputStream(msgOut.toByteArray()))
|
.onInputStream(ByteArrayInputStream(msgOut.toByteArray()))
|
||||||
.withOptions(ConsumerOptions.get()
|
.withOptions(ConsumerOptions.get()
|
||||||
.addVerificationCert(signingKeyOnCard.toCertificate())
|
.addVerificationCert(hardwareBasedSigningKey.toCertificate())
|
||||||
).use {
|
).use {
|
||||||
it.readAllBytes()
|
it.readAllBytes()
|
||||||
it.close()
|
it.close()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package org.pgpainless.yubikey
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation
|
||||||
|
import org.opentest4j.TestAbortedException
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
abstract class YubikeyTest() {
|
||||||
|
|
||||||
|
val adminPin: CharArray
|
||||||
|
val userPin: CharArray
|
||||||
|
val allowedSerialNumber: Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
javaClass.classLoader.getResourceAsStream("yubikey.properties").use {
|
||||||
|
val props = Properties().apply { load(it) }
|
||||||
|
|
||||||
|
adminPin = getProperty(props, "ADMIN_PIN").toCharArray()
|
||||||
|
userPin = getProperty(props, "USER_PIN").toCharArray()
|
||||||
|
allowedSerialNumber = getProperty(props, "ALLOWED_DEVICE_SERIAL").toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
?: 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
7
pgpainless-yubikey/src/test/resources/yubikey.properties
Normal file
7
pgpainless-yubikey/src/test/resources/yubikey.properties
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
ADMIN_PIN=12345678
|
||||||
|
USER_PIN=123456
|
||||||
|
ALLOWED_DEVICE_SERIAL=15472425
|
||||||
Loading…
Add table
Add a link
Reference in a new issue