atproto utils for zig zat.dev
atproto sdk zig
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304//! multicodec key parsing//!//! parses multicodec-prefixed public keys from DID documents.//! extracts key type and raw key bytes.//!//! see: https://github.com/multiformats/multicodec
const std = @import("std");
/// supported key types for AT Protocolpub const KeyType = enum { secp256k1, // ES256K - used by most AT Protocol accounts p256, // ES256 - also supported};
/// parsed public key with type and raw bytespub const PublicKey = struct { key_type: KeyType, /// raw compressed public key (33 bytes for secp256k1/p256) raw: []const u8,};
/// multicodec prefixes (unsigned varint encoding)/// secp256k1-pub: 0xe7 = 231, varint encoded as 0xe7 0x01 (2 bytes)/// p256-pub: 0x1200 = 4608, varint encoded as 0x80 0x24 (2 bytes)/// parse a multicodec-prefixed public key/// returns the key type and a slice pointing to the raw key bytespub fn parsePublicKey(data: []const u8) !PublicKey { if (data.len < 2) return error.TooShort;
// check for secp256k1-pub (varint 0xe7 = 231 encoded as 0xe7 0x01) if (data.len >= 2 and data[0] == 0xe7 and data[1] == 0x01) { const raw = data[2..]; if (raw.len != 33) return error.InvalidKeyLength; return .{ .key_type = .secp256k1, .raw = raw, }; }
// check for p256-pub (varint 0x1200 = 4608 encoded as 0x80 0x24) if (data.len >= 2 and data[0] == 0x80 and data[1] == 0x24) { const raw = data[2..]; if (raw.len != 33) return error.InvalidKeyLength; return .{ .key_type = .p256, .raw = raw, }; }
return error.UnsupportedKeyType;}
/// encode a raw public key with multicodec prefixpub fn encodePublicKey(allocator: std.mem.Allocator, key_type: KeyType, raw: []const u8) ![]u8 { if (raw.len != 33) return error.InvalidKeyLength;
const result = try allocator.alloc(u8, 2 + raw.len); switch (key_type) { .secp256k1 => { result[0] = 0xe7; result[1] = 0x01; }, .p256 => { result[0] = 0x80; result[1] = 0x24; }, } @memcpy(result[2..], raw); return result;}
/// format a raw public key as a did:key stringpub fn formatDidKey(allocator: std.mem.Allocator, key_type: KeyType, raw: []const u8) ![]u8 { const multibase = @import("multibase.zig");
const mc_bytes = try encodePublicKey(allocator, key_type, raw); defer allocator.free(mc_bytes);
const multibase_str = try multibase.encode(allocator, .base58btc, mc_bytes); defer allocator.free(multibase_str);
// "did:key:" + multibase string (which already has 'z' prefix) const result = try allocator.alloc(u8, did_key_prefix.len + multibase_str.len); @memcpy(result[0..did_key_prefix.len], did_key_prefix); @memcpy(result[did_key_prefix.len..], multibase_str); return result;}
const did_key_prefix = "did:key:";
/// parse a did:key string into key type and raw public key bytes./// caller owns the returned slice (raw field).pub fn parseDidKey(allocator: std.mem.Allocator, did: []const u8) !struct { key_type: KeyType, raw: []u8 } { const multibase = @import("multibase.zig");
if (!std.mem.startsWith(u8, did, did_key_prefix)) return error.InvalidDidKey; const multibase_str = did[did_key_prefix.len..]; if (multibase_str.len == 0) return error.InvalidDidKey;
const mc_bytes = try multibase.decode(allocator, multibase_str); defer allocator.free(mc_bytes);
const parsed = try parsePublicKey(mc_bytes); const raw = try allocator.dupe(u8, parsed.raw); return .{ .key_type = parsed.key_type, .raw = raw };}
/// verify an ECDSA signature given a did:key string./// dispatches to the correct curve based on the key type encoded in the did:key.pub fn verifyDidKeySignature(allocator: std.mem.Allocator, did: []const u8, message: []const u8, sig_bytes: []const u8) !void { const jwt = @import("jwt.zig");
const parsed = try parseDidKey(allocator, did); defer allocator.free(parsed.raw);
switch (parsed.key_type) { .secp256k1 => try jwt.verifySecp256k1(message, sig_bytes, parsed.raw), .p256 => try jwt.verifyP256(message, sig_bytes, parsed.raw), }}
// === tests ===
test "parse secp256k1 key" { // 0xe7 0x01 prefix (varint) + 33-byte compressed key var data: [35]u8 = undefined; data[0] = 0xe7; data[1] = 0x01; data[2] = 0x02; // compressed point prefix @memset(data[3..], 0xaa);
const key = try parsePublicKey(&data); try std.testing.expectEqual(KeyType.secp256k1, key.key_type); try std.testing.expectEqual(@as(usize, 33), key.raw.len);}
test "parse p256 key" { // 0x80 0x24 prefix + 33-byte compressed key var data: [35]u8 = undefined; data[0] = 0x80; data[1] = 0x24; data[2] = 0x03; // compressed point prefix @memset(data[3..], 0xbb);
const key = try parsePublicKey(&data); try std.testing.expectEqual(KeyType.p256, key.key_type); try std.testing.expectEqual(@as(usize, 33), key.raw.len);}
test "reject unsupported key type" { const data = [_]u8{ 0xff, 0x02, 0x00 }; try std.testing.expectError(error.UnsupportedKeyType, parsePublicKey(&data));}
test "reject too short" { const data = [_]u8{0xe7}; try std.testing.expectError(error.TooShort, parsePublicKey(&data));}
test "encode-decode round-trip secp256k1" { const alloc = std.testing.allocator; var raw: [33]u8 = undefined; raw[0] = 0x02; @memset(raw[1..], 0xaa);
const encoded = try encodePublicKey(alloc, .secp256k1, &raw); defer alloc.free(encoded);
const parsed = try parsePublicKey(encoded); try std.testing.expectEqual(KeyType.secp256k1, parsed.key_type); try std.testing.expectEqualSlices(u8, &raw, parsed.raw);}
test "did:key round-trip secp256k1" { const alloc = std.testing.allocator; const multibase = @import("multibase.zig");
var raw: [33]u8 = undefined; raw[0] = 0x02; @memset(raw[1..], 0xcc);
const did_key_str = try formatDidKey(alloc, .secp256k1, &raw); defer alloc.free(did_key_str);
// should start with "did:key:z" try std.testing.expect(std.mem.startsWith(u8, did_key_str, "did:key:z"));
// parse back: strip "did:key:" prefix, decode multibase, parse multicodec const multibase_str = did_key_str["did:key:".len..]; const mc_bytes = try multibase.decode(alloc, multibase_str); defer alloc.free(mc_bytes);
const parsed = try parsePublicKey(mc_bytes); try std.testing.expectEqual(KeyType.secp256k1, parsed.key_type); try std.testing.expectEqualSlices(u8, &raw, parsed.raw);}
test "did:key round-trip p256" { const alloc = std.testing.allocator; const multibase = @import("multibase.zig");
var raw: [33]u8 = undefined; raw[0] = 0x03; @memset(raw[1..], 0xdd);
const did_key_str = try formatDidKey(alloc, .p256, &raw); defer alloc.free(did_key_str);
const multibase_str = did_key_str["did:key:".len..]; const mc_bytes = try multibase.decode(alloc, multibase_str); defer alloc.free(mc_bytes);
const parsed = try parsePublicKey(mc_bytes); try std.testing.expectEqual(KeyType.p256, parsed.key_type); try std.testing.expectEqualSlices(u8, &raw, parsed.raw);}
test "parseDidKey round-trip secp256k1" { const alloc = std.testing.allocator;
var raw: [33]u8 = undefined; raw[0] = 0x02; @memset(raw[1..], 0xcc);
const did_str = try formatDidKey(alloc, .secp256k1, &raw); defer alloc.free(did_str);
const parsed = try parseDidKey(alloc, did_str); defer alloc.free(parsed.raw);
try std.testing.expectEqual(KeyType.secp256k1, parsed.key_type); try std.testing.expectEqualSlices(u8, &raw, parsed.raw);}
test "parseDidKey round-trip p256" { const alloc = std.testing.allocator;
var raw: [33]u8 = undefined; raw[0] = 0x03; @memset(raw[1..], 0xdd);
const did_str = try formatDidKey(alloc, .p256, &raw); defer alloc.free(did_str);
const parsed = try parseDidKey(alloc, did_str); defer alloc.free(parsed.raw);
try std.testing.expectEqual(KeyType.p256, parsed.key_type); try std.testing.expectEqualSlices(u8, &raw, parsed.raw);}
test "parseDidKey with real indigo test vector" { // from bluesky-social/indigo jwt test fixtures const alloc = std.testing.allocator;
const parsed = try parseDidKey(alloc, "did:key:zQ3shscXNYZQZSPwegiv7uQZZV5kzATLBRtgJhs7uRY7pfSk4"); defer alloc.free(parsed.raw);
try std.testing.expectEqual(KeyType.secp256k1, parsed.key_type); try std.testing.expectEqual(@as(usize, 33), parsed.raw.len); try std.testing.expect(parsed.raw[0] == 0x02 or parsed.raw[0] == 0x03);}
test "parseDidKey rejects invalid prefix" { const alloc = std.testing.allocator; try std.testing.expectError(error.InvalidDidKey, parseDidKey(alloc, "did:web:example.com")); try std.testing.expectError(error.InvalidDidKey, parseDidKey(alloc, "did:key:")); try std.testing.expectError(error.InvalidDidKey, parseDidKey(alloc, ""));}
test "verifyDidKeySignature secp256k1" { const alloc = std.testing.allocator; const jwt = @import("jwt.zig"); const crypto = std.crypto; const Scheme = crypto.sign.ecdsa.EcdsaSecp256k1Sha256;
const sk_bytes = [_]u8{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, };
const message = "verify via did:key"; const sig = try jwt.signSecp256k1(message, &sk_bytes);
// derive public key and format as did:key const sk = try Scheme.SecretKey.fromBytes(sk_bytes); const kp = try Scheme.KeyPair.fromSecretKey(sk); const pk_bytes = kp.public_key.toCompressedSec1(); const did = try formatDidKey(alloc, .secp256k1, &pk_bytes); defer alloc.free(did);
// should verify try verifyDidKeySignature(alloc, did, message, &sig.bytes);
// should reject wrong message try std.testing.expectError( error.SignatureVerificationFailed, verifyDidKeySignature(alloc, did, "wrong message", &sig.bytes), );}