• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ebourg / jsign / #386

24 Oct 2025 09:55AM UTC coverage: 80.64% (-2.4%) from 83.057%
#386

push

ebourg
CI build with Java 25

4965 of 6157 relevant lines covered (80.64%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.52
/jsign-crypto/src/main/java/net/jsign/jca/PIVCard.java
1
/*
2
 * Copyright 2024 Emmanuel Bourg
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package net.jsign.jca;
18

19
import java.io.ByteArrayInputStream;
20
import java.io.IOException;
21
import java.io.InputStream;
22
import java.nio.ByteBuffer;
23
import java.security.PublicKey;
24
import java.security.cert.Certificate;
25
import java.security.cert.CertificateException;
26
import java.security.cert.CertificateFactory;
27
import java.security.interfaces.ECKey;
28
import java.security.interfaces.RSAKey;
29
import java.security.spec.ECParameterSpec;
30
import java.util.Arrays;
31
import java.util.LinkedHashSet;
32
import java.util.Set;
33
import java.util.zip.GZIPInputStream;
34
import javax.smartcardio.CardChannel;
35
import javax.smartcardio.CardException;
36
import javax.smartcardio.CommandAPDU;
37
import javax.smartcardio.ResponseAPDU;
38

39
/**
40
 * Simple smart card interface for PIV cards.
41
 *
42
 * @see <a href="https://csrc.nist.gov/pubs/sp/800/73/4/upd1/final">NIST SP 800-73-4 Interfaces for Personal Identity Verification</a>
43
 * @see <a href="https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-78-5.ipd.pdf">NIST SP 800-78-5 Cryptographic Algorithms and Key Sizes for Personal Identity Verification</a>
44
 * @see <a href="https://docs.yubico.com/yesdk/users-manual/application-piv/commands.html">Yubikey User's Manual - PIV commands</a>
45
 * @since 6.0
46
 */
47
class PIVCard extends SmartCard {
48

49
    public enum Key {
1✔
50
        AUTHENTICATION(0x9A, 0x5FC105, "X.509 Certificate for PIV Authentication"),
1✔
51
        SIGNATURE(0x9C, 0x5FC10A, "X.509 Certificate for Digital Signature"),
1✔
52
        KEY_MANAGEMENT(0x9D, 0x5FC10B, "X.509 Certificate for Key Management"),
1✔
53
        CARD_AUTHENTICATION(0x9E, 0x5FC101, "X.509 Certificate for Card Authentication"),
1✔
54
        RETIRED1(0x82, 0x5FC10D, "X.509 Certificate for Retired Key 1"),
1✔
55
        RETIRED2(0x83, 0x5FC10E, "X.509 Certificate for Retired Key 2"),
1✔
56
        RETIRED3(0x84, 0x5FC10F, "X.509 Certificate for Retired Key 3"),
1✔
57
        RETIRED4(0x85, 0x5FC110, "X.509 Certificate for Retired Key 4"),
1✔
58
        RETIRED5(0x86, 0x5FC111, "X.509 Certificate for Retired Key 5"),
1✔
59
        RETIRED6(0x87, 0x5FC112, "X.509 Certificate for Retired Key 6"),
1✔
60
        RETIRED7(0x88, 0x5FC113, "X.509 Certificate for Retired Key 7"),
1✔
61
        RETIRED8(0x89, 0x5FC114, "X.509 Certificate for Retired Key 8"),
1✔
62
        RETIRED9(0x8A, 0x5FC115, "X.509 Certificate for Retired Key 9"),
1✔
63
        RETIRED10(0x8B, 0x5FC116, "X.509 Certificate for Retired Key 10"),
1✔
64
        RETIRED11(0x8C, 0x5FC117, "X.509 Certificate for Retired Key 11"),
1✔
65
        RETIRED12(0x8D, 0x5FC118, "X.509 Certificate for Retired Key 12"),
1✔
66
        RETIRED13(0x8E, 0x5FC119, "X.509 Certificate for Retired Key 13"),
1✔
67
        RETIRED14(0x8F, 0x5FC11A, "X.509 Certificate for Retired Key 14"),
1✔
68
        RETIRED15(0x90, 0x5FC11B, "X.509 Certificate for Retired Key 15"),
1✔
69
        RETIRED16(0x91, 0x5FC11C, "X.509 Certificate for Retired Key 16"),
1✔
70
        RETIRED17(0x92, 0x5FC11D, "X.509 Certificate for Retired Key 17"),
1✔
71
        RETIRED18(0x93, 0x5FC11E, "X.509 Certificate for Retired Key 18"),
1✔
72
        RETIRED19(0x94, 0x5FC11F, "X.509 Certificate for Retired Key 19"),
1✔
73
        RETIRED20(0x95, 0x5FC120, "X.509 Certificate for Retired Key 20");
1✔
74

75
        Key(int slot, int tag, String alias) {
1✔
76
            this.slot = slot;
1✔
77
            this.tag = tag;
1✔
78
            this.alias = alias;
1✔
79
        }
1✔
80

81
        final int slot;
82
        final int tag;
83
        final String alias;
84

85
        /**
86
         * Return the key for the specified name or slot.
87
         *
88
         * @param name The name, the alias or the slot of the key
89
         * @return the key or null if not found
90
         */
91
        public static Key of(String name) {
92
            if (name == null) {
1✔
93
                return null;
1✔
94
            }
95

96
            if (name.length() == 2) {
1✔
97
                int slot = Integer.parseInt(name, 16);
1✔
98
                for (Key key : values()) {
1✔
99
                    if (key.slot == slot) {
1✔
100
                        return key;
1✔
101
                    }
102
                }
103
            } else {
×
104
                for (Key key : values()) {
1✔
105
                    if (key.name().equalsIgnoreCase(name) || key.alias.equalsIgnoreCase(name)) {
1✔
106
                        return key;
1✔
107
                    }
108
                }
109
            }
110

111
            return null;
1✔
112
        }
113
    }
114

115
    public static class KeyInfo {
×
116
        public String algorithm;
117

118
        /**
119
         * The PIV algorithm identifier.
120
         *
121
         * <ul>
122
         *   <li>0x06: RSA-1024</li>
123
         *   <li>0x07: RSA-2048</li>
124
         *   <li>0x05: RSA-3072</li>
125
         *   <li>0x16: RSA-4096</li>
126
         *   <li>0x11: ECC-P256</li>
127
         *   <li>0x14: ECC-P384</li>
128
         * </ul>
129
         */
130
        public int algorithmId;
131

132
        /** Key size in bits */
133
        public int size;
134
    }
135

136
    private PIVCard(CardChannel channel) throws CardException {
137
        super(channel);
×
138
        select();
×
139
    }
×
140

141
    /**
142
     * Select the PIV application on the card.
143
     */
144
    private void select() throws CardException {
145
        select("PIV", new byte[] { (byte) 0xA0, 0x00, 0x00, 0x03, 0x08, 0x00, 0x00, 0x10, 0x00 });
×
146
    }
×
147

148
    /**
149
     * Verify the PIN required for the protected operations.
150
     *
151
     * @param p1  0x00: verify, 0xFF: reset
152
     * @param p2  0x80: PIN
153
     * @param pin the PIN
154
     */
155
    public void verify(int p1, int p2, String pin) throws CardException {
156
        if (pin == null) {
×
157
            pin = "";
×
158
        }
159
        byte[] mask = new byte[8];
×
160
        Arrays.fill(mask, (byte) 0xFF);
×
161
        System.arraycopy(pin.getBytes(), 0, mask, 0, pin.length());
×
162
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x20, p1, p2, mask)); // VERIFY
×
163
        handleError(response);
×
164
    }
