JavaCard (erste Schritte): Difference between revisions
(18 intermediate revisions by 3 users not shown) | |||
Line 50: | Line 50: | ||
</pre> |
</pre> |
||
Alternativ kann das Guthaben auch mit <code>B030000001[AMOUNT, 1 Bytes]</code> reduziert werden. |
Alternativ kann das Guthaben auch mit <code>B030000001[AMOUNT, 1 Bytes]</code> reduziert werden. |
||
Von besonderem Interesse sind hier die mit "A" vorangestellten Teile des Trace, da diese die Kommunikation mit der Karte beschreiben. Im ersten Schritt wird hier sichtbar per Command APDU das Applet ausgewählt, was die Karte mit dem Returncode <code>0x9000</code> (steht für "Erfolgreich") quittiert. Danach erfolgt die Übertragung der für die Guthabenerhöhung zuständige APDU, welche von der Karte ebenfalls mit <code>0x9000</code> beantwortet wird. An den Antworten der Karte ist jeweils auch die Zeit bis zum Eintreffen der Antwort annotiert, was Rückschlüsse auf die Performance der in der Karte ausgeführten Algorithmen, wie des späteren Unwrappings, zulässt. |
|||
===Guthaben auslesen=== |
===Guthaben auslesen=== |
||
Line 121: | Line 123: | ||
public class SimpleHello extends Applet { |
public class SimpleHello extends Applet { |
||
final static byte HELLO_CLA = (byte) |
final static byte HELLO_CLA = (byte)0xB0; |
||
private final byte INS_HELLO = (byte)0x01; |
|||
private static final byte[] helloWorld = |
private static final byte[] helloWorld = |
||
{ 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D' }; |
|||
private SimpleHello() { |
private SimpleHello() { |
||
Line 135: | Line 139: | ||
@Override |
@Override |
||
public void process(APDU apdu) throws ISOException { |
public void process(APDU apdu) throws ISOException { |
||
byte[] |
byte[] buffer = apdu.getBuffer(); |
||
// Return control to the JCRE if SELECT APDU |
|||
if ((buf[ISO7816.OFFSET_CLA] == 0) && (buf[ISO7816.OFFSET_INS] == (byte)0xa4)) { |
|||
if ((buffer[ISO7816.OFFSET_CLA] == 0) && (buffer[ISO7816.OFFSET_INS] == (byte)0xa4)) { |
|||
return; |
return; |
||
} |
} |
||
// Check for correct applet CLA |
|||
if (buf[ISO7816.OFFSET_CLA] != HELLO_CLA) { |
|||
if (buffer[ISO7816.OFFSET_CLA] != HELLO_CLA) { |
|||
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); |
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); |
||
} |
} |
||
// Call function corresponding to instruction |
|||
sendHelloWorld(apdu); |
|||
switch (buffer[ISO7816.OFFSET_INS]) { |
|||
case INS_HELLO: |
|||
sendHelloWorld(apdu); |
|||
break; |
|||
default: |
|||
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); |
|||
} |
|||
} |
} |
||
private void sendHelloWorld(APDU apdu) { |
private void sendHelloWorld(APDU apdu) { |
||
byte[] buffer = apdu.getBuffer(); |
byte[] buffer = apdu.getBuffer(); |
||
short length = (short) helloWorld.length; |
short length = (short) helloWorld.length; |
||
Util.arrayCopyNonAtomic(helloWorld, (short) 0, buffer, (short) 0, length); |
Util.arrayCopyNonAtomic(helloWorld, (short) 0, buffer, (short) 0, length); |
||
apdu.setOutgoingAndSend((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); |
|||
} |
} |
||
} |
} |
||
Line 193: | Line 219: | ||
</pre> |
</pre> |
||
Der Aufruf mit <code>java -jar gp.jar -d -applet 0102030405060708 --apdu |
Der Aufruf mit <code>java -jar gp.jar -d -applet 0102030405060708 --apdu B00100000B</code> liefert nun die Buchstaben "HELLO WORLD" als Hexcode <code>0x48454C4C4F20574F524C44</code> zurück. |
||
==Key Unwrapping== |
==Key Unwrapping== |
||
Im folgenden wird das tls-crypt-v2 [https://github.com/OpenVPN/openvpn/blob/master/doc/tls-crypt-v2.txt] Verfahren besprochen, welches von OpenVPN Servern genutzt werden kann, um |
Im folgenden wird das tls-crypt-v2 [https://github.com/OpenVPN/openvpn/blob/master/doc/tls-crypt-v2.txt] Verfahren besprochen, welches von OpenVPN Servern genutzt werden kann, um bereits den TLS-Handshake symmetrisch zu verschlüsseln, um eine quantensichere Verbindung mit Klienten zu ermöglichen (also den Diffie-Hellmann-Schlüsselaustausch zusätzlich zu schützen). 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 Kompromittierung 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. |
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. |
||
Line 204: | Line 230: | ||
===Ablauf=== |
===Ablauf=== |
||
Anfangs wird ein "tls-crypt-v2 server key" mit <code>openvpn --genkey tls-crypt-v2-server</code> |
Anfangs, in der Initialisierungsphase, wird ein "tls-crypt-v2 server key" erstellt. Dies kann mit <code>openvpn --genkey tls-crypt-v2-server</code> getan werden und resultiert in einem Schlüssel im PEM-Format, d.h ein base64-kodierter Schlüssel zwischen Fuß- und Endzeile. |
||
Dieser base64-kodierte Schlüssel enthält zwei 512 Bit lange Schlüssel, wovon der Server jeweils die ersten 256 Bit wie folgt verwendet: |
|||
<pre> |
|||
* die ersten 256 Bit des ersten Schlüssels als AES-256-CTR Schlüssel <code>Ke</code> zur Verschlüsselung |
|||
-----BEGIN OpenVPN tls-crypt-v2 server key----- |
|||
* die ersten 256 Bit des zweiten Schlüssels als HMAC-SHA-256 Schlüssel <code>Ka</code> zur Authentifizierung |
|||
4UZWT3jcUdfa8M5XqDPPKe7FpPnTSpYeWflgOQftdT7rdtgvm4ZIhC8phtrgTBy7 |
|||
qx298aaNhIxwBBWh7wnt/oh6yfPeu3lIX+Y7GazCYqMGPm/obzaHHM4aV5u9q/UP |
|||
VSFkWQH8Lat3dk0jlR3XFT46930qLlcjbiLG7Fpv6X0= |
|||
-----END OpenVPN tls-crypt-v2 server key----- |
|||
</pre> |
|||
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 <code>Ke</code> (AES-Schlüssel) und <code>Ka</code> (HMAC-Schlüssel). Die folgende Abbildung macht dies nochmal deutlich. |
|||
Diese Schlüssel werden sowohl für das initiale Wrapping als auch Unwrapping genutzt. |
|||
[[File:server_key.png|600px]] |
|||
Ähnlich erfolgt die Erstellung des Klientenschlüssels, dieser wird mit <code>openvpn --genkey tls-crypt-v2-client --tls-crypt-v2 <server_key></code>, unter Nennung des Serverschlüssels, erzeugt. Dieser ist jedoch deutlich einfacher aufgebaut und besteht lediglich aus dem Klientenschlüssel <code>Kc</code>, sowie dem mit den Serverschlüsseln bereits "gewrappten" Schlüssel <code>WKc</code>. Der Schlüssel <code>Kc</code> hat eine feste Länge von 256 Byte, der gewrappte Schlüssel <code>WKc</code> hat eine flexible Länge, je nachdem welche Metadaten hinzugefügt werden. Die folgenden Abbildungen verdeutlichen dies. |
|||
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 |
|||
<pre> |
|||
<code>openvpn --genkey tls-crypt-v2-client --tls-crypt-v2 server.key</code>. Das ebenfalls base64-kodierte Ergebnis setzt sich aus einem 2048-Bit langen Client-Key <code>Kc</code>, sowie dem eigentlichen gewrappten Key <code>WKc</code> zusammen. Das Wrapping, welches hierbei vorgenommen wird, erfolgt folgendermaßen: |
|||
-----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----- |
|||
</pre> |
|||
[[File:tls_crypt_v2_client_key.png|600px]] |
|||
Im Detail läuft das Wrapping wie folgt ab: |
|||
<pre> |
<pre> |
||
Line 226: | Line 276: | ||
</pre> |
</pre> |
||
Wie man sehen kann, werden die Serverschlüssel <code>Ka</code> und <code>Ke</code> zum "Wrappen" |
Wie man sehen kann, werden die Serverschlüssel <code>Ka</code> und <code>Ke</code> 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 <code>WKc</code> lässt sich vorab bestimmen, da der HMAC-SHA256 <code>T</code> eine Länge von 32 Byte hat, der AES-256-CTR kein Padding erfordert, d.h. die Länge des Inputs ist gleich der Länge des Outputs, und <code>len</code> 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 <code>Ke</code> und dem Initialisierungsvektor <code>IV</code> entschlüsselt der Server sowohl den Klient-Key <code>Kc</code> 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 <code>T</code> verglichen wird. Ist dies erfolgreich, kann die verschlüsselte Kommunikation zwischen Server und Client beginnen, da beide im Besitz des Schlüssels <code>Kc</code> 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. |
|||
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 <code>Ke</code> und <code>Ka</code> entschlüsselt und authentifiziert der Server den Klienten. Er ist zudem nun in Besitz des Schlüssels <code>Kc</code>, über den im weiteren Verlauf die verschlüsselte Kommunikation erfolgt. Optional werden zuvor die Metadaten überprüft. |
|||
[[File:tls_crypt_v2_client_overview.png|750px]] |
|||
===Implementierung=== |
===Implementierung=== |
||
Die folgende Implementierung zeigt, wie das Unwrapping auf einer Java Card der Version 3.0.4 umgesetzt werden kann. Implementiert wurde die AES-256-CTR Entschlüsselung, der HMAC-SHA256 sowie eine Funktion, die eingehende Daten einer Extended APDU verarbeitet. Die folgenden Instruktionen können an die Java Card gesendet werden: |
|||
# INS_IMPORT_KEY: |
|||
Da der gewrappte Klientenschlüssel <code>WKc</code> 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. |
|||
B0 | 10 | 01 00 | 20 | Ke |
|||
B0 | 10 | 02 00 | 20 | Ka |
|||
# INS_UNWRAP_KEY: |
|||
B0 | 20 | 00 00 | len(WKc) | WKc | 01 00 |
|||
Anmerkung: Die Länge von <code>WKc</code> wird bei der letzten Instruktion durch drei Bytes angegeben (Extended APDU). |
|||
Unsere Tests haben ergeben, dass ein Unwrapping im Durchschnitt 400ms braucht. Rechnet man dies auf 60s hoch, kann man von ca. 150 Unwraps pro Minute ausgehen. |
|||
<syntaxhighlight lang="java"> |
<syntaxhighlight lang="java"> |
||
package org.seminar; |
|||
// 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); |
|||
import javacard.framework.*; |
|||
private short handleData(APDU apdu) throws ISOException { |
|||
import javacard.security.AESKey; |
|||
byte[] buffer = apdu.getBuffer(); // Buffer der eingehenden APDU |
|||
import javacard.security.KeyBuilder; |
|||
short offset_cdata = apdu.getOffsetCdata(); |
|||
import javacard.security.MessageDigest; |
|||
short incoming_length = apdu.getIncomingLength(); // Die angekündigte Menge von Daten |
|||
import javacardx.apdu.ExtendedLength; |
|||
short received_length = apdu.setIncomingAndReceive(); // Empfange die ersten Daten und schreibe sie in 'buffer' |
|||
import javacardx.crypto.Cipher; |
|||
public class UnwrapApplet extends Applet implements ExtendedLength { |
|||
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); |
|||
private final byte CLA = (byte)0xB0; |
|||
// Kopiere die Daten von 'buffer' nach 'ram_buf' |
|||
private final byte INS_IMPORT_KEY = (byte)0x10; |
|||
Util.arrayCopyNonAtomic(buffer, offset_cdata, ram_buf, ram_buf_position, received_length); |
|||
private final byte INS_UNWRAP = (byte)0x20; |
|||
ram_buf_position += received_length; |
|||
/* Sonstige Buffer */ |
|||
// Empfange weitere Daten |
|||
private final short RAM_BUF_SIZE = 512; |
|||
received_length = apdu.receiveBytes(offset_cdata); |
|||
private byte[] ram_buf = null; // In diesen Buffer werden die Daten der Extended APDUs geschrieben |
|||
private byte[] working_buf_32 = null; // Buffer, der als Zwischenspeicher für die Algorithmen dient. |
|||
/* AES-256-CTR */ |
|||
private final short AES_BLOCK_SIZE = 16; |
|||
private final short AES_KEY_LENGTH = 32; |
|||
private final short KC_LENGTH = 256; |
|||
private AESKey Ke = null; // Encryption-Key des Servers 'Ke' |
|||
private Cipher aesCipher = null; // AES Instanz |
|||
private byte[] iv = null; // Initialisierungvektor |
|||
/* HMAC-SHA256 */ |
|||
private final short HMAC_BLOCK_SIZE = 64; |
|||
private final short HMAC_HASH_SIZE = 32; |
|||
private final short HMAC_KEY_LENGTH = 32; |
|||
private final short HMAC_BUFFER_SIZE = 512; |
|||
private byte[] Ka = null; // Authentication-Key des Servers 'Ka' |
|||
private byte[] hmac_buffer = null; // Buffer, der für den HMAC-Algorithmus genutzt wird. |
|||
private byte[] hmac_init = null; // Buffer, der genutzt wird um die zu hashende Nachricht vorzubereiten |
|||
private MessageDigest sha_256 = null; // SHA256 Instanz |
|||
/* In dem Konstruktor werden sämtliche Buffer initialisiert. */ |
|||
private UnwrapApplet(byte[] bArray, short bOffset, byte bLength) { |
|||
ram_buf = JCSystem.makeTransientByteArray(RAM_BUF_SIZE, JCSystem.CLEAR_ON_DESELECT); |
|||
// AES |
|||
iv = JCSystem.makeTransientByteArray((short)16, JCSystem.CLEAR_ON_DESELECT); |
|||
working_buf_32 = JCSystem.makeTransientByteArray(HMAC_HASH_SIZE, JCSystem.CLEAR_ON_DESELECT); |
|||
Ke = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false); |
|||
aesCipher = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_ECB_NOPAD, false); |
|||
// HMAC |
|||
Ka = new byte[HMAC_BLOCK_SIZE]; |
|||
Util.arrayFillNonAtomic(Ka, (short)0, HMAC_BLOCK_SIZE, (byte)0); |
|||
sha_256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false); |
|||
hmac_buffer = JCSystem.makeTransientByteArray(HMAC_BUFFER_SIZE, JCSystem.CLEAR_ON_DESELECT); |
|||
hmac_init = JCSystem.makeTransientByteArray((short)512, JCSystem.CLEAR_ON_DESELECT); |
|||
register(); |
|||
} |
} |
||
// 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); |
|||
public static void install(byte[] bArray, short bOffset, byte bLength) throws ISOException { |
|||
return received_length; |
|||
new UnwrapApplet(bArray, bOffset, bLength); |
|||
} |
|||
} |
|||
</syntaxhighlight> |
|||
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. |
|||
@Override |
|||
<syntaxhighlight lang="java"> |
|||
public boolean select() { |
|||
// Buffer für den Initialisierungsvektor (Counter) |
|||
return true; |
|||
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; |
|||
@Override |
|||
// Durchlaufe jedes Byte |
|||
public void process(APDU apdu) throws ISOException { |
|||
for (short i = 0; i < incoming_length; i++) { |
|||
byte[] buffer = apdu.getBuffer(); |
|||
// Falls ein neuer Block erreicht wird, wird dieser verschlüsselt und 'iv' inkrementiert. |
|||
if (offset == 0) { |
|||
// Return control to the JCRE if SELECT APDU |
|||
aesCipher.doFinal(iv, (short)0, AES_BLOCK_SIZE, working_buf_32, (short)0); |
|||
if ((buffer[ISO7816.OFFSET_CLA] == 0) && (buffer[ISO7816.OFFSET_INS] == (byte)0xA4)) |
|||
increment_iv(); |
|||
return; |
|||
// Check for correct applet CLA |
|||
if (buffer[ISO7816.OFFSET_CLA] != CLA) |
|||
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); |
|||
switch (buffer[ISO7816.OFFSET_INS]) { |
|||
case INS_IMPORT_KEY: |
|||
importKey(apdu); |
|||
break; |
|||
case INS_UNWRAP: |
|||
unwrap(apdu); |
|||
break; |
|||
default: |
|||
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); |
|||
} |
} |
||
} |
|||
// Einfache XOR-Operation |
|||
output[(short)(i + output_offset)] = (byte)(input[(short)(i + input_offset)] ^ working_buf[offset]); |
|||
/* Diese Funktion verarbeitet eingehende Extended APDUs und schreibt deren Inhalt in 'ram_buf' */ |
|||
// Aktuellen Block-Offset berechnen |
|||
private short handleData(APDU apdu) throws ISOException { |
|||
offset = (short)((short)(offset + 1) % AES_BLOCK_SIZE); |
|||
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) { // Solange weiterhin Daten empfangen werden |
|||
// 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 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; |
|||
} |
} |
||
} |
|||
/* Diese Funktion inkrementiert den Initialisierungsvektor. */ |
|||
/* Funktion, die zum importieren der Serverschlüssel genutzt wird. */ |
|||
private void increment_iv() { |
|||
private void importKey(APDU apdu) { |
|||
short i; |
|||
byte[] buffer = apdu.getBuffer(); |
|||
short cdata_offset = apdu.getOffsetCdata(); |
|||
short incoming_length = apdu.getIncomingLength(); |
|||
if((++iv[(short)(i - 1)] != 0)) { |
|||
switch (buffer[ISO7816.OFFSET_P1]) { // Je nach Parameter Byte wird entweder Ka oder Ke gesetzt |
|||
end = 1; |
|||
case 0x01: |
|||
if (incoming_length != AES_KEY_LENGTH) break; |
|||
} else { |
|||
Ke.setKey(buffer, cdata_offset); |
|||
return; |
|||
case 0x02: |
|||
if (incoming_length != HMAC_KEY_LENGTH) break; |
|||
Util.arrayCopyNonAtomic(buffer, cdata_offset, Ka, (short)0, HMAC_KEY_LENGTH); |
|||
return; |
|||
default: |
|||
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); |
|||
} |
} |
||
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); |
|||
} |
} |
||
} |
|||
</syntaxhighlight> |
|||
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. |
|||
/* Funktion, die den eingehenden 'WKc' unwrappt */ |
|||
<syntaxhighlight lang="java"> |
|||
private void unwrap(APDU apdu) { |
|||
// Buffer, der zum speichern von Zwischenergebnissen genutzt wird. Dieser muss groß genug sein |
|||
byte[] buf = apdu.getBuffer(); |
|||
private byte[] hmac_buffer = JCSystem.makeTransientByteArray(HMAC_BUFFER_SIZE, JCSystem.CLEAR_ON_DESELECT); |
|||
short received_length = handleData(apdu); // Extended APDU wird behandelt |
|||
// SHA256 |
|||
// Werfe Fehler, falls der eingehende Datenblock zu klein ist. |
|||
private MessageDigest sha_256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false); |
|||
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. |
|||
/* Erstellt eine HMAC-SHA256 Prüfsumme. */ |
|||
final short t_offset = 0; |
|||
private void HMAC_SHA_256(byte[] msg, short msg_offset, short msg_length, byte[] mac, short mac_offset) { |
|||
final short a_offset = t_offset + HMAC_HASH_SIZE; |
|||
// Inner Hash, wobei Ka der HMAC-Schlüssel ist. |
|||
final short l_offset = (short)(received_length - 2); |
|||
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); |
|||
// Die Länge von 'WKc' vom Ende des Buffers lesen |
|||
sha_256.reset(); |
|||
byte length_b1 = ram_buf[l_offset]; |
|||
sha_256.doFinal(hmac_buffer, (short)0, (short)(HMAC_BLOCK_SIZE + msg_length), hmac_buffer, HMAC_BLOCK_SIZE); |
|||
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); |
|||
// HMAC Nachricht wird vorbereitet und gehasht |
|||
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); |
|||
// Vergleicht den berechneten HMAC mit dem mitgesendeten. Falls unterschiedlich, ist die Authentifizierung fehlgeschlagen. |
|||
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); |
|||
} |
|||
/* Entschlüsselung mit dem AES-256-CTR */ |
|||
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 der Initialisierungvektor '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_32[offset]); |
|||
// Aktuellen Blockoffset berechnen. |
|||
offset = (short)((short)(offset + 1) % AES_BLOCK_SIZE); |
|||
} |
|||
} |
|||
/* Diese Funktion inkrementiert unseren Initialisierungvektor 'iv' */ |
|||
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++; |
|||
} |
|||
} |
|||
} |
|||
/* 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); |
|||
} |
|||
// 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); |
|||
} |
} |
||
</syntaxhighlight> |
</syntaxhighlight> |
||
Mit diesen Funktionen kann nun das Unwrapping vonstattengehen. |
Latest revision as of 13:27, 8 November 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.
Von besonderem Interesse sind hier die mit "A" vorangestellten Teile des Trace, da diese die Kommunikation mit der Karte beschreiben. Im ersten Schritt wird hier sichtbar per Command APDU das Applet ausgewählt, was die Karte mit dem Returncode 0x9000
(steht für "Erfolgreich") quittiert. Danach erfolgt die Übertragung der für die Guthabenerhöhung zuständige APDU, welche von der Karte ebenfalls mit 0x9000
beantwortet wird. An den Antworten der Karte ist jeweils auch die Zeit bis zum Eintreffen der Antwort annotiert, was Rückschlüsse auf die Performance der in der Karte ausgeführten Algorithmen, wie des späteren Unwrappings, zulässt.
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 bereits den TLS-Handshake symmetrisch zu verschlüsseln, um eine quantensichere Verbindung mit Klienten zu ermöglichen (also den Diffie-Hellmann-Schlüsselaustausch zusätzlich zu schützen). 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 Kompromittierung 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.
Ä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, erzeugt. 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-----
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 erfordert, 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.
Implementierung
Die folgende Implementierung zeigt, wie das Unwrapping auf einer Java Card der Version 3.0.4 umgesetzt werden kann. Implementiert wurde die AES-256-CTR Entschlüsselung, der HMAC-SHA256 sowie eine Funktion, die eingehende Daten einer Extended APDU verarbeitet. Die folgenden Instruktionen können an die Java Card gesendet werden:
- INS_IMPORT_KEY:
B0 | 10 | 01 00 | 20 | Ke B0 | 10 | 02 00 | 20 | Ka
- INS_UNWRAP_KEY:
B0 | 20 | 00 00 | len(WKc) | WKc | 01 00
Anmerkung: Die Länge von WKc
wird bei der letzten Instruktion durch drei Bytes angegeben (Extended APDU).
Unsere Tests haben ergeben, dass ein Unwrapping im Durchschnitt 400ms braucht. Rechnet man dies auf 60s hoch, kann man von ca. 150 Unwraps pro Minute ausgehen.
package org.seminar;
import javacard.framework.*;
import javacard.security.AESKey;
import javacard.security.KeyBuilder;
import javacard.security.MessageDigest;
import javacardx.apdu.ExtendedLength;
import javacardx.crypto.Cipher;
public class UnwrapApplet extends Applet implements ExtendedLength {
private final byte CLA = (byte)0xB0;
private final byte INS_IMPORT_KEY = (byte)0x10;
private final byte INS_UNWRAP = (byte)0x20;
/* Sonstige Buffer */
private final short RAM_BUF_SIZE = 512;
private byte[] ram_buf = null; // In diesen Buffer werden die Daten der Extended APDUs geschrieben
private byte[] working_buf_32 = null; // Buffer, der als Zwischenspeicher für die Algorithmen dient.
/* AES-256-CTR */
private final short AES_BLOCK_SIZE = 16;
private final short AES_KEY_LENGTH = 32;
private final short KC_LENGTH = 256;
private AESKey Ke = null; // Encryption-Key des Servers 'Ke'
private Cipher aesCipher = null; // AES Instanz
private byte[] iv = null; // Initialisierungvektor
/* HMAC-SHA256 */
private final short HMAC_BLOCK_SIZE = 64;
private final short HMAC_HASH_SIZE = 32;
private final short HMAC_KEY_LENGTH = 32;
private final short HMAC_BUFFER_SIZE = 512;
private byte[] Ka = null; // Authentication-Key des Servers 'Ka'
private byte[] hmac_buffer = null; // Buffer, der für den HMAC-Algorithmus genutzt wird.
private byte[] hmac_init = null; // Buffer, der genutzt wird um die zu hashende Nachricht vorzubereiten
private MessageDigest sha_256 = null; // SHA256 Instanz
/* In dem Konstruktor werden sämtliche Buffer initialisiert. */
private UnwrapApplet(byte[] bArray, short bOffset, byte bLength) {
ram_buf = JCSystem.makeTransientByteArray(RAM_BUF_SIZE, JCSystem.CLEAR_ON_DESELECT);
// AES
iv = JCSystem.makeTransientByteArray((short)16, JCSystem.CLEAR_ON_DESELECT);
working_buf_32 = JCSystem.makeTransientByteArray(HMAC_HASH_SIZE, JCSystem.CLEAR_ON_DESELECT);
Ke = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false);
aesCipher = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_ECB_NOPAD, false);
// HMAC
Ka = new byte[HMAC_BLOCK_SIZE];
Util.arrayFillNonAtomic(Ka, (short)0, HMAC_BLOCK_SIZE, (byte)0);
sha_256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false);
hmac_buffer = JCSystem.makeTransientByteArray(HMAC_BUFFER_SIZE, JCSystem.CLEAR_ON_DESELECT);
hmac_init = JCSystem.makeTransientByteArray((short)512, JCSystem.CLEAR_ON_DESELECT);
register();
}
public static void install(byte[] bArray, short bOffset, byte bLength) throws ISOException {
new UnwrapApplet(bArray, bOffset, bLength);
}
@Override
public boolean select() {
return true;
}
@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] != CLA)
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
switch (buffer[ISO7816.OFFSET_INS]) {
case INS_IMPORT_KEY:
importKey(apdu);
break;
case INS_UNWRAP:
unwrap(apdu);
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
/* Diese Funktion verarbeitet eingehende Extended APDUs und schreibt deren Inhalt in 'ram_buf' */
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) { // Solange weiterhin Daten empfangen werden
// 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 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;
}
/* Funktion, die zum importieren der Serverschlüssel genutzt wird. */
private void importKey(APDU apdu) {
byte[] buffer = apdu.getBuffer();
short cdata_offset = apdu.getOffsetCdata();
short incoming_length = apdu.getIncomingLength();
switch (buffer[ISO7816.OFFSET_P1]) { // Je nach Parameter Byte wird entweder Ka oder Ke gesetzt
case 0x01:
if (incoming_length != AES_KEY_LENGTH) break;
Ke.setKey(buffer, cdata_offset);
return;
case 0x02:
if (incoming_length != HMAC_KEY_LENGTH) break;
Util.arrayCopyNonAtomic(buffer, cdata_offset, Ka, (short)0, HMAC_KEY_LENGTH);
return;
default:
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
}
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
/* Funktion, die den eingehenden 'WKc' unwrappt */
private void unwrap(APDU apdu) {
byte[] buf = apdu.getBuffer();
short received_length = handleData(apdu); // Extended APDU wird behandelt
// Werfe Fehler, falls der eingehende Datenblock 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);
// HMAC Nachricht wird vorbereitet und gehasht
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);
// Vergleicht den berechneten HMAC mit dem mitgesendeten. Falls unterschiedlich, ist die Authentifizierung fehlgeschlagen.
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);
}
/* Entschlüsselung mit dem AES-256-CTR */
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 der Initialisierungvektor '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_32[offset]);
// Aktuellen Blockoffset berechnen.
offset = (short)((short)(offset + 1) % AES_BLOCK_SIZE);
}
}
/* Diese Funktion inkrementiert unseren Initialisierungvektor 'iv' */
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++;
}
}
}
/* 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);
}
}