JavaCard: Secure Channel

From
Jump to navigation Jump to search

Java Cards sind programmierbare Smart Cards.

In diesem Seminar ging es darum einen sicheren Kanal zwischen einer solchen Java Card und einem Kartenlesegrät herzustellen, sodass eine PIN sicher kontaktlos übertragen werden kann.

Dazu haben wir uns zuerst mit der Programmierung von Java Card Applets im Allgemeinen beschäftigt.

Dann haben wir in einer Recherche die sicheren Übertagungsprotokolle SCP (von GlobalPlatform) und Secure Messaging (von OpenPGP card und NIST PIV) gefunden.

Ein PIV SM Client existiert bereits in OpenSC und wir haben versucht PIV SM in das existierende PivApplet zu integrieren.

Kompilieren und Installieren von Applets

Die Werkzeuge zum Bauen eines Java-Card-Applets werden von Oracle zur Verfügung gestellt: Java Card Downloads.

Informationen zum Installieren der Werkzeuge sind hier zu finden: Java Card 3.2 Documentation. Obwohl unsere Java Cards die Version 3.0.5 haben, sollte dennoch immer das aktuellste Java Card Development Kit verwendet werden.

Zunächst wird der Quellcode eines Java-Card-Applets in Form einer .java-Datei benötigt. Dieses wird mit dem Java-Compiler javac in eine .class-Datei konvertiert:

javac -cp /path/to/tools/lib/api_classic-3.0.5 -g -source 7 -target 7 -d bin/ src/package/Class.java

Diese .class-Datei kann mit dem converter.sh Skript aus den Java Card Development Kit Tools in eine .cap-Datei konvertiert werden:


/path/to/tools/bin/converter.sh -applet 0x01:0x02:0x03:0x04:0x05:0x00 package.Class -classdir bin/ -d deliverables/ -target 3.0.5 package 0x01:0x02:0x03:0x04:0x05:0x01 1.0

Installation der Treiber für den Kartenleser

sudo pacman -S pcsclite ccid
sudo systemctl start pcscd.service
sudo systemctl enable pcscd.service

Der Treiber für Kartenleser von REINER SCT kann aus dem AUR installiert werden:

git clone https://aur.archlinux.org/pcsc-cyberjack.git
cd pcsc-cyberjack
makepkg
sudo pacman -U pcsc-cyberjack-3.99.5_SP16-1-x86_64.pkg.tar.zst

Prüfen ob der Kartenleser und die Karte erkannt werden:

sudo pacman -S pcsc-tools
pcsc_scan

GlobalPlatformPro

GlobalPlatformPro is ein Werkzeug zur Verwaltung von Applets und Packages auf einer Java Card. Zum Beispiel können damit die kompilierten .cap-Dateien installiert werden:

java -jar gp.jar --install Applet.cap

Um ein bereits existierendes Applet zu überschreiben muss zusätzlich noch --force angegeben werden.

Mit --delete können Applets und Packages wieder gelöscht werden.

Automatisierung des Buildprozesses

Es gibt verschiedene Möglichkeiten den Build-Prozess zu automatisieren, zum Beispiel:

  • Verwendung des Java Card Eclipse Plugins von Oracle
  • Makefile
  • ant-javacard
  • Gradle

Eclipse Plugin

Oracle stellt ein Java Card Eclipse Plugin zur Verfügung, welches unter dem folgenden Link heruntergeladen werden kann: Java Card Downloads.

Das Plugin ist Teil des Java Card Development Kits, welches wiederum aus den Java Card Development Kit Tools, Simulator, und Eclipse-Plugin besteht. Um das Eclipse-Plugin verwenden zu können, müssen sowohl die Tools, als auch der Simulator installiert sein.

Das Eclipse-Plugin ermöglicht eine einfache Erstellung von Java-Card-Applets in Eclipse, das automatische Bauen einer .cap-Datei, sowie die Verwendung des Java-Debuggers mit dem Simulator. Der Simulator ist wie eine virtuelle Java Card, auf der eigene Applets installiert und ausgeführt werden können.

Installation des Simulators

Obwohl die Installation der Tools, des Simulators, und des Eclipse-Plugins in der Java Card 3.2 Documentation ausführlich beschrieben sind, sollen hier die wesentlichen Punkte der Installation nocheinmal aufgeführt werden.

Die Tools und der Simulator müssen zunächst entpackt werden.

Schreibe nach /etc/reader.conf.d/jcsdk_config:

# Configuration to interact with the Oracle PCSC Reader for Linux
FRIENDLYNAME   "Oracle JCSDK PCSC Reader Demo 1"
DEVICENAME     127.0.0.1:9025
LIBPATH        /home/user/its-workshop/simulator/drivers/IFDHandler/libjcsdkifdh.so

Starte den pcscd-Dienst neu:

sudo systemctl restart pcscd.service

Prüfe ob der virtuelle Kartenleser erkannt wird:

pcsc_scan

Es müsste ein Kartenleser mir dem Namen Oracle JCSDK PCSC Reader Demo 1 zu sehen sein.

Der Simulator unterstützt in der von uns getesteten Version 3.2 vom Februar 2024 den GlobalPlatform-Standard. Daher müssen vor der ersten Verwendung des Simulators die GlobalPlatform-Schlüssel, sowie eine PIN gesetzt werden. Dafür befindet sich in dem Verzeichnis, in das der Simulator entpackt wurde, ein Konfigurationswerkzeug. Abstrakt muss folgender Befehl ausgeführt werden:

java -jar Configurator.jar -binary <simulator-binary> -SCP-keyset <k_enc> <k_mac> <k_dek> -global-pin <pin> <tries_count>

Konkret haben wir unseren Simlator (das Binary mit dem Namen jcsl) folgendermaßen konfiguriert:

java -jar /home/user/its-workshop/simulator/tools/Configurator.jar -binary /home/user/its-workshop/simulator/runtime/bin/jcsl -SCP-keyset 1111111111111111111111111111111111111111111111111111111111111111 2222222222222222222222222222222222222222222222222222222222222222 3333333333333333333333333333333333333333333333333333333333333333 -global-pin 01020304050f 03

