Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 4 Min.

HMAC mit C# und T-SQL

Kompatible Signaturberechnung über Systemgrenzen hinweg.
© EMGenie

In verteilten Systemen ist die Absicherung von Datenintegrität und Absenderauthentizität eine zentrale Anforderung. Häufig werden Daten zwischen Anwendungen, APIs oder Datenbanken im JSON-Format ausgetauscht. Um sicherzustellen, dass diese Daten während der Übertragung nicht verändert wurden und vom erwarteten Absender stammen, wird häufig ein HMAC (Hash-based Message Authentication Code) eingesetzt.

Dieser Artikel beschreibt, wie ein HMAC-SHA256-Hashcode über JSON-Inhalte so berechnet wird, dass C#-Anwendungen und SQL Server (T-SQL) exakt denselben Hash erzeugen. Der Schwerpunkt liegt dabei auf der korrekten Behandlung von JSON, Zeichencodierung und Byte-Konsistenz.

 


HMAC light

Da HASHBYTES() in SQL Server keinen Initialisierungsvektor vorsieht, kann dieser in der C#-Implementierung auch nicht verwendet werden. Die daraus resultierende Einschränkung ist in der Praxis jedoch in den meisten Fällen vernachlässigbar.


 

Was ist HMAC – und was nicht?

HMAC kombiniert drei Dinge:

  • eine Nachricht (Message)
  • ein geheimes Schlüsselmaterial (Secret)
  • einen kryptografischen Hash-Algorithmus (zum Beispiel SHA-256)

Das Ergebnis ist ein Prüfwert, der folgende Eigenschaften besitzt:

  • Jede Änderung der Nachricht führt zu einem anderen HMAC.
  • Ohne Kenntnis des geheimen Schlüssels kann der HMAC nicht reproduziert werden.

HMAC dient nicht der Verschlüsselung, sondern ausschließlich der Integritäts- und Authentizitätsprüfung. Der Inhalt ist also nach wie vor lesbar.

Die zentrale Herausforderung bei JSON

JSON ist textbasiert und semantisch flexibel. Die folgenden JSON-Dokumente (Variante 1 und Variante 2) sind inhaltlich identisch, erzeugen aber unterschiedliche HMAC-Werte. JSON-Variante 1:

 

{"a":1,"b":2}

 

JSON-Variante 2:

 

{
  "b": 2,
  "a": 1
}

 

Für kryptografische Verfahren zählt ausschließlich die exakte Bytefolge. Daher ist es zwingend erforderlich, JSON vor der HMAC-Berechnung in eine kanonische, deterministische Form zu überführen.

Definition von Canonical JSON

Für die folgenden Beispiele gilt daher verbindlich:

  • UTF-8-Encoding
  • Keine Whitespaces oder Einrückungen
  • Objekt-Properties lexikografisch sortiert (Ordinal)
  • Arrays behalten ihre Reihenfolge
  • Strings, Zahlen, Boolean- und Null-Werte bleiben typkonform
  • Ausgabe ohne „Pretty Printing“

Diese Regeln müssen auf allen beteiligten Systemen identisch umgesetzt werden. Dazu werden folgende Beispiel-Daten verwendet:

 

{
  "user": "thorsten",
  "timestamp": "2025-01-01T10:00:00Z",
  "roles": ["dba", "dev"],
  "active": true
}

 

Der geheime Schlüssel soll dabei, völlig willkürlich und vielleicht nicht extrem sicher, „SuperSecretSharedKey123“ sein.

C#-Implementierung

Die C#-Implementierung besteht im Kern aus einer Funktion, um das Canonical JSON sicherzustellen (Listing 1), und einer für die Arbeit mit dem SHA256-Algorithmus sowie Formatierung des Endergebnisses als Hex, da es sich um Bytes und nicht um Zeichen handelt.

Listing 1: CanonicalizeJson()
public static string CanonicalizeJson(string json)
{
    using var jsonDoc = JsonDocument.Parse(json);
    return CanonicalizeElement(jsonDoc.RootElement);
    static string CanonicalizeElement(JsonElement element)
    {
        switch (element.ValueKind)
        {
            case JsonValueKind.Object:
                var properties = element.EnumerateObject()
                                        .OrderBy(p => p.Name, StringComparer.Ordinal);
                var objBuilder = new StringBuilder();
                objBuilder.Append('{');
                bool firstProp = true;
                foreach (var prop in properties)
                {
                    if (!firstProp) objBuilder.Append(',');
                    firstProp = false;
                    objBuilder.Append(JsonSerializer.Serialize(prop.Name));
                    objBuilder.Append(':');
                    objBuilder.Append(CanonicalizeElement(prop.Value));
                }
                objBuilder.Append('}');
                return objBuilder.ToString();
            case JsonValueKind.Array:
                var arrayBuilder = new StringBuilder();
                arrayBuilder.Append('[');
                bool firstItem = true;
                foreach (var item in element.EnumerateArray())
                {
                    if (!firstItem) arrayBuilder.Append(',');
                    firstItem = false;
                    arrayBuilder.Append(CanonicalizeElement(item));
                }
                arrayBuilder.Append(']');
                return arrayBuilder.ToString();
            default:
                JsonSerializerOptions options = new()
                {
                    WriteIndented = false,
                    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
                };
                return JsonSerializer.Serialize(element, options);
        }
    }
} 

