Verschlüsseln
Über FIT-Connect versendete Daten sind oft schützenswert und müssen daher verschlüsselt werden.
Eine Übertragung über FIT-Connect besteht aus
- einem Metadatensatz
- einem Fachdatensatz
- beliebig vielen Anlagen (optional)
Alle drei Datensatzarten müssen mit JSON Web Encryption (JWE) verschlüsselt und mit JWE-Compact Serialisierung serialisiert werden. Dokumente müssen als Binärdateien und nicht als Zeichenketten kodiert verschlüsselt werden.
Der vorher abgerufene Zustellpunkt beinhaltet die Schlüssel-ID des öffentlichen Verschlüsselungsschlüssels unter dem Feld encryptionKid
.
Damit können wir den öffentlichen Schlüssel des Zustellpunktes im JSON Web Key (JWK)-Format abrufen um Daten zu verschlüsseln.
$ KID=... # Wert des Feldes `encryptionKid`
$ SUBMISSION_API=https://submission-api-testing.fit-connect.fitko.dev
$ DESTINATION_ID=...
$ curl -X GET "$SUBMISSION_API/v1/destinations/$DESTINATION_ID/keys/$KID"
{
"kty": "RSA",
"key_ops": ["wrapKey"],
"x5c": [
"LS0tLS1CRUdJTiBDRVJU...jN1NGKzQKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
],
"kid": "787f3a1c-7da7-44d7-9b79-9783b1ea9be8",
"alg": "RSA-OAEP-256",
"n": "sX2DX7rG5BoJd23...FlxHZt8T6ZqjRa1QcFnkq3_M4-tk",
"e": "AQAB"
}
Mit diesem Schlüssel könnten jetzt die oben genannten Datensätze verschlüsselt werden. Die so verschlüsselten Daten können ausschließlich von diesem Zustellpunkt gelesen werden.
Bevor wir mit der Verschlüsselung loslegen können müssen wir den eben abgerufenen JWK noch auf Gültigkeit überprüfen.
Überprüfen des öffentlichen Schlüssels (Zertifikatsprüfung)
In der Testumgebung ist die Absicherung der öffentlichen Schlüssel eines Zustellpunktes durch Zertifikate optional. Eine Prüfung der Zertifikate kann in diesem Fall zu Testzwecken entfallen.
Öffentliche Schlüssel MÜSSEN vor der Verwendung zwingend im Client auf Gültigkeit geprüft werden. Diese Prüfung umfasst folgende Schritte:
- Überprüfung, dass der JSON Web Key gemäß den Vorgaben für kryptographische Verfahren eine Schlüssellänge von mind. 4096 Bit aufweist
- Überprüfung, dass der JSON Web Key gemäß den Vorgaben für kryptographische Verfahren für die Verschlüsselung geeignet ist (
"key_ops": ["wrapKey"]
) - Überprüfung, dass der JSON Web Key gemäß den Vorgaben für kryptographische Verfahren zur Nutzung des Verfahrens
RSA-OAEP-256
geeignet ist ("kty":"RSA", "alg": "RSA-OAEP-256"
) - Überprüfung, dass der öffentliche Schlüssel mit dem im JSON Web Key hinterlegten Zertifikat übereinstimmt (Attribute
n
unde
) - Überprüfung, dass das Zertifikat der Verwaltungs-PKI entstammt, d.h Überprüfung der Zertifikats-Kette bis zum Wurzelzertifikat (BSI) (Attribut
x5c
) - Überprüfung, dass der im Zertifikat angegebene Gültigkeitszeitraum nicht überschritten wurde
- Überprüfung, dass das Zertifikat nicht zurückgezogen wurde (mittels Validierungsdienst, gegen eine Certificate Revocation List (CRL) und/oder einen OCSP-Endpunkt mit signierten Antworten)
Weitere Informationen zur Gültigkeitsprüfung finden sich in der technischen Richtlinie BSI TR-02103 des BSI.
Der folgende Code kann zur Validierung der Vorgaben für kryptographische Verfahren benutzt werden. Zusätzlich MUSS eine Zertifikatsprüfung inkl. Prüfung der Zertifikatskette erfolgen.
- .NET (SDK)
- JavaScript
- Java
Diese Funktionalität wird durch das .NET-SDK bereits intern umgesetzt
und ist durch einen Aufruf der SDK-Methode ClientFactory.GetSenderClient(...).SendAsync(...)
automatisch mit abgedeckt:
var sender = ClientFactory
.GetSenderClient(FitConnectEnvironment.Testing, clientId, clientSecret, logger)
.SendAsync(sendableSubmission);
Der Quellcode oben ist ein Auszug aus dem Projekt ConsoleAppExample
,
das im Repository Codebeispiele - examples von FIT-Connect hinterlegt ist.
Eine Beschreibung des .NET-SDKs finden Sie im Hauptmenü unter "SDKs > .NET-SDK".
// Validierung des öffentlichen Schlüssels
var modulus = publicKey.algorithm.modulusLength
if(modulus < 4096){
throw new Error("Public key length is wrong. It should be at least 4096!")
}
if(publicKey.usages.length > 1 || publicKey.usages[0] != 'wrapKey'){
throw new Error("Keyops must be wrapKey!");
}
if(publicKey.algorithm.name != "RSA-OAEP"){
throw new Error("Wrong public key type! Only RSA is allowed");
}
if(publicKey.algorithm.hash.name != "SHA-256"){
throw new Error("Wrong hash algorithm! Only SHA-256 is allowed.");
}
if(publicKey.algorithm.publicExponent[0] != 1 ||
publicKey.algorithm.publicExponent[1] != 0 ||
publicKey.algorithm.publicExponent[2] != 1){
throw new Error("Public exponent must be AQAB!")
}
// Validierung des privaten Schlüssels
var modulus = privateKey.algorithm.modulusLength
if(modulus < 4096){
throw new Error("Private key length is wrong. It should be at least 4096!")
}
if(privateKey.algorithm.name != "RSA-OAEP"){
throw new Error("Wrong private key type! Only RSA is allowed");
}
if(privateKey.algorithm.hash.name != "SHA-256"){
throw new Error("Wrong hash algorithm! Only SHA-256 is allowed.");
}
if(privateKey.usages.length > 1 || privateKey.usages[0] != 'unwrapKey'){
throw new Error("Keyops must be unwrapKey!");
}
if(privateKey.algorithm.publicExponent[0] != 1 ||
privateKey.algorithm.publicExponent[1] != 0 ||
privateKey.algorithm.publicExponent[2] != 1){
throw new Error("Public exponent must be AQAB!")
}
public static void validateRSAKey(RSAKey RSAKey, boolean isPrivate){
validateTrueOrElseThrow((RSAKey.getModulus().decodeToBigInteger().bitLength() >= 4096), "JWK has wrong key length.");
if(isPrivate){
validateTrueOrElseThrow(RSAKey.getKeyOperations().size() == 1 &&
RSAKey.getKeyOperations().contains(KeyOperation.UNWRAP_KEY),
"The specified private key is not intended for 'unwrapKey' as specified through key operation.");
}
else{
validateTrueOrElseThrow(RSAKey.getKeyOperations().size() == 1 &&
RSAKey.getKeyOperations().contains(KeyOperation.WRAP_KEY),
"The specified public key is not intended for 'wrapKey' as specified through key operation.");
}
validateTrueOrElseThrow(RSAKey.getAlgorithm().equals(JWEAlgorithm.RSA_OAEP_256), "Key algorithm must be RSA-OAEP-256!");
validateTrueOrElseThrow(RSAKey.getPublicExponent().toString().equals("AQAB"), "Key must have e: \"AQAB\"");
}
private static void validateTrueOrElseThrow(boolean expression, String msg) {
if (!expression) {
throw new RuntimeException(msg);
}
}
Nutzung des öffentlichen Schlüssels zur Verschlüsselung
- .NET (SDK)
- JavaScript
- Java
Diese Funktionalität wird durch das .NET-SDK bereits intern umgesetzt
und ist durch einen Aufruf der SDK-Methode ClientFactory.GetSenderClient(...).SendAsync(...)
automatisch mit abgedeckt:
var sender = ClientFactory
.GetSenderClient(FitConnectEnvironment.Testing, clientId, clientSecret, logger)
.SendAsync(sendableSubmission);
Der Quellcode oben ist ein Auszug aus dem Projekt ConsoleAppExample
,
das im Repository Examples von FIT-Connect hinterlegt ist.
Eine Beschreibung des .NET-SDKs finden Sie im Hauptmenü unter "SDKs > .NET-SDK".
Die Umwandelung des JWK in eine Struktur, die für die Bibliothek panva/jose nutzbar ist, kann mithilfe einer Methode aus dieser Bibliothek durchgeführt werden.
import { importJWK } from 'jose'
import { CompactEncrypt } from 'jose'
const publicKey = await importJWK({
"kty": "RSA",
"key_ops": ["wrapKey"],
"x5c": [
"LS0tLS1CRUd...LS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
],
"kid": "787f3a1c-7da7-44d7-9b79-9783b1ea9be8",
"alg": "RSA-OAEP-256",
"n": "sX2DX7rG5BoJd23A0...6ZqjRa1QcFnkq3_M4-tk",
"e": "AQAB"
}, "RSA-OAEP-256")
Mit dem zuvor eingelesenen Schlüssel können nun Zeichenketten und Binärdaten verschlüsselt werden.
Für die Verschlüsselung von Zeichenketten (z.B. serialisierte JSON- oder XML-Objekte) kann die in Browser verfügbare TextEncoder API verwendet werden, die den String UTF8-kodiert in ein Uint8Array
umwandelt.
const data = {
config: 'option1',
age: 42
}
const encodedText = new TextEncoder().encode(JSON.stringify(data))
const encryptedText = await new CompactEncrypt(encodedText)
.setProtectedHeader({ alg: "RSA-OAEP-256", enc: "A256GCM" })
.encrypt(publicKey)
Anhänge wie PDF-Dateien, Bilder o. ä. liegen meist in einem Uint8Array
vor bzw. können einfach darin umgewandelt werden.
Im Folgenden Beispiel wird eine Datei aus einem HTML-File-Input direkt heraus verschlüsselt.
// .arrayBuffer() => https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer
// Verschlüsselung der ersten Datei aus einem Datei-Input: <input type="file" id="my-file-input" />
const buffer = await document.getElementById("my-file-input").files[0].arrayBuffer()
const encryptedText = await new CompactEncrypt(buffer)
.setProtectedHeader({ alg: "RSA-OAEP-256", enc: "A256GCM" })
.encrypt(publicKey)
Wenn ein sendendes System Daten einer Einreichung verschlüsseln möchte, kann es dies mithilfe der Java-Bibliothek nimbus-jose-jwt
durchführen.
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.X.X</version>
</dependency>
Da in FIT-Connect ausschließlich RSA-Schlüssel erlaubt sind, muss der RSAKey-Parser aus der Bibliothek verwendet werden, um den JWK einzulesen.
// ⤹ InputStream
String publicKeyAsString = new String(existingPublicKey.readAllBytes());
RSAPublicKey publicKey = RSAKey.parse(publicKeyAsString).toRSAPublicKey();
Mit diesem umgewandelten Schlüssel können nun Zeichenketten und Binärdaten verschlüsselt werden.
Über die Payload-Klasse können verschiedene Typen, wie byte[]
oder String
, verschlüsselt werden.
// Schl üssel validieren
validateRSAKey(publicKey, false);
// Header erstellen und Header-Attribute gemäß kryptographischen Vorgaben setzen
JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM);
// Payload definieren - Option 1: Zeichenkette
Payload payload = new Payload("{ \"Hello\": \"World\"}");
try {
// Payload definieren - Option 2: InputStream (z.B. für Datei)
Payload payload = new Payload(aFileInputStream.readAllBytes());
JWEObject jweObject = new JWEObject(header, payload);
jweObject.encrypt(new RSAEncrypter(publicKey));
String encrypted = jweObject.serialize();
System.out.println("Encrypted Text");
System.out.println(encrypted);
} catch (JOSEException e) {
e.printStackTrace();
}