atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

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

fix: reject high-S ECDSA signatures in verification

AT Protocol requires low-S normalization (BIP-62 style). Signatures
where S > curve_order/2 are now rejected for both secp256k1 and P-256.

Without this, malleable signatures would verify successfully. The
verify functions are now pub for direct use outside JWT context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

zzstoatzz f2d7202d ddccc8a4

+41 -2
+41 -2
src/internal/jwt.zig
··· 219 219 }; 220 220 } 221 221 222 - fn verifySecp256k1(message: []const u8, sig_bytes: []const u8, public_key_raw: []const u8) !void { 222 + /// compare two 32-byte big-endian values: true if a > b 223 + fn bigEndianGt(a: [32]u8, b: [32]u8) bool { 224 + for (a, b) |ab, bb| { 225 + if (ab > bb) return true; 226 + if (ab < bb) return false; 227 + } 228 + return false; 229 + } 230 + 231 + /// reject high-S signatures (atproto requires low-S normalization). 232 + /// s is high-S if s > curve_order / 2. 233 + fn rejectHighS(comptime half_order: [32]u8, s_bytes: [32]u8) error{HighSSignature}!void { 234 + if (bigEndianGt(s_bytes, half_order)) return error.HighSSignature; 235 + } 236 + 237 + // secp256k1 order/2 (big-endian) 238 + // order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 239 + const secp256k1_half_order: [32]u8 = .{ 240 + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 241 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 242 + 0x5D, 0x57, 0x6E, 0x73, 0x57, 0xA4, 0x50, 0x1D, 243 + 0xDF, 0xE9, 0x2F, 0x46, 0x68, 0x1B, 0x20, 0xA0, 244 + }; 245 + 246 + // P-256 order/2 (big-endian) 247 + // order = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 248 + const p256_half_order: [32]u8 = .{ 249 + 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 250 + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 251 + 0xDE, 0x73, 0x7D, 0x56, 0xD3, 0x8B, 0xCF, 0x42, 252 + 0x79, 0xDC, 0xE5, 0x61, 0x7E, 0x31, 0x92, 0xA8, 253 + }; 254 + 255 + pub fn verifySecp256k1(message: []const u8, sig_bytes: []const u8, public_key_raw: []const u8) !void { 223 256 const Scheme = crypto.sign.ecdsa.EcdsaSecp256k1Sha256; 224 257 225 258 // parse signature (r || s, 64 bytes) 226 259 if (sig_bytes.len != 64) return error.InvalidSignature; 227 260 const sig = Scheme.Signature.fromBytes(sig_bytes[0..64].*); 261 + 262 + // reject high-S signatures (atproto requires low-S) 263 + rejectHighS(secp256k1_half_order, sig.s) catch return error.SignatureVerificationFailed; 228 264 229 265 // parse public key from SEC1 compressed format 230 266 if (public_key_raw.len != 33) return error.InvalidPublicKey; ··· 234 270 sig.verify(message, public_key) catch return error.SignatureVerificationFailed; 235 271 } 236 272 237 - fn verifyP256(message: []const u8, sig_bytes: []const u8, public_key_raw: []const u8) !void { 273 + pub fn verifyP256(message: []const u8, sig_bytes: []const u8, public_key_raw: []const u8) !void { 238 274 const Scheme = crypto.sign.ecdsa.EcdsaP256Sha256; 239 275 240 276 // parse signature (r || s, 64 bytes) 241 277 if (sig_bytes.len != 64) return error.InvalidSignature; 242 278 const sig = Scheme.Signature.fromBytes(sig_bytes[0..64].*); 279 + 280 + // reject high-S signatures (atproto requires low-S) 281 + rejectHighS(p256_half_order, sig.s) catch return error.SignatureVerificationFailed; 243 282 244 283 // parse public key from SEC1 compressed format 245 284 if (public_key_raw.len != 33) return error.InvalidPublicKey;