Skip to content

Commit 0a0596b

Browse files
committed
Fix DM with 2kRSA; add receipt verification (AES only)
1 parent 3ee023d commit 0a0596b

File tree

8 files changed

+247
-42
lines changed

8 files changed

+247
-42
lines changed

library/src/main/java/pro/javacard/gp/DMTokenizer.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,7 @@ protected boolean canTokenize(CommandAPDU apdu) {
111111
protected byte[] getToken(CommandAPDU apdu) {
112112
int keylen = (privateKey.getModulus().bitLength() + 7) / 8;
113113
byte[] dtbs = dtbs(apdu);
114-
log.info("Signing DM with {} RSA", privateKey.getModulus().bitLength());
115-
log.debug("DM token contents: {}", HexUtils.bin2hex(dtbs));
114+
116115
try {
117116
final byte[] token;
118117
if (keylen == 128) {
@@ -131,7 +130,7 @@ protected byte[] getToken(CommandAPDU apdu) {
131130
signer.update(dtbs);
132131
token = signer.sign();
133132
}
134-
log.debug("DM token: {}", HexUtils.bin2hex(token));
133+
log.trace("DM token: {}", HexUtils.bin2hex(token));
135134
return token;
136135
} catch (GeneralSecurityException e) {
137136
throw new GPException("Can not calculate DM token: " + e.getMessage(), e);

library/src/main/java/pro/javacard/gp/GPSession.java

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public class GPSession {
105105
private APDUBIBO channel;
106106
private GPRegistry registry = null;
107107
private DMTokenizer tokenizer = DMTokenizer.none();
108+
private ReceiptVerifier verifier = new ReceiptVerifier.NullVerifier();
109+
108110
private boolean dirty = true; // True if registry is dirty.
109111

110112
/*
@@ -220,6 +222,10 @@ public void setTokenizer(DMTokenizer tokenizer) {
220222
this.tokenizer = tokenizer;
221223
}
222224

225+
public void setVerifier(ReceiptVerifier verifier) {
226+
this.verifier = verifier;
227+
}
228+
223229
public DMTokenizer getTokenizer() {
224230
return tokenizer;
225231
}
@@ -433,24 +439,12 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[
433439
} else if (this.scpVersion.scp == SCP03 && update_response.length == 32) {
434440
seq = Arrays.copyOfRange(update_response, offset, 32);
435441
offset += seq.length;
436-
437-
if ((scpVersion.i & 0x10) == 0x10) {
438-
byte[] ctx = GPUtils.concatenate(seq, this.sdAID.getBytes());
439-
logger.trace("Challenge calculation context: {}", HexUtils.bin2hex(ctx));
440-
byte[] my_card_challenge = keys.scp3_kdf(KeyPurpose.ENC, GPCrypto.scp03_kdf_blocka((byte) 0x02, 64), ctx, 8);
441-
if (!Arrays.equals(my_card_challenge, card_challenge)) {
442-
logger.warn("Pseudorandom card challenge does not match expected: {} vs {}", HexUtils.bin2hex(my_card_challenge), HexUtils.bin2hex(card_challenge));
443-
} else {
444-
logger.debug("Pseudorandom card challenge matches expected value: {}", HexUtils.bin2hex(my_card_challenge));
445-
}
446-
}
447442
} else {
448443
seq = null;
449444
}
450445

451446
if (offset != update_response.length) {
452447
logger.error("Unhandled data in INITIALIZE UPDATE response: {}", HexUtils.bin2hex(Arrays.copyOfRange(update_response, offset, update_response.length)));
453-
//throw new GPDataException("Unhandled data in INITIALIZE UPDATE response", Arrays.copyOfRange(update_response, offset, update_response.length));
454448
}
455449

456450
logger.debug("KDD: {}", HexUtils.bin2hex(diversification_data));
@@ -477,6 +471,19 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[
477471

478472
logger.info("Diversified card keys: {}", cardKeys);
479473

474+
// Check pseudorandom card challenge. This must happen _after_ key diversification.
475+
if (scpVersion.scp == SCP03 && (scpVersion.i & 0x10) == 0x10) {
476+
byte[] ctx = GPUtils.concatenate(seq, this.sdAID.getBytes());
477+
logger.trace("Challenge calculation context: {}", HexUtils.bin2hex(ctx));
478+
byte[] my_card_challenge = keys.scp3_kdf(KeyPurpose.ENC, GPCrypto.scp03_kdf_blocka((byte) 0x02, 64), ctx, 8);
479+
if (!Arrays.equals(my_card_challenge, card_challenge)) {
480+
logger.warn("Pseudorandom card challenge does not match expected: {} vs {}", HexUtils.bin2hex(my_card_challenge), HexUtils.bin2hex(card_challenge));
481+
} else {
482+
logger.debug("Pseudorandom card challenge matches expected value: {}", HexUtils.bin2hex(my_card_challenge));
483+
}
484+
}
485+
486+
480487
// Derive session keys
481488
if (this.scpVersion.scp == GPSecureChannelVersion.SCP.SCP02) {
482489
sessionContext = seq.clone();
@@ -540,11 +547,23 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[
540547
// Pipe through secure channel
541548
public ResponseAPDU transmit(CommandAPDU command) throws IOException {
542549
try {
543-
// TODO: BIBO pretty printer
544-
//logger.trace("PT> {}", HexUtils.bin2hex(command.getBytes()));
545-
ResponseAPDU unwrapped = wrapper.unwrap(channel.transmit(wrapper.wrap(command)));
546-
//logger.trace("PT < {}", HexUtils.bin2hex(unwrapped.getBytes()));
547-
return unwrapped;
550+
CommandAPDU wrapped = wrapper.wrap(command);
551+
ResponseAPDU resp = null;
552+
553+
// GPC 2.3.1 11.1.5.1
554+
List<byte[]> chunks = GPUtils.splitArray(wrapped.getData(), blockSize);
555+
if (chunks.size() > 1)
556+
logger.debug("Chaining in {} chunks", chunks.size());
557+
558+
for (int i = 0; i < chunks.size(); i++) {
559+
boolean last = i == chunks.size() - 1;
560+
int p1 = last ? command.getP1() : command.getP1() | 0x80; // XXX: should check if instruction is eligible for this treatment
561+
resp = channel.transmit(new CommandAPDU(wrapped.getCLA(), wrapped.getINS(), p1, wrapped.getP2(), chunks.get(i), 256));
562+
if (!last) {
563+
GPException.check(resp);
564+
}
565+
}
566+
return wrapper.unwrap(resp);
548567
} catch (GPException e) {
549568
throw new IOException("Secure channel failure: " + e.getMessage(), e);
550569
}
@@ -584,13 +603,13 @@ public void loadCapFile(CAPFile cap, AID targetDomain, AID dapDomain, byte[] dap
584603
byte[] hash = hashFunction == null ? new byte[0] : cap.getLoadFileDataHash(hashFunction.algo);
585604
byte[] code = cap.getCode();
586605
byte[] loadParams = new byte[0]; // FIXME
587-
606+
AID pkg = cap.getPackageAID();
588607

589608
ByteArrayOutputStream bo = new ByteArrayOutputStream();
590609

591610
try {
592-
bo.write(cap.getPackageAID().getLength());
593-
bo.write(cap.getPackageAID().getBytes());
611+
bo.write(pkg.getLength());
612+
bo.write(pkg.getBytes());
594613

595614
bo.write(targetDomain.getLength());
596615
bo.write(targetDomain.getBytes());
@@ -609,6 +628,7 @@ public void loadCapFile(CAPFile cap, AID targetDomain, AID dapDomain, byte[] dap
609628
command = tokenizer.tokenize(command);
610629
ResponseAPDU response = transmitLV(command);
611630
GPException.check(response, "INSTALL [for load] failed");
631+
verifier.check(response, ReceiptVerifier.load(pkg, targetDomain));
612632

613633
// Construct load block
614634
ByteArrayOutputStream loadBlock = new ByteArrayOutputStream();
@@ -637,7 +657,7 @@ public void loadCapFile(CAPFile cap, AID targetDomain, AID dapDomain, byte[] dap
637657

638658
for (int i = 0; i < blocks.size(); i++) {
639659
byte p1 = (i == (blocks.size() - 1)) ? P1_LAST_BLOCK : P1_MORE_BLOCKS;
640-
CommandAPDU load = new CommandAPDU(CLA_GP, INS_LOAD, p1, (byte) i, blocks.get(i));
660+
CommandAPDU load = new CommandAPDU(CLA_GP, INS_LOAD, p1, (byte) i, blocks.get(i), 256);
641661
response = transmit(load);
642662
GPException.check(response, "LOAD failed");
643663
}
@@ -654,6 +674,8 @@ public void installAndMakeSelectable(AID packageAID, AID appletAID, AID instance
654674
command = tokenizer.tokenize(command);
655675
ResponseAPDU response = transmitLV(command);
656676
GPException.check(response, "INSTALL [for install and make selectable] failed");
677+
678+
verifier.check(response, ReceiptVerifier.install_make_selectable(packageAID, instanceAID));
657679
dirty = true;
658680
}
659681

@@ -664,6 +686,7 @@ private byte[] buildInstallData(AID packageAID, AID appletAID, AID instanceAID,
664686
if (installParams == null || installParams.length == 0) {
665687
installParams = new byte[]{(byte) 0xC9, 0x00};
666688
}
689+
// FIXME: if the parameters parse as tlv, check for presence of 0xC9 before prepending
667690
// Simple use: only application parameters without tag, prepend 0xC9
668691
if (installParams[0] != (byte) 0xC9) {
669692
byte[] newparams = new byte[installParams.length + 2];
@@ -719,6 +742,8 @@ public void extradite(AID what, AID to) throws GPException, IOException {
719742
command = tokenizer.tokenize(command);
720743
ResponseAPDU response = transmitLV(command);
721744
GPException.check(response, "INSTALL [for extradition] failed");
745+
746+
verifier.check(response, ReceiptVerifier.extradite(sdAID, what, to));
722747
dirty = true;
723748
}
724749

@@ -848,6 +873,7 @@ public void deleteAID(AID aid, boolean deleteDeps) throws GPException, IOExcepti
848873
command = tokenizer.tokenize(command);
849874
ResponseAPDU response = transmitTLV(command);
850875
GPException.check(response, "DELETE failed");
876+
verifier.check(response, ReceiptVerifier.delete(aid));
851877
dirty = true;
852878
}
853879

@@ -858,7 +884,7 @@ public void deleteKey(Integer keyver, Integer keyid) throws GPException, IOExcep
858884
throw new IllegalArgumentException("Must specify either key version or key ID");
859885

860886
ByteArrayOutputStream bo = new ByteArrayOutputStream();
861-
if (keyid != null ) {
887+
if (keyid != null) {
862888
bo.write(0xd0); // Key Identifier
863889
bo.write(1);
864890
bo.write(0x01);
@@ -981,10 +1007,10 @@ byte[] encodeRSAKey(RSAPublicKey key) {
9811007
byte[] exponent = GPUtils.positive(key.getPublicExponent());
9821008

9831009
bo.write(0xA1); // Modulus
984-
bo.write(modulus.length);
1010+
bo.write(GPUtils.encodeLength(modulus.length));
9851011
bo.write(modulus);
9861012
bo.write(0xA0);
987-
bo.write(exponent.length);
1013+
bo.write(GPUtils.encodeLength(exponent.length));
9881014
bo.write(exponent);
9891015
bo.write(0x00); // No KCV
9901016
} catch (IOException e) {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* GlobalPlatformPro - GlobalPlatform tool
3+
*
4+
* Copyright (C) 2024-present Martin Paljak, [email protected]
5+
*
6+
* This library is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3.0 of the License, or (at your option) any later version.
10+
*
11+
* This library is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public
17+
* License along with this library; if not, write to the Free Software
18+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19+
*/
20+
package pro.javacard.gp;
21+
22+
import apdu4j.core.HexUtils;
23+
import apdu4j.core.ResponseAPDU;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
import pro.javacard.capfile.AID;
27+
28+
import java.io.ByteArrayOutputStream;
29+
import java.io.IOException;
30+
import java.io.UncheckedIOException;
31+
import java.util.Arrays;
32+
33+
public abstract class ReceiptVerifier {
34+
35+
private static final Logger log = LoggerFactory.getLogger(ReceiptVerifier.class);
36+
37+
static byte[] get_receipt(byte[] response) {
38+
return Arrays.copyOfRange(response, 2, 2 + response[1]);
39+
}
40+
41+
static byte[] get_confirmation_data(byte[] response) {
42+
return Arrays.copyOfRange(response, 2 + response[1], response[0] + 1);
43+
}
44+
45+
abstract boolean check(ResponseAPDU response, byte[] context);
46+
47+
static public class AESReceiptVerifier extends ReceiptVerifier {
48+
private final byte[] aes_key;
49+
50+
public AESReceiptVerifier(byte[] aesKey) {
51+
aes_key = aesKey.clone();
52+
}
53+
54+
@Override
55+
boolean check(ResponseAPDU response, byte[] context) {
56+
byte[] data = response.getData();
57+
if (data[0] == 0x00) {
58+
log.debug("No receipt");
59+
return true;
60+
}
61+
try {
62+
GPUtils.trace_lv(Arrays.copyOfRange(data, 1, data[0]), log);
63+
} catch (Exception e) {
64+
log.error("Invalid LV in response: {}", HexUtils.bin2hex(data));
65+
}
66+
byte[] card = get_receipt(data);
67+
byte[] confdata = get_confirmation_data(data);
68+
69+
byte[] my = GPCrypto.scp03_mac(aes_key, GPUtils.concatenate(confdata, context), 128);
70+
boolean verified = Arrays.equals(my, card);
71+
if (!verified) {
72+
log.error("Receipt verification: {}", verified);
73+
} else {
74+
log.info("Receipt verification: {}", verified);
75+
}
76+
77+
return verified;
78+
}
79+
}
80+
81+
static public class NullVerifier extends ReceiptVerifier {
82+
83+
public NullVerifier() {
84+
}
85+
86+
@Override
87+
boolean check(ResponseAPDU response, byte[] context) {
88+
return true;
89+
}
90+
}
91+
92+
public static byte[] load(AID pkg, AID sd) {
93+
try {
94+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
95+
baos.write(pkg.getLength());
96+
baos.write(pkg.getBytes());
97+
baos.write(sd.getLength());
98+
baos.write(sd.getBytes());
99+
return baos.toByteArray();
100+
} catch (IOException e) {
101+
throw new UncheckedIOException(e);
102+
}
103+
}
104+
105+
public static byte[] install_make_selectable(AID pkg, AID instance) {
106+
try {
107+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
108+
baos.write(pkg.getLength());
109+
baos.write(pkg.getBytes());
110+
baos.write(instance.getLength());
111+
baos.write(instance.getBytes());
112+
return baos.toByteArray();
113+
} catch (IOException e) {
114+
throw new UncheckedIOException(e);
115+
}
116+
}
117+
118+
119+
public static byte[] extradite(AID from, AID what, AID to) {
120+
try {
121+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
122+
baos.write(from.getLength());
123+
baos.write(from.getBytes());
124+
baos.write(what.getLength());
125+
baos.write(what.getBytes());
126+
baos.write(to.getLength());
127+
baos.write(to.getBytes());
128+
return baos.toByteArray();
129+
} catch (IOException e) {
130+
throw new UncheckedIOException(e);
131+
}
132+
}
133+
134+
public static byte[] delete(AID what) {
135+
try {
136+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
137+
baos.write(what.getLength());
138+
baos.write(what.getBytes());
139+
return baos.toByteArray();
140+
} catch (IOException e) {
141+
throw new UncheckedIOException(e);
142+
}
143+
}
144+
145+
// TODO: registry update
146+
// TODO: Combined Load, Install and Make Selectable
147+
}

library/src/main/java/pro/javacard/gp/SCP03Wrapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ protected CommandAPDU wrap(CommandAPDU command) throws GPException {
9999
// 8 bytes for actual mac
100100
cmd_mac = Arrays.copyOf(cmac, 8);
101101
}
102-
// Constructing new a new command APDU ensures that the coding of LC and NE is correct; especially for Extend Length APDUs
102+
// Constructing a new command APDU ensures that the coding of LC and NE is correct; especially for Extend Length APDUs
103103
CommandAPDU newAPDU = null;
104104

105105
ByteArrayOutputStream newData = new ByteArrayOutputStream();

tool/src/main/java/pro/javacard/gptool/GPCommandLineInterface.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ abstract class GPCommandLineInterface {
108108
// Delegated management
109109
protected static OptionSpec<Key> OPT_DM_KEY = parser.accepts("dm-key", "Delegated Management key").withRequiredArg().ofType(Key.class).describedAs("PEM or hex");
110110
protected static OptionSpec<HexBytes> OPT_DM_TOKEN = parser.accepts("dm-token", "Delegated Management token").availableUnless(OPT_DM_KEY).withRequiredArg().ofType(HexBytes.class).describedAs("token");
111+
protected static OptionSpec<HexBytes> OPT_RECEIPT_KEY = parser.accepts("receipt-key", "Receipt verification key (AES)").withRequiredArg().ofType(HexBytes.class).describedAs("key");
111112

112113
// SSD-s
113114
protected static OptionSpec<AID> OPT_MOVE = parser.accepts("move", "Move something").withRequiredArg().ofType(AID.class);
@@ -117,11 +118,11 @@ abstract class GPCommandLineInterface {
117118

118119
// DAP
119120
protected static OptionSpec<AID> OPT_DAP_DOMAIN = parser.accepts("dap-domain", "Domain to use for DAP verification").withRequiredArg().ofType(AID.class);
120-
protected static OptionSpec<Void> OPT_SHA256 = parser.accepts("sha256", "Use SHA-256 for LFDB hash, not SHA-1");
121+
protected static OptionSpec<Void> OPT_SHA256 = parser.accepts("sha256", "Use SHA-256 for LFDB hash");
122+
protected static OptionSpec<Void> OPT_SHA1 = parser.accepts("sha1", "Use SHA-1 for LFDB hash");
121123

122124
protected static OptionSpec<Key> OPT_DAP_KEY = parser.accepts("dap-key", "DAP key").withRequiredArg().ofType(Key.class).describedAs("PEM or hex");
123125
protected static OptionSpec<HexBytes> OPT_DAP_SIGNATURE = parser.accepts("dap-signature", "DAP signature").availableUnless(OPT_DAP_KEY).withRequiredArg().ofType(HexBytes.class).describedAs("signature");
124-
protected static OptionSpec<File> OPT_DAP_SIGN = parser.accepts("dap-sign", "Create DAP signature").availableIf(OPT_DAP_KEY).withRequiredArg().ofType(File.class).describedAs("CAP file");
125126

126127
// Personalization and store data
127128
protected static OptionSpec<HexBytes> OPT_STORE_DATA = parser.accepts("store-data", "STORE DATA blob").withRequiredArg().ofType(HexBytes.class).describedAs("data");

0 commit comments

Comments
 (0)