From a807da1efe2a5fcad53033056728522ef808cb98 Mon Sep 17 00:00:00 2001 From: Martin Paljak Date: Tue, 15 Oct 2024 09:43:35 +0300 Subject: [PATCH] Fix #362: add S16 mode to SCP03 --- .../main/java/pro/javacard/gp/GPCrypto.java | 12 +- .../main/java/pro/javacard/gp/GPSession.java | 103 +++++++++++++----- .../java/pro/javacard/gp/SCP03Wrapper.java | 23 ++-- .../gptool/GPCommandLineInterface.java | 5 + .../main/java/pro/javacard/gptool/GPTool.java | 3 +- .../pro/javacard/gptool/PlaintextKeys.java | 3 +- 6 files changed, 106 insertions(+), 43 deletions(-) diff --git a/library/src/main/java/pro/javacard/gp/GPCrypto.java b/library/src/main/java/pro/javacard/gp/GPCrypto.java index 176f3ca4..4cff4181 100644 --- a/library/src/main/java/pro/javacard/gp/GPCrypto.java +++ b/library/src/main/java/pro/javacard/gp/GPCrypto.java @@ -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); diff --git a/library/src/main/java/pro/javacard/gp/GPSession.java b/library/src/main/java/pro/javacard/gp/GPSession.java index a798d6ee..60aa72cf 100644 --- a/library/src/main/java/pro/javacard/gp/GPSession.java +++ b/library/src/main/java/pro/javacard/gp/GPSession.java @@ -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()); } @@ -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"); } } @@ -368,18 +368,25 @@ private void normalizeSecurityLevel(EnumSet 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 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)); } @@ -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?"); @@ -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++; @@ -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)); @@ -466,16 +514,17 @@ 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 { @@ -483,7 +532,6 @@ public void openSecureChannel(GPCardKeys keys, GPSecureChannelVersion scp, byte[ } } - // Derive session keys if (this.scpVersion.scp == GPSecureChannelVersion.SCP.SCP02) { sessionContext = seq.clone(); @@ -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. @@ -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"); @@ -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); diff --git a/library/src/main/java/pro/javacard/gp/SCP03Wrapper.java b/library/src/main/java/pro/javacard/gp/SCP03Wrapper.java index 15c8b9f5..f933b101 100644 --- a/library/src/main/java/pro/javacard/gp/SCP03Wrapper.java +++ b/library/src/main/java/pro/javacard/gp/SCP03Wrapper.java @@ -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(); @@ -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); @@ -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; @@ -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) { @@ -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); @@ -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)); diff --git a/tool/src/main/java/pro/javacard/gptool/GPCommandLineInterface.java b/tool/src/main/java/pro/javacard/gptool/GPCommandLineInterface.java index ffe58a2e..e4f63f3d 100644 --- a/tool/src/main/java/pro/javacard/gptool/GPCommandLineInterface.java +++ b/tool/src/main/java/pro/javacard/gptool/GPCommandLineInterface.java @@ -41,6 +41,8 @@ abstract class GPCommandLineInterface { protected static OptionSpec OPT_HELP = parser.acceptsAll(Arrays.asList("h", "?", "help"), "Shows this help").forHelp(); protected static OptionSpec OPT_CONNECT = parser.acceptsAll(Arrays.asList("c", "connect"), "Connect to app/domain").withRequiredArg().ofType(AID.class); protected static OptionSpec OPT_DEBUG = parser.acceptsAll(Arrays.asList("d", "debug"), "Show PC/SC and APDU trace"); + protected static OptionSpec OPT_S16 = parser.accepts("s16", "Use SCP03 S16 mode"); + protected static OptionSpec OPT_VERBOSE = parser.acceptsAll(Arrays.asList("v", "verbose"), "Be verbose about operations"); protected static OptionSpec OPT_READER = parser.acceptsAll(Arrays.asList("r", "reader"), "Use specific reader").withOptionalArg().describedAs("reader"); protected static OptionSpec OPT_LIST = parser.acceptsAll(Arrays.asList("l", "list"), "List the contents of the card"); @@ -69,6 +71,9 @@ abstract class GPCommandLineInterface { protected static OptionSpec OPT_DELETE = parser.accepts("delete", "Delete applet/package").withRequiredArg().ofType(AID.class); protected static OptionSpec OPT_DEFAULT = parser.accepts("default", "Indicate Default Selected privilege"); + protected static OptionSpec OPT_DEFAULT_CONTACT = parser.accepts("default-contact", "Default Selected on contact interface"); + protected static OptionSpec OPT_DEFAULT_CONTACTLESS = parser.accepts("default-contactless", "Default Selected on contactless interface"); + protected static OptionSpec OPT_DOMAIN = parser.accepts("domain", "Create supplementary security domain").withRequiredArg().ofType(AID.class); // Card an applet lifecycle management diff --git a/tool/src/main/java/pro/javacard/gptool/GPTool.java b/tool/src/main/java/pro/javacard/gptool/GPTool.java index 8994f730..487d9483 100644 --- a/tool/src/main/java/pro/javacard/gptool/GPTool.java +++ b/tool/src/main/java/pro/javacard/gptool/GPTool.java @@ -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; diff --git a/tool/src/main/java/pro/javacard/gptool/PlaintextKeys.java b/tool/src/main/java/pro/javacard/gptool/PlaintextKeys.java index 978461bc..982ed06a 100644 --- a/tool/src/main/java/pro/javacard/gptool/PlaintextKeys.java +++ b/tool/src/main/java/pro/javacard/gptool/PlaintextKeys.java @@ -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]);