Skip to content

Commit

Permalink
support Ed25519
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamVe committed May 21, 2024
1 parent 70166aa commit fb259b2
Show file tree
Hide file tree
Showing 20 changed files with 260 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,16 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController

import com.google.android.material.navigation.NavigationView

import com.yubico.yubikit.android.YubiKitManager
import com.yubico.yubikit.android.app.databinding.DialogAboutBinding
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
import com.yubico.yubikit.android.transport.usb.UsbConfiguration

import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.slf4j.LoggerFactory

import java.util.*

import java.security.Security
import java.util.Locale
import kotlin.properties.Delegates

class MainActivity : AppCompatActivity() {
Expand All @@ -62,6 +59,9 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

Security.removeProvider("BC")
Security.insertProviderAt(BouncyCastleProvider(), 1)

val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class PivCertificateFragment : Fragment() {
.generatePrivate(PKCS8EncodedKeySpec(Base64.decode(DER_KEY, Base64.DEFAULT)))
lifecycleScope.launch(Dispatchers.Main) {
pivViewModel.pendingAction.value = {
authenticate(pivViewModel.mgmtKeyType, pivViewModel.mgmtKey)
authenticate(pivViewModel.getMgmtKeyType(this), pivViewModel.mgmtKey)
putKey(slot, PrivateKeyValues.fromPrivateKey(key), PinPolicy.DEFAULT, TouchPolicy.DEFAULT)
putCertificate(slot, cert)
"Imported certificate ${cert.subjectDN} issued by ${cert.issuerDN}"
Expand All @@ -165,7 +165,7 @@ class PivCertificateFragment : Fragment() {
lifecycleScope.launch(Dispatchers.Main) {
getSecret(requireContext(), R.string.enter_pin)?.let { pin ->
pivViewModel.pendingAction.value = {
authenticate(pivViewModel.mgmtKeyType, pivViewModel.mgmtKey)
authenticate(pivViewModel.getMgmtKeyType(this), pivViewModel.mgmtKey)

val provider = PivProvider(this)
val factory = KeyPairGenerator.getInstance("YKPivEC", provider)
Expand Down Expand Up @@ -214,11 +214,66 @@ class PivCertificateFragment : Fragment() {
}
}

// Generate a key, then a self-signed certificate, and import
binding.generateEd25519Cert.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
getSecret(requireContext(), R.string.enter_pin)?.let { pin ->
pivViewModel.pendingAction.value = {
authenticate(pivViewModel.getMgmtKeyType(this), pivViewModel.mgmtKey)

val provider = PivProvider(this)
val factory = KeyPairGenerator.getInstance("YKPivEC", provider)
factory.initialize(
PivAlgorithmParameterSpec(
slot,
KeyType.ED25519,
PinPolicy.DEFAULT,
TouchPolicy.DEFAULT,
pin.toCharArray()
)
)
val keyPair = factory.generateKeyPair()

// Generate a certificate
val name = X500Name("CN=Generated Ed25519 Example")
val serverCertGen = X509v3CertificateBuilder(
name,
BigInteger("123456789"),
Date(),
Date(),
name,
SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(keyPair.public.encoded))
)

val certBytes = serverCertGen.build(object : ContentSigner {
val messageBuffer = ByteArrayOutputStream()
override fun getAlgorithmIdentifier(): AlgorithmIdentifier =
AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256)

override fun getOutputStream(): OutputStream = messageBuffer
override fun getSignature(): ByteArray {
return Signature.getInstance("Ed25519", provider).apply {
initSign(keyPair.private)
update(messageBuffer.toByteArray())
}.sign()
}
}).encoded

val cert = CertificateFactory.getInstance("X.509")
.generateCertificate(ByteArrayInputStream(certBytes)) as X509Certificate
putCertificate(slot, cert)

"Generated Ed25519 key in slot $slot"
}
}
}
}

binding.generateRsaCert.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
getSecret(requireContext(), R.string.enter_pin)?.let { pin ->
pivViewModel.pendingAction.value = {
authenticate(pivViewModel.mgmtKeyType, pivViewModel.mgmtKey)
authenticate(pivViewModel.getMgmtKeyType(this), pivViewModel.mgmtKey)

val provider = PivProvider(this)
val factory = KeyPairGenerator.getInstance("YKPivRSA", provider)
Expand Down Expand Up @@ -278,7 +333,7 @@ class PivCertificateFragment : Fragment() {
// Delete the certificate
binding.delete.setOnClickListener {
pivViewModel.pendingAction.value = {
authenticate(pivViewModel.mgmtKeyType, pivViewModel.mgmtKey)
authenticate(pivViewModel.getMgmtKeyType(this), pivViewModel.mgmtKey)
deleteCertificate(slot)
"Deleted certificate in slot $slot"
}
Expand All @@ -296,9 +351,11 @@ class PivCertificateFragment : Fragment() {
keyStore.load(null)
val publicKey = keyStore.getCertificate(slot.stringAlias).publicKey
val privateKey = keyStore.getKey(slot.stringAlias, pin.toCharArray()) as PrivateKey
val algorithm = when (KeyType.fromKey(publicKey).params.algorithm) {

val keyType = KeyType.fromKey(publicKey)
val algorithm = when (keyType.params.algorithm) {
KeyType.Algorithm.RSA -> "SHA256withRSA"
KeyType.Algorithm.EC -> "SHA256withECDSA"
KeyType.Algorithm.EC -> if (keyType == KeyType.ED25519) "Ed25519" else "SHA256withECDSA"
}

// Create signature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,17 @@ class PivViewModel : YubiKeyViewModel<PivSession>() {
private val _certificates = MutableLiveData<SparseArray<X509Certificate>?>()
val certificates: LiveData<SparseArray<X509Certificate>?> = _certificates

var mgmtKeyType = ManagementKeyType.TDES
var mgmtKey: ByteArray =
byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8)

fun getMgmtKeyType(session: PivSession) : ManagementKeyType {
return try {
session.managementKeyMetadata.keyType
} catch (ignored: Exception) {
ManagementKeyType.TDES
}
}

override fun getSession(
device: YubiKeyDevice,
onError: (Throwable) -> Unit,
Expand Down
21 changes: 21 additions & 0 deletions AndroidDemo/src/main/res/layout/fragment_piv_certifiate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,27 @@
android:text="@string/piv_generate_rsa_cert" />
</TableRow>

<TableRow
android:layout_weight="1"
android:gravity="center"
android:weightSum="2" >

<Button
android:id="@+id/generate_ed25519_cert"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/piv_generate_ed25519_cert" />

<Button
android:id="@+id/generate_x25519_cert"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:enabled="false"
android:text="@string/piv_generate_x25519_cert" />
</TableRow>

<TableRow
android:layout_weight="1"
android:gravity="center"
Expand Down
2 changes: 2 additions & 0 deletions AndroidDemo/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
<string name="piv_attest">Attest</string>
<string name="piv_generate_ec_cert">Generate EC256</string>
<string name="piv_generate_rsa_cert">Generate RSA2048</string>
<string name="piv_generate_ed25519_cert">Generate ED25519</string>
<string name="piv_generate_x25519_cert">Generate X25519</string>
<string name="piv_import_cert">Import</string>
<string name="piv_message_hint">Message</string>
<string name="piv_sign">Sign</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ public static class Ec extends PrivateKeyValues {
private final EllipticCurveValues ellipticCurveValues;
private final byte[] secret;


protected Ec(EllipticCurveValues ellipticCurveValues, byte[] secret) {
super(ellipticCurveValues.getBitLength());
this.ellipticCurveValues = ellipticCurveValues;
Expand Down
4 changes: 4 additions & 0 deletions piv/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ dependencies {

test {
systemProperty "org.slf4j.simpleLogger.defaultLogLevel", "trace"

dependencies {
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.77'
}
}

ext.pomName = "Yubico YubiKit ${project.name.capitalize()}"
Expand Down
19 changes: 17 additions & 2 deletions piv/src/main/java/com/yubico/yubikit/piv/KeyType.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019-2022 Yubico.
* Copyright (C) 2019-2022,2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,9 @@
import com.yubico.yubikit.core.keys.PrivateKeyValues;
import com.yubico.yubikit.core.keys.PublicKeyValues;

import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey;

import java.security.Key;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
Expand All @@ -46,7 +49,15 @@ public enum KeyType {
/**
* Elliptic Curve key, using NIST Curve P-384.
*/
ECCP384((byte) 0x14, new EcKeyParams(EllipticCurveValues.SECP384R1));
ECCP384((byte) 0x14, new EcKeyParams(EllipticCurveValues.SECP384R1)),
/**
* Edwards Digital Signature Algorithm (EdDSA) key, using Curve25519.
*/
ED25519((byte) 0xE0, new EcKeyParams(EllipticCurveValues.Ed25519)),
/**
* Elliptic-Curve Diffie-Hellman (ECDH) protocol key, using Curve25519.
*/
X25519((byte) 0xE1, new EcKeyParams(EllipticCurveValues.X25519));

public final byte value;
public final KeyParams params;
Expand Down Expand Up @@ -105,6 +116,10 @@ public static KeyType fromKey(Key key) {
ellipticCurveValues = ((PublicKeyValues.Ec) PublicKeyValues.fromPublicKey((ECPublicKey) key)).getCurveParams();
} else if (key instanceof ECPrivateKey) {
ellipticCurveValues = ((PrivateKeyValues.Ec) PrivateKeyValues.fromPrivateKey((ECPrivateKey) key)).getCurveParams();
} else if (key instanceof BCEdDSAPrivateKey) {
ellipticCurveValues = ((PrivateKeyValues.Ec) PrivateKeyValues.fromPrivateKey((BCEdDSAPrivateKey) key)).getCurveParams();
} else if (key instanceof BCEdDSAPublicKey) {
ellipticCurveValues = ((PublicKeyValues.Cv25519) PublicKeyValues.fromPublicKey((BCEdDSAPublicKey) key)).getCurveParams();
} else {
throw new IllegalArgumentException("Unsupported key type");
}
Expand Down
18 changes: 14 additions & 4 deletions piv/src/main/java/com/yubico/yubikit/piv/PivSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ public boolean isSupportedBy(Version version) {
*/
public static final Feature<PivSession> FEATURE_MOVE_KEY = new Feature.Versioned<>("Move or delete keys", 5, 7, 0);

/**
* Support for the curve 25519 keys.
*/
public static final Feature<PivSession> FEATURE_CV25519 = new Feature.Versioned<>("Curve 25519", 5, 7, 0);

private static final int PIN_LEN = 8;

// Special slot for the Management Key
Expand Down Expand Up @@ -357,7 +362,9 @@ public byte[] sign(Slot slot, KeyType keyType, byte[] message, Signature algorit
public byte[] rawSignOrDecrypt(Slot slot, KeyType keyType, byte[] payload) throws IOException, ApduException, BadResponseException {
int byteLength = keyType.params.bitLength / 8;
byte[] padded;
if (payload.length > byteLength) {
if (keyType == KeyType.ED25519 || keyType == KeyType.X25519) {
padded = payload;
} else if (payload.length > byteLength) {
if (keyType.params.algorithm == KeyType.Algorithm.EC) {
// Truncate
padded = Arrays.copyOf(payload, byteLength);
Expand Down Expand Up @@ -842,8 +849,8 @@ static PublicKeyValues parsePublicKeyFromDevice(KeyType keyType, byte[] encoded)
BigInteger exponent = new BigInteger(1, dataObjects.get(0x82));
return new PublicKeyValues.Rsa(modulus, exponent);
} else {
if (!(keyType.params instanceof KeyType.EcKeyParams)) {
throw new IllegalArgumentException("Unsupported key type");
if (keyType == KeyType.ED25519) {
return new PublicKeyValues.Cv25519(((KeyType.EcKeyParams) keyType.params).getCurveParams(), dataObjects.get(0x86));
}
return PublicKeyValues.Ec.fromEncodedPoint(((KeyType.EcKeyParams) keyType.params).getCurveParams(), dataObjects.get(0x86));
}
Expand All @@ -862,6 +869,9 @@ public void checkKeySupport(KeyType keyType, PinPolicy pinPolicy, TouchPolicy to
return;
}

if (keyType == KeyType.ED25519 || keyType == KeyType.X25519) {
require(FEATURE_CV25519);
}
if (keyType == KeyType.ECCP384) {
require(FEATURE_P384);
}
Expand Down Expand Up @@ -996,7 +1006,7 @@ public KeyType putKey(Slot slot, PrivateKeyValues key, PinPolicy pinPolicy, Touc
break;
case EC:
PrivateKeyValues.Ec ecPrivateKey = (PrivateKeyValues.Ec) key;
tlvs.put(0x06, ecPrivateKey.getSecret()); // s
tlvs.put((keyType == KeyType.ED25519) ? 0x07 : 0x06, ecPrivateKey.getSecret()); // s
break;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022,2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,7 +35,7 @@
public abstract class PivEcSignatureSpi extends SignatureSpi {
private final Callback<Callback<Result<PivSession, Exception>>> provider;
@Nullable
private PivPrivateKey.EcKey privateKey;
private PivPrivateKey privateKey;

protected PivEcSignatureSpi(Callback<Callback<Result<PivSession, Exception>>> provider) {
this.provider = provider;
Expand All @@ -50,6 +50,8 @@ protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
if (privateKey instanceof PivPrivateKey.EcKey) {
this.privateKey = (PivPrivateKey.EcKey) privateKey;
} else if (privateKey instanceof PivPrivateKey.Ed25519Key) {
this.privateKey = (PivPrivateKey.Ed25519Key) privateKey;
} else {
throw new InvalidKeyException("Unsupported key type");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022,2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -71,7 +71,7 @@ public KeyPair generateKeyPair() {
BlockingQueue<Result<KeyPair, Exception>> queue = new ArrayBlockingQueue<>(1);
provider.invoke(result -> queue.add(Result.of(() -> {
PivSession session = result.getValue();
PublicKey publicKey = session.generateKey(spec.slot, spec.keyType, spec.pinPolicy, spec.touchPolicy);
PublicKey publicKey = session.generateKeyValues(spec.slot, spec.keyType, spec.pinPolicy, spec.touchPolicy).toPublicKey();
PrivateKey privateKey = PivPrivateKey.from(publicKey, spec.slot, spec.pinPolicy, spec.touchPolicy, spec.pin);
return new KeyPair(publicKey, privateKey);
})));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022,2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,7 @@
package com.yubico.yubikit.piv.jca;

import com.yubico.yubikit.core.application.BadResponseException;
import com.yubico.yubikit.core.keys.PrivateKeyValues;
import com.yubico.yubikit.core.smartcard.ApduException;
import com.yubico.yubikit.core.smartcard.SW;
import com.yubico.yubikit.core.util.Callback;
Expand Down Expand Up @@ -59,7 +60,7 @@ private void putEntry(Slot slot, @Nullable PrivateKey key, PinPolicy pinPolicy,
provider.invoke(result -> queue.add(Result.of(() -> {
PivSession piv = result.getValue();
if (key != null) {
piv.putKey(slot, key, pinPolicy, touchPolicy);
piv.putKey(slot, PrivateKeyValues.fromPrivateKey(key), pinPolicy, touchPolicy);
}
if (certificate != null) {
piv.putCertificate(slot, certificate);
Expand All @@ -79,7 +80,7 @@ public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyEx
PivSession session = result.getValue();
if (session.supports(PivSession.FEATURE_METADATA)) {
SlotMetadata data = session.getSlotMetadata(slot);
return PivPrivateKey.from(data.getPublicKey(), slot, data.getPinPolicy(), data.getTouchPolicy(), password);
return PivPrivateKey.from(data.getPublicKeyValues().toPublicKey(), slot, data.getPinPolicy(), data.getTouchPolicy(), password);
} else {
PublicKey publicKey = session.getCertificate(slot).getPublicKey();
return PivPrivateKey.from(publicKey, slot, null, null, password);
Expand Down Expand Up @@ -143,7 +144,7 @@ public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter
PrivateKey key;
if (session.supports(PivSession.FEATURE_METADATA)) {
SlotMetadata data = session.getSlotMetadata(slot);
key = PivPrivateKey.from(data.getPublicKey(), slot, data.getPinPolicy(), data.getTouchPolicy(), pin);
key = PivPrivateKey.from(data.getPublicKeyValues().toPublicKey(), slot, data.getPinPolicy(), data.getTouchPolicy(), pin);
} else {
PublicKey publicKey = certificate.getPublicKey();
key = PivPrivateKey.from(publicKey, slot, null, null, pin);
Expand Down
Loading

0 comments on commit fb259b2

Please sign in to comment.