Zwar verfügt .NET über eine System.Security.Cryptography.HMACSHA256-Klasse, die von Namen her geeignet scheint, die aber im Vergleich zum SQL Server immer abweichende Ergebnis geliefert hat. Als Lösung wird im Beispiel direkt mit der HMACSHA256-Klasse aus dem gleichen Namensraum gearbeitet (Listing 2).

Listing 2: ComputeHmacSha256Hex()
public static string ComputeHmacSha256Hex(string canonicalJson, string secret)
{
    using SHA256 hasher = SHA256.Create();
    // Convert the input string to a byte array and compute the hash.
    string input = secret + ':' + canonicalJson;
    byte[] data = hasher.ComputeHash(Encoding.Unicode.GetBytes(input));
    // StringBuilder für das Ergebnis
    StringBuilder sBuilder = new(input.Length * 2);
    // Byteweise in Hex umwandeln
    for (int i = 0; i < data.Length; i++)
        sBuilder.Append(data[i].ToString("x2"));
    // Rückgabe
    return sBuilder.ToString();
    //byte[] keyBytes = Encoding.Unicode.GetBytes(secret);
    //byte[] messageBytes = Encoding.Unicode.GetBytes(canonicalJson);
    //using var hmac = new HMACSHA256(keyBytes);
    //byte[] hash = hmac.ComputeHash(messageBytes);
    //var sb = new StringBuilder(hash.Length * 2);
    //foreach (var b in hash)
    //    sb.Append(b.ToString("x2"));
    //return sb.ToString();
} 

Zusammen werden dann beide nacheinander aufgerufen (Listing 3). Die so erzeugte Zeichenkette kann dann für den Vergleich verwendet werden.

Listing 3: Gesamtaufruf C#
string canonical = JsonHmacHelper.CanonicalizeJson(json);
// {"active":true,"roles":["dba","dev"],"timestamp":"2025-01-01T10:00:00Z","user":"thorsten"}
string hmacHex = JsonHmacHelper.ComputeHmacSha256Hex(canonical, secret);
// 7586df0441ea83163311043efb0413b4d2e930e52ef627a733d11a62562687b2 

SQL-Server-(T-SQL-)Implementierung

Die Implementierung für SQL Server besteht im Kern ebenfalls aus zwei Funktionen. Wieder eine für das das Canonical JSON (Listing 4) und eine zweite für die Arbeit mit dem Hash-Algorithmus (Listing 5).

Listing 4: dbo.CanonicalizeJson() T-SQL
CREATE OR ALTER FUNCTION dbo.CanonicalizeJson (@json NVARCHAR(MAX))
RETURNS NVARCHAR(MAX)
AS
BEGIN
    DECLARE @result NVARCHAR(MAX);
    -- Primitive? (OPENJSON liefert bei nicht-Objekt/Array NULL)
    IF ISJSON(@json) = 0
        RETURN @json;
    -- Versuche Object
    IF EXISTS (SELECT 1 FROM OPENJSON(@json))
    BEGIN
        -- Entscheiden: Object oder Array?
        -- Array: OPENJSON liefert keys 0..n, Object: keys sind Property-Namen
        IF EXISTS (SELECT 1 FROM OPENJSON(@json) WHERE TRY_CONVERT(INT, [key]) IS NULL)
        BEGIN
            -- Object
            SELECT @result =
                N'{' + STRING_AGG(
                    -- Property name als JSON-String + ":" + kanonischer value
                    STRING_ESCAPE([key], 'json') 
                    -- STRING_ESCAPE gibt keinen umschließenden "
                    , N''
                ) WITHIN GROUP (ORDER BY [key])
            FROM OPENJSON(@json);
            -- Oben bauen wir nur die Keys; jetzt korrekt zusammensetzen:
            ;WITH kv AS
            (
                SELECT 
                    [key],
                    value,
                    type
                FROM OPENJSON(@json)
            )
            SELECT @result =
                N'{' +
                STRING_AGG(
                    N'"' + STRING_ESCAPE([key], 'json') + N'":' +
                    CASE 
                        WHEN type IN (4,5) THEN dbo.CanonicalizeJson(value)  -- array/object
                        WHEN type = 1 THEN N'"' + STRING_ESCAPE(value, 'json') + N'"' -- string
                        ELSE value -- number, true, false, null
                    END
                , N',') WITHIN GROUP (ORDER BY [key])
                + N'}'
            FROM kv;
            RETURN @result;
        END
        ELSE
        BEGIN
            -- Array
            ;WITH a AS
            (
                SELECT 
                    TRY_CONVERT(INT, [key]) AS idx,
                    value,
                    type
                FROM OPENJSON(@json)
            )
            SELECT @result =
                N'[' +
                STRING_AGG(
                    CASE 
                        WHEN type IN (4,5) THEN dbo.CanonicalizeJson(value)
                        WHEN type = 1 THEN N'"' + STRING_ESCAPE(value, 'json') + N'"'
                        ELSE value
                    END
                , N',') WITHIN GROUP (ORDER BY idx)
                + N']'
            FROM a;
            RETURN @result;
        END
    END
    RETURN @json;
