atproto pds in zig pds.zat.dev
pds atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230const std = @import("std");const zat = @import("zat");const http = @import("../http/api.zig");
pub fn header(request: *const http.Request, name: []const u8) ![]const u8 { var value: ?[]const u8 = null; for (request.headers.keys[0..request.headers.len], request.headers.values[0..request.headers.len]) |key, item| { if (!std.ascii.eqlIgnoreCase(key, name)) continue; if (value != null) return error.BadSpaceSignature; value = item; } return value orelse error.BadSpaceSignature;}
pub fn verifyRequest(allocator: std.mem.Allocator, request: *const http.Request, kid: ?[]const u8, audience: ?[]const u8) ![]const u8 { const authorization = try header(request, "authorization"); if (audience) |expected| { const actual = header(request, "atproto-space-audience") catch return error.BadSpaceAudience; if (!std.mem.eql(u8, expected, actual)) return error.BadSpaceAudience; } return verify(allocator, authorization, audience, try header(request, "signature-input"), try header(request, "signature"), kid);}
pub fn signatureBase(allocator: std.mem.Allocator, authorization: []const u8, audience: ?[]const u8, params: []const u8) ![]u8 { if (audience) |did| return std.fmt.allocPrint(allocator, "\"authorization\": {s}\n\"atproto-space-audience\": {s}\n\"@signature-params\": {s}", .{ authorization, did, params }); return std.fmt.allocPrint(allocator, "\"authorization\": {s}\n\"@signature-params\": {s}", .{ authorization, params });}
fn dictionaryMember(raw: []const u8) ![]const u8 { var start: usize = 0; var quoted = false; var escaped = false; var found: ?[]const u8 = null; for (raw, 0..) |c, i| { if (escaped) { escaped = false; continue; } if (quoted and c == '\\') { escaped = true; continue; } if (c == '"') quoted = !quoted; if ((c == ',' and !quoted) or i == raw.len - 1) { const end = if (c == ',' and !quoted) i else i + 1; const item = std.mem.trim(u8, raw[start..end], " \t"); start = i + 1; const eq = std.mem.indexOfScalar(u8, item, '=') orelse continue; if (std.mem.eql(u8, item[0..eq], "atproto-space")) { if (found != null) return error.BadSpaceSignature; found = item[eq + 1 ..]; } } } if (quoted or escaped) return error.BadSpaceSignature; return found orelse error.BadSpaceSignature;}
const Parser = struct { text: []const u8, pos: usize = 0, fn spaces(self: *Parser) void { while (self.pos < self.text.len and self.text[self.pos] == ' ') self.pos += 1; } fn take(self: *Parser, c: u8) bool { if (self.pos == self.text.len or self.text[self.pos] != c) return false; self.pos += 1; return true; } fn string(self: *Parser, allocator: std.mem.Allocator) ![]const u8 { if (!self.take('"')) return error.BadSpaceSignature; var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); while (self.pos < self.text.len) { var c = self.text[self.pos]; self.pos += 1; if (c == '"') return allocator.dupe(u8, out.written()); if (c == '\\') { if (self.pos == self.text.len) return error.BadSpaceSignature; c = self.text[self.pos]; self.pos += 1; if (c != '\\' and c != '"') return error.BadSpaceSignature; } if (c < 0x20 or c > 0x7e) return error.BadSpaceSignature; try out.writer.writeByte(c); } return error.BadSpaceSignature; }};
fn writeString(writer: *std.Io.Writer, value: []const u8) !void { try writer.writeByte('"'); for (value) |c| { if (c == '"' or c == '\\') try writer.writeByte('\\'); try writer.writeByte(c); } try writer.writeByte('"');}
pub fn verify(allocator: std.mem.Allocator, authorization: []const u8, audience: ?[]const u8, input: []const u8, signature: []const u8, expected_kid: ?[]const u8) ![]const u8 { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const a = arena.allocator(); var p: Parser = .{ .text = try dictionaryMember(input) }; if (!p.take('(')) return error.BadSpaceSignature; p.spaces(); const first = try p.string(a); if (!std.mem.eql(u8, first, "authorization")) return error.BadSpaceSignature; if (audience != null) { if (!p.take(' ')) return error.BadSpaceSignature; p.spaces(); if (!std.mem.eql(u8, try p.string(a), "atproto-space-audience")) return error.BadSpaceSignature; } p.spaces(); if (!p.take(')')) return error.BadSpaceSignature; var canonical: std.Io.Writer.Allocating = .init(a); try canonical.writer.writeAll(if (audience != null) "(\"authorization\" \"atproto-space-audience\")" else "(\"authorization\")"); var kid: ?[]const u8 = null; var seen: std.StringHashMapUnmanaged(void) = .empty; while (p.take(';')) { p.spaces(); const start = p.pos; while (p.pos < p.text.len and (std.ascii.isLower(p.text[p.pos]) or std.ascii.isDigit(p.text[p.pos]) or std.mem.indexOfScalar(u8, "_.*-", p.text[p.pos]) != null)) p.pos += 1; const name = p.text[start..p.pos]; if (name.len == 0 or (!std.ascii.isLower(name[0]) and name[0] != '*') or seen.contains(name)) return error.BadSpaceSignature; try seen.put(a, name, {}); try canonical.writer.print(";{s}", .{name}); var string_value: ?[]const u8 = null; if (p.take('=')) { if (p.pos == p.text.len) return error.BadSpaceSignature; if (p.text[p.pos] == '"') { string_value = try p.string(a); try canonical.writer.writeByte('='); try writeString(&canonical.writer, string_value.?); } else { const begin = p.pos; while (p.pos < p.text.len and p.text[p.pos] != ';') p.pos += 1; const value = p.text[begin..p.pos]; if (std.mem.eql(u8, value, "?1")) {} else if (std.mem.eql(u8, value, "?0")) { try canonical.writer.writeAll("=?0"); } else if (value.len > 0 and (value[0] == '-' or std.ascii.isDigit(value[0]))) { if (std.mem.indexOfScalar(u8, value, '.')) |dot| { const integer = value[0..dot]; const fraction = value[dot + 1 ..]; const number = std.fmt.parseInt(i64, integer, 10) catch return error.BadSpaceSignature; if (integer.len > 13 or @abs(number) > 999999999999 or fraction.len == 0 or fraction.len > 3) return error.BadSpaceSignature; for (fraction) |c| if (!std.ascii.isDigit(c)) return error.BadSpaceSignature; var length = fraction.len; while (length > 1 and fraction[length - 1] == '0') length -= 1; const negative_zero = number == 0 and value[0] == '-' and (std.fmt.parseInt(u16, fraction, 10) catch 0) != 0; try canonical.writer.print("={s}{d}.{s}", .{ if (negative_zero) "-" else "", number, fraction[0..length] }); } else { const number = std.fmt.parseInt(i64, value, 10) catch return error.BadSpaceSignature; if (value.len > 16 or number < -999999999999999 or number > 999999999999999) return error.BadSpaceSignature; try canonical.writer.print("={d}", .{number}); } } else if (value.len >= 2 and value[0] == ':' and value[value.len - 1] == ':') { const encoded_value = value[1 .. value.len - 1]; const length = std.base64.standard.Decoder.calcSizeForSlice(encoded_value) catch return error.BadSpaceSignature; const bytes = try a.alloc(u8, length); std.base64.standard.Decoder.decode(bytes, encoded_value) catch return error.BadSpaceSignature; const encoded_bytes = try a.alloc(u8, std.base64.standard.Encoder.calcSize(length)); try canonical.writer.print("=:{s}:", .{std.base64.standard.Encoder.encode(encoded_bytes, bytes)}); } else if (value.len > 0 and (std.ascii.isAlphabetic(value[0]) or value[0] == '*')) { for (value) |c| if (!std.ascii.isAlphanumeric(c) and std.mem.indexOfScalar(u8, "!#$%&'*+-.^_`|~:/", c) == null) return error.BadSpaceSignature; try canonical.writer.print("={s}", .{value}); } else return error.BadSpaceSignature; } } if (std.mem.eql(u8, name, "keyid")) kid = string_value orelse return error.BadSpaceSignature; if (std.mem.eql(u8, name, "alg") and !std.mem.eql(u8, string_value orelse return error.BadSpaceSignature, "ecdsa-p256-sha256")) return error.BadSpaceSignature; } if (p.pos != p.text.len) return error.BadSpaceSignature; const key_id = expected_kid orelse kid orelse return error.BadSpaceSignature; if (kid) |supplied| { if (!std.mem.eql(u8, supplied, key_id)) return error.BadSpaceSignature; } const key = zat.multicodec.parseDidKey(a, key_id) catch return error.BadSpaceSignature; if (key.key_type != .p256) return error.BadSpaceSignature; const encoded = try dictionaryMember(signature); if (encoded.len < 2 or encoded[0] != ':' or encoded[encoded.len - 1] != ':') return error.BadSpaceSignature; const b64 = encoded[1 .. encoded.len - 1]; const decoder = if (std.mem.endsWith(u8, b64, "=")) std.base64.standard.Decoder else std.base64.standard_no_pad.Decoder; if ((decoder.calcSizeForSlice(b64) catch return error.BadSpaceSignature) != 64) return error.BadSpaceSignature; var sig: [64]u8 = undefined; decoder.decode(&sig, b64) catch return error.BadSpaceSignature; const base = try signatureBase(a, authorization, audience, canonical.written()); zat.jwt.verifyJose(.ES256, base, &sig, key.raw) catch return error.BadSpaceSignature; return allocator.dupe(u8, key_id);}
test "space signatures bind authorization and audience and accept reusable high-S proofs" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator(); var key = try zat.Keypair.fromSecretKey(.p256, .{0x42} ** 32); const kid = try key.did(a); const auth = "Atproto-Space credential"; const audience = "did:plc:alice"; const params = "(\"authorization\" \"atproto-space-audience\");alg=\"ecdsa-p256-sha256\";created=123"; const base = try signatureBase(a, auth, audience, params); const low = try key.sign(base); var high = low.bytes; high[32..64].* = try std.crypto.ecc.P256.scalar.neg(low.bytes[32..64].*, .big); for ([_][64]u8{ low.bytes, high }) |sig| { const input = try std.fmt.allocPrint(a, "other=(\"date\"), atproto-space={s}", .{params}); var encoded: [88]u8 = undefined; const signature = try std.fmt.allocPrint(a, "other=:YWJj:, atproto-space=:{s}:", .{std.base64.standard.Encoder.encode(&encoded, &sig)}); for (0..2) |_| try std.testing.expectEqualStrings(kid, try verify(a, auth, audience, input, signature, kid)); try std.testing.expectError(error.BadSpaceSignature, verify(a, "Atproto-Space changed", audience, input, signature, kid)); try std.testing.expectError(error.BadSpaceSignature, verify(a, auth, "did:plc:bob", input, signature, kid)); try std.testing.expectError(error.BadSpaceSignature, verify(a, auth, audience, input, "atproto-space=:YWJj:", kid)); } const issuance = try std.fmt.allocPrint(a, "(\"authorization\");keyid=\"{s}\"", .{kid}); const exchange_base = try signatureBase(a, "Bearer delegation", null, issuance); const sig = try key.sign(exchange_base); var encoded: [88]u8 = undefined; const signature = try std.fmt.allocPrint(a, "atproto-space=:{s}:", .{std.base64.standard.Encoder.encode(&encoded, &sig.bytes)}); const input = try std.fmt.allocPrint(a, "atproto-space={s}", .{issuance}); try std.testing.expectEqualStrings(kid, try verify(a, "Bearer delegation", null, input, signature, null)); try std.testing.expectError(error.BadSpaceSignature, verify(a, "Bearer another", null, input, signature, null));}
test "published October 1 SDK unpadded signature fixture" { const a = std.testing.allocator; const key = try verify(a, "Bearer delegation", null, "atproto-space=(\"authorization\");keyid=\"did:key:zDnaemcv8qtM7jjjoq6598VzPCUrMjGuHrAqZskUEz6RKX4jp\"", "atproto-space=:bF+qE5M/uGZLeaP7j5MjUiAPtbgOxvBlKtxQoyClv5hi/Yvi3UgF8TUz3BKHa7jiiHQ9shc+B98kcIp8drOBww:", null); defer a.free(key); try std.testing.expectEqualStrings("did:key:zDnaemcv8qtM7jjjoq6598VzPCUrMjGuHrAqZskUEz6RKX4jp", key);}