Skip to content

Commit

Permalink
Fix #362: add S16 mode to SCP03
Browse files Browse the repository at this point in the history
  • Loading branch information
martinpaljak committed Oct 15, 2024
1 parent 73d7aec commit a807da1
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 43 deletions.
12 changes: 9 additions & 3 deletions library/src/main/java/pro/javacard/gp/GPCrypto.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,23 @@ private GPCrypto() {
static final String AES_CBC_CIPHER = "AES/CBC/NoPadding";

// Shared random
public static final SecureRandom random;
private static final SecureRandom rnd;

static {
try {
random = SecureRandom.getInstance("SHA1PRNG");
random.nextBytes(new byte[2]); // Force seeding
rnd = SecureRandom.getInstance("SHA1PRNG");
rnd.nextBytes(new byte[2]); // Force seeding
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Need SecureRandom to run");
}
}

public static byte[] random(int num) {
byte[] bytes = new byte[num];
rnd.nextBytes(bytes);
return bytes;
}

public static byte[] pad80(byte[] text, int blocksize) {
int total = (text.length / blocksize + 1) * blocksize;
byte[] result = Arrays.copyOfRange(text, 0, total);
Expand Down
103 changes: 75 additions & 28 deletions library/src/main/java/pro/javacard/gp/GPSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,10 @@ void select(AID sdAID) throws GPException {

// If the ISD is locked, log it, but do not stop
if (resp.getSW() == 0x6283) {
logger.warn("SELECT ISD returned 6283 - CARD_LOCKED");
logger.warn("SELECT returned 6283 - CARD_LOCKED");
}

GPException.check(resp, "Could not SELECT Security Domain", 0x6283);
GPException.check(resp, "Could not SELECT", 0x6283);
parse_select_response(resp.getData());
}

Expand Down Expand Up @@ -309,10 +309,10 @@ private void parse_select_response(byte[] fci) throws GPException {
// SCP version
logger.debug("Auto-detected SCP version: {}", GPSecureChannelVersion.valueOf(data[7] & 0xFF, data[8] & 0xFF));
} else {
logger.warn("Unrecognized card recgnition data: {}", HexUtils.bin2hex(oidtag.getBytesValue()));
logger.warn("Unrecognized card recognition data: {}", HexUtils.bin2hex(oidtag.getBytesValue()));
}
} else {
logger.warn("Not global platform OID");
logger.warn("No Global Platform OID found");
}
}

Expand Down Expand Up @@ -368,18 +368,25 @@ private void normalizeSecurityLevel(EnumSet<APDUMode> securityLevel) {
}

