atproto pds in zig pds.zat.dev
pds atproto
24

Configure Feed

Select the types of activity you want to include in your feed.

Extract JOSE signature verifier

zzstoatzz (May 24, 2026, 9:07 PM -0500) dc3153a7 3e3823ce

+89 -81
+3
docs/architecture.md
··· 20 20 - Invite codes follow the official PDS split between code metadata and recorded 21 21 uses; account creation consumes a code while holding the store lock. 22 22 - Blob bytes live in the disk blobstore rooted at `ZDS_BLOBSTORE_PATH`. 23 + - JOSE compatibility helpers live under `internal` until they are clean enough 24 + to upstream to `zat`; OAuth JWT verification must accept normal JOSE ECDSA 25 + signatures without relaxing repo signature verification. 23 26 24 27 ## repo write invariants 25 28
+2 -81
src/atproto/oauth.zig
··· 2 2 const auth = @import("../auth/tokens.zig"); 3 3 const config = @import("../core/config.zig"); 4 4 const http_api = @import("../http/api.zig"); 5 + const jose = @import("../internal/jose.zig"); 5 6 const permission_sets = @import("oauth/permission_sets.zig"); 6 7 const store = @import("../storage/store.zig"); 7 8 const zat = @import("zat"); ··· 418 419 defer allocator.free(public_key); 419 420 const signature = try zat.jwt.base64UrlDecode(allocator, signature_part); 420 421 defer allocator.free(signature); 421 - try verifyClientAssertionSignature(allocator, alg, signing_input, signature, public_key); 422 + try jose.verifyEcdsaJwtSignature(allocator, alg, signing_input, signature, public_key); 422 423 } 423 424 424 425 fn clientPublicKeyFromJwks(allocator: std.mem.Allocator, jwks_doc: std.json.Value, kid: ?[]const u8, alg: []const u8) ![]u8 { ··· 445 446 return error.KeyNotFound; 446 447 } 447 448 448 - fn verifyClientAssertionSignature(allocator: std.mem.Allocator, alg: []const u8, signing_input: []const u8, signature: []const u8, public_key: []const u8) !void { 449 - if (std.mem.eql(u8, alg, "ES256")) { 450 - zat.jwt.verifyP256(signing_input, signature, public_key) catch { 451 - const normalized = try normalizeEcdsaHighS(allocator, signature, p256_order); 452 - defer allocator.free(normalized); 453 - try zat.jwt.verifyP256(signing_input, normalized, public_key); 454 - }; 455 - } else { 456 - zat.jwt.verifySecp256k1(signing_input, signature, public_key) catch { 457 - const normalized = try normalizeEcdsaHighS(allocator, signature, secp256k1_order); 458 - defer allocator.free(normalized); 459 - try zat.jwt.verifySecp256k1(signing_input, normalized, public_key); 460 - }; 461 - } 462 - } 463 - 464 - const p256_order: [32]u8 = .{ 465 - 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 466 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 467 - 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, 468 - 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x51, 469 - }; 470 - 471 - const secp256k1_order: [32]u8 = .{ 472 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 473 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 474 - 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 475 - 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41, 476 - }; 477 - 478 - fn normalizeEcdsaHighS(allocator: std.mem.Allocator, signature: []const u8, order: [32]u8) ![]u8 { 479 - if (signature.len != 64) return error.InvalidSignature; 480 - const normalized = try allocator.dupe(u8, signature); 481 - subtractBigEndian(normalized[32..64], &order, signature[32..64]); 482 - return normalized; 483 - } 484 - 485 - fn subtractBigEndian(out: []u8, lhs: []const u8, rhs: []const u8) void { 486 - std.debug.assert(out.len == lhs.len and lhs.len == rhs.len); 487 - var borrow: u16 = 0; 488 - var i = lhs.len; 489 - while (i > 0) { 490 - i -= 1; 491 - const left: u16 = lhs[i]; 492 - const right: u16 = rhs[i] + borrow; 493 - if (left >= right) { 494 - out[i] = @intCast(left - right); 495 - borrow = 0; 496 - } else { 497 - out[i] = @intCast(left + 256 - right); 498 - borrow = 1; 499 - } 500 - } 501 - } 502 - 503 449 const Form = struct { 504 450 body: []const u8, 505 451 ··· 824 770 try std.testing.expect(isGranularScope("rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app%23bsky_appview")); 825 771 try std.testing.expect(!isGranularScope("transition:generic")); 826 772 } 827 - 828 - test "client assertion verifier normalizes high-S ECDSA signatures" { 829 - const Scheme = std.crypto.sign.ecdsa.EcdsaP256Sha256; 830 - const sk_bytes = [_]u8{ 831 - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 832 - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 833 - 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 834 - 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 835 - }; 836 - const message = "oauth client assertion"; 837 - const sig = try zat.jwt.signP256(message, &sk_bytes); 838 - 839 - var high_s_sig = sig.bytes; 840 - subtractBigEndian(high_s_sig[32..64], &p256_order, sig.bytes[32..64]); 841 - 842 - const normalized = try normalizeEcdsaHighS(std.testing.allocator, &high_s_sig, p256_order); 843 - defer std.testing.allocator.free(normalized); 844 - 845 - try std.testing.expectEqualSlices(u8, &sig.bytes, normalized); 846 - 847 - const sk = try Scheme.SecretKey.fromBytes(sk_bytes); 848 - const kp = try Scheme.KeyPair.fromSecretKey(sk); 849 - const pk = kp.public_key.toCompressedSec1(); 850 - try verifyClientAssertionSignature(std.testing.allocator, "ES256", message, &high_s_sig, &pk); 851 - }
+83
src/internal/jose.zig
··· 1 + const std = @import("std"); 2 + const zat = @import("zat"); 3 + 4 + pub fn verifyEcdsaJwtSignature(allocator: std.mem.Allocator, alg: []const u8, signing_input: []const u8, signature: []const u8, public_key: []const u8) !void { 5 + if (std.mem.eql(u8, alg, "ES256")) { 6 + zat.jwt.verifyP256(signing_input, signature, public_key) catch { 7 + const normalized = try normalizeEcdsaHighS(allocator, signature, p256_order); 8 + defer allocator.free(normalized); 9 + try zat.jwt.verifyP256(signing_input, normalized, public_key); 10 + }; 11 + } else if (std.mem.eql(u8, alg, "ES256K")) { 12 + zat.jwt.verifySecp256k1(signing_input, signature, public_key) catch { 13 + const normalized = try normalizeEcdsaHighS(allocator, signature, secp256k1_order); 14 + defer allocator.free(normalized); 15 + try zat.jwt.verifySecp256k1(signing_input, normalized, public_key); 16 + }; 17 + } else { 18 + return error.UnsupportedAlgorithm; 19 + } 20 + } 21 + 22 + const p256_order: [32]u8 = .{ 23 + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 24 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 25 + 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, 26 + 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x51, 27 + }; 28 + 29 + const secp256k1_order: [32]u8 = .{ 30 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 31 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 32 + 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 33 + 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41, 34 + }; 35 + 36 + fn normalizeEcdsaHighS(allocator: std.mem.Allocator, signature: []const u8, order: [32]u8) ![]u8 { 37 + if (signature.len != 64) return error.InvalidSignature; 38 + const normalized = try allocator.dupe(u8, signature); 39 + subtractBigEndian(normalized[32..64], &order, signature[32..64]); 40 + return normalized; 41 + } 42 + 43 + fn subtractBigEndian(out: []u8, lhs: []const u8, rhs: []const u8) void { 44 + std.debug.assert(out.len == lhs.len and lhs.len == rhs.len); 45 + var borrow: u16 = 0; 46 + var i = lhs.len; 47 + while (i > 0) { 48 + i -= 1; 49 + const left: u16 = lhs[i]; 50 + const right: u16 = rhs[i] + borrow; 51 + if (left >= right) { 52 + out[i] = @intCast(left - right); 53 + borrow = 0; 54 + } else { 55 + out[i] = @intCast(left + 256 - right); 56 + borrow = 1; 57 + } 58 + } 59 + } 60 + 61 + test "JOSE ECDSA verifier normalizes high-S P-256 signatures" { 62 + const Scheme = std.crypto.sign.ecdsa.EcdsaP256Sha256; 63 + const sk_bytes = [_]u8{ 64 + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 65 + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 66 + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 67 + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 68 + }; 69 + const message = "oauth client assertion"; 70 + const sig = try zat.jwt.signP256(message, &sk_bytes); 71 + 72 + var high_s_sig = sig.bytes; 73 + subtractBigEndian(high_s_sig[32..64], &p256_order, sig.bytes[32..64]); 74 + 75 + const normalized = try normalizeEcdsaHighS(std.testing.allocator, &high_s_sig, p256_order); 76 + defer std.testing.allocator.free(normalized); 77 + try std.testing.expectEqualSlices(u8, &sig.bytes, normalized); 78 + 79 + const sk = try Scheme.SecretKey.fromBytes(sk_bytes); 80 + const kp = try Scheme.KeyPair.fromSecretKey(sk); 81 + const pk = kp.public_key.toCompressedSec1(); 82 + try verifyEcdsaJwtSignature(std.testing.allocator, "ES256", message, &high_s_sig, &pk); 83 + }
+1
src/root.zig
··· 31 31 32 32 pub const internal = struct { 33 33 pub const cli = @import("internal/cli.zig"); 34 + pub const jose = @import("internal/jose.zig"); 34 35 pub const sharded_locks = @import("internal/sharded_locks.zig"); 35 36 }; 36 37