Private keys and certificates in the YubiKey PIV application can be accessed and managed through the standard Java Cryptography Architecture (JCA) interfaces through a custom Provider. This guide shows examples for common JCA operations. Read more about JCA in the official documentation.
Please note, that for brevity, error and exception handling code is omitted from the examples.
For even more usage examples, the source code of the :testing
module is recommended.
The YKPiv PivProvider needs an active PivSession to be able to perform any actions.
You can either pass an instance of PivSession directly to the constructor of PivProvider, or you can provide a Callback
which will be invoked to supply a PivSession whenever one is required.
To obtain a PivSession, the following code can be used:
// device instance is obtained on a successful USB/NFC connection
// see YubiKit documentation
YubikeyDevice device = ... ;
device.requestConnection(SmartCardConnection::class.java) {
PivSession pivSession = new PivSession(it.value);
// use pivSession
}
Before calling methods which modify data on the YubiKey, such as key generation or key import, the YubiKey PIV Session must be authenticated with a management key by calling piv.authenticate()
.
YKPiv provider can be installed into the application/process or used directly.
Before you can use the YKPiv Provider you will need to make it accessible to the application by adding it to the list of Security Providers.
The YKPiv Provider uses custom PrivateKey classes which aren’t usable by other Providers. To avoid a different Provider from being used with these PrivateKeys, we recommend installing the PivProvider in the first position:
PivProvider pivProvider = new PivProvider(pivSession)
Security.insertProviderAt(pivProvider, 1); // JCA Security providers are indexed from 1
Inserting YKPiv provider in the first position makes it the preferred provider, and calling JCA APIs which don’t specify a provider by name or pointer will return PIV JCA implementations (where applicable).
The Provider uses services from existing other Providers to perform padding and hashing of messages. If additional Signature or Cipher schemes are required for use with YKPiv, you can install additional JCA Providers prior to instantiating the YKPiv PivProvider. For example, to be able to use an algorithm which is provided by the third party Provider Bouncy Castle, you could do the following:
// This example installs an updated version of Bouncy Castle JCA provider
// for this, we first need to remove the original Bouncy Castle provider
Security.removeProvider("BC");
// after the system Bouncy Castle provider has been removed, we install a new instance
Security.addProvider(new BouncyCastle());
// we call the PivProvider constructor after we have installed additional providers
PivProvider pivProvider = new PivProvider(pivSession);
Security.insertProviderAt(pivProvider, 1);
To use YKPiv without global installation (avoiding call to Security.insertProviderAt()
, the application needs to instantiate the provider class and use it in the JCA getInstance
methods.
For example:
PivProvider pivProvider = new PivProvider(pivSession)
KeyStore keyStore = KeyStore.getInstance("YKPiv", pivProvider);
The KeyPairGenerator
service is used for generating new keys.
Depending on requested key type, use the following code to acquire an instance of the service:
// get instance of RSA KeyPairGenerator
KeyPairGenerator rsaKpg = KeyPairGenerator.getInstance("YKPivRSA");
// get instance of EC KeyPairGenerator
KeyPairGenerator ecKpg = KeyPairGenerator.getInstance("YKPivEC");
The instance then needs to be initialized with PivAlgorithmParameterSpec
object which describes the properties of the new key:
PivAlgorithmParameterSpec(
Slot slot,
KeyType keyType,
@Nullable PinPolicy pinPolicy,
@Nullable TouchPolicy touchPolicy,
@Nullable char[] pin)
slot
-
is the PIV slot, one of
Slot.AUTHENTICATION
,Slot.SIGNATURE
,Slot.KEY_MANAGEMENT
orSlot.CARD_AUTH
keyType
-
one of
KeyType.RSA1024
,KeyType.RSA2048
,KeyType.ECCP256
,KeyType.ECCP384
; must match theKeyPairGenerator
type pinPolicy
-
defines the pin policy for using the key.
PinPolicy.DEFAULT
,PinPolicy.NEVER
,PinPolicy.ONCE
orPinPolicy.ALWAYS
touchPolicy
-
defines the whether or not a user presence is required (physical touch) when using the key. One of
TouchPolicy.DEFAULT
,TouchPolicy.NEVER
,TouchPolicy.ALWAYS
,TouchPolicy.CACHED
pin
-
the PIV application PIN; valid PIN is required depending on the
PinPolicy
value and the operation which is planned to be performed with the resulting PrivateKey - signing and decrypting require PIN.
The DEFAULT
values of PinPolicy
and TouchPolicy
depend on the slot
value.
See YubiKit PIV module JavaDoc and PIV Certificate slots for more details.
Follows an example of generation of an RSA key pair.
KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("YKPivRSA");
rsaGen.initialize(
new PivAlgorithmParameterSpec(
Slot.AUTHENTICATION,
KeyType.RSA1024,
null, // PinPolicy
null, // TouchPolicy
DEFAULT_PIN // PIV PIN
)
);
KeyPair keyPair = rsaGen.generateKeyPair();
Private keys can be stored in YKPiv KeyStore
with the setEntry
method.
The slot, pin and touch policies have the same values as when generating new keys.
Example snippet:
KeyStore keyStore = KeyStore.getInstance("YKPiv");
keyStore.load(null);
KeyPair keyPair = ...;
X509Certificate cert = ...;
keyStore.setEntry(
Slot.SIGNATURE,
new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{cert}),
new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)
);
To get a private key stored in a specific slot of the KeyStore
, use getKey
method.
KeyStore keyStore = KeyStore.getInstance("YKPiv");
keyStore.load(null);
PrivateKey privateKey = (PrivateKey) keyStore.getKey(Slot.SIGNATURE, DEFAULT_PIN);
The YKPiv private keys can be used for digital signatures:
// note: the signature algorithm and key have to be compatible
PrivateKey privateKey = keyPair.getPrivate();
Signature signature = Signature.getInstance("SHA256withECDSA");
byte[] message = "message to sign".getBytes(StandardCharsets.UTF_8);
signature.initSign(privateKey);
signature.update(message);
byte[] messageSignature = signature.sign();
To verify a digital signature, following code can be used:
// note: the signature algorithm and key have to be compatible
PublicKey publicKey = keyPair.getPublic();
Signature signature = Signature.getInstance("SHA256withECDSA");
byte[] message = "message to sign".getBytes(StandardCharsets.UTF_8);
byte[] messageSignature = ...;
signature.initVerify(publicKey);
signature.update(message);
bool success = signature.verify(messageSignature);
YKPiv keys can be used for encryption and decryption of data. The following example shows how:
KeyPair keyPair = ...;
String cipherAlgorithm = "RSA/ECB/PKCS1Padding"; // or other algorithm
byte[] message = "message to encrypt".getBytes(StandardCharsets.UTF_8);
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
byte[] encrypted = cipher.doFinal(message);
cipher = Cipher.getInstance(cipherAlgorithm);
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
byte[] decrypted = cipher.doFinal(encrypted);
// decrypted == message
YKPiv implements a KeyAgreement
service.
Key agreement is a protocol by which 2 or more parties can establish the same cryptographic keys, without having to exchange any secret information.
The following example shows how to use the KeyAgreement
instance for two different key pairs (one of them is YKPiv key pair) for getting a common secret.
// generate EC key with the YKPiv provider
KeyPairGenerator pivKpg = KeyPairGenerator.getInstance("YkPivEC");
pivKpg.initialize(
new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, KeyType.ECCP256, null, null, DEFAULT_PIN));
KeyPair pivKeyPair = pivKpg.generateKeyPair();
// generate EC key with another provider, based on pivKeyPair
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
kpg.initialize(((ECKey) pivKeyPair.getPublic()).getParams());
KeyPair peerPair = kpg.generateKeyPair();
// this is YKPiv KeyAgreement service
KeyAgreement ka = KeyAgreement.getInstance("ECDH");
ka.init(pivKeyPair.getPrivate());
ka.doPhase(peerPair.getPublic(), true);
byte[] secret = ka.generateSecret();
ka = KeyAgreement.getInstance("ECDH");
ka.init(peerPair.getPrivate());
ka.doPhase(pivKeyPair.getPublic(), true);
byte[] peerSecret = ka.generateSecret();
// secret == peerSecret