JavaCard (erste Schritte)

From
Revision as of 16:40, 13 October 2022 by Kantiemp (talk | contribs) (→‎Ablauf)
Jump to navigation Jump to search

Erste Schritte: Installation einer Beispielanwendung

Hier erfolgt zunächst der erste Test der Funktionalität der SmartCard durch Installation eines Applets, das eine einfache Guthabenkarte implementiert, hier Simple Wallet[1].

Installiert und angesprochen kann die JavaCard mittels GlobalPlatformPro[2].

Installiert wird die Applikation durch:

java -jar gp.jar -install simplewallet.cap -default

Angesprochen wird die Karte durch APDU Kommandos[3][4]. Vor jeder Operation wird das Simple Wallet Applet mit ihrer AID F000A0000E00 ausgewählt.

Guthaben verändern

Das Guthaben kann beispielsweise mit der APDU B040000001[AMOUNT, 1 Bytes] erhöht werden:

java -jar gp.jar -a "00A4040006F000A0000E0000" -a "B04000000105" -debug -verbose

Ausgabe:

GlobalPlatformPro v20.01.23-0-g5ad373b
Running on Linux 5.15.0-48-generic amd64, Java 17.0.4 by Private Build
# Detected readers from JNA2PCSC
[*] REINER SCT cyberJack RFID basis 00 00
SCardConnect("REINER SCT cyberJack RFID basis 00 00", T=*) -> T=1, 3B80800101
SCardBeginTransaction("REINER SCT cyberJack RFID basis 00 00")
Reader: REINER SCT cyberJack RFID basis 00 00
ATR: 3B80800101
More information about your card:
    http://smartcard-atr.appspot.com/parse?ATR=3B80800101

A>> T=1 (4+0006) 00A40400 06 F000A0000E00 00
A<< (0000+2) (33ms) 9000
A>> T=1 (4+0001) B0400000 01 05
A<< (0000+2) (25ms) 9000
A>> T=1 (4+0000) 00A40400 00 
A<< (0018+2) (19ms) 6F108408A000000151000000A5049F6501FF 9000
[TRACE] GPSession -  [6F]
[TRACE] GPSession -      [84] A000000151000000
[TRACE] GPSession -      [A5]
[TRACE] GPSession -          [9F65] FF
[DEBUG] GPSession - Auto-detected ISD: A000000151000000
SCardEndTransaction("REINER SCT cyberJack RFID basis 00 00")
SCardDisconnect("REINER SCT cyberJack RFID basis 00 00", true) tx:23/rx:24

Alternativ kann das Guthaben auch mit B030000001[AMOUNT, 1 Bytes] reduziert werden. Die Antwort 0x9000 steht hier für "Erfolgreich".

Guthaben auslesen

Das Guthaben kann dann mit der APDU B050000002 ausgelesen werden:

java -jar gp.jar -a "00A4040006F000A0000E0000" -a "B050000002" -debug -verbose

Ausgabe:

GlobalPlatformPro v20.01.23-0-g5ad373b
Running on Linux 5.15.0-48-generic amd64, Java 17.0.4 by Private Build
# Detected readers from JNA2PCSC
[*] REINER SCT cyberJack RFID basis 00 00
SCardConnect("REINER SCT cyberJack RFID basis 00 00", T=*) -> T=1, 3B80800101
SCardBeginTransaction("REINER SCT cyberJack RFID basis 00 00")
Reader: REINER SCT cyberJack RFID basis 00 00
ATR: 3B80800101
More information about your card:
    http://smartcard-atr.appspot.com/parse?ATR=3B80800101

A>> T=1 (4+0006) 00A40400 06 F000A0000E00 00
A<< (0000+2) (28ms) 9000
A>> T=1 (4+0000) B0500000 02 
A<< (0002+2) (15ms) 0005 9000
A>> T=1 (4+0000) 00A40400 00 
A<< (0018+2) (23ms) 6F108408A000000151000000A5049F6501FF 9000
[TRACE] GPSession -  [6F]
[TRACE] GPSession -      [84] A000000151000000
[TRACE] GPSession -      [A5]
[TRACE] GPSession -          [9F65] FF
[DEBUG] GPSession - Auto-detected ISD: A000000151000000
SCardEndTransaction("REINER SCT cyberJack RFID basis 00 00")
SCardDisconnect("REINER SCT cyberJack RFID basis 00 00", true) tx:22/rx:26

