OpenPGPCard.java
/*
* Copyright 2023 Emmanuel Bourg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.jsign.jca;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
/**
* Simple smart card interface for OpenPGP cards.
*
* @see <a href="https://www.gnupg.org/ftp/specs/OpenPGP-smart-card-application-3.4.1.pdf">Functional Specification of the OpenPGP application on ISO Smart Card Operating Systems</a>
* @since 5.0
*/
class OpenPGPCard extends SmartCard {
/** The extended capabilities flag list */
private byte[] extendedCapabilities;
/** Information about the keys */
private KeyInfo[] keyInfos;
public enum Key {
SIGNATURE, ENCRYPTION, AUTHENTICATION
}
public static class KeyInfo {
public byte[] fingerprint;
public int algorithm;
public int size;
public boolean isRSA() {
return algorithm == 1 || algorithm == 2 || algorithm == 3;
}
public boolean isEC() {
return algorithm == 18 || algorithm == 19;
}
public boolean isPresent() {
return !Arrays.equals(fingerprint, new byte[20]);
}
}
private OpenPGPCard(CardChannel channel) throws CardException {
super(channel);
select();
}
/**
* Select the OpenPGP application on the card.
*/
private void select() throws CardException {
ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xA4, 0x04, 0x00, new byte[] { (byte) 0xD2, 0x76, 0x00, 0x01, 0x24, 0x01 })); // SELECT
switch (response.getSW()) {
case 0x6A82:
case 0x6A86:
throw new CardException("OpenPGP application not found on the card/token");
}
handleError(response);
}
/**
* Verify the PIN required for the protected operations.
*
* @param p1 0x00: verify, 0xFF: reset
* @param p2 0x81: PW1 (PSO:CDS), 0x82: PW1, 0x83: PW3 (PSO:DECIPHER)
* @param pin the PIN
*/
public void verify(int p1, int p2, String pin) throws CardException {
if (pin == null) {
pin = "";
}
ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x20, p1, p2, p1 == 0 ? pin.getBytes() : new byte[0])); // VERIFY
handleError(response);
}
/**
* Select the n-th occurence of a data object.
*
* @param tag the tag of the data object (only 0x7F21 is supported)
* @param index the index of the data object (0-based)
*/
public void selectData(int tag, int index) throws CardException {
byte[] data = new byte[] { 0x60, 0x04, 0x5C, 0x02, (byte) ((tag & 0xFF00) >> 8), (byte) (tag & 0xFF) };
ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xA5, index, 0x04, data)); // SELECT DATA
handleError(response);
}
/**
* Read a data object from the card.
*/
public byte[] getData(int tag) throws CardException {
if (dataObjectCache.containsKey(tag)) {
return dataObjectCache.get(tag);
}
ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xCA, (tag & 0xFF00) >> 8, tag & 0xFF, 0x10000)); // GET DATA
if (response.getSW() == 0x6A88) {
throw new CardException("Data object 0x" + Integer.toHexString(tag).toUpperCase() + " not found");
}
handleError(response);
if (tag != 0x7F21) {
dataObjectCache.put(tag, response.getData());
}
return response.getData();
}
/**
* Return the application identifier.
*/
public byte[] getAID() throws CardException {
return getData(0x4F);
}
/**
* Return the version of the OpenPGP specification implemented by the card.
*/
public float getVersion() throws CardException {
byte[] aid = getAID();
int major = aid[6];
int minor = aid[7];
return major + minor / 10f;
}
/**
* Return the keys available for signing.
*/
public Set<Key> getAvailableKeys() throws CardException {
Set<Key> keys = new LinkedHashSet<>();
for (Key key : Key.values()) {
if (getKeyInfo(key).isPresent() && (key != Key.ENCRYPTION || supportsManageSecurityEnvironment())) {
keys.add(key);
}
}
return keys;
}
/**
* Return the certificate for the specified key.
*/
public byte[] getCertificate(Key key) throws CardException {
if (key == Key.AUTHENTICATION) {
return getData(0x7F21);
}
if (getVersion() < 3) {
return new byte[0];
}
int position = 0;
if (key == Key.ENCRYPTION) {
position = 1;
} else if (key == Key.SIGNATURE) {
position = 2;
}
selectData(0x7F21, position);
return getData(0x7F21);
}
/**
* Return the key information for the specified key.
*/
public KeyInfo getKeyInfo(Key key) throws CardException {
if (keyInfos == null) {
this.keyInfos = getKeyInfo();
}
return keyInfos[key.ordinal()];
}
private KeyInfo[] getKeyInfo() throws CardException {
KeyInfo[] keyInfos = new KeyInfo[3];
keyInfos[0] = new KeyInfo();
keyInfos[1] = new KeyInfo();
keyInfos[2] = new KeyInfo();
TLV relatedData = TLV.parse(ByteBuffer.wrap(getData(0x6E)));
// read the fingerprints
TLV fingerprints = relatedData.find("73", "C5");
if (fingerprints != null) {
byte[] data = fingerprints.value();
for (Key key : Key.values()) {
byte[] fingerprint = new byte[20];
System.arraycopy(data, 20 * key.ordinal(), fingerprint, 0, 20);
keyInfos[key.ordinal()].fingerprint = fingerprint;
}
}
// read the algorithm attributes
for (Key key : Key.values()) {
TLV algorithmAttributes = relatedData.find("73", "C" + (key.ordinal() + 1));
ByteBuffer buffer = ByteBuffer.wrap(algorithmAttributes.value());
keyInfos[key.ordinal()].algorithm = buffer.get();
if (keyInfos[key.ordinal()].isRSA()) {
keyInfos[key.ordinal()].size = buffer.getShort() & 0xFFFF;
}
}
extendedCapabilities = relatedData.find("73", "C0").value();
return keyInfos;
}
/**
* Return the extended capabilities.
*/
private byte[] getExtendedCapabilities() throws CardException {
if (extendedCapabilities == null) {
TLV relatedData = TLV.parse(ByteBuffer.wrap(getData(0x6E)));
extendedCapabilities = relatedData.find("73", "C0").value();
}
return extendedCapabilities;
}
/**
* Tell if the MANAGE SECURITY ENVIRONMENT command is supported.
*/
protected boolean supportsManageSecurityEnvironment() throws CardException {
return getVersion() > 3 && (getExtendedCapabilities()[9] & 0x01) != 0;
}
/**
* Put the specified data object on the card.
*/
public void putData(int tag, byte[] data) throws CardException {
ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xDA, (tag & 0xFF00) >> 8, tag & 0xFF, data)); // PUT DATA
handleError(response);
// clear the cache
dataObjectCache.clear();
}
/**
* Sign the specified data.
*
* @param key the key to use for the signature
* @param data the data to sign
*/
public byte[] sign(Key key, byte[] data) throws CardException {
if (key == Key.SIGNATURE) {
verify(0, 0x81, pin);
return computeDigitalSignature(data);
} else {
verify(0, 0x82, pin);
if (key == Key.ENCRYPTION) {
manageSecurityEnvironment(0xA4, (byte) 2);
}
return authenticate(data);
}
}
/**
* Sign the specified data.
*/
public byte[] computeDigitalSignature(byte[] data) throws CardException {
ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, data)); // COMPUTE DIGITAL SIGNATURE
if (response.getSW() == 0x6a88) {
throw new CardException("Signature key not found");
}
handleError(response);
return response.getData();
}
/**
* Sign the specified data with the authentication key.
*/
public byte[] authenticate(byte[] data) throws CardException {
ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x88, 0x00, 0x00, data)); // INTERNAL AUTHENTICATE
if (response.getSW() == 0x6a88) {
throw new CardException("Authentication key not found");
}
handleError(response);
return response.getData();
}
/**
* Assign the encryption of the authentication key to the DECIPHER and INTERNAL AUTHENTICATE operations
*
* @param p2 the operation (0xA4: INTERNAL AUTHENTICATE, 0xB8: DECIPHER)
* @param keyRef the reference of the key (2: encryption key, 3: authentication key)
*/
public void manageSecurityEnvironment(int p2, byte keyRef) throws CardException {
ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x22, 0x41, p2, new byte[] {(byte) 0x83, 0x01, keyRef})); // MANAGE SECURITY ENVIRONMENT
handleError(response);
}
/**
* Get the OpenPGP card.
*/
public static OpenPGPCard getCard() throws CardException {
return getCard(null);
}
/**
* Get the OpenPGP card with the specified name.
*
* @param name the partial name of the card
*/
public static OpenPGPCard getCard(String name) throws CardException {
killSmartCardDaemon();
CardChannel channel = openChannel(name);
return channel != null ? new OpenPGPCard(channel) : null;
}
/**
* Kill scdaemon to release the card.
*/
private static void killSmartCardDaemon() {
try {
new ProcessBuilder("gpgconf", "--kill", "scdaemon").start().waitFor(5, TimeUnit.SECONDS);
} catch (Exception e) {
// gpgconf not found, let's continue
}
}
}