×
165

166
    /**
167
     * Read a data object from the card.
168
     */
169
    public byte[] getData(int tag) throws CardException {
170
        if (dataObjectCache.containsKey(tag)) {
×
171
            return dataObjectCache.get(tag);
×
172
        }
173
        byte[] data;
174
        if (tag < 0x100) {
×
175
            data = new byte[] { 0x5C, 0x01, (byte) (tag & 0xFF) };
×
176
        } else if (tag < 0x10000) {
×
177
            data = new byte[] { 0x5C, 0x02, (byte) ((tag & 0xFF00) >> 8), (byte) (tag & 0xFF) };
×
178
        } else if (tag < 0x1000000) {
×
179
            data = new byte[] { 0x5C, 0x03, (byte) ((tag & 0xFF0000) >> 16), (byte) ((tag & 0xFF00) >> 8), (byte) (tag & 0xFF) };
×
180
        } else {
181
            throw new CardException("Invalid tag 0x" + Integer.toHexString(tag).toUpperCase());
×
182
        }
183
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xCB, 0x3F, 0xFF, data)); // GET DATA
×
184
        if (response.getSW() == 0x6A88) {
×
185
            throw new CardException("Data object 0x" + Integer.toHexString(tag).toUpperCase() + " not found");
×
186
        }