Der Simulator ist ein 32-Bit-Executable, also müssen wir noch glibc und openssl in der 32-Bit Version installieren. Unter Arch Linux muss dafür das multilib-repo aktiviert werden (siehe Arch Wiki), und die Pakete lib32-glibc sowie lib32-openssl installiert werden.

Installation des Eclipse-Plugins

Die Installation des Eclipse-Plugins ist im Developer Kit Simulator User Guide zu finden. In den darauffolgenden Abschnitten wird darüberhinaus noch die Konfiguration der Sample Platform und des Sample Device beschrieben. Das beinhaltet die Bearbeitung der Datei simulator/samples/client.config.properties.

Verwendung des Eclipse-Plugins

Im Developer Kit Simulator User Guide ist auch die Verwendung des Plugins beschrieben. In Eclipse kann nach erfolgreicher Installation ein neues Java-Card-Projekt angelegt werden. Dabei wird ein kleines Applet erstellt, welches auch direkt kompiliert wird.

Die Kompiltation schlug allerdings in Eclipse 09-2024 mit dem Java Card Development Kit 3.2 vom Februar 2024 fehl:

Compiling for Java version '1.7' is no longer supported. Minimal supported version is '1.8'

Die target-Version des .class-Files darf maximal 7 sein, d.h. Java 1.7. Class-Dateien höhrerer Versionen werden vom Java Card Converter nicht akzeptiert. Jedoch verweigert Eclipse die Erstellung von Class-Dateien der Version 1.7, da diese veraltet sind.

Mittlerweile existiert eine neue Version des Java Card Development Kits 3.2 vom 6. Oktober 2024. Wir haben nicht geprüft, ob dieser Fehler mit dieser Version immernoch auftritt.

Verwendung der Smartcardio-Bibliothek in Eclipse

Smartcardio ist der Name des Teils der Java-Standardbibliothek, mit dem aus einem Java-Programm heraus mit Java-Card-Applets kommuniziert werden kann. Konkret stellt diese Bibliothek Methoden zur Verfügung, mit denen Kommando-APDUs an ein Java-Card-Applet gesendet, und Antwort-APDUs ausgewertet werden können.

Da Smartcardio Teil der Standardbibliothek ist, kann sie einfach importiert werden; es ist nicht nötig einen Classpath beim Kompilieren anzugeben. In Eclipse jedoch wird diese Bibliothek nicht direkt erkannt. Sie muss erst als Modul-Abhängigkeit angegeben werden.

Dafür muss in der Datei src/module-info.java die Zeile

requires java.smartcardio;

hinzugefügt werden. Sollte die Datei nicht existieren, kann sie mit folgendem Inhalt neu angelegt werden:

module <module_name> {
    requires java.smartcardio;
}

Alternativ kann auch die grafische Benutzeroberfläche von Eclipse verwendet werden, sofern die module-info.java bereits existiert: Rechtsklick auf das Projekt im Datei-Baum, build path, configure build path, module dependencies, add system module, dann wähle java.smartcardio.

VS Code Plugin

Alternativ zu Eclipse kann VS Code für syntax highlighting und auto completions verwendet werden. Dazu müssen zumindest diese beiden Plugins installiert werden:

  • redhat.java (“Language Support for Java(TM) by Red Hat”)
  • vscjava.vscode-java-dependency (“Project Manager for Java”)

Außerdem wird eine lokale Kopie des Java Card SDK benötigt. Das kann zum Beispiel von https://github.com/martinpaljak/oracle_javacard_sdks heruntergeladen oder als git submodule eingebunden werden.

Das Projekt lässt sich durch die Datei .vsode/setting.json konfiguren. (Datei im lokalen Projekt erstellen falls nicht vorhanden und (optional) im globalen ~/.gitignore ignorieren.)

{
    "java.project.sourcePaths": [
        "src"
    ],
    "java.project.referencedLibraries": [
        "PATH_TO_SDKS/jc320v24.0_kit/lib/*.jar"
    ],
    "java.import.maven.enabled": false
}

Die Pfade zu src und dem SDK können entsprechend angepasst werden. Falls in einem Unterordner zusätzlich noch Maven-Projekte liegen, dann muss java.import.maven.enabled auf false gesetzt werden, da diese sonst über die java.project.*-Konfiguration bevorzugt werden.

Makefile

Da das Eclipse-Plugin nicht wie gewünscht funktioniert hat, haben wir die Werkzeuge direkt von der Kommandozeile verwendet. Um aus dem Quellcode direkt eine .cap-Datei zu bauen, haben wir ein Makefile geschrieben:

package := secureChannelTest
class := SecureChannelTest

package_aid := 0x01:0x02:0x03:0x04:0x05:0x24
applet_aid := 0x01:0x02:0x03:0x04:0x05:0x25

javacard_api := /home/user/its-workshop/tools/lib/api_classic-3.0.5.jar
globalplatform_api := /home/user/its-workshop/gpapi-globalplatform.jar
converter := /home/user/its-workshop/tools/bin/converter.sh
javacard_version := 3.0.5

sourcedir := src
builddir := bin
outputdir := deliverables
exportdir := exports

sourcefile := ${sourcedir}/${package}/${class}.java
classfile := ${builddir}/${package}/${class}.class
capfile := ${outputdir}/${package}/javacard/${package}.cap
exportfile := ${exportdir}/org/globalplatform/javacard/globalplatform.exp

all: applet

applet: ${capfile}

clean:
    rm -rf ${builddir}
    rm -rf ${outputdir}

${builddir}:
    mkdir ${builddir}

${classfile}: ${sourcefile} ${builddir} ${javacard_api} ${globalplatform_api}
    javac \
        -cp ${javacard_api}:${globalplatform_api} \
        -d ${builddir} \
        -g \
        -source 7 \
        -target 7 \
        ${sourcefile}

${outputdir}:
    mkdir ${outputdir}

${capfile}: ${classfile} ${exportfile} ${converter}
    ${converter} \
        -applet ${applet_aid} ${package}.${class} \
        -classdir ${builddir} \
        -d ${outputdir} \
        -exportpath ${exportdir} \
        -target ${javacard_version} \
        ${package} ${package_aid} 1.0

