• 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

1.82
/jsign-crypto/src/main/java/net/jsign/jca/OpenPGPCard.java
1
/*
2
 * Copyright 2023 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.nio.ByteBuffer;
20
import java.util.Arrays;
21
import java.util.LinkedHashSet;
22
import java.util.Set;
23
import java.util.concurrent.TimeUnit;
24
import javax.smartcardio.CardChannel;
25
import javax.smartcardio.CardException;
26
import javax.smartcardio.CommandAPDU;
27
import javax.smartcardio.ResponseAPDU;
28

29
/**
30
 * Simple smart card interface for OpenPGP cards.
31
 *
32
 * @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>
33
 * @since 5.0
34
 */
35
class OpenPGPCard extends SmartCard {
36

37
    /** The extended capabilities flag list */
38
    private byte[] extendedCapabilities;
39

40
    /** Information about the keys */
41
    private KeyInfo[] keyInfos;
42

43
    public enum Key {
1✔
44
        SIGNATURE, ENCRYPTION, AUTHENTICATION
1✔
45
    }
46

47
    public static class KeyInfo {
×
48
        public byte[] fingerprint;
49
        public int algorithm;
50
        public int size;
51

52
        public boolean isRSA() {
53
            return algorithm == 1 || algorithm == 2 || algorithm == 3;
×
54
        }
55

56
        public boolean isEC() {
57
            return algorithm == 18 || algorithm == 19;
×
58
        }
59

60
        public boolean isPresent() {
61
            return !Arrays.equals(fingerprint, new byte[20]);
×
62
        }
63
    }
64

65
    private OpenPGPCard(CardChannel channel) throws CardException {
66
        super(channel);
×
67
        select();
×
68
    }
×
69

70
    /**
71
     * Select the OpenPGP application on the card.
72
     */
73
    private void select() throws CardException {
74
        select("OpenPGP", new byte[] { (byte) 0xD2, 0x76, 0x00, 0x01, 0x24, 0x01 });
×
75
    }
×
76

77
    /**
78
     * Verify the PIN required for the protected operations.
79
     *
80
     * @param p1  0x00: verify, 0xFF: reset
81
     * @param p2  0x81: PW1 (PSO:CDS), 0x82: PW1, 0x83: PW3 (PSO:DECIPHER)
82
     * @param pin the PIN
83
     */
84
    public void verify(int p1, int p2, String pin) throws CardException {
85
        if (pin == null) {
×
86
            pin = "";
×
87
        }
88
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x20, p1, p2, p1 == 0 ? pin.getBytes() : new byte[0])); // VERIFY
×
89
        handleError(response);
×
90
    }
×
91

92
    /**
93
     * Select the n-th occurence of a data object.
94
     *
95
     * @param tag   the tag of the data object (only 0x7F21 is supported)
96
     * @param index the index of the data object (0-based)
97
     */
98
    public void selectData(int tag, int index) throws CardException {
99
        byte[] data = new byte[] { 0x60, 0x04, 0x5C, 0x02, (byte) ((tag & 0xFF00) >> 8), (byte) (tag & 0xFF) };
×
100
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xA5, index, 0x04, data)); // SELECT DATA
×
101
        handleError(response);
×
102
    }
×
103

104
    /**
105
     * Read a data object from the card.
106
     */
107
    public byte[] getData(int tag) throws CardException {
108
        if (dataObjectCache.containsKey(tag)) {
×
109
            return dataObjectCache.get(tag);
×
110
        }
111
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xCA, (tag & 0xFF00) >> 8, tag & 0xFF, 0x10000)); // GET DATA
×
112
        if (response.getSW() == 0x6A88) {
×
113
            throw new CardException("Data object 0x" + Integer.toHexString(tag).toUpperCase() + " not found");
×
114
        }
115
        handleError(response);
×
116
        if (tag != 0x7F21) {
×
117
            dataObjectCache.put(tag, response.getData());
×
118
        }
119
        return response.getData();
×
120
    }
