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

Configure Feed

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

Accept JOSE ECDSA client assertions

zzstoatzz (May 24, 2026, 8:09 PM -0500) 3e3823ce 785c3c6b

+81 -5
+81 -5
src/atproto/oauth.zig
··· 418 418 defer allocator.free(public_key); 419 419 const signature = try zat.jwt.base64UrlDecode(allocator, signature_part); 420 420 defer allocator.free(signature); 421 - if (std.mem.eql(u8, alg, "ES256")) { 422 - try zat.jwt.verifyP256(signing_input, signature, public_key); 423 - } else { 424 - try zat.jwt.verifySecp256k1(signing_input, signature, public_key); 425 - } 421 + try verifyClientAssertionSignature(allocator, alg, signing_input, signature, public_key); 426 422 } 427 423 428 424 fn clientPublicKeyFromJwks(allocator: std.mem.Allocator, jwks_doc: std.json.Value, kid: ?[]const u8, alg: []const u8) ![]u8 { ··· 447 443 return public_key; 448 444 } 449 445 return error.KeyNotFound; 446 + } 447 + 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 + } 450 501 } 451 502 452 503 const Form = struct { ··· 773 824 try std.testing.expect(isGranularScope("rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app%23bsky_appview")); 774 825 try std.testing.expect(!isGranularScope("transition:generic")); 775 826 } 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 + }