Dieses Makefile baut das Secure Channel Beispiel. Die Pfade zu den Java-Card-Tools müssen angepasst werden. Außerdem werden die .jar-Datei und die .exp-Datei der GlobalPlatform API benötigt. Für unsere Java Cards benötigen wir die GlobalPlatform API in der Version 1.6 (mehr dazu hier im Abschnitt 6.1) und das Exportfile in der Version 2.3, zu finden im Verzeichnis exports23.

Sollte das Makefile verwendet werden um ein Applet zu bauen welches die GlobalPlatform API nicht verwendet, können die entsprechenden Variablen, die GlobalPlatform API aus dem Classpath des javac-Befehels, und die -exportpath-Option des converter-Befehls entfernt werden.

ant-javacard

ant-javacard ist ein Plugin für Apache Ant (Ant ist wie Makefiles für Java), das von den meisten open source applets zum Kompilieren per Komandozeile verwentet wird. Mit ant-javacard ist das Setup einfacher und weniger Fehleranfälling als mit einfachen Makefiles.

Gradle

Gradle ist ein Buildsystem, das direkt mit Ant und IDEs (z.B. VS Code) integrierbar ist und deshalb haben wir versucht den Workflow durch den Einsatz von Gradle zu vereinfachen. Es hat sich aber herausgestellt, dass die Konfiguration von Gradle wesentlich umfangreicher als von ant-javacard kombiniert mit dem VS Code setting.json ist. Im Gegensatz zum KISS-Prinzip implementiert Gradle viele für unsere Zwecke unnötige Features und erschwert somit für uns die Entwicklung.

Insgesamt würden wir aktuell vom Einsatz von Gradle zur Entwicklung von (einfachen) Java-Card-Applets abraten.

GlobalPlatform – Secure Channel Protocol

GlobalPlatform standardisiert die Installation und Deinstallation von Applets. Zu diesem Zweck wurde das Secure Channel Protokoll entwickelt.

Es existiert eine Implementation, welche sowohl die Client- als auch die Card-Application beinhaltet, und eine gegenseitige Authentisierung vornimmt, wie sie in SCP03, Abschnitt 5.2 beschrieben ist.

Das Java-Card-Applet verwendet die Security Domain der Java Card beim Aufbau des Secure Channels. Das bedeutet, dass das Secure Channel Protokoll auf der Karte nicht selber implementiert werden muss, sondern die Implementation der Karte verwendet werden kann.

Die Client-Seite ist eine eigene Implementation von SCP03, die jedoch nur die gegenseitige Authentifikation beinhaltet. Es können noch keine APDUs sicher übermittelt werden.

GlobalPlatform C-Bibliothek

Die C-Bibliothek GlobalPlatform ist eine quelloffene Implementation des GlobalPlatform Standards. Wir haben versucht einen Client zu schreiben, der diese Bibliothek verwendet. Die API-Dokumentation befindet sich unter: GlobalPlatform API. Das Projekt beinhaltet die GPShell, welche selber die C-Bibliothek verwendet, und daher Beispielcode zur Verwendung der Bibliothek enthält.

Wir haben ein Programm gp-client.c erstellt, welches zunächst nur eine Verbindung zum Kartenleser herstellen sollte. Leider gab es bereits beim Auslesen der Anzahl angeschlossener Kartenleser Probleme, in diesem Fall einen Segmentation Fault:

#include <globalplatform/globalplatform.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static OPGP_CARD_CONTEXT cardContext;

