001/*
002 * Copyright 2025 Emmanuel Bourg
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package net.jsign.jca;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.InputStream;
022import java.nio.ByteBuffer;
023import java.nio.ByteOrder;
024import java.nio.charset.StandardCharsets;
025import java.security.cert.CertificateException;
026import java.security.cert.CertificateFactory;
027import java.security.cert.X509Certificate;
028import java.util.ArrayList;
029import java.util.List;
030import javax.smartcardio.CardChannel;
031import javax.smartcardio.CardException;
032import javax.smartcardio.CommandAPDU;
033import javax.smartcardio.ResponseAPDU;
034
035/**
036 * Simple smart card interface for Certum cards (cryptoCertum 3.6 Common Profile).
037 *
038 * @since 7.4
039 */
040public class CryptoCertumCard extends SmartCard {
041
042    /** AID of the eSign application with the Common profile */
043    static final byte[] ESIGN_COMMON_PROFILE_AID = new byte[] { (byte) 0xA0, 0x00, 0x00, 0x01, 0x67, 0x45, 0x53, 0x49, 0x47, 0x4F };
044
045    /** AID of the eSign application with the Secure profile (eIDAS) */
046    //static final byte[] ESIGN_SECURE_PROFILE_AID = new byte[] { (byte) 0xA0, 0x00, 0x00, 0x01, 0x67, 0x45, 0x53, 0x49, 0x47, 0x4E };
047
048    private CryptoCertumCard(CardChannel channel) throws CardException {
049        super(channel);
050        select();
051    }
052
053    /**
054     * Select the eSign application on the card.
055     */
056    private void select() throws CardException {
057        select("Certum", ESIGN_COMMON_PROFILE_AID);
058    }
059
060    /**
061     * Verify the PIN required for the protected operations.
062     *
063     * @param p1  0x00: verify, 0xFF: reset
064     * @param p2  0x83: PIN, 0x84: PUK
065     * @param pin the PIN
066     */
067    public void verify(int p1, int p2, String pin) throws CardException {
068        if (pin == null) {
069            pin = "";
070        }
071        byte[] mask = new byte[16]; // ASCII, zero-padded
072        System.arraycopy(pin.getBytes(), 0, mask, 0, pin.length());
073        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x20, p1, p2, pin.isEmpty() ? null : mask)); // VERIFY
074        handleError(response);
075    }
076
077    /**
078     * Get a challenge from the card.
079     *
080     * @param length the length of the challenge in bytes (8 or 16)
081     */
082    public byte[] getChallenge(int length) throws CardException {
083        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x84, 0x00, 0x00, length)); // GET CHALLENGE
084        handleError(response);
085        return response.getData();
086    }
087
088    /**
089     * Get a file from the card.
090     *
091     * Known FIDs:
092     * <ul>
093     *   <li>0x1021-0x102B: RSA Key #1-10</li>
094     *   <li>0x1031-0x103B: EC Key #1-10</li>
095     *   <li>0x2001-0x2015: Certificate #1-20</li>
096     *   <li>0x5032: object directory</li>
097     *   <li>0x5034: card info (factory id, serial number)</li>
098     * </ul>
099     *
100     * @param fid the identifier of the file
101     */
102    public byte[] getFile(int fid) throws CardException {
103        return getFile(fid, false);
104    }
105
106    /**
107     * Get a file from the card.
108     *
109     * Known FIDs:
110     * <ul>
111     *   <li>0x1021-0x102B: RSA Key #1-10</li>
112     *   <li>0x1031-0x103B: EC Key #1-10</li>
113     *   <li>0x2001-0x2015: Certificate #1-20</li>
114     *   <li>0x5032: object directory</li>
115     *   <li>0x5034: card info (factory id, serial number)</li>
116     * </ul>
117     *
118     * @param fid the identifier of the file
119     * @param partial if true, return only the first 256 bytes of the file
120     */
121    public byte[] getFile(int fid, boolean partial) throws CardException {
122        int cacheId = partial ? (fid | 0x80000000) : fid;
123        if (dataObjectCache.containsKey(cacheId)) {
124            return dataObjectCache.get(cacheId);
125        }
126        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xA4, 0x02, 0x0C, new byte[]{(byte) ((fid & 0xFF00) >> 8), (byte) (fid & 0xFF)})); // SELECT FILE
127        handleError(response);
128
129        byte[] data = readBinary(partial);
130        dataObjectCache.put(cacheId, data);
131        return data;
132    }
133
134    private byte[] readBinary(boolean partial) throws CardException {
135        int offset = 0;   // page
136        int length = 256; // max length per page
137        ByteArrayOutputStream bout = new ByteArrayOutputStream();
138        while (true) {
139            ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xB0, offset & 0xFF, (offset & 0xFF00) >> 8, length)); // READ BINARY
140            handleError(response);
141            byte[] data = response.getData();
142            bout.write(data, 0, data.length);
143            if (data.length < length || partial) {
144                break;
145            }
146            offset++;
147        }
148
149        return bout.toByteArray();
150    }
151
152    /**
153     * Return the public key data (modulus for RSA keys)
154     *
155     * @param keyref the reference of the key
156     */
157    public byte[] getKeyData(int keyref) throws CardException {
158        byte[] template = {(byte) 0xB6, 0x03, (byte) 0x83, 0x01, (byte) keyref, 0x7F, 0x49, 0x02, (byte) 0x81, 0x00};
159
160        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xCB, 0x00, 0xFF, template));
161        handleError(response);
162
163        TLV tlv = TLV.parse(ByteBuffer.wrap(response.getData()));
164
165        return tlv.children().get(1).children().get(0).value();
166    }
167
168    /**
169     * An entry on the card (key or certificate).
170     */
171    public abstract class Entry {
172        /** Index of the object (1-10) */
173        public int index;
174
175        public abstract int fid();
176
177        public byte[] data() throws CardException {
178            return getFile(fid());
179        }
180
181        public String name() throws CardException {
182            byte[] data = getFile(fid(), true);
183            int length = data[2];
184            return new String(data, 3, length, StandardCharsets.UTF_8);
185        }
186    }
187
188    public class Key extends Entry {
189        /** Type of key (0: RSA key, 1: EC key) */
190        public int type;
191
192        /** Key size in bits */
193        public int size;
194
195        public byte ref() {
196            return (byte) ((type == 0 ? 0x20 : 0x30) + index);
197        }
198
199        public int fid() {
200            return 0x1000 + ref();
201        }
202    }
203
204    public class Certificate extends Entry {
205
206        public int fid() {
207            return 0x2000 + index;
208        }
209
210        public X509Certificate getCertificate() throws CardException {
211            ByteBuffer buffer = ByteBuffer.wrap(data()).order(ByteOrder.BIG_ENDIAN);
212            buffer.position(0x84);
213            int length = buffer.getShort() & 0xFFFF;
214
215            byte[] data = new byte[length];
216            buffer.get(data);
217
218            try {
219                InputStream in = new ByteArrayInputStream(data);
220                return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(in);
221            } catch (CertificateException e) {
222                throw new CardException("Invalid data for certificate #" + index, e);
223            }
224        }
225    }
226
227    /**
228     * Return the list of keys and certificates available on the card.
229     */
230    public List<Entry> getEntries() throws CardException {
231        byte[] data = getFile(0x5032);
232
233        List<Entry> objects = new ArrayList<>();
234        for (int i = 0x24; i < data.length - 2; i += 2) {
235            int type = (data[i] & 0xF0) >> 4;
236            int index = data[i] & 0x0F;
237
238            Entry entry = null;
239            if (index != 0) {
240                if (type == 2) {
241                    Certificate certificate = new Certificate();
242                    certificate.index = index;
243                    entry = certificate;
244
245                } else {
246                    Key key = new Key();
247                    key.index = index;
248                    key.type = type;
249
250                    // Key Algorithm (0x40: certificate, 0x41: RSA 2048 or P256, 0x42: RSA 3072 or P384, 0x43: RSA 4096 or P521, 0x44: RSA 1024)
251                    int algorithm = data[i + 1];
252                    switch (type) {
253                        case 0: // RSA
254                            switch (algorithm) {
255                                case 0x41: key.size = 2048; break;
256                                case 0x42: key.size = 3072; break;
257                                case 0x43: key.size = 4096; break;
258                                case 0x44: key.size = 1024; break;
259                            }
260                            break;
261                        case 1: // EC
262                            switch (algorithm) {
263                                case 0x41: key.size = 256; break;
264                                case 0x42: key.size = 384; break;
265                                case 0x43: key.size = 521; break;
266                            }
267                            break;
268                    }
269
270                    entry = key;
271                }
272            }
273
274            if (entry != null) {
275                objects.add(entry);
276            }
277        }
278
279        return objects;
280    }
281
282    /**
283     * Return the key with the specified name.
284     */
285    public Key getKey(String name) throws CardException {
286        for (Entry entry : getEntries()) {
287            if (entry instanceof Key && entry.name().equals(name)) {
288                return (Key) entry;
289            }
290        }
291        return null;
292    }
293
294    /**
295     * Return the certificate with the specified name.
296     */
297    public Certificate getCertificate(String name) throws CardException {
298        for (Entry entry : getEntries()) {
299            if (entry instanceof Certificate && entry.name().equals(name)) {
300                return (Certificate) entry;
301            }
302        }
303        return null;
304    }
305
306    /**
307     * Return the name of the keys available on the card.
308     */
309    public List<String> aliases() throws CardException {
310        List<String> aliases = new ArrayList<>();
311        for (Entry entry : getEntries()) {
312            if (entry instanceof Key) {
313                aliases.add(entry.name());
314            }
315        }
316        return aliases;
317    }
318
319    /**
320     * Sign the specified hash with the specified key.
321     *
322     * @param key  the key to use for signing
323     * @param hash the hash to sign
324     */
325    public byte[] sign(Key key, byte[] hash) throws CardException {
326        if (pin != null) {
327            verify(0x00, 0x83, pin);
328        }
329
330        manageSecurityEnvironment(key);
331        hash(hash);
332        return computeDigitalSignature();
333    }
334
335    /**
336     * Assign the specified key to the COMPUTE DIGITAL SIGNATURE operation
337     *
338     * @param key the key
339     */
340    private void manageSecurityEnvironment(Key key) throws CardException {
341        byte[] template = new byte[] {(byte) 0x80, 0x01, (byte) (key.type == 0 ? 0x42 : 0x44), (byte) 0x84, 0x01, key.ref()};
342
343        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x22, 0x81, 0xB6, template)); // MANAGE SECURITY ENVIRONMENT
344        handleError(response);
345    }
346
347    /**
348     * Set the hash of the data to sign
349     */
350    private void hash(byte[] hash) throws CardException {
351        TLV template = new TLV("90", hash);
352
353        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x2A, 0x90, 0xA0, template.getEncoded())); // HASH
354        handleError(response);
355    }
356
357    /**
358     * Sign the specified data.
359     */
360    private byte[] computeDigitalSignature() throws CardException {
361        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, 0x80)); // COMPUTE DIGITAL SIGNATURE
362        if (response.getSW() == 0x6a88) {
363            throw new CardException("Signature key not found");
364        }
365        handleError(response);
366        return response.getData();
367    }
368
369    /**
370     * Get the CryptoCertum card.
371     */
372    public static CryptoCertumCard getCard() throws CardException {
373        CardChannel channel = openChannel(ESIGN_COMMON_PROFILE_AID);
374        return channel != null ? new CryptoCertumCard(channel) : null;
375    }
376}