JavaCard (erste Schritte): Difference between revisions

From
Jump to navigation Jump to search
Line 237: Line 237:


<syntaxhighlight lang="java">
<syntaxhighlight lang="java">
// Buffer der groß genug ist, um alle Daten zu empfangen. (Bereits im Konstruktor initialisieren.)
// 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 byte[] ram_buf = JCSystem.makeTransientByteArray(RAM_BUF_SIZE, JCSystem.CLEAR_ON_DESELECT);


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


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


// Kopiere die Daten in den dafür vorgesehenen Buffer
// Kopiere die Daten in den dafür vorgesehenen Buffer
Util.arrayCopyNonAtomic(buffer, offset_cdata, ram_buf, ram_buf_position, received_length);
Util.arrayCopyNonAtomic(buffer, offset_cdata, ram_buf, ram_buf_position, received_length);
ram_buf_position += received_length;
ram_buf_position += received_length;


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


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


return received_length;
return received_length;
}
}
</syntaxhighlight>
</syntaxhighlight>

Revision as of 09:37, 13 October 2022

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 static final byte[] helloWorld = { 'H' };

    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[] buf = apdu.getBuffer();

        if ((buf[ISO7816.OFFSET_CLA] == 0) && (buf[ISO7816.OFFSET_INS] == (byte)0xa4)) {
            return;
        }

        if (buf[ISO7816.OFFSET_CLA] != HELLO_CLA) {
            ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
        }

        sendHelloWorld(apdu);
    }

    private void sendHelloWorld(APDU apdu) {
        byte[] buffer = apdu.getBuffer();

        short length = (short) helloWorld.length;
        Util.arrayCopyNonAtomic(helloWorld, (short) 0, buffer, (short) 0, length);
        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 B0000000 liefert nun den Buchstaben H als Hexcode 0x48 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 wird ein "tls-crypt-v2 server key" mit openvpn --genkey tls-crypt-v2-server erstellt. Dieser base64-kodierte Schlüssel enthält zwei 512 Bit lange Schlüssel, wovon der Server jeweils die ersten 256 Bit wie folgt verwendet:

  • die ersten 256 Bit des ersten Schlüssels als AES-256-CTR Schlüssel Ke zur Verschlüsselung
  • die ersten 256 Bit des zweiten Schlüssels als HMAC-SHA-256 Schlüssel Ka zur Authentifizierung

Diese Schlüssel werden sowohl für das initiale Wrapping als auch Unwrapping genutzt.


Für die Klienten wird, unter Angabe des gerade erstellten Keys des Servers, ein "tls-crypt-v2 client key" generiert. Dies erfolgt mit OpenVPN's openvpn --genkey tls-crypt-v2-client --tls-crypt-v2 server.key. Das ebenfalls base64-kodierte Ergebnis setzt sich aus einem 2048-Bit langen Client-Key Kc, sowie dem eigentlichen gewrappten Key WKc zusammen. Das Wrapping, welches hierbei vorgenommen wird, erfolgt folgendermaßen:

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 Einpacken benutzt. Die Erstellung von Metadaten ist optional, kann aber zur späteren Authentifizierung beitragen.


Während der Nutzung (also beim Verbindungsaufbau) entpackt der Server den gewrappten Key, den er vom Klienten gesendet bekommt. Mit den server-eigenen Schlüsseln Ke und Ka entschlüsselt und authentifiziert der Server den Klienten. Er ist zudem nun in Besitz des Schlüssels Kc, über den im weiteren Verlauf die verschlüsselte Kommunikation erfolgt. Optional werden zuvor die Metadaten überprüft.

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. 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 in den dafür vorgesehenen Buffer
        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;
}