int main()
{
    OPGP_enable_trace_mode(OPGP_TRACE_MODE_ENABLE, stderr);

    // Create the error status structure.
    OPGP_ERROR_STATUS status;
    OPGP_ERROR_CREATE_NO_ERROR(status);

    _tcsncpy(cardContext.libraryName, _T("gppcscconnectionplugin"), _tcslen(_T("gppcscconnectionplugin"))+1);
    _tcsncpy(cardContext.libraryVersion, _T("1"), _tcslen(_T("1"))+1);

    // Establish context.
    status = OPGP_establish_context(&cardContext);
    if (OPGP_ERROR_CHECK(status)) {
        _tprintf(_T("establish_context failed with error 0x%08X (%s)\n"),
                (unsigned int)status.errorCode, status.errorMessage);
        return EXIT_FAILURE;
    }

    // Release context.
    status = OPGP_release_context(&cardContext);
    if (OPGP_ERROR_CHECK(status)) {
        _tprintf(_T("release_context failed with error 0x%08X (%s)\n"),
                (unsigned int)status.errorCode, status.errorMessage);
        return EXIT_FAILURE;
    }

    // Get the number of card readers.
    DWORD len = 0;
    status = OPGP_list_readers(cardContext, NULL, &len);
    if (OPGP_ERROR_CHECK(status)) {
        _tprintf(_T("list_readers failed with error 0x%08X (%s)\n"),
                (unsigned int)status.errorCode, status.errorMessage);
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Das Makefile dazu:

source_file := gp-client.c
target_file := gp-client
library_path := /home/user/its-workshop/globalplatform/lib

run: ${target_file}
    LD_LIBRARY_PATH=${library_path} ./${target_file}

${target_file}: ${source_file} ./Makefile
    cc \
        -Wall \
        -I /home/user/its-workshop/globalplatform/include \
        -I /usr/include/PCSC \
        -L ${library_path} \
        ${source_file} \
        -l globalplatform \
        -o ${target_file}

Secure Messaging

Secure Messaging ist eine end-to-end encryption (+MAC) zwischen dem Terminal (Kartenleser) und der Karte und wird bei kontaktlosen Datenübertragungen eingesetzt. Bei kontaktbehafteten Anwendungen ist kein SM vorgesehen (zumindest bei PIV und openPGP card).

Der Vorteil von SM gegenüber SCP ist, dass nicht die statischen GlobalPlatform-Schlüssel bekannt sein müssen, um einen sicheren Kanal aufzubauen. Dadurch ist der Aufbau eines sicheren Kanals auch für Anwenungen möglich, die nicht zur (De-)Installation der Applets berechtigt sind.

Es gibt zwei verbreitete Standards, die Secure Messaging einsetzen:

  • openPGP card
  • PIV (NIST SP 800-73-5)
    • Part 1: Allgemeines, z.B. welche Daten auf der Karte existieren und welches Autorisierunglevel für den Zugriff benötigt wird
    • Part 2: Protokoll Details, z.B. wie die Schlüsselaushandlung und die Verschlüsselung funktioniert
    • Part 3 enthält eine Spezifikation einer API zwischen einer off-card Middleware und einem Anwendungsprogramm – unsere Middleware (OpenSC) implementiert diese API nicht

Der wesentliche Unterschied zwischen dem SM von openPGP card und PIV ist, dass openPGP card statische AES-Schlüssel für Verschlüsselung und MACing verwendet und PIV ephemäre Sitzungsschlüssel aus einem durch Eliptic-Curve-Diffie-Hellman erzeugtem geteiltem Gehemnis ableitet.

In NIST SP 800-73-5 Part 2, Abschnitt 4 wird das PIV SM ausführlich beschrieben.

Wenn ein Applet SM unterstützt, dann muss das Applet dies in der Anwort auf das SELECT-APDU durch die unterstützten Ciphersuites CS2 und/oder CS7 melden.

OpenSC

OpenSC ist eine open source middleware (library + CLI tools) für smart cards und implementiert unter anderem auch PIV einschließlich SM auf der Client-Seite.

Die Implementation von SM ist in card-piv.c zu finden. Die Funktion, in der die client-seitige Schlüsselübereinkunft implementiert ist, heißt piv_sm_open.

kompilieren

OpenSC zu compilieren ist leider wegen der Benutzung des antiken GNU-Buildsystems nicht trivial. Standardmäßig versucht dieses Buildsystem immer alles global zu installieren, was für “printf-Debugging” eher ungeeignet ist. Um das zu unterbinden, muss für alles was normalerweise global installiert wird ein lokaler Pfad angegeben werden.

Außerdem ist SM standardmäßig durch den C-Präprozessor statisch deaktiviert und muss zur Kompilezeit mit --enable-piv-sm aktiviert werden.

Das Build-Script muss vom Wurzelverzeichnis des Repos ausgeführt werden.

prefix="path/to/prefix" # e.g. prefix="$(realpath "$(dirname "$0")")/dist"
autoreconf --verbose --install
./configure --prefix="$prefix" --sysconfdir="$prefix/etc" --with-completiondir="$prefix/etc/bash_completion.d" --enable-piv-sm
make -j "$(nproc)"
make install

Um den Integrationstest p11test (zum testen der PIV Karte) mit zu kompilieren muss außerdem vor dem Kompilieren das package cmocka installiert werden, ansonsten wird das GNU-Buildsystem hilfreicherweise den Test einfach ohne sämtliche Warnungen ignoreren und nicht mit kompilieren.

sudo pacman -S cmocka

bash completions fix

Die bash completions von OpenSC funktionieren nicht mehr mit der aktuellen Version von bash-completions, da die Funktion _split_longopt entfernt wurde. Mit diesem Patch haben wir die bash completions gefixed. (Die gepatchte Datei gibt aus autogeneriert zu sein, aber wir haben nicht herausgefunden wie die Datei generiert wird.)

Patch für OpenSC bash completions
diff --git a/doc/tools/completion-template b/doc/tools/completion-template
index 895a3a9ceb52970e5ff7dd4515e473b649945090..85f80fac6d4576f40b2529209d4700f76d4c3003 100644
--- a/doc/tools/completion-template
+++ b/doc/tools/completion-template
@@ -3,10 +3,9 @@ _FUNCTION_NAME()
 {
     COMPREPLY=()
     local cur prev split=false
+    _comp_initialize -s
     _get_comp_words_by_ref -n : cur prev
 
-    _split_longopt && split=true
-
     opts="ALLOPTS"
 
     if [ ${COMP_CWORD} -eq 1 ]; then
@@ -37,7 +36,7 @@ _FUNCTION_NAME()
             ;;
     esac
 
-    $split && return 0
+    $was_split && return 0
 
     if [[ "$cur" == -* ]]; then
         _longopt $1

benutzen

Wenn OpenSC nicht global installiert ist, dann muss PATH und LD_LIBRARY_PATH entsprechent gesetzt werden. Wir haben dazu eine bashrc-Datei geschrieben, die diese Umgebungsvariablen setzt und zusätzlich noch die bash completions einbindet.

Eine Test-Umgebung kann dann mit bash --rcfile "path/to/bashrc" gestartet werden.

#!/usr/bin/env bash
[[ -f ~/.bashrc ]] && source ~/.bashrc # include user's bashrc
prefix="path/to/prefix" # e.g. prefix="$(realpath "$(dirname "${BASH_SOURCE[0]}")")/dist"
export PATH="$prefix/bin:$PATH"
export LD_LIBRARY_PATH="$prefix/lib:$LD_LIBRARY_PATH"
for file in "$prefix/etc/bash_completion.d/"*; do
    [[ -f "$file" ]] && source "$file"
done

OpenSC beinhaltet ein CLI-Tool piv-tool was angeblich zur Verwaltung von Schlüsseln und Zertifikaten auf der Karte verwentet werden kann, allerdings hat das Tool für uns nicht funktioniert und das CI testing von OpenSC benutzt selber das yubico-piv-tool statt ihrem eigenem Tool (???), deswegen haben wir das dann auch benutzt.

Nach der initializierung kann mit dem CLI-Tool pkcs11-tool auf die PIV-Funktionalität zugegriffen werden oder der Integrationstest in src/tests/p11test verwendet werden, um das Applet vollständig zu testen.

Standardmäßig verwendet OpenSC kein SM, auch wenn es mit --penable-piv-sm kompiliert wurde. Es muss zusätzlich noch die Umgebungsvariable PIV_USE_SM=always gesetzt werden.

Außerdem wird PIV_USE_SM=always aktuell ignoriert, wenn die Karte als Antwort auf das SELECT-APDU meldet, dass sie kein SM unterstützt. Das erscheint für uns wie ein möglicher Downgrade-Angriff auf OpenSC.

yubico-piv-tool

YubiKeys implementieren unter anderem auch PIV aber ohne SM. Das yubico-piv-tool kann verwendet werden, um die PIV-Schlüssel auf der Karte generieren zu lassen und um die X.509 Zertifikate der Schlüssel auf die Karte zu laden:

# Schlüsselpaar erstellen und öffentlichen Schlüssel ausgeben
yubico-piv-tool --verbose --reader '' --pin 123456 --slot 9e --action generate --algorithm RSA2048 | tee 9e.pub
# Zertifikat importieren (nach Signieren durch eigene CA)
yubico-piv-tool --verbose --reader '' --pin 123456 --slot 9e --action import-certificate < 9e.cert

Durch einen einfachen Patch kann dieses Tool auch einen SM-Schlüssel generieren:

Patch zum erstellen von SM-Schlüsseln mit yubico-piv-tool
diff --git a/common/util.c b/common/util.c
index f0a9d5c9da9b4837a3bf3df5e0c5c8c2223938c8..6d9dbdd5d42431d0b7af460732cf2ea8e3576c6e 100644
--- a/common/util.c
+++ b/common/util.c
@@ -374,6 +374,9 @@ int get_slot_hex(enum enum_slot slot_enum) {
     case slot_arg_f9:
       slot = 0xf9;
       break;
+    case slot_arg_04:
+      slot = 0x04;
+      break;
     case slot__NULL:
     default:
       slot = -1;
diff --git a/tool/cmdline.ggo b/tool/cmdline.ggo
index 7fe347f597214099f3cfd0e31696e9f8bd9053d9..dbc10ceea9415a099de42393fab4cfe21b69f129 100644
--- a/tool/cmdline.ggo
+++ b/tool/cmdline.ggo
@@ -37,14 +37,16 @@ option "action" a "Action to take" values="version","generate","set-mgm-key",
 text   "
        Multiple actions may be given at once and will be executed in order
        for example --action=verify-pin --action=request-certificate\n"
-option "slot" s "What key slot to operate on" values="9a","9c","9d","9e","82","83","84","85","86","87","88","89","8a","8b","8c","8d","8e","8f","90","91","92","93","94","95","f9" enum optional
+option "slot" s "What key slot to operate on" values="9a","9c","9d","9e","82","83","84","85","86","87","88","89","8a","8b","8c","8d","8e","8f","90","91","92","93","94","95","f9","04" enum optional
 text   "
        9a is for PIV Authentication
        9c is for Digital Signature (PIN always checked)
        9d is for Key Management
        9e is for Card Authentication (PIN never checked)
        82-95 is for Retired Key Management
-       f9 is for Attestation\n"
+       f9 is for Attestation
+       04 is for Secure Messaging
+       "
 option "to-slot" - "What slot to move an existing key to" values="9a","9c","9d","9e","82","83","84","85","86","87","88","89","8a","8b","8c","8d","8e","8f","90","91","92","93","94","95","f9" enum optional
 text   "
        9a is for PIV Authentication
@@ -52,7 +54,8 @@ text   "
        9d is for Key Management
        9e is for Card Authentication (PIN never checked)
        82-95 is for Retired Key Management
-       f9 is for Attestation\n"
+       f9 is for Attestation
+       "
 option "algorithm" A "What algorithm to use" values="RSA1024","RSA2048","RSA3072", "RSA4096", "ECCP256","ECCP384", "ED25519", "X25519" enum optional default="RSA2048"
 option "hash" H "Hash to use for signatures" values="SHA1","SHA256","SHA384","SHA512" enum optional default="SHA256"
 option "new-key" n "New management key to use for action set-mgm-key, if omitted key will be asked for" string optional

Generieren des SM-Schlüssels nach Patch (muss ECCP384 oder ECCP256 sein!):

yubico-piv-tool --verbose --reader '' --pin 123456 --slot 04 --action generate --algorithm ECCP384

PivApplet

Das PivApplet ist eine Open-Source-Implementation von PIV auf der Kartenseite, unterstützt aber aktuell noch kein SM.

kompilieren & installieren

Das PivApplet wird mit ant-javacard gebaut:

ant dist

Dabei wird die .cap-Datei in bin/PivApplet.cap erstellt und kann dann wie alle Applets mit GlobalPlatformPro installiert werden:

java -jar path/to/gp.jar --install bin/PivApplet.cap --force

In dem repo existiert auch ein Skript build.rb das verschiede Varianten (Preprozessor defines) hintereinander kompiliert – dieses Skript wird nicht benötigt und wurde von uns nicht verwendet.

Migration zu JavaCard 3.0.5

Als ersten Schritt haben wir das PivApplet zu der Java-Card-Version unserer physischen Java Cards (3.0.5) und dem neusten SDK (24.0) geupdated.

Dazu musste als erstes ant-javacard, das als git submodule in das Repo eingebunden ist, auf die neueste Version gebracht werden.

git submouldes crash course

Um ein Submodul lokal zu updaten muss in das Verzeichnis des Submoduls gewechselt werden, dort die HEAD ref des inneren Repos auf den gewünschten Commit gesetzt werden und anschließend das Submodul im äußerem Repo committed werden:

cd ext/ant
git fetch
git switch --detach origin/master
cd ../..
git add ext/ant
git commit -m 'update ant-javacard submodule'

Der resultierende Commit sollte dann etwa so aussehen (siehe git show):

diff --git a/ext/ant b/ext/ant
index 2733c30..e2e294b 160000
--- a/ext/ant
+++ b/ext/ant
@@ -1 +1 @@
-Subproject commit 2733c303704ae1356991285456d0d1a06b3dcb24
+Subproject commit e2e294b4444d6b25d77fd0162d32071b8650c69c

Dieser Commit kann dann mit git push und git pull in ein anderes Repo transferiert werden. Anschließend müssen in dem anderem Repo dann die Submodule erneut augecheckt werden:

git submodule update --init --recursive

Fall irgendwas schief geht, dann hilft häufig das Zurücksetzen der Submodule:

git submodule deinit --all --force

Anschließend haben wir im build.xml die entsprechenden Versionsnummern eingetragen und in PivApplet.java veraltete (deprecated) Methoden ersetzt:

Patch für Versionsupdate
diff --git a/build.xml b/build.xml
index 5417422598ccf67a9561ef05b8abb9745fbbeef7..7fd393a3ef9eef5903836f86bdf2535462dbb23b 100644
--- a/build.xml
+++ b/build.xml
@@ -72,8 +72,8 @@
     <ant dir="ext/ant"/>
     <!-- Create the distribution directory -->
     <taskdef name="javacard" classname="pro.javacard.ant.JavaCard" classpath="ext/ant/ant-javacard.jar"/>
-    <javacard>
-      <cap aid="a0:00:00:03:08:00:00:10" output="bin/PivApplet.cap" sources="src-gen" classes="bin" version="1.0">
+    <javacard jckit="ext/ant/sdks/jc320v24.0_kit">
+      <cap aid="a0:00:00:03:08:00:00:10" output="bin/PivApplet.cap" sources="src-gen" classes="bin" version="1.0" targetsdk="3.0.5">
         <applet class="net.cooperi.pivapplet.PivApplet" aid="a0:00:00:03:08:00:00:10:00:01:00"/>
       </cap>
     </javacard>
diff --git a/src/net/cooperi/pivapplet/PivApplet.java b/src/net/cooperi/pivapplet/PivApplet.java
index 9abbfeb453e060ba08386f4354121086f138ecec..8b9c6bf5591f03682a55a0d0283ff370d1b61aa9 100644
--- a/src/net/cooperi/pivapplet/PivApplet.java
+++ b/src/net/cooperi/pivapplet/PivApplet.java
@@ -271,7 +271,7 @@ public class PivApplet extends Applet
    protected
    PivApplet()
    {
-       randData = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM);
+       randData = RandomData.getInstance(RandomData.ALG_KEYGENERATION);
 //#if PIV_SUPPORT_3DES
        tripleDes = Cipher.getInstance(Cipher.ALG_DES_CBC_NOPAD, false);
 //#endif
@@ -376,15 +376,15 @@ public class PivApplet extends Applet
            JCSystem.CLEAR_ON_DESELECT);
 
        guid = new byte[16];
-       randData.generateData(guid, (short)0, (short)16);
+       randData.nextBytes(guid, (short)0, (short)16);
        cardId = new byte[21];
        Util.arrayCopy(CARD_ID_FIXED, (short)0, cardId, (short)0,
            (short)CARD_ID_FIXED.length);
-       randData.generateData(cardId, (short)CARD_ID_FIXED.length,
+       randData.nextBytes(cardId, (short)CARD_ID_FIXED.length,
            (short)(21 - (short)CARD_ID_FIXED.length));
 
        serial = new byte[4];
-       randData.generateData(serial, (short)0, (short)4);
+       randData.nextBytes(serial, (short)0, (short)4);
        serial[0] |= (byte)0x80;
 
        certSerial = new byte[16];
@@ -1903,7 +1903,7 @@ public class PivApplet extends Applet
            outgoingLe = apdu.setOutgoing();
            wtlv.useApdu((short)0, outgoingLe);
 
-           randData.generateData(challenge, (short)0, len);
+           randData.nextBytes(challenge, (short)0, len);
            chalValid[0] = true;
            /*for (byte i = 0; i < (byte)len; ++i)
                challenge[i] = (byte)(i + 1);*/
@@ -1926,7 +1926,7 @@ public class PivApplet extends Applet
            outgoingLe = apdu.setOutgoing();
            wtlv.useApdu((short)0, outgoingLe);
 
-           randData.generateData(challenge, (short)0, len);
+           randData.nextBytes(challenge, (short)0, len);
            chalValid[0] = true;
            /*for (byte i = 0; i < (byte)len; ++i)
                challenge[i] = (byte)(i + 1);*/
@@ -2682,9 +2682,9 @@ public class PivApplet extends Applet
        pukPin.update(DEFAULT_PUK, (short)0, (byte)8);
        pukPinIsDefault = true;
 
-       randData.generateData(guid, (short)0, (short)16);
+       randData.nextBytes(guid, (short)0, (short)16);
 
-       randData.generateData(cardId, (short)CARD_ID_FIXED.length,
+       randData.nextBytes(cardId, (short)CARD_ID_FIXED.length,
            (short)(21 - (short)CARD_ID_FIXED.length));
 
        /*
@@ -3539,7 +3539,7 @@ public class PivApplet extends Applet
            return;
        }
 
-       randData.generateData(certSerial, (short)0,
+       randData.nextBytes(certSerial, (short)0,
            (short)certSerial.length);
        certSerial[0] = (byte)(certSerial[0] & (byte)0x7F);

Dieses geupdatete Applet konnte dann erfolgreich auf unseren Java Cards installiert und getestet werden.

Generation des SM-Schlüsselpaars

Wir haben das Applet gepatcht, um die Generation eines SM-Schlüsselpaars zu erlauben und an die Client-Anwendung zu melden, dass SM unterstützt ist.

Dazu haben wir im Applet den entsprechenden key slot (04) für das SM-Schlüsselpaar angelegt und ermöglichen für diesen slot eine Schlüsselgeneration per GENERATE ASYMMETRIC KEY-APDU mit den erlaubten Schlüsseltypen (NIST P-256 oder P-384). Wenn sich in dem SM key slot ein Schlüsselpaar befindet, dann wird als Antwort auf das SELECT-APDU die entsprechende cipher suite als unterstüzt gemeldet (CS2 für P-256 / CS7 für P-384).

Patch zum generieren von SM-Schlüsselpaaren im Applet
diff --git a/src/net/cooperi/pivapplet/PivApplet.java b/src/net/cooperi/pivapplet/PivApplet.java
index 8b9c6bf5591f03682a55a0d0283ff370d1b61aa9..4a6d4aff832d941b36c90fb092a82021dbf48311 100644
--- a/src/net/cooperi/pivapplet/PivApplet.java
+++ b/src/net/cooperi/pivapplet/PivApplet.java
@@ -196,7 +196,7 @@ public class PivApplet extends Applet
    private KeyAgreement ecdh = null;
    private KeyAgreement ecdhSha = null;
 
-   private static final byte MAX_SLOTS = (byte)17;
+   private static final byte MAX_SLOTS = (byte)18;
 
    private static final byte SLOT_9A = (byte)0;
    private static final byte SLOT_9B = (byte)1;
@@ -206,6 +206,7 @@ public class PivApplet extends Applet
    private static final byte SLOT_82 = (byte)5;
    private static final byte SLOT_8C = (byte)15;
    private static final byte SLOT_F9 = (byte)16;
+   private static final byte SLOT_04 = (byte)17; // Secure messaging (SM) key.
    private PivSlot[] slots = null;
    private byte retiredKeys = 0;
 
@@ -223,6 +224,10 @@ public class PivApplet extends Applet
    private static final byte PIV_ALG_ECCP256 = (byte)0x11;
    private static final byte PIV_ALG_ECCP384 = (byte)0x14;
 
+   // Cipher suites for secure messaging from NIST SP 800-73-5 part 2.
+   private static final byte PIV_ALG_CS2 = (byte)0x27;
+   private static final byte PIV_ALG_CS7 = (byte)0x2E;
+
    private static final byte GA_TAG_WITNESS = (byte)0x80;
    private static final byte GA_TAG_CHALLENGE = (byte)0x81;
    private static final byte GA_TAG_RESPONSE = (byte)0x82;
@@ -400,6 +405,8 @@ public class PivApplet extends Applet
        slots[SLOT_F9] = new PivSlot((byte)0xF9);
 //#endif
 
+       slots[SLOT_04] = new PivSlot((byte)0x04);
+
        files = new File[TAG_MAX + 1];
        ykFiles = new File[YK_TAG_MAX + 1];
 
@@ -941,6 +948,17 @@ public class PivApplet extends Applet
        }
 //#endif
 
+       if (slots[SLOT_04] != null) {
+           switch (slots[SLOT_04].asymAlg) {
+           case PIV_ALG_ECCP256:
+               pushAlgorithm(PIV_ALG_CS2);
+               break;
+           case PIV_ALG_ECCP384:
+               pushAlgorithm(PIV_ALG_CS7);
+               break;
+           }
+       }
+
        wtlv.pop();
        wtlv.end();
 
@@ -1062,6 +1080,8 @@ public class PivApplet extends Applet
            slot = slots[idx];
        } else if (key == (byte)0xF9) {
            slot = slots[SLOT_F9];
+       } else if (key == (byte)0x04) {
+           slot = slots[SLOT_04];
        } else {
            ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
            return;
@@ -1172,6 +1192,12 @@ public class PivApplet extends Applet
            return;
        }
 
+       // The SM key must be an ECC key.
+       if (key == (byte)0x04 && alg != PIV_ALG_ECCP256 && alg != PIV_ALG_ECCP384) {
+           ISOException.throwIt(ISO7816.SW_WRONG_DATA);
+           return;
+       }
+
 //#if PIV_SUPPORT_EC
        final ECPrivateKey ecPriv;
        final ECPublicKey ecPub;

Integrationstest

Testen mit virtueller Java Card

OpenSC verwendet das PivApplet um die PIV-Implementation zu testen. Dafür gibt es einen GitHub Workflow, der eine Testumgebung aufsetzt in der OpenSC gebaut, und das PivApplet auf einer virtuellen Java Card installiert wird. Da wir das PivApplet erweitern wollten, haben wir versucht diese virtuelle Testumgebung einzurichten.

Als erstes muss OpenSC gebaut werden. Dafür muss das Programm cmocka installiert werden.

sudo pacman -S cmocka

git clone https://github.com/OpenSC/OpenSC
cd OpenSC
./bootstrap
./configure --enable-piv-sm
make -j 4
make install

Als nächstes wird das Programm vsmartcard gebaut. Dafür wird das Programm help2man benötigt.

sudo pacman -S help2man

git clone https://github.com/frankmorgner/vsmartcard.git
cd vsmartcard/virtualsmartcard
autoreconf -vis
./configure
make -j 4
sudo make install

Dann kompilieren wir das Programm jcardsim. Dafür installieren wir zunächst Java Version 8 und aktivieren es. Außerdem benötigen wir ant.

sudo pacman -S jdk8-openjdk ant
sudo archlinux-java set java-8-openjdk

git clone https://github.com/Jakuje/jcardsim.git
cd jcardsim
sudo pacman -S maven
mvn initialize
mvn clean install

Jetzt kann das PivApplet gebaut werden (siehe hier). Danach muss pcscd gestartet werden und die virtuelle Java Card kann verwendet werden:

sudo systemctl start pcscd.socket
sudo systemctl start pcscd.service

java -noverify -cp PivApplet/bin/:jcardsim/target/jcardsim-3.0.5-SNAPSHOT.jar com.licel.jcardsim.remote.VSmartCard PivApplet/test/jcardsim.cfg >/dev/null &

Dies führte bei uns jedoch zu folgendem Fehler:

Exception in thread "main" java.net.ConnectException: Connection refused (Connection refused)

Mit dem durch vsmartcard installierten Programm vpcd-config kann die Konfiguration eingesehen werden. Die Konfiguration befindet sich in PivApplet/test/jcardsim.cfg. Dort ist als Host localhost und der Port 35963 angegeben. Mit ss -tunlp konnte kein Programm identifiziert werden, welches diesen Port verwendet. Wir haben an dieser Stelle nicht weiter investigiert, und stattdessen unsere physischen Java Cards verwendet.

Initialisierung der Schlüssel

Bevor das PivApplet getestet werden kann, müssen PIV-Schlüssel initialisiert und auf die Karte geschrieben werden. Dabei gehen wir so vor, wie OpenSC beim Testen von PIV.

Zunächst brauchen wir den Namen unseres Kartenlesers.

opensc-tool -l

Den Namen des Kartenlesers, der die Karte mit dem PivApplet enthält, wird in eine Variable geschrieben:

export GP_READER="REINER SCT cyberJack RFID standard (0954493871) 00 00"

Außerdem verwenden wir die Standard PIN:

export PIN="123456"

Dann können wir mit dem yubico-piv-tool die PIV-Schlüssel generieren, signieren, und das Zerifikat auf die Karte laden:

yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9e -a generate -A RSA2048 | tee 9e.pub
yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9e -S'/CN=barCard/OU=test/O=example.com/' -averify-pin -aselfsign < 9e.pub | tee 9e.cert
yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9e -aimport-certificate < 9e.cert

yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9a -a generate -A RSA2048 | tee 9a.pub
yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9a -S'/CN=bar/OU=test/O=example.com/' -averify-pin -aselfsign < 9a.pub | tee 9a.cert
yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9a -aimport-certificate < 9a.cert

yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9c -a generate -A ECCP256 | tee 9c.pub
yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9c -S'/CN=bar/OU=test/O=example.com/' -averify-pin -aselfsign < 9c.pub | tee 9c.cert
yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9c -aimport-certificate < 9c.cert

yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9d -a generate -A ECCP256 | tee 9d.pub
yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9d -S'/CN=bar/OU=test/O=example.com/' -averify-pin -aselfsign < 9d.pub | tee 9d.cert
yubico-piv-tool -v 9999 -r "$GP_READER" -P "$PIN" -s 9d -aimport-certificate < 9d.cert

Testen des PIV-Applets

cd OpenSC/src/tests/p11test
./p11test -v -p "$PIN" -o piv.json
diff -u3 piv{_ref,}.json

Ausblick

Die folgenden Schritte werden noch benötigt, um den Schlüsselaustausch im PivApplet zu implementieren.

Card Verifiable Certificate

Das PIV Secure Messaging Key Establishment ist in NIST SP 800-73-5, Teil 2, Abschnitt 4.1 beschrieben. Der erste Schritt der PIV Card Application ist die Berechnung des Hashes über dem Secure Messaging Card Verifiable Certificate.

Mit dem Werkzeug cvc-create aus OpenPACE lassen sich CVCs erstellen. Wir haben dieses Tool jedoch nicht getestet.

Der Aufbau eines CVC ist in NIST SP 800-73-5, Teil 2, Abschnitt 4.1.5 beschrieben.

Unklar ist noch, wie ein CVC auf die Karte geladen werden kann. Für die X.509 Zertifikate der PIV-Schlüssel sind in NIST SP 800-73-5, Teil 1 Object Identifier spezifiziert. Diese können mit der PUT DATA Instruktion verwendet werden, um Daten auf die Karte zu laden. Die PUT DATA Instruktion ist in NIST SP 800-73-5, Teil 2 standardisiert und wird vom PivApplet implementiert. Es existiert jedoch kein Object Identifier für das CVC des PIV Secure Messaging Schlüssels.

Eine Möglichkeit das CVC in das Applet zu laden besteht darin, eine eigene Instruktion dafür zu implementieren. Jedoch müsste dann auch die Client-Seite, also OpenSC oder yubico-piv-tool, diese Instruktion verwenden, um das CVC zu installieren.

Key-derivation function

Ein weiteres Problem bei der Implementation der Schlüsselübereinkunft auf der Java Card ist die verwendete Schlüsselableitungsfunktion (Schritt C7 in NIST SP 800-73-5, Teil 2, Abschnitt 4.1).

Sie ist spezifiziert in NIST SP 800-73-5, Teil 2, Abschnitt 4.1.6; in SP800-56A, Abschnitt 5.8.1; sowie in NIST SP 800-56C.

Wir haben keine existierende Java Card Implementation dieser Schlüsselableitungsfunktion finden können. Diese muss also voraussichtlich selber implementiert werden.

Gemeinsames Geheimnis

Zur Berechnung des gemeinsamen Geheimnisses (Schritt C5 in NIST SP 800-73-5, Teil 2, Abschnitt 4.1), kann (voraussichtlich) die Java Card API verwendet werden.

Dazu wird ein Objekt der Klasse KeyAgreement verwendet. Dieses wird mit der Methode init und dem Algorithmus ALG_EC_SVDP_DHC_PLAIN initialisiert. Mit der Methode generateSecret kann das gemeinsame Geheimnis berechnet werden. Dies konnten wir jedoch nicht testen.

Implementation im PivApplet

Die Schlüsselübereinkunft im PivApplet kann implementiert werden in src/net/cooperi/pivapplet/PivApplet.java. In der Methode processGeneralAuth wird zunächst der Keyslot ausgewählt. Dann wird die neu erstellte Methode processGenAuthSm gerufen.

Der erste Schritt besteht darin, die Daten auszulesen und zugreifbar zu machen, die der Client der Karte sendet. Dazu stellt das PivApplet Hilfsfunktionen bereit. Bis hierhin haben wir eine Implementation:

diff --git a/src/net/cooperi/pivapplet/PivApplet.java b/src/net/cooperi/pivapplet/PivApplet.java
index 4a6d4af..ac3d2ee 100644
--- a/src/net/cooperi/pivapplet/PivApplet.java
+++ b/src/net/cooperi/pivapplet/PivApplet.java
@@ -2217,6 +2217,19 @@ public class PivApplet extends Applet
    }
 //#endif
 
+   private void
+   processGenAuthSm(final APDU apdu, final PivSlot slot,
+       byte alg, final byte key, final Readable input,
+       final byte wanted, final byte tag)
+   {
+       // Read the data from the client
+       final short payload_len = tlv.tagLength();
+       final short read = tlv.read(tempBuf, payload_len);
+       if (read != payload_len)
+           ISOException.throwIt(ISO7816.SW_UNKNOWN);
+       final byte[] payload = tempBuf.data();
+   }
+
    private void
    processGeneralAuth(final APDU apdu)
    {
@@ -2237,6 +2250,8 @@ public class PivApplet extends Applet
            final byte idx = (byte)(SLOT_MIN_HIST +
                (byte)(key - MIN_HIST_SLOT));
            slot = slots[idx];
+       } else if (key == 0x04 && (alg == PIV_ALG_CS2 || alg == PIV_ALG_CS7)) {
+           slot = slots[SLOT_04];
        } else {
            ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
            return;
@@ -2371,6 +2386,10 @@ public class PivApplet extends Applet
                wanted, tag);
            break;
 //#endif
+       case PIV_ALG_CS2:
+       case PIV_ALG_CS7:
+           processGenAuthSm(apdu, slot, alg, key, input, wanted, tag);
+           break;
        default:
            tlv.abort();
            ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);

Danach muss in processGenAuthSm die eigentliche Schlüsselübereinkunft implementiert werden.