187
        handleError(response);
×
188
        dataObjectCache.put(tag, response.getData());
×
189
        return response.getData();
×
190
    }
191

192
    /**
193
     * Return the version of the firmware.
194
     */
195
    public String getVersion() throws CardException {
196
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xFD, 0x00, 0x00));
×
197
        handleError(response);
×
198
        byte[] version = response.getData();
×
199
        int major = version[0];
×
200
        int minor = version[1];
×
201
        int patch = version[2];
×
202
        return major + "." + minor + "." + patch;
×
203
    }
204

205
    /**
206
     * Return the available keys.
207
     */
208
    public Set<Key> getAvailableKeys() throws CardException {
209
        Set<Key> keys = new LinkedHashSet<>();
×
210

211
        for (Key key : Key.values()) {
×
212
            if (getCertificate(key) != null) {
×
213
                keys.add(key);
×
214
            }
215
        }
216

217
        return keys;
×
218
    }
219

220
    /**
221
     * Return the certificate for the specified key.
222
     */
223
    public Certificate getCertificate(Key key) throws CardException {
224
        byte[] data;
225
        try {
226
            data = getData(key.tag);
×
227
        } catch (CardException e) {
×
228
            if ("Incorrect P1 or P2 parameter".equals(e.getMessage())) {
×
229
                return null;
×
230
            } else {
231
                throw e;
×
232
            }
233
        }
×
234

235
        // parse the encoded certificate
236
        // (see https://docs.yubico.com/yesdk/users-manual/application-piv/commands.html#encoded-certificate)
237
        TLV tlv = TLV.parse(ByteBuffer.wrap(data));
×
238
        tlv = TLV.parse(ByteBuffer.wrap(tlv.value()), false);
×
239

240
        boolean compressed = false;
×
241
        TLV compressionField = tlv.find("71");
×
242
        if (compressionField != null) {
×
243
            compressed = compressionField.value()[0] == 1;
×
244
        }
245

246
        try {
247
            TLV certificateField = tlv.find("70");
×
248
            InputStream in = new ByteArrayInputStream(certificateField.value());
×
249
            if (compressed) {
×
250
                in = new GZIPInputStream(in);
×
251
            }
252
            return CertificateFactory.getInstance("X.509").generateCertificate(in);
×
253
        } catch (IOException | CertificateException e) {
×
254
            throw new CardException("Invalid certificate for " + key.name() + " key", e);
×
255
        }
256
    }
257

258
    /**
259
     * Return the key information for the specified key.
260
     */
