CryptoCertumCard.java

/*
 * Copyright 2025 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.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;

/**
 * Simple smart card interface for Certum cards (cryptoCertum 3.6 Common Profile).
 *
 * @since 7.4
 */
public class CryptoCertumCard extends SmartCard {

    /** AID of the eSign application with the Common profile */
    static final byte[] ESIGN_COMMON_PROFILE_AID = new byte[] { (byte) 0xA0, 0x00, 0x00, 0x01, 0x67, 0x45, 0x53, 0x49, 0x47, 0x4F };

    /** AID of the eSign application with the Secure profile (eIDAS) */
    //static final byte[] ESIGN_SECURE_PROFILE_AID = new byte[] { (byte) 0xA0, 0x00, 0x00, 0x01, 0x67, 0x45, 0x53, 0x49, 0x47, 0x4E };

    private CryptoCertumCard(CardChannel channel) throws CardException {
        super(channel);
        select();
    }

    /**
     * Select the eSign application on the card.
     */
    private void select() throws CardException {
        select("Certum", ESIGN_COMMON_PROFILE_AID);
    }

    /**
     * Verify the PIN required for the protected operations.
     *
     * @param p1  0x00: verify, 0xFF: reset
     * @param p2  0x83: PIN, 0x84: PUK
     * @param pin the PIN
     */
    public void verify(int p1, int p2, String pin) throws CardException {
        if (pin == null) {
            pin = "";
        }
        byte[] mask = new byte[16]; // ASCII, zero-padded
        System.arraycopy(pin.getBytes(), 0, mask, 0, pin.length());
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x20, p1, p2, pin.isEmpty() ? null : mask)); // VERIFY
        handleError(response);
    }

    /**
     * Get a challenge from the card.
     *
     * @param length the length of the challenge in bytes (8 or 16)
     */
    public byte[] getChallenge(int length) throws CardException {
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x84, 0x00, 0x00, length)); // GET CHALLENGE
        handleError(response);
        return response.getData();
    }

    /**
     * Get a file from the card.
     *
     * Known FIDs:
     * <ul>
     *   <li>0x1021-0x102B: RSA Key #1-10</li>
     *   <li>0x1031-0x103B: EC Key #1-10</li>
     *   <li>0x2001-0x2015: Certificate #1-20</li>
     *   <li>0x5032: object directory</li>
     *   <li>0x5034: card info (factory id, serial number)</li>
     * </ul>
     *
     * @param fid the identifier of the file
     */
    public byte[] getFile(int fid) throws CardException {
        return getFile(fid, false);
    }

    /**
     * Get a file from the card.
     *
     * Known FIDs:
     * <ul>
     *   <li>0x1021-0x102B: RSA Key #1-10</li>
     *   <li>0x1031-0x103B: EC Key #1-10</li>
     *   <li>0x2001-0x2015: Certificate #1-20</li>
     *   <li>0x5032: object directory</li>
     *   <li>0x5034: card info (factory id, serial number)</li>
     * </ul>
     *
     * @param fid the identifier of the file
     * @param partial if true, return only the first 256 bytes of the file
     */
    public byte[] getFile(int fid, boolean partial) throws CardException {
        int cacheId = partial ? (fid | 0x80000000) : fid;
        if (dataObjectCache.containsKey(cacheId)) {
            return dataObjectCache.get(cacheId);
        }
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xA4, 0x02, 0x0C, new byte[]{(byte) ((fid & 0xFF00) >> 8), (byte) (fid & 0xFF)})); // SELECT FILE
        handleError(response);

        byte[] data = readBinary(partial);
        dataObjectCache.put(cacheId, data);
        return data;
    }

    private byte[] readBinary(boolean partial) throws CardException {
        int offset = 0;   // page
        int length = 256; // max length per page
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        while (true) {
            ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xB0, offset & 0xFF, (offset & 0xFF00) >> 8, length)); // READ BINARY
            handleError(response);
            byte[] data = response.getData();
            bout.write(data, 0, data.length);
            if (data.length < length || partial) {
                break;
            }
            offset++;
        }

        return bout.toByteArray();
    }

    /**
     * Return the public key data (modulus for RSA keys)
     *
     * @param keyref the reference of the key
     */
    public byte[] getKeyData(int keyref) throws CardException {
        byte[] template = {(byte) 0xB6, 0x03, (byte) 0x83, 0x01, (byte) keyref, 0x7F, 0x49, 0x02, (byte) 0x81, 0x00};

        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xCB, 0x00, 0xFF, template));
        handleError(response);

        TLV tlv = TLV.parse(ByteBuffer.wrap(response.getData()));

        return tlv.children().get(1).children().get(0).value();
    }

    /**
     * An entry on the card (key or certificate).
     */
    public abstract class Entry {
        /** Index of the object (1-10) */
        public int index;

        public abstract int fid();

        public byte[] data() throws CardException {
            return getFile(fid());
        }

        public String name() throws CardException {
            byte[] data = getFile(fid(), true);
            int length = data[2];
            return new String(data, 3, length, StandardCharsets.UTF_8);
        }
    }

    public class Key extends Entry {
        /** Type of key (0: RSA key, 1: EC key) */
        public int type;

        /** Key size in bits */
        public int size;

        public byte ref() {
            return (byte) ((type == 0 ? 0x20 : 0x30) + index);
        }

        public int fid() {
            return 0x1000 + ref();
        }
    }

    public class Certificate extends Entry {

        public int fid() {
            return 0x2000 + index;
        }

        public X509Certificate getCertificate() throws CardException {
            ByteBuffer buffer = ByteBuffer.wrap(data()).order(ByteOrder.BIG_ENDIAN);
            buffer.position(0x84);
            int length = buffer.getShort() & 0xFFFF;

            byte[] data = new byte[length];
            buffer.get(data);

            try {
                InputStream in = new ByteArrayInputStream(data);
                return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(in);
            } catch (CertificateException e) {
                throw new CardException("Invalid data for certificate #" + index, e);
            }
        }
    }

    /**
     * Return the list of keys and certificates available on the card.
     */
    public List<Entry> getEntries() throws CardException {
        byte[] data = getFile(0x5032);

        List<Entry> objects = new ArrayList<>();
        for (int i = 0x24; i < data.length - 2; i += 2) {
            int type = (data[i] & 0xF0) >> 4;
            int index = data[i] & 0x0F;

            Entry entry = null;
            if (index != 0) {
                if (type == 2) {
                    Certificate certificate = new Certificate();
                    certificate.index = index;
                    entry = certificate;

                } else {
                    Key key = new Key();
                    key.index = index;
                    key.type = type;

                    // Key Algorithm (0x40: certificate, 0x41: RSA 2048 or P256, 0x42: RSA 3072 or P384, 0x43: RSA 4096 or P521, 0x44: RSA 1024)
                    int algorithm = data[i + 1];
                    switch (type) {
                        case 0: // RSA
                            switch (algorithm) {
                                case 0x41: key.size = 2048; break;
                                case 0x42: key.size = 3072; break;
                                case 0x43: key.size = 4096; break;
                                case 0x44: key.size = 1024; break;
                            }
                            break;
                        case 1: // EC
                            switch (algorithm) {
                                case 0x41: key.size = 256; break;
                                case 0x42: key.size = 384; break;
                                case 0x43: key.size = 521; break;
                            }
                            break;
                    }

                    entry = key;
                }
            }

            if (entry != null) {
                objects.add(entry);
            }
        }

        return objects;
    }

    /**
     * Return the key with the specified name.
     */
    public Key getKey(String name) throws CardException {
        for (Entry entry : getEntries()) {
            if (entry instanceof Key && entry.name().equals(name)) {
                return (Key) entry;
            }
        }
        return null;
    }

    /**
     * Return the certificate with the specified name.
     */
    public Certificate getCertificate(String name) throws CardException {
        for (Entry entry : getEntries()) {
            if (entry instanceof Certificate && entry.name().equals(name)) {
                return (Certificate) entry;
            }
        }
        return null;
    }

    /**
     * Return the name of the keys available on the card.
     */
    public List<String> aliases() throws CardException {
        List<String> aliases = new ArrayList<>();
        for (Entry entry : getEntries()) {
            if (entry instanceof Key) {
                aliases.add(entry.name());
            }
        }
        return aliases;
    }

    /**
     * Sign the specified hash with the specified key.
     *
     * @param key  the key to use for signing
     * @param hash the hash to sign
     */
    public byte[] sign(Key key, byte[] hash) throws CardException {
        if (pin != null) {
            verify(0x00, 0x83, pin);
        }

        manageSecurityEnvironment(key);
        hash(hash);
        return computeDigitalSignature();
    }

    /**
     * Assign the specified key to the COMPUTE DIGITAL SIGNATURE operation
     *
     * @param key the key
     */
    private void manageSecurityEnvironment(Key key) throws CardException {
        byte[] template = new byte[] {(byte) 0x80, 0x01, (byte) (key.type == 0 ? 0x42 : 0x44), (byte) 0x84, 0x01, key.ref()};

        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x22, 0x81, 0xB6, template)); // MANAGE SECURITY ENVIRONMENT
        handleError(response);
    }

    /**
     * Set the hash of the data to sign
     */
    private void hash(byte[] hash) throws CardException {
        TLV template = new TLV("90", hash);

        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x2A, 0x90, 0xA0, template.getEncoded())); // HASH
        handleError(response);
    }

    /**
     * Sign the specified data.
     */
    private byte[] computeDigitalSignature() throws CardException {
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, 0x80)); // COMPUTE DIGITAL SIGNATURE
        if (response.getSW() == 0x6a88) {
            throw new CardException("Signature key not found");
        }
        handleError(response);
        return response.getData();
    }

    /**
     * Get the CryptoCertum card.
     */
    public static CryptoCertumCard getCard() throws CardException {
        CardChannel channel = openChannel(ESIGN_COMMON_PROFILE_AID);
        return channel != null ? new CryptoCertumCard(channel) : null;
    }
}