/*
* Establishes a secure channel to the security domain or application
* Establishes a secure channel (INITIALIZE UPDATE + EXTERNAL AUTHENTICATE) to a security domain or application
*/
public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[] host_challenge, EnumSet<APDUMode> securityLevel)
throws IOException, GPException {

normalizeSecurityLevel(securityLevel);

logger.info("Using card master keys with version {} for setting up session with {} ", keys.getKeyInfo().getVersion(), securityLevel.stream().map(Enum::name).collect(Collectors.joining(", ")));
logger.info("Using card master key(s) with version {} for setting up session with {} ", keys.getKeyInfo().getVersion(), securityLevel.stream().map(Enum::name).collect(Collectors.joining(", ")));

// XXX: more explicit SCP indication from tool
boolean s16 = (scp != null && scp.scp == SCP03 && (scp.i & 0x01) == 0x01) || (host_challenge != null && host_challenge.length == 16);

if (s16) {
logger.debug("Using S16 mode");
}

// DWIM: Generate host challenge
if (host_challenge == null) {
host_challenge = new byte[8];
GPCrypto.random.nextBytes(host_challenge);
host_challenge = GPCrypto.random(s16 ? 16 : 8);
logger.trace("Generated host challenge: " + HexUtils.bin2hex(host_challenge));
}

Expand All @@ -391,6 +398,14 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[
ResponseAPDU response = channel.transmit(initUpdate);
int sw = response.getSW();

// XXX: Handle 6700 and try again with S16 mode
if (sw == 0x6700 && !s16) {
logger.warn("Wrong length with implicit S8 mode. Hoping for S16 mode and trying again.");
s16 = true;
host_challenge = GPCrypto.random(s16 ? 16 : 8);
response = channel.transmit(new CommandAPDU(CLA_GP, INS_INITIALIZE_UPDATE, keys.getKeyInfo().getVersion(), init_p2, host_challenge, 256));
}

// Detect and report locked cards in a more sensible way.
if ((sw == SW_SECURITY_STATUS_NOT_SATISFIED) || (sw == SW_AUTHENTICATION_METHOD_BLOCKED)) {
throw new GPException(sw, "INITIALIZE UPDATE failed, card LOCKED?");
Expand All @@ -400,14 +415,51 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[
GPException.check(response, "INITIALIZE UPDATE failed");
byte[] update_response = response.getData();

// Verify response length (SCP01/SCP02 + SCP03 + SCP03 w/ pseudorandom)
if (update_response.length != 28 && update_response.length != 29 && update_response.length != 32) {
// SCP01: kdd (10) | key info (2) | card challenge (8) | card cryptogram (8) = 28
// SCP02: kdd (10) | key info (2) | seq (2) | card challenge (6) | card cryptogram (8) = 28
// SCP03 S8: kdd (10) | key info (3) | card challenge (8) | card cryptogram (8) | seq (3, optional) = 29 (32)
// SCP03 S16: kdd (10) | key info (3) | card challenge (16) | card cryptogram (16) | seq (3, optional) = 45 (48)
// key info = kvn | scp | i (scp03) or kvn | scp (scp01/02)

// Minimal length, as we look into fixed offsets
if (update_response.length < 28) {
throw new GPDataException("INITIALIZE UPDATE response with too small length", update_response);
}

int update_len = 0;

switch (update_response[11]) {
case 0x01:
case 0x02:
update_len = 28;
break;
case 0x03:
update_len = 29;
int i = update_response[12];
if ((i & 0x10) == 0x10) {
update_len += 3;
}
if ((i & 0x01) == 0x01) {
if (!s16) {
logger.warn("S16 mode requested but not reported by card!");
}
update_len += 16; // +8 for both challenges
}
break;
default:
throw new GPDataException("Unsupported SCP version", update_response);
}

// Verify response length (SCP01/SCP02 + SCP03 + SCP03 w/ pseudorandom + SCP03 w/ S16)
if (update_len != update_response.length) {
throw new GPException("Invalid INITIALIZE UPDATE response length: " + update_response.length);
}

// Parse the response
int offset = 0;
byte[] diversification_data = Arrays.copyOfRange(update_response, 0, 10);
offset += diversification_data.length;

// Get used key version from response
scpKeyVersion = update_response[offset] & 0xFF;
offset++;
Expand All @@ -425,28 +477,24 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[
}

// get card challenge
byte[] card_challenge = Arrays.copyOfRange(update_response, offset, offset + 8);
byte[] card_challenge = Arrays.copyOfRange(update_response, offset, offset + (s16 ? 16 : 8));
offset += card_challenge.length;

// get card cryptogram
byte[] card_cryptogram = Arrays.copyOfRange(update_response, offset, offset + 8);
byte[] card_cryptogram = Arrays.copyOfRange(update_response, offset, offset + (s16 ? 16 : 8));
offset += card_cryptogram.length;

// Extract ssc
final byte[] seq;
if (this.scpVersion.scp == SCP02) {
seq = Arrays.copyOfRange(update_response, 12, 14);
} else if (this.scpVersion.scp == SCP03 && update_response.length == 32) {
seq = Arrays.copyOfRange(update_response, offset, 32);
offset += seq.length;
} else if (this.scpVersion.scp == SCP03 && (this.scpVersion.i & 0x10) == 0x10) {
// XXX instead of throwing if missing, show an error.
seq = Arrays.copyOfRange(update_response, offset, offset + 3);
} else {
seq = null;
}

if (offset != update_response.length) {
logger.error("Unhandled data in INITIALIZE UPDATE response: {}", HexUtils.bin2hex(Arrays.copyOfRange(update_response, offset, update_response.length)));
}

logger.debug("KDD: {}", HexUtils.bin2hex(diversification_data));
if (seq != null)
logger.debug("SSC: {}", HexUtils.bin2hex(seq));
Expand All @@ -466,24 +514,24 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[
logger.warn("SCP01 does not support RMAC, removing.");
}

// Give the card key a chance to be automatically diversifed based on KDD from INITIALIZE UPDATE
// Give the card key a chance to be automatically diversified based on KDD from INITIALIZE UPDATE
cardKeys = keys.diversify(this.scpVersion.scp, diversification_data);

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

// Check pseudorandom card challenge. This must happen _after_ key diversification.
// Check pseudorandom card challenge. NOTE: this MUST happen _after_ key diversification.
if (scpVersion.scp == SCP03 && (scpVersion.i & 0x10) == 0x10) {
byte[] ctx = GPUtils.concatenate(seq, this.sdAID.getBytes());
logger.trace("Challenge calculation context: {}", HexUtils.bin2hex(ctx));
byte[] my_card_challenge = keys.scp3_kdf(KeyPurpose.ENC, GPCrypto.scp03_kdf_blocka((byte) 0x02, 64), ctx, 8);
// XXX: remove double length in kdf invocation and harmonize bits vs bytes
byte[] my_card_challenge = keys.scp3_kdf(KeyPurpose.ENC, GPCrypto.scp03_kdf_blocka((byte) 0x02, s16 ? 128 : 64), ctx, s16 ? 16 : 8);
if (!Arrays.equals(my_card_challenge, card_challenge)) {
logger.warn("Pseudorandom card challenge does not match expected: {} vs {}", HexUtils.bin2hex(my_card_challenge), HexUtils.bin2hex(card_challenge));
} else {
logger.debug("Pseudorandom card challenge matches expected value: {}", HexUtils.bin2hex(my_card_challenge));
}
}


// Derive session keys
if (this.scpVersion.scp == GPSecureChannelVersion.SCP.SCP02) {
sessionContext = seq.clone();
Expand All @@ -502,7 +550,7 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[
if (this.scpVersion.scp == SCP01 || this.scpVersion.scp == SCP02) {
my_card_cryptogram = GPCrypto.mac_3des(cntx, encKey, new byte[8]);
} else {
my_card_cryptogram = GPCrypto.scp03_kdf(macKey, (byte) 0x00, cntx, 64);
my_card_cryptogram = GPCrypto.scp03_kdf(macKey, (byte) 0x00, cntx, s16 ? 128 : 64);
}

// This is the main check for possible successful authentication.
Expand All @@ -527,8 +575,8 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[
wrapper = new SCP02Wrapper(encKey, macKey, rmacKey, blockSize);
break;
case SCP03:
host_cryptogram = GPCrypto.scp03_kdf(macKey, (byte) 0x01, cntx, 64);
wrapper = new SCP03Wrapper(encKey, macKey, rmacKey, blockSize);
host_cryptogram = GPCrypto.scp03_kdf(macKey, (byte) 0x01, cntx, s16 ? 128 : 64);
wrapper = new SCP03Wrapper(encKey, macKey, rmacKey, blockSize, s16);
break;
default:
throw new IllegalStateException("Unknown SCP");
Expand Down Expand Up @@ -927,8 +975,7 @@ private byte[] encodeKey(GPCardKeys dek, byte[] other, GPKeyInfo.GPKey type) {
if (type == GPKey.AES) {
// Pad with random
int n = other.length % 16 + 1;
byte[] plaintext = new byte[n * other.length];
GPCrypto.random.nextBytes(plaintext);
byte[] plaintext = GPCrypto.random(n * other.length);
System.arraycopy(other, 0, plaintext, 0, other.length);

byte[] cgram = dek.encrypt(plaintext, sessionContext);
Expand Down
23 changes: 14 additions & 9 deletions library/src/main/java/pro/javacard/gp/SCP03Wrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ class SCP03Wrapper extends SecureChannelWrapper {
private String buggyCounterEnv = System.getenv().getOrDefault(COUNTER_WORKAROUND.replace(".", "_").toUpperCase(), "false");
private boolean counterIsBuggy = System.getProperty(COUNTER_WORKAROUND, buggyCounterEnv).equalsIgnoreCase("true");

SCP03Wrapper(byte[] enc, byte[] mac, byte[] rmac, int bs) {
private boolean s16 = false; // S16 mode
SCP03Wrapper(byte[] enc, byte[] mac, byte[] rmac, int bs, boolean s16) {
super(enc, mac, rmac, bs);
this.s16 = s16;
}

@Override
protected CommandAPDU wrap(CommandAPDU command) throws GPException {
byte[] cmd_mac = null;
int maclen = s16 ? 16 : 8;

try {
int cla = command.getCLA();
Expand Down Expand Up @@ -77,7 +80,7 @@ protected CommandAPDU wrap(CommandAPDU command) throws GPException {
// Calculate C-MAC
if (mac) {
cla |= 0x4;
lc = lc + 8;
lc = lc + maclen;

ByteArrayOutputStream bo = new ByteArrayOutputStream();
bo.write(chaining_value);
Expand All @@ -91,8 +94,8 @@ protected CommandAPDU wrap(CommandAPDU command) throws GPException {
byte[] cmac = GPCrypto.aes_cmac(macKey, cmac_input, 128);
// Set new chaining value
System.arraycopy(cmac, 0, chaining_value, 0, chaining_value.length);
// 8 bytes for actual mac
cmd_mac = Arrays.copyOf(cmac, 8);
// 8 or 16 bytes for actual mac
cmd_mac = Arrays.copyOf(cmac, maclen);
}
// Constructing a new command APDU ensures that the coding of LC and NE is correct; especially for Extend Length APDUs
CommandAPDU newAPDU = null;
Expand Down Expand Up @@ -120,9 +123,11 @@ protected CommandAPDU wrap(CommandAPDU command) throws GPException {

@Override
protected ResponseAPDU unwrap(ResponseAPDU response) throws GPException {
int maclen = s16 ? 16 : 8;

try {
if (rmac) {
if (response.getData().length < 8) {
if (response.getData().length < maclen) {
// Per GP 2.2, Amendment D, v1.1.1(+), section 6.2.5, all non-error R-APDUs must have a MAC.
// R-APDUs representing an error status shall not have a data segment or MAC.
if (response.getSW() == 0x9000 || response.getSW1() == 0x62 || response.getSW1() == 0x63) {
Expand All @@ -134,10 +139,10 @@ protected ResponseAPDU unwrap(ResponseAPDU response) throws GPException {
// We therefore return unaltered.
return response;
}
int respLen = response.getData().length - 8;
int respLen = response.getData().length - maclen;

byte[] actualMac = new byte[8];
System.arraycopy(response.getData(), respLen, actualMac, 0, 8);
byte[] actualMac = new byte[maclen];
System.arraycopy(response.getData(), respLen, actualMac, 0, maclen);

ByteArrayOutputStream bo = new ByteArrayOutputStream();
bo.write(chaining_value);
Expand All @@ -150,7 +155,7 @@ protected ResponseAPDU unwrap(ResponseAPDU response) throws GPException {
byte[] cmac = GPCrypto.aes_cmac(rmacKey, cmac_input, 128);

// 8 bytes for actual mac
byte[] resp_mac = Arrays.copyOf(cmac, 8);
byte[] resp_mac = Arrays.copyOf(cmac, maclen);

if (!Arrays.equals(resp_mac, actualMac)) {
throw new GPException("RMAC invalid: " + HexUtils.bin2hex(actualMac) + " vs " + HexUtils.bin2hex(resp_mac));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ abstract class GPCommandLineInterface {
protected static OptionSpec<Void> OPT_HELP = parser.acceptsAll(Arrays.asList("h", "?", "help"), "Shows this help").forHelp();
protected static OptionSpec<AID> OPT_CONNECT = parser.acceptsAll(Arrays.asList("c", "connect"), "Connect to app/domain").withRequiredArg().ofType(AID.class);
protected static OptionSpec<Void> OPT_DEBUG = parser.acceptsAll(Arrays.asList("d", "debug"), "Show PC/SC and APDU trace");
protected static OptionSpec<Void> OPT_S16 = parser.accepts("s16", "Use SCP03 S16 mode");

protected static OptionSpec<Void> OPT_VERBOSE = parser.acceptsAll(Arrays.asList("v", "verbose"), "Be verbose about operations");
protected static OptionSpec<String> OPT_READER = parser.acceptsAll(Arrays.asList("r", "reader"), "Use specific reader").withOptionalArg().describedAs("reader");
protected static OptionSpec<Void> OPT_LIST = parser.acceptsAll(Arrays.asList("l", "list"), "List the contents of the card");
Expand Down Expand Up @@ -69,6 +71,9 @@ abstract class GPCommandLineInterface {
protected static OptionSpec<AID> OPT_DELETE = parser.accepts("delete", "Delete applet/package").withRequiredArg().ofType(AID.class);

protected static OptionSpec<Void> OPT_DEFAULT = parser.accepts("default", "Indicate Default Selected privilege");
protected static OptionSpec<Void> OPT_DEFAULT_CONTACT = parser.accepts("default-contact", "Default Selected on contact interface");
protected static OptionSpec<Void> OPT_DEFAULT_CONTACTLESS = parser.accepts("default-contactless", "Default Selected on contactless interface");

protected static OptionSpec<AID> OPT_DOMAIN = parser.accepts("domain", "Create supplementary security domain").withRequiredArg().ofType(AID.class);

// Card an applet lifecycle management
Expand Down
3 changes: 2 additions & 1 deletion tool/src/main/java/pro/javacard/gptool/GPTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,8 @@ public int run(BIBO bibo, String[] argv) {
// IMPORTANT PLACE. Possibly brick the card now, if keys don't match.

try {
gp.openSecureChannel(keys, null, null, mode);
// S16 will be gracefully tried if default (S8) returns wrong length, but can be forced by challenge length
gp.openSecureChannel(keys, null, args.has(OPT_S16) ? GPCrypto.random(16) : null, mode);
} catch (GPException e) {
System.err.println("Failed to open secure channel: " + e.getMessage() + "\nRead more from https://github.com/martinpaljak/GlobalPlatformPro/wiki/Keys");
return 1;
Expand Down
3 changes: 1 addition & 2 deletions tool/src/main/java/pro/javacard/gptool/PlaintextKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,7 @@ public byte[] encryptKey(GPCardKeys key, KeyPurpose p, byte[] sessionContext) th
byte[] otherkey = other.cardKeys.get(p);
// Pad with random
int n = otherkey.length % 16 + 1;
byte[] plaintext = new byte[n * otherkey.length];
GPCrypto.random.nextBytes(plaintext);
byte[] plaintext = GPCrypto.random(n * otherkey.length);
System.arraycopy(otherkey, 0, plaintext, 0, otherkey.length);
// encrypt
return GPCrypto.aes_cbc(plaintext, cardKeys.get(KeyPurpose.DEK), new byte[16]);
Expand Down

0 comments on commit a807da1

Please sign in to comment.