121

122
    /**
123
     * Return the application identifier.
124
     */
125
    public byte[] getAID() throws CardException {
126
        return getData(0x4F);
×
127
    }
128

129
    /**
130
     * Return the version of the OpenPGP specification implemented by the card.
131
     */
132
    public float getVersion() throws CardException {
133
        byte[] aid = getAID();
×
134
        int major = aid[6];
×
135
        int minor = aid[7];
×
136
        return major + minor / 10f;
×
137
    }
138

139
    /**
140
     * Return the keys available for signing.
141
     */
142
    public Set<Key> getAvailableKeys() throws CardException {
143
        Set<Key> keys = new LinkedHashSet<>();
×
144

145
        for (Key key : Key.values()) {
×
146
            if (getKeyInfo(key).isPresent() && (key != Key.ENCRYPTION || supportsManageSecurityEnvironment())) {
×
147
                keys.add(key);
×
148
            }
149
        }
150

151
        return keys;
×
152
    }
153

154
    /**
155
     * Return the certificate for the specified key.
156
     */
157
    public byte[] getCertificate(Key key) throws CardException {
158
        if (key == Key.AUTHENTICATION) {
×
159
            return getData(0x7F21);
×
160
        }
161

162
        if (getVersion() < 3) {
×
163
            return new byte[0];
×
164
        }
165

166
        int position = 0;
×
167
        if (key == Key.ENCRYPTION) {
×
168
            position = 1;
×
169
        } else if (key == Key.SIGNATURE) {
×
170
            position = 2;
×
171
        }
172
        selectData(0x7F21, position);
×
173
        return getData(0x7F21);
×
174
    }
175

176
    /**
177
     * Return the key information for the specified key.
178
     */
179
    public KeyInfo getKeyInfo(Key key) throws CardException {
180
        if (keyInfos == null) {
×
181
            this.keyInfos = getKeyInfo();
×
182
        }
183
        return keyInfos[key.ordinal()];
×
184
    }
185

186
    private KeyInfo[] getKeyInfo() throws CardException {
187
        KeyInfo[] keyInfos = new KeyInfo[3];
×
188
        keyInfos[0] = new KeyInfo();
×
189
        keyInfos[1] = new KeyInfo();
×
190
        keyInfos[2] = new KeyInfo();
×
191

192
        TLV relatedData = TLV.parse(ByteBuffer.wrap(getData(0x6E)));
×
193

194
        // read the fingerprints
195
        TLV fingerprints = relatedData.find("73", "C5");
×
196
        if (fingerprints != null) {
×
197
            byte[] data = fingerprints.value();
×
198
            for (Key key : Key.values()) {
×
199
                byte[] fingerprint = new byte[20];
×
200
                System.arraycopy(data, 20 * key.ordinal(), fingerprint, 0, 20);
×
201
                keyInfos[key.ordinal()].fingerprint = fingerprint;
×
202
            }
203
        }
204

205
        // read the algorithm attributes
206
        for (Key key : Key.values()) {
×
207
            TLV algorithmAttributes = relatedData.find("73", "C" + (key.ordinal() + 1));
×
208
            ByteBuffer buffer = ByteBuffer.wrap(algorithmAttributes.value());
×
209
            keyInfos[key.ordinal()].algorithm = buffer.get();
×
210
            if (keyInfos[key.ordinal()].isRSA()) {
×
211
                keyInfos[key.ordinal()].size = buffer.getShort() & 0xFFFF;
×
212
            }
213
        }
214

215
        extendedCapabilities = relatedData.find("73", "C0").value();
×
216

217
        return keyInfos;
×
218
    }
219

220
    /**
221
     * Return the extended capabilities.
222
     */
223
    private byte[] getExtendedCapabilities() throws CardException {
224
        if (extendedCapabilities == null) {
×
225
            TLV relatedData = TLV.parse(ByteBuffer.wrap(getData(0x6E)));
×
226
            extendedCapabilities = relatedData.find("73", "C0").value();
×
227
        }
228
        return extendedCapabilities;
×
229
    }