Das Guthaben wird hier mit 0x0005 vor dem Returncode 0x9000 (Erfolgreich) zurückgegeben.

Erstellen eines Hello World Applet

Das Erstellen eines Builds erfolgt mit Ant für JavaCards[5], dessen .jar im Projektverzeichnis platziert wird. Außerdem wird eine SDK benötigt. Hier wird die SDK Version 3.0.4 (hier[6]) und die Java Version 8 verwendet.

Projektdateien

Die build.xml für den Ant-Task sieht dann wiefolgt aus:

<project name="JavaCardHelloWorld" basedir=".">

    <taskdef name="javacard" classname="pro.javacard.ant.JavaCard" classpath="ant-javacard.jar"/>

    <javacard>
        <cap jckit="./sdks/jc304_kit" aid="0102030405" package="main" output="SimpleHello.cap" sources="src/main/">
            <applet class="main.SimpleHello" aid="0102030405060708"/>
        </cap>
    </javacard>

</project>

Und das HelloWorld SimpleHello.java:

package main;

import javacard.framework.*;

public class SimpleHello extends Applet {
    final static byte HELLO_CLA = (byte)0xB0;
    private final byte INS_HELLO = (byte)0x01;

    private static final byte[] helloWorld = 
        { 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D' };

    private SimpleHello() {
        register();
    }

    public static void install(byte bArray[], short bOffset, byte bLength) throws ISOException {
        new SimpleHello();
    }

    @Override
    public void process(APDU apdu) throws ISOException {
        byte[] buffer = apdu.getBuffer();

        // Return control to the JCRE if SELECT APDU
        if ((buffer[ISO7816.OFFSET_CLA] == 0) && (buffer[ISO7816.OFFSET_INS] == (byte)0xa4)) {
            return;
        }

        // Check for correct applet CLA
        if (buffer[ISO7816.OFFSET_CLA] != HELLO_CLA) {
            ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
        }
        
        // Call function corresponding to instruction
        switch (buffer[ISO7816.OFFSET_INS]) {
            case INS_HELLO:
                sendHelloWorld(apdu);
                break;
            default:
                ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
        }
    }

    private void sendHelloWorld(APDU apdu) {
        byte[] buffer = apdu.getBuffer();
        short length = (short) helloWorld.length;
        Util.arrayCopyNonAtomic(helloWorld, (short) 0, buffer, (short) 0, length);
        
        // Check if Le was provided correctly
        short outgoing_length = apdu.setOutgoing();
        if (length > outgoing_length) {
            ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
        }
        
        // Send
        apdu.setOutgoingLength(length);
        apdu.sendBytes ((short)0 , length);
        
        // Note that for a short response as in the case illustrated here
        // the three APDU method calls shown : setOutgoing(),setOutgoingLength() & sendBytes()
        // could be replaced by one APDU method call : setOutgoingAndSend().
        // apdu.setOutgoingAndSend((short) 0, length);
    }
}

Das Projektverzeichnis:

.
|------	src/
|	|------	main/
|		|------	SimpleHello.java
|------	sdks/
|	|------	jc304_kit/
|------	ant-javacard.jar
|------	gp.jar
|------	build.xml
|------	SimpleHello.cap

Auf die Karte bringen

Schließlich wird für den Build die Java-Homevariable auf Version 8 gesetzt:

export JAVA_HOME="/usr/lib/jvm/java-8-openjdk-amd64/"

...Der Ant-Build ausgeführt:

ant

...Und das Applet installiert:

java -jar gp.jar -install SimpleHello.cap -default

Der Aufruf mit java -jar gp.jar -d -applet 0102030405060708 --apdu B00100000B liefert nun die Buchstaben "HELLO WORLD" als Hexcode 0x48454C4C4F20574F524C44 zurück.

Key Unwrapping

Im folgenden wird das tls-crypt-v2 [7] Verfahren besprochen, welches von OpenVPN Servern genutzt werden kann, um eine verschlüsselte TLS-Verbindung mit Klienten zu ermöglichen. Bei der älteren Version tls-crypt(-v1), bei dem sowohl der Server als auch die Klienten einen vorher geteilten Schlüssel nutzen, gab es das Risiko, dass nach Komprimitierung eines einzelnen Schlüssels, die gesamte Sicherheit verloren ist. tls-crypt-v2 unterscheidet sich dadurch, dass jeder Klient einen eigenen Schlüssel bekommt.

Jeder klientspezifische Schlüssel wird zusätzlich mit den Schlüssen des Servers "gewrappt", d.h. er wird verschlüsselt und ein HMAC gebildet. Dieser gewrappte Key wird in der operativen Phase, also beim Verbindungsaufbau, an den Server geschickt. Bei erfolgreicher Entschlüsselung und Authentifizierung kann die verschlüsselte Kommunikation über den Klientenschlüssel beginnen.


Ablauf

Anfangs, in der Initialisierungsphase, wird ein "tls-crypt-v2 server key" erstellt. Dies kann mit openvpn --genkey tls-crypt-v2-server getan werden und resultiert in einem Schlüssel im PEM-Format, d.h ein base64-kodierter Schlüssel zwischen Fuß- und Endzeile.

-----BEGIN OpenVPN tls-crypt-v2 server key-----
4UZWT3jcUdfa8M5XqDPPKe7FpPnTSpYeWflgOQftdT7rdtgvm4ZIhC8phtrgTBy7
qx298aaNhIxwBBWh7wnt/oh6yfPeu3lIX+Y7GazCYqMGPm/obzaHHM4aV5u9q/UP
VSFkWQH8Lat3dk0jlR3XFT46930qLlcjbiLG7Fpv6X0=
-----END OpenVPN tls-crypt-v2 server key-----

Einmal base64-dekodiert sieht man, dass der Schlüssel 128 Byte lang ist. Tatsächlich setzt sich dieser aus zwei einzelnen 64-Byte Schlüsseln zusammen. Für das tls-crypt-v2 Verfahren nutzen wir davon jeweils nur die ersten 32 Byte für die Serverschlüssel Ke (AES-Schlüssel) und Ka (HMAC-Schlüssel). Die folgende Abbildung macht dies nochmal deutlich.

Server key.png

Ähnlich erfolgt die Erstellung des Klientenschlüssels, dieser wird mit openvpn --genkey tls-crypt-v2-client --tls-crypt-v2 <server_key>, unter Nennung des Serverschlüssels, getan. Dieser ist jedoch deutlich einfacher aufgebaut und besteht lediglich aus dem Klientenschlüssel Kc, sowie dem mit den Serverschlüsseln bereits "gewrappten" Schlüssel WKc. Der Schlüssel Kc hat eine feste Länge von 256 Byte, der gewrappte Schlüssel WKc hat eine flexible Länge, je nachdem welche Metadaten hinzugefügt werden. Die folgenden Abbildungen verdeutlichen dies.

-----BEGIN OpenVPN tls-crypt-v2 client key-----
Z5Ex9fanrA7z18PkSVaF7ePjdxwo7u9RUBdXXcbTENxPcv6AIumGjc4cx5yscoS3
oART/UqrkadZ8EMXvA9yRM8UrKqcxVS97vWz6lBMhU6V80LrcQIbMJ9w9qI8OxAm
F0p1lm7Ph4RRM9rg395A+D51NWg17wlPcxuATNhsPsZGuBpbZztpU+WxGrxervPb
lPS/4uIDdyY0CxPT+pHv1+MScjQhTvGERFgJis5KT62l0z2n2231dCVvVBk6dyNU
yJk1T3IcHpapjY5ogSnfAtXJJoOENEeNW6s25zwW0n6OdDKWxjnBQD4Mgak9Fhzc
Ri8IhvY947RFi/3Ji8N78tiMPtTN/pg4afUI4KurSUnSmchq9xkN4tx0dkeHuZTh
27jJ4Mrpc3A15WlHY5iPcqcXXsVvQrDIaSaANlAnnx69nTYvIFxO0+QwW+SCZ8V3
of2xHVdY6zJAp4FxGnGRakyYuuQLVYbvvapJQMA/lmgNDou8lcealnmGWxZ3e9Tw
cmcxtJiCxcJ/NlGZVIvQjiZyj1qDIFjktWXETNf/jvBZc2DCnlbP3O8zqPf84NUI
+95Fxk7T8SdA0EXXItCHRnAfmJGd0k3a8FlCKU+/JW64K86MiBNXvTDqkWauh9wH
weK0ZAibWgeNJ5NtKJ/iKi9fOtiC505qTZpcuWWSz1lgS0+GaXF9dUzim/1S5zpz
5Y0nrNNU0A6gcv8uHIJ6Try7RhH75zHLgwEr
-----END OpenVPN tls-crypt-v2 client key-----

Tls crypt v2 client key.png

Im Detail läuft das Wrapping wie folgt ab:

len = len(WKc)`` (16 bit, network byte order)

T = HMAC-SHA256(Ka, len || Kc || metadata)``

IV = 128 most significant bits of T``

WKc = T || AES-256-CTR(Ke, IV, Kc || metadata) || len

Wie man sehen kann, werden die Serverschlüssel Ka und Ke zum "Wrappen" also zum verschlüsseln und zum Bilden eines HMACs verwendet. Die Erstellung von Metadaten ist optional, kann aber zur späteren Authentifizierung beitragen, z.B. durch das Angeben einer Nutzer-ID, oder um die Gültigkeit des Schlüssels nachzuweisen, z.B. durch einen Timestamp. Die Länge des gewrappten Schlüssels WKc lässt sich vorab bestimmen, da der HMAC-SHA256 T eine Länge von 32 Byte hat, der AES-256-CTR kein Padding erzeugt, d.h. die Länge des Inputs ist gleich der Länge des Outputs, und len eine festgelegte Größe von 2 Byte hat.

In der operativen Phase - also beim Verbindungsaufbau - entpackt der Server den "gewrappten" Schlüssel, welchen er von dem jeweiligen Klienten gesendet bekommt. Mit dem AES-Entschlüsselungsschlüssel Ke und dem Initialisierungsvektor IV entschlüsselt der Server sowohl den Klient-Key Kc als auch die Metadaten, und kann deren Integrität durch Hinzunahme der Länge und dem folgenden Erstellen des HMACs überprüfen, indem dieser mit T verglichen wird. Ist dies erfolgreich, kann die verschlüsselte Kommunikation zwischen Server und Client beginnen, da beide im Besitz des Schlüssels Kc sind.

Die Idee ist, dass das Unwrapping auf der Java Card geschieht, sodass die Serverschlüssel durch diese gesichert sind. Im Rahmen dieses Seminars implementieren wir nur das Unwrapping auf der Java Card (in der Abbildung orange markiert). Die eigentliche Einbindung dieses Verfahrens in den Verbindungsaufbau mit einem OpenVPN Server erfordert weitere Arbeit.

Tls crypt v2 client overview.png

Implementierung

Je nach Version der Java Card werden die benötigten Algorithmen AES-256-CTR und HMAC-SHA256 bereits unterstützt. Ist dies nicht der Fall, und werden zumindestens SHA256 und andere AES-Modi unterstützt, können die nachfolgenden Algorithmen genutzt werden.

Da der gewrappte Klientenschlüssel WKc garantiert größer als 256 Byte ist, müssen zunächst Extended APDUs richtig verarbeitet werden. Zu diesem Zweck ist es notwendig eine Funktion zu implementieren, die alle Daten die an die Java Card gesendet werden, in einen dafür vorgesehenen Buffer schreibt. Vorab muss dafür gesorgt werden, dass ein solcher Buffer existiert. Der nachfolgende Codeausschnitt zeigt eine mögliche Implementierung.

// Buffer der groß genug ist, um alle Daten zu empfangen. (Bereits im Konstruktor initialisieren.)
private byte[] ram_buf = JCSystem.makeTransientByteArray(RAM_BUF_SIZE, JCSystem.CLEAR_ON_DESELECT);

private short handleData(APDU apdu) throws ISOException {
    byte[] buffer = apdu.getBuffer(); // Buffer der eingehenden APDU
    short offset_cdata = apdu.getOffsetCdata();
    short incoming_length = apdu.getIncomingLength(); // Die angekündigte Menge von Daten
    short received_length = apdu.setIncomingAndReceive(); // Empfange die ersten Daten und schreibe sie in 'buffer'

    short ram_buf_position = 0;
    while (received_length > 0) {
        // Werfe Fehler, falls mehr Daten ankommen als Platz im Buffer ist.
        if ((short) (ram_buf_position + received_length) > RAM_BUF_SIZE)
            ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);

        // Kopiere die Daten von 'buffer' nach 'ram_buf'
        Util.arrayCopyNonAtomic(buffer, offset_cdata, ram_buf, ram_buf_position, received_length);
        ram_buf_position += received_length;

        // Empfange weitere Daten
        received_length = apdu.receiveBytes(offset_cdata);
    }