END;
GO 

Für die Arbeit mit SHA256 wird die interne Funktion HASHBYTES() verwendet.

Listing 5: dbo.ComputeHmacSha256Hex() T-SQL
CREATE OR ALTER FUNCTION dbo.ComputeHmacSha256Hex
(
    @canonicalJson NVARCHAR(MAX),
    @secret NVARCHAR(4000)
)
RETURNS VARCHAR(64)
AS
BEGIN
    -- in Unicode-Bytefolgen überführen:
    DECLARE @input VARBINARY(MAX) = CONVERT(VARBINARY(MAX), @secret + N':' + @canonicalJson);
    DECLARE @hash VARBINARY(32) = HASHBYTES('SHA2_256', @input);
    RETURN LOWER(CONVERT(VARCHAR(64), @hash, 2));
END;
GO
Listing 8 führt dann den Gesamtaufruf durch und liefert als Ergebnis die gleich hexadezimale Zeichenfolge wie das C# Pendant 
Listing 8: Gesamtaufruf C#
DECLARE @canonical NVARCHAR(MAX) = dbo.CanonicalizeJson(CAST(@json AS NVARCHAR(MAX)));
DECLARE @hmac VARCHAR(64) = dbo.ComputeHmacSha256Hex(@canonical, @secret);
SELECT @canonical AS CanonicalJson, @hmac AS HmacHex;
-- {"active":true,"roles":["dba","dev"],"timestamp":"2025-01-01T10:00:00Z","user":"thorsten"}
-- 7586df0441ea83163311043efb0413b4d2e930e52ef627a733d11a62562687b2 

Fazit

Vielleicht etwas codeintensiv, aber leistbar. Berechnet werden Signaturen, die sich an dem (kanonischen) Inhalt orientieren und über Systemgrenze hinweg prüfen, ob Inhalt modifiziert wurden. Das Verfahren ist auch ohne SSL/TLS und daher nicht nur für Webhooks nutzbar. Lesbare Tabellen-/Dateiinhalte können so zuverlässig geprüft werden.

Neueste Beiträge

Deep Learning in .NET – TensorFlow.NET und TorchSharp - .NET, Python und KI, Teil 3
Mit modernen KI-Frameworks lassen sich Deep-Learning-Modelle direkt in C# entwickeln.
6 Minuten
Vom Python-Modell zur .NET-Anwendung - .NET, Python und KI, Teil 4
Am Szenario einer Sentiment-Analyse verdeutlicht ein durchgängiges Anwendungsbeispiel, wie aus einem isolierten Data-Science-Ergebnis eine konkret genutzte Funktion innerhalb einer .NET-Business-Anwendung entsteht.
7 Minuten
JSON mit T-SQL auswerten - Neues in SQL Server 2025, Teil 2
Die JSON-Unterstützung in SQL Server 2025 erweitert das relationale Modell um die direkte Verarbeitung dokumentbasierter Daten.
6 Minuten
13. Mai 2026

Das könnte Dich auch interessieren

Volltextsuche mit SQLite: FTS5 und Fuzzy Search - SQLite für .NET-Entwickler, Teil 4
Hochperformante Suche ohne externe Suchmaschine? Wie man mit der in SQLite eingebauten Volltextsuch-Engine FTS5 eine effiziente Suche mit Tippfehlertoleranz implementiert – und in welchen Fällen Elasticsearch doch die bessere Wahl ist.
6 Minuten
22. Apr 2026
libSQL und Turso: SQLite für verteilte Systeme - SQLite für .NET-Entwickler, Teil 3
libSQL und Turso lösen die größte Einschränkung von SQLite: die Bindung an eine einzelne Instanz.
6 Minuten
15. Apr 2026
SQLite als Dokumentenspeicher: Kann das gut gehen? - SQLite für .NET-Entwickler, Teil 5
Die Embedded SQL-Datenbank SQLite kann auch als objektorientierte Datenbank beziehungsweise Dokumentenspeicher genutzt werden – nach Konzepten also, wie sie NoSQL-Datenbanken wie MongoDB einsetzen.
6 Minuten
29. Apr 2026
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige