Commit 48fd92365a

Frank Denis <124872+jedisct1@users.noreply.github.com>
2022-07-01 11:37:41
std.crypto.hash: allow creating hash functions from compositions (#11965)
A hash function cascade was a common way to avoid length-extension attacks with traditional hash functions such as the SHA-2 family. Add `std.crypto.hash.composition` to do exactly that using arbitrary hash functions, and pre-define the common SHA2-based ones. With this, we can now sign and verify Bitcoin signatures in pure Zig.
1 parent 902dc8c
Changed files (3)
lib/std/crypto/ecdsa.zig
@@ -18,6 +18,10 @@ pub const EcdsaP256Sha3_256 = Ecdsa(crypto.ecc.P256, crypto.hash.sha3.Sha3_256);
 pub const EcdsaP384Sha384 = Ecdsa(crypto.ecc.P384, crypto.hash.sha2.Sha384);
 /// ECDSA over P-384 with SHA3-384.
 pub const EcdsaP256Sha3_384 = Ecdsa(crypto.ecc.P384, crypto.hash.sha3.Sha3_384);
+/// ECDSA over Secp256k1 with SHA-256.
+pub const EcdsaSecp256k1Sha256 = Ecdsa(crypto.ecc.Secp256k1, crypto.hash.sha2.Sha256);
+/// ECDSA over Secp256k1 with SHA-256(SHA-256()) -- The Bitcoin signature system.
+pub const EcdsaSecp256k1Sha256oSha256 = Ecdsa(crypto.ecc.Secp256k1, crypto.hash.composition.Sha256oSha256);
 
 /// Elliptic Curve Digital Signature Algorithm (ECDSA).
 pub fn Ecdsa(comptime Curve: type, comptime Hash: type) type {
@@ -293,7 +297,7 @@ pub fn Ecdsa(comptime Curve: type, comptime Hash: type) type {
     };
 }
 
-test "ECDSA - Basic operations" {
+test "ECDSA - Basic operations over EcdsaP384Sha384" {
     const Scheme = EcdsaP384Sha384;
     const kp = try Scheme.KeyPair.create(null);
     const msg = "test";
@@ -307,6 +311,20 @@ test "ECDSA - Basic operations" {
     try sig2.verify(msg, kp.public_key);
 }
 
+test "ECDSA - Basic operations over Secp256k1" {
+    const Scheme = EcdsaSecp256k1Sha256oSha256;
+    const kp = try Scheme.KeyPair.create(null);
+    const msg = "test";
+
+    var noise: [Scheme.noise_length]u8 = undefined;
+    crypto.random.bytes(&noise);
+    const sig = try kp.sign(msg, noise);
+    try sig.verify(msg, kp.public_key);
+
+    const sig2 = try kp.sign(msg, null);
+    try sig2.verify(msg, kp.public_key);
+}
+
 const TestVector = struct {
     key: []const u8,
     msg: []const u8,
lib/std/crypto/hash_composition.zig
@@ -0,0 +1,80 @@
+const std = @import("../std.zig");
+const sha2 = std.crypto.hash.sha2;
+
+/// The composition of two hash functions: H1 o H2, with the same API as regular hash functions.
+///
+/// The security level of a hash cascade doesn't exceed the security level of the weakest function.
+///
+/// However, Merkle–Damgård constructions such as SHA-256 are vulnerable to length-extension attacks,
+/// where under some conditions, `H(x||e)` can be efficiently computed without knowing `x`.
+/// The composition of two hash functions is a common defense against such attacks.
+///
+/// This is not necessary with modern hash functions, such as SHA-3, BLAKE2 and BLAKE3.
+pub fn Composition(comptime H1: type, comptime H2: type) type {
+    return struct {
+        const Self = @This();
+
+        H1: H1,
+        H2: H2,
+
+        /// The length of the hash output, in bytes.
+        pub const digest_length = H1.digest_length;
+        /// The block length, in bytes.
+        pub const block_length = H1.block_length;
+
+        /// Options for both hashes.
+        pub const Options = struct {
+            /// Options for H1.
+            H1: H1.Options = .{},
+            /// Options for H2.
+            H2: H2.Options = .{},
+        };
+
+        /// Initialize the hash composition with the given options.
+        pub fn init(options: Options) Self {
+            return Self{ .H1 = H1.init(options.H1), .H2 = H2.init(options.H2) };
+        }
+
+        /// Compute H1(H2(b)).
+        pub fn hash(b: []const u8, out: *[digest_length]u8, options: Options) void {
+            var d = Self.init(options);
+            d.update(b);
+            d.final(out);
+        }
+
+        /// Add content to the hash.
+        pub fn update(d: *Self, b: []const u8) void {
+            d.H2.update(b);
+        }
+
+        /// Compute the final hash for the accumulated content: H1(H2(b)).
+        pub fn final(d: *Self, out: *[digest_length]u8) void {
+            var H2_digest: [H2.digest_length]u8 = undefined;
+            d.H2.final(&H2_digest);
+            d.H1.update(&H2_digest);
+            d.H1.final(out);
+        }
+    };
+}
+
+/// SHA-256(SHA-256())
+pub const Sha256oSha256 = Composition(sha2.Sha256, sha2.Sha256);
+/// SHA-384(SHA-384())
+pub const Sha384oSha384 = Composition(sha2.Sha384, sha2.Sha384);
+/// SHA-512(SHA-512())
+pub const Sha512oSha512 = Composition(sha2.Sha512, sha2.Sha512);
+
+test "Hash composition" {
+    const Sha256 = sha2.Sha256;
+    const msg = "test";
+
+    var out: [Sha256oSha256.digest_length]u8 = undefined;
+    Sha256oSha256.hash(msg, &out, .{});
+
+    var t: [Sha256.digest_length]u8 = undefined;
+    Sha256.hash(msg, &t, .{});
+    var out2: [Sha256.digest_length]u8 = undefined;
+    Sha256.hash(&t, &out2, .{});
+
+    try std.testing.expectEqualSlices(u8, &out, &out2);
+}
lib/std/crypto.zig
@@ -76,6 +76,7 @@ pub const hash = struct {
     pub const Sha1 = @import("crypto/sha1.zig").Sha1;
     pub const sha2 = @import("crypto/sha2.zig");
     pub const sha3 = @import("crypto/sha3.zig");
+    pub const composition = @import("crypto/hash_composition.zig");
 };
 
 /// Key derivation functions.
@@ -215,6 +216,7 @@ test {
     _ = hash.Sha1;
     _ = hash.sha2;
     _ = hash.sha3;
+    _ = hash.composition;
 
     _ = kdf.hkdf;