230

231
    /**
232
     * Tell if the MANAGE SECURITY ENVIRONMENT command is supported.
233
     */
234
    protected boolean supportsManageSecurityEnvironment() throws CardException {
235
        return getVersion() > 3 && (getExtendedCapabilities()[9] & 0x01) != 0;
×
236
    }
237

238
    /**
239
     * Put the specified data object on the card.
240
     */
241
    public void putData(int tag, byte[] data) throws CardException {
242
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0xDA, (tag & 0xFF00) >> 8, tag & 0xFF, data)); // PUT DATA
×
243
        handleError(response);
×
244

245
        // clear the cache
246
        dataObjectCache.clear();
×
247
    }
×
248

249
    /**
250
     * Sign the specified data.
251
     *
252
     * @param key  the key to use for the signature
253
     * @param data the data to sign
254
     */
255
    public byte[] sign(Key key, byte[] data) throws CardException {
256
        if (key == Key.SIGNATURE) {
×
257
            verify(0, 0x81, pin);
×
258
            return computeDigitalSignature(data);
×
259
        } else {
260
            verify(0, 0x82, pin);
×
261
            if (key == Key.ENCRYPTION) {
×
262
                manageSecurityEnvironment(0xA4, (byte) 2);
×
263
            }
264
            return authenticate(data);
×
265
        }
266
    }
267

268
    /**
269
     * Sign the specified data.
270
     */
271
    public byte[] computeDigitalSignature(byte[] data) throws CardException {
272
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, data)); // COMPUTE DIGITAL SIGNATURE
×
273
        if (response.getSW() == 0x6a88) {
×
274
            throw new CardException("Signature key not found");
×
275
        }
276
        handleError(response);
×
277
        return response.getData();
×
278
    }
279

280
    /**
281
     * Sign the specified data with the authentication key.
282
     */
283
    public byte[] authenticate(byte[] data) throws CardException {
284
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x88, 0x00, 0x00, data)); // INTERNAL AUTHENTICATE
×
285
        if (response.getSW() == 0x6a88) {
×
286
            throw new CardException("Authentication key not found");
×
287
        }
288
        handleError(response);
×
289
        return response.getData();
×
290
    }
291

292
    /**
293
     * Assign the encryption of the authentication key to the DECIPHER and INTERNAL AUTHENTICATE operations
294
     *
295
     * @param p2     the operation (0xA4: INTERNAL AUTHENTICATE, 0xB8: DECIPHER)
296
     * @param keyRef the reference of the key (2: encryption key, 3: authentication key)
297
     */
298
    public void manageSecurityEnvironment(int p2, byte keyRef) throws CardException {
299
        ResponseAPDU response = transmit(new CommandAPDU(0x00, 0x22, 0x41, p2, new byte[] {(byte) 0x83, 0x01, keyRef})); // MANAGE SECURITY ENVIRONMENT
×
300
        handleError(response);
×
301
    }
×
302

303
    /**
304
     * Get the OpenPGP card.
305
     */
306
    public static OpenPGPCard getCard() throws CardException {
307
        return getCard(null);
×
308
    }
309

310
    /**
311
     * Get the OpenPGP card with the specified name.
312
     *
313
     * @param name the partial name of the card
314
     */
315
    public static OpenPGPCard getCard(String name) throws CardException {
316
        killSmartCardDaemon();
×
317

318
        CardChannel channel = openChannel(name);
×
319
        return channel != null ? new OpenPGPCard(channel) : null;
×
320
    }
321

322
    /**
323
     * Kill scdaemon to release the card.
324
     */
325
    private static void killSmartCardDaemon() {
326
        try {
327
            new ProcessBuilder("gpgconf", "--kill", "scdaemon").start().waitFor(5, TimeUnit.SECONDS);
×
328
        } catch (Exception e) {
×
329
            // gpgconf not found, let's continue
330
        }
×
331
    }
×
332
}
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