    // Werfe Fehler, falls sich die Größe der empfangenen Daten von der angekündigten unterscheidet.
    received_length = (short) (ram_buf_position + received_length);
    if (received_length != incoming_length)
        ISOException.throwIt(ISO7816.SW_DATA_INVALID);

    return received_length;
}

Weiterhin wird eine Funktion für die Entschlüsselung mit AES-256 im Counter-Modus benötigt. Da dieser auf einigen Karten nicht nativ unterstützt wird, hier eine eigene Implementierung.

// Buffer für den Initialisierungsvektor (Counter)
private byte[] iv = JCSystem.makeTransientByteArray(AES_BLOCK_SIZE, JCSystem.CLEAR_ON_DESELECT);
// Buffer um Zwischenergebnisse zu speichern.
private byte[] working_buf = JCSystem.makeTransientByteArray(AES_BLOCK_SIZE, JCSystem.CLEAR_ON_DESELECT);
// AES-Algorithmus initialisieren
private Cipher aesCipher = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_ECB_NOPAD, false);

/* Entschlüsselung mit dem AES-256-CTR Algorithmus */
private void AES_256_CTR(byte[] input, short input_offset, byte[] output, short output_offset, short incoming_length) {
    aesCipher.init(Ke, Cipher.MODE_ENCRYPT);
    short offset = 0;

    // Durchlaufe jedes Byte
    for (short i = 0; i < incoming_length; i++) {
        // Falls ein neuer Block erreicht wird, wird dieser verschlüsselt und 'iv' inkrementiert.
        if (offset == 0) {
            aesCipher.doFinal(iv, (short)0, AES_BLOCK_SIZE, working_buf_32, (short)0);
            increment_iv();
        }

        // Einfache XOR-Operation
        output[(short)(i + output_offset)] = (byte)(input[(short)(i + input_offset)] ^ working_buf[offset]);

        // Aktuellen Block-Offset berechnen
        offset = (short)((short)(offset + 1) % AES_BLOCK_SIZE);
    }
}

