implement sign and verify for HMAC

This commit is contained in:
Halil Durak
2026-01-11 23:43:12 +03:00
parent d5e9ae23ef
commit 9945a5f9cc
2 changed files with 137 additions and 35 deletions

View File

@@ -35,35 +35,41 @@ const SubtleCrypto = @This();
/// Don't optimize away the type. /// Don't optimize away the type.
_pad: bool = false, _pad: bool = false,
/// NOTE: I think we can use extern union and cast this to intended algorithm const Params = struct {
/// by `name` field. Not sure if it'd make difference memory/performance wise.
const Algorithm = union(enum) {
rsa_hashed_key_gen: RSA,
hmac_key_gen: HMAC,
/// https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams /// https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams
const RSA = struct { const RsaHashedKeyGen = struct {
name: []const u8, name: []const u8,
/// This should be at least 2048. /// This should be at least 2048.
/// Some organizations are now recommending that it should be 4096. /// Some organizations are now recommending that it should be 4096.
modulusLength: u32, modulusLength: u32,
publicExponent: js.TypedArray(u8), publicExponent: js.TypedArray(u8),
hash: []const u8, hash: union(enum) {
string: []const u8,
object: struct { name: []const u8 },
},
}; };
/// https://developer.mozilla.org/en-US/docs/Web/API/HmacKeyGenParams /// https://developer.mozilla.org/en-US/docs/Web/API/HmacKeyGenParams
const HMAC = struct { const HmacKeyGen = struct {
/// Always HMAC.
name: []const u8, name: []const u8,
/// Its also possible to pass this in an object. /// Its also possible to pass this in an object.
hash: union(enum) { hash: union(enum) {
str: []const u8, string: []const u8,
obj: struct { name: []const u8 }, object: struct { name: []const u8 },
}, },
/// If omitted, default is the block size of the chosen hash function. /// If omitted, default is the block size of the chosen hash function.
length: ?usize, length: ?usize,
}; };
}; };
/// NOTE: I think we can use extern union and cast this to intended algorithm
/// by `name` field. Not sure if it'd make difference memory/performance wise.
const Algorithm = union(enum) {
rsa_hashed_key_gen: Params.RsaHashedKeyGen,
hmac_key_gen: Params.HmacKeyGen,
};
/// Returns the desired digest by its name. /// Returns the desired digest by its name.
fn getDigest(name: []const u8) error{Invalid}!*const crypto.EVP_MD { fn getDigest(name: []const u8) error{Invalid}!*const crypto.EVP_MD {
const digest = std.meta.stringToEnum(enum { const digest = std.meta.stringToEnum(enum {
@@ -81,16 +87,19 @@ fn getDigest(name: []const u8) error{Invalid}!*const crypto.EVP_MD {
}; };
} }
/// Represents a cryptographic key obtained from one of the SubtleCrypto methods
/// generateKey(), deriveKey(), importKey(), or unwrapKey().
pub const CryptoKey = struct { pub const CryptoKey = struct {
_algorithm: Algorithm, /// Algorithm being used.
_type: Type,
/// Whether the key is extractable. /// Whether the key is extractable.
_extractable: bool, _extractable: bool,
/// Bit flags of `usages`. /// Bit flags of `usages`; see `Usages` type.
_usages: u8, _usages: u8,
/// Algorithm specific data. _key: []const u8,
_internal: union { _digest: *const crypto.EVP_MD,
hmac: []u8,
}, pub const Type = enum(u8) { hmac, rsa };
pub const Usages = struct { pub const Usages = struct {
// zig fmt: off // zig fmt: off
@@ -119,10 +128,10 @@ pub const CryptoKey = struct {
}; };
} }
fn initHMAC(algorithm: Algorithm.HMAC, extractable: bool, page: *Page) !*CryptoKey { fn initHMAC(algorithm: Params.HmacKeyGen, extractable: bool, page: *Page) !*CryptoKey {
const hash = switch (algorithm.hash) { const hash = switch (algorithm.hash) {
.str => |str| str, .string => |str| str,
.obj => |obj| obj.name, .object => |obj| obj.name,
}; };
// Find digest. // Find digest.
const digest = try getDigest(hash); const digest = try getDigest(hash);
@@ -144,19 +153,64 @@ pub const CryptoKey = struct {
std.debug.assert(res == 1); std.debug.assert(res == 1);
return page._factory.create(CryptoKey{ return page._factory.create(CryptoKey{
._algorithm = .{ ._type = .hmac,
.hmac_key_gen = .{
.name = "HMAC",
.hash = .{ .obj = .{ .name = hash } },
.length = block_size,
},
},
._extractable = extractable, ._extractable = extractable,
._usages = 0, ._usages = 0,
._internal = .{ .hmac = key }, ._key = key,
._digest = digest,
}); });
} }
fn signHMAC(self: *const CryptoKey, data: []const u8, page: *Page) !js.Promise {
const buffer = try page.arena.alloc(u8, crypto.EVP_MD_size(self._digest));
errdefer page.arena.free(buffer);
var out_len: u32 = 0;
// Try to sign.
const signed = crypto.HMAC(
self._digest,
@ptrCast(self._key.ptr),
self._key.len,
data.ptr,
data.len,
buffer.ptr,
&out_len,
);
if (signed != null) {
return page.js.resolvePromise(js.ArrayBuffer{ .values = buffer[0..out_len] });
}
return error.Invalid;
}
fn verifyHMAC(
self: *const CryptoKey,
signature: []const u8,
data: []const u8,
page: *Page,
) !js.Promise {
var buffer: [crypto.EVP_MAX_MD_BLOCK_SIZE]u8 = undefined;
var out_len: u32 = 0;
// Try to sign.
const signed = crypto.HMAC(
self._digest,
@ptrCast(self._key.ptr),
self._key.len,
data.ptr,
data.len,
&buffer,
&out_len,
);
if (signed != null) {
// CRYPTO_memcmp compare in constant time so prohibits time-based attacks.
const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len);
return page.js.resolvePromise(res == 0);
}
return page.js.resolvePromise(false);
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(CryptoKey); pub const bridge = js.Bridge(CryptoKey);
@@ -180,16 +234,58 @@ pub fn generateKey(
return CryptoKey.init(algorithm, extractable, key_usages, page); return CryptoKey.init(algorithm, extractable, key_usages, page);
} }
const SignatureAlgorithm = union(enum) {
string: []const u8,
object: struct { name: []const u8 },
pub fn isHMAC(self: SignatureAlgorithm) bool {
const name = switch (self) {
.string => |string| string,
.object => |object| object.name,
};
if (name.len < 4) return false;
const hmac: u32 = @bitCast([4]u8{ 'H', 'M', 'A', 'C' });
return @as(u32, @bitCast(name[0..4].*)) == hmac;
}
};
/// Generate a digital signature. /// Generate a digital signature.
pub fn sign( pub fn sign(
_: *const SubtleCrypto, _: *const SubtleCrypto,
algorithm: []const u8, /// This can either be provided as string or object.
/// We can't use the `Algorithm` type defined before though since there
/// are couple of changes between the two.
/// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#algorithm
algorithm: SignatureAlgorithm,
key: *CryptoKey,
data: []const u8, // ArrayBuffer.
page: *Page,
) !js.Promise {
// Verify algorithm.
if (!algorithm.isHMAC()) return error.InvalidAccess;
return switch (key._type) {
.hmac => key.signHMAC(data, page),
else => return error.InvalidAccess,
};
}
/// Verify a digital signature.
pub fn verify(
_: *const SubtleCrypto,
algorithm: SignatureAlgorithm,
key: *const CryptoKey, key: *const CryptoKey,
data: []const u8, signature: []const u8, // ArrayBuffer.
) void { data: []const u8, // ArrayBuffer.
_ = algorithm; page: *Page,
_ = key; ) !js.Promise {
_ = data; if (!algorithm.isHMAC()) return error.InvalidAccess;
return switch (key._type) {
.hmac => key.verifyHMAC(signature, data, page),
else => return error.InvalidAccess,
};
} }
pub const JsApi = struct { pub const JsApi = struct {
@@ -203,5 +299,6 @@ pub const JsApi = struct {
}; };
pub const generateKey = bridge.function(SubtleCrypto.generateKey, .{}); pub const generateKey = bridge.function(SubtleCrypto.generateKey, .{});
pub const sign = bridge.function(SubtleCrypto.sign, .{}); pub const sign = bridge.function(SubtleCrypto.sign, .{ .dom_exception = true, .as_typed_array = false });
pub const verify = bridge.function(SubtleCrypto.verify, .{});
}; };

View File

@@ -10,8 +10,13 @@ pub extern fn EVP_sha256() *const EVP_MD;
pub extern fn EVP_sha384() *const EVP_MD; pub extern fn EVP_sha384() *const EVP_MD;
pub extern fn EVP_sha512() *const EVP_MD; pub extern fn EVP_sha512() *const EVP_MD;
pub const EVP_MAX_MD_BLOCK_SIZE = 128;
pub extern fn EVP_MD_size(md: ?*const EVP_MD) usize;
pub extern fn EVP_MD_block_size(md: ?*const EVP_MD) usize; pub extern fn EVP_MD_block_size(md: ?*const EVP_MD) usize;
pub extern fn CRYPTO_memcmp(a: ?*const anyopaque, b: ?*const anyopaque, len: usize) c_int;
pub extern fn HMAC( pub extern fn HMAC(
evp_md: *const EVP_MD, evp_md: *const EVP_MD,
key: *const anyopaque, key: *const anyopaque,