261
    public KeyInfo getKeyInfo(Key key) throws CardException {
262
        Certificate certificate = getCertificate(key);
×
263
        // todo read the metadata if the certificate is missing
264
        if (certificate == null) {
×
265
            throw new CardException(key.name() + " key not found");
×
266
        }
267

268
        PublicKey publicKey = certificate.getPublicKey();
×
269

270
        KeyInfo info = new KeyInfo();
×
271
        info.algorithm = publicKey.getAlgorithm();
×
272
        if ("RSA".equals(info.algorithm)) {
×
273
            info.size = ((RSAKey) publicKey).getModulus().bitLength();
×
274
        } else if ("EC".equals(info.algorithm)) {
×
275
            ECParameterSpec spec = ((ECKey) publicKey).getParams();
×
276
            if (spec != null) {
×
277
                info.size = spec.getOrder().bitLength();
×
278
            }
279
        }
280
        info.algorithmId = getAlgorithmId(info.algorithm, info.size);
×
281

282
        return info;
×
283
    }
284

285
    private int getAlgorithmId(String algorithm, int size) {
286
        if ("RSA".equals(algorithm)) {
×
287
            switch (size) {
×
288
                case 1024:
289
                    return 0x06;
×
290
                case 2048:
291
                    return 0x07;
×
292
                case 3072:
293
                    return 0x05;
×
294
                case 4096:
295
                    return 0x16;
×
296
            }
297
        } else if ("EC".equals(algorithm)) {
×
298
            switch (size) {
×
299
                case 256:
300
                    return 0x11;
×
301
                case 384:
302
                    return 0x14;
×
303
            }
304
        }
305

306
        throw new IllegalArgumentException("Unsupported algorithm " + algorithm + " with key size " + size);
×
307
    }
308

309
    /**
310
     * Sign the specified data.
311
     *
312
     * @param key  the key to use for the signature
313
     * @param data the data to sign (the encoded DigestInfo structure for RSA, or the hash for ECDSA)
314
     */
315
    public byte[] sign(Key key, byte[] data) throws CardException {
316
        KeyInfo info = getKeyInfo(key);
×
317
        if ("RSA".equalsIgnoreCase(info.algorithm)) {
×
318
            data = rsaPadding(data, info.size);
×
319
        }
320

321
        if (pin != null) {
×
322
            verify(0, 0x80, pin);
×
323
        }
324

325
        // Dynamic Authentication Template
326
        TLV template = new TLV("7C");
×
327
        template.children().add(new TLV("82", new byte[0])); // Response
×
328
        template.children().add(new TLV("81", data)); // Challlenge
×
329

330
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x87, info.algorithmId, key.slot, template.getEncoded())); // GENERAL AUTHENTICATE
×
331
        handleError(response);
×
332

333
        TLV tlv = TLV.parse(ByteBuffer.wrap(response.getData()), true);
×
334
        return tlv.find("82").value();
×
335
    }
336

337
    /**
338
     * PKCS #1 v1.5 padding.
339
     */
340
    private byte[] rsaPadding(byte[] message, int keyLength) {
341
        byte[] padded = new byte[keyLength / 8];
×
342
        Arrays.fill(padded, (byte) 0xFF);
×
343
        padded[0] = 0x00;
×
344
        padded[1] = 0x01;
×
345
        System.arraycopy(message, 0, padded, padded.length - message.length, message.length);
×
346
        padded[padded.length - message.length - 1] = 0;
×
347
        return padded;
×
348
    }
349

350
    /**
351
     * Get the PIV card.
352
     */
353
    public static PIVCard getCard() throws CardException {
354
        return getCard(null);
×
355
    }
356

357
    /**
358
     * Get the PIV card with the specified name.
359
     *
360
     * @param name the partial name of the card
361
     */
362
    public static PIVCard getCard(String name) throws CardException {
363
        CardChannel channel = openChannel(name);
×
364
        return channel != null ? new PIVCard(channel) : null;
×
365
    }
366
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc