diff --git a/src/browser/WebBotAuth.zig b/src/browser/WebBotAuth.zig index 4af598fb..842f02d2 100644 --- a/src/browser/WebBotAuth.zig +++ b/src/browser/WebBotAuth.zig @@ -16,8 +16,153 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); +const crypto = @import("../crypto.zig"); + +const Http = @import("../http/Http.zig"); + const WebBotAuth = @This(); -key_file: []const u8, +pkey: *crypto.EVP_PKEY, keyid: []const u8, -domain: []const u8, +directory_url: [:0]const u8, + +pub const Config = struct { + key_file: []const u8, + keyid: []const u8, + domain: []const u8, +}; + +fn parsePemPrivateKey(pem: []const u8) !*crypto.EVP_PKEY { + const begin = "-----BEGIN PRIVATE KEY-----"; + const end = "-----END PRIVATE KEY-----"; + const start_idx = std.mem.indexOf(u8, pem, begin) orelse return error.InvalidPem; + const end_idx = std.mem.indexOf(u8, pem, end) orelse return error.InvalidPem; + + const b64 = std.mem.trim(u8, pem[start_idx + begin.len .. end_idx], &std.ascii.whitespace); + + // strip newlines from b64 + var clean: [4096]u8 = undefined; + var clean_len: usize = 0; + for (b64) |ch| { + if (ch != '\n' and ch != '\r') { + clean[clean_len] = ch; + clean_len += 1; + } + } + + var der: [128]u8 = undefined; + const decoded_len = try std.base64.standard.Decoder.calcSizeForSlice(clean[0..clean_len]); + try std.base64.standard.Decoder.decode(der[0..decoded_len], clean[0..clean_len]); + + // Ed25519 PKCS#8: key bytes are at offset 16, 32 bytes long + const key_bytes = der[16..48]; + + const pkey = crypto.EVP_PKEY_new_raw_private_key(crypto.EVP_PKEY_ED25519, null, key_bytes.ptr, 32); + return pkey orelse error.InvalidKey; +} + +fn signEd25519(pkey: *crypto.EVP_PKEY, message: []const u8, out: *[64]u8) !void { + const ctx = crypto.EVP_MD_CTX_new() orelse return error.OutOfMemory; + defer crypto.EVP_MD_CTX_free(ctx); + + if (crypto.EVP_DigestSignInit(ctx, null, null, null, pkey) != 1) + return error.SignInit; + + var sig_len: usize = 64; + if (crypto.EVP_DigestSign(ctx, out.ptr, &sig_len, message.ptr, message.len) != 1) + return error.SignFailed; +} + +pub fn fromConfig(allocator: std.mem.Allocator, config: *const Config) !WebBotAuth { + const pem = try std.fs.cwd().readFileAlloc(allocator, config.key_file, 1024 * 4); + defer allocator.free(pem); + + const pkey = try parsePemPrivateKey(pem); + errdefer crypto.EVP_PKEY_free(pkey); + + const directory_url = try std.fmt.allocPrintSentinel( + allocator, + "https://{s}/.well-known/http-message-signatures-directory", + .{config.domain}, + 0, + ); + errdefer allocator.free(directory_url); + + return .{ + .pkey = pkey, + // Owned by the Config so it's okay. + .keyid = config.keyid, + .directory_url = directory_url, + }; +} + +pub fn signRequest( + self: *const WebBotAuth, + allocator: std.mem.Allocator, + headers: *Http.Headers, + authority: []const u8, +) !void { + const now = std.time.timestamp(); + const expires = now + 60; + + // build the signature-input value (without the sig1= label) + const sig_input_value = try std.fmt.allocPrint( + allocator, + "(\"@authority\" \"signature-agent\");created={d};expires={d};keyid=\"{s}\";alg=\"ed25519\";tag=\"web-bot-auth\"", + .{ now, expires, self.keyid }, + ); + defer allocator.free(sig_input_value); + + // build the canonical string to sign + const canonical = try std.fmt.allocPrint( + allocator, + "\"@authority\": {s}\n\"signature-agent\": \"{s}\"\n\"@signature-params\": {s}", + .{ authority, self.directory_url, sig_input_value }, + ); + defer allocator.free(canonical); + + // sign it + var sig: [64]u8 = undefined; + try signEd25519(self.pkey, canonical, &sig); + + // base64 encode + const encoded_len = std.base64.standard.Encoder.calcSize(sig.len); + const encoded = try allocator.alloc(u8, encoded_len); + defer allocator.free(encoded); + _ = std.base64.standard.Encoder.encode(encoded, &sig); + + // build the 3 headers and add them + const sig_agent = try std.fmt.allocPrintSentinel( + allocator, + "Signature-Agent: \"{s}\"", + .{self.directory_url}, + 0, + ); + defer allocator.free(sig_agent); + + const sig_input = try std.fmt.allocPrintSentinel( + allocator, + "Signature-Input: sig1={s}", + .{sig_input_value}, + 0, + ); + defer allocator.free(sig_input); + + const signature = try std.fmt.allocPrintSentinel( + allocator, + "Signature: sig1=:{s}:", + .{encoded}, + 0, + ); + defer allocator.free(signature); + + try headers.add(sig_agent); + try headers.add(sig_input); + try headers.add(signature); +} + +pub fn deinit(self: WebBotAuth, allocator: std.mem.Allocator) void { + crypto.EVP_PKEY_free(self.pkey); + allocator.free(self.directory_url); +} diff --git a/src/crypto.zig b/src/crypto.zig index 012ca96e..58f6349f 100644 --- a/src/crypto.zig +++ b/src/crypto.zig @@ -228,6 +228,8 @@ pub extern fn X25519_keypair(out_public_value: *[32]u8, out_private_key: *[32]u8 pub const NID_X25519 = @as(c_int, 948); pub const EVP_PKEY_X25519 = NID_X25519; +pub const NID_ED25519 = 949; +pub const EVP_PKEY_ED25519 = NID_ED25519; pub extern fn EVP_PKEY_new_raw_private_key(@"type": c_int, unused: ?*ENGINE, in: [*c]const u8, len: usize) [*c]EVP_PKEY; pub extern fn EVP_PKEY_new_raw_public_key(@"type": c_int, unused: ?*ENGINE, in: [*c]const u8, len: usize) [*c]EVP_PKEY; @@ -236,3 +238,11 @@ pub extern fn EVP_PKEY_CTX_free(ctx: ?*EVP_PKEY_CTX) void; pub extern fn EVP_PKEY_derive_init(ctx: ?*EVP_PKEY_CTX) c_int; pub extern fn EVP_PKEY_derive(ctx: ?*EVP_PKEY_CTX, key: [*c]u8, out_key_len: [*c]usize) c_int; pub extern fn EVP_PKEY_derive_set_peer(ctx: ?*EVP_PKEY_CTX, peer: [*c]EVP_PKEY) c_int; +pub extern fn EVP_PKEY_free(pkey: ?*EVP_PKEY) void; + +pub extern fn EVP_DigestSignInit(ctx: ?*EVP_MD_CTX, pctx: ?*?*EVP_PKEY_CTX, typ: ?*const EVP_MD, e: ?*ENGINE, pkey: ?*EVP_PKEY) c_int; +pub extern fn EVP_DigestSign(ctx: ?*EVP_MD_CTX, sig: [*c]u8, sig_len: *usize, data: [*c]const u8, data_len: usize) c_int; +pub extern fn EVP_MD_CTX_new() ?*EVP_MD_CTX; +pub extern fn EVP_MD_CTX_free(ctx: ?*EVP_MD_CTX) void; +pub const struct_evp_md_ctx_st = opaque {}; +pub const EVP_MD_CTX = struct_evp_md_ctx_st;