/* Diese Funktion inkrementiert den Initialisierungsvektor. */
private void increment_iv() {
    short i;
    byte end = 0, dummy = 0;
    for (i = (short) iv.length; i > 0; i--) {
        if (end == 0) {
            if((++iv[(short)(i - 1)] != 0)) {
                end = 1;
            }
        } else {
            dummy++;
        }
    }
}

Weiterhin wird eine Möglichkeit gebraucht, einen HMAC-SHA256 zu generieren. Sofern SHA256 von der Java Card unterstützt wird, kann folgende Funktion verwendet werden.

// Buffer, der zum speichern von Zwischenergebnissen genutzt wird. Dieser muss groß genug sein
private byte[] hmac_buffer = JCSystem.makeTransientByteArray(HMAC_BUFFER_SIZE, JCSystem.CLEAR_ON_DESELECT);
// SHA256
private MessageDigest sha_256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false);

/* Erstellt eine HMAC-SHA256 Prüfsumme. */
private void HMAC_SHA_256(byte[] msg, short msg_offset, short msg_length, byte[] mac, short mac_offset) {
    // Inner Hash, wobei Ka der HMAC-Schlüssel ist.
    for (short i = 0; i < HMAC_BLOCK_SIZE; i++) hmac_buffer[i] = (byte)((byte)0x36 ^ Ka[i]);
    Util.arrayCopyNonAtomic(msg, msg_offset, hmac_buffer, HMAC_BLOCK_SIZE, msg_length);
    sha_256.reset();
    sha_256.doFinal(hmac_buffer, (short)0, (short)(HMAC_BLOCK_SIZE + msg_length), hmac_buffer, HMAC_BLOCK_SIZE);

    // Outer Hash, wobei Ka der HMAC-Schlüssel ist.
    for (short i = 0; i < HMAC_BLOCK_SIZE; i++) hmac_buffer[i] = (byte)((byte)0x5C ^ Ka[i]);
    sha_256.reset();
    sha_256.doFinal(hmac_buffer, (short)0, (short)(HMAC_BLOCK_SIZE + HMAC_HASH_SIZE), mac, mac_offset);
}

