// C# (.NET 6+). Check or make a MARZI signature. // NOTE: this file was written to match the other languages and the test vector, but it has NOT been compiled or run by // the MARZI team. Check it against the test vector in the documentation before you rely on it. using System; using System.Security.Cryptography; using System.Text; public static class MarziSignature { /// hex( HMAC-SHA256( secret, timestamp \n METHOD \n path?query \n sha256hex(body) ) ); body is the raw bytes (empty for GET). public static string Sign(string secret, string timestamp, string method, string uri, byte[] body) { var bodyHash = Convert.ToHexString(SHA256.HashData(body)).ToLowerInvariant(); var payload = $"{timestamp}\n{method}\n{uri}\n{bodyHash}"; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); return Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(payload))).ToLowerInvariant(); } /// True when the request really comes from MARZI. `uri` is the path + query WITHOUT the folder your connector lives in. public static bool IsFromMarzi(string secret, string? timestamp, string? signature, string method, string uri, byte[] body, int maxSkewSeconds = 300) { if (string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(signature) || !long.TryParse(timestamp, out var seconds)) return false; if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - seconds) > maxSkewSeconds) return false; // too old: maybe a replay var expected = Encoding.ASCII.GetBytes(Sign(secret, timestamp, method, uri, body)); var actual = Encoding.ASCII.GetBytes(signature.ToLowerInvariant()); return CryptographicOperations.FixedTimeEquals(expected, actual); // never compare with == } // Test vector from the documentation: Sign("test-secret-key-0123456789abcdef", "1700000000", "POST", "/v1/auth/verify?x=1", // Encoding.UTF8.GetBytes("{\"username\":\"khach\",\"password\":\"mat khau\"}")) // must return 6555990f154cc9a91c0fb5182bfb991bebcca1fed0a1f137a11f0be74d5913d7 }