Mit diesen Funktionen kann nun das Unwrapping vonstattengehen. Dabei müssen die gerade vorgestellten Funktionen nur in der richtigen Reihenfolge angewendet werden. Der folgende Codeausschnitt zeigt die Implementierung.

// Eigener Buffer um die HMAC Nachricht vorzubereiten. Dieser muss entsprechend der Metadaten groß genug sein.
private byte[] hmac_init = JCSystem.makeTransientByteArray(HMAC_INIT_SIZE, JCSystem.CLEAR_ON_DESELECT);

private void unwrap(APDU apdu) {
    byte[] buf = apdu.getBuffer();
    short received_length = handleData(apdu); // Hier wird die Extended APDU behandelt

    // Werfe Fehler, falls der eingehende Datenblob zu klein ist.
    if (received_length < (short)(HMAC_HASH_SIZE + KC_LENGTH + 2))
        ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);

    // Die Offsets der einzelnen Komponenten von 'WKc' können vorberechnet werden.
    final short t_offset = 0;
    final short a_offset = t_offset + HMAC_HASH_SIZE;
    final short l_offset = (short)(received_length - 2);

    // Die Länge von 'WKc' vom Ende des Buffers lesen
    byte length_b1 = ram_buf[l_offset];
    byte length_b2 = ram_buf[(short)(l_offset + 1)];
    short wkc_length = Util.makeShort(length_b1, length_b2);

    // Den mittleren Block von 'WKc' entschlüsseln
    short wrapped_aes_length = (short)(wkc_length - HMAC_HASH_SIZE - 2);
    Util.arrayCopyNonAtomic(ram_buf, t_offset, iv, (short)0, AES_BLOCK_SIZE); // Extract 128 MSB from T
    AES_256_CTR(ram_buf, a_offset, ram_buf, a_offset, wrapped_aes_length);

    // Die HMAC-Nachricht vorbereiten
    hmac_init[0] = length_b1;
    hmac_init[1] = length_b2;
    Util.arrayCopyNonAtomic(ram_buf, a_offset, hmac_init, (short)2, wrapped_aes_length);
    HMAC_SHA_256(hmac_init, (short)0, (short)(wrapped_aes_length + 2), working_buf_32, (short)0);

    // Vergleiche den erstellten HMAC mit dem mitgesendeten. Falls unterschiedlich -> werfe Fehler
    if (Util.arrayCompare(ram_buf, t_offset, working_buf_32, (short)0, HMAC_HASH_SIZE) != (byte)0)
        ISOException.throwIt((short)0x9862);

    // Schließlich wird der entschlüsselte und authentifizierte Schlüssel zurückgesendet.
    short le = apdu.setOutgoing();
    if (le != KC_LENGTH) ISOException.throwIt((short)6981);
    apdu.setOutgoingLength(KC_LENGTH);
    Util.arrayCopyNonAtomic(ram_buf, a_offset, buf, (short)0, KC_LENGTH);
    apdu.sendBytes((short)0, KC_LENGTH);
}