From 0e0495606fce2d64df4093321f9732070e8c2769 Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:03:54 -0500 Subject: [PATCH] fix(attest): bound did:key method-id length before base58 decode (#360) gitlawb-core added the same guard in 73bf132c; the second copy of the parser in gitlawb-attest never got it, so an untrusted signer string of arbitrary length reaches a decoder whose cost is quadratic in input. --- crates/gitlawb-attest/src/attestation.rs | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/gitlawb-attest/src/attestation.rs b/crates/gitlawb-attest/src/attestation.rs index 0e29e3a9..d2fa7566 100644 --- a/crates/gitlawb-attest/src/attestation.rs +++ b/crates/gitlawb-attest/src/attestation.rs @@ -202,6 +202,17 @@ fn verifying_key_from_did_key(did: &str) -> Result { "did:key must use base58btc (z-prefix): {did}" ))); } + // Refuse an oversized id before decoding. base58 decoding is quadratic + // in its input, and this string comes from an untrusted artifact. An + // ed25519 did:key method-id is a fixed 48 characters, so this bound is + // slack rather than a behavior change, and it has to sit ahead of the + // decode to be worth anything. + const MAX_METHOD_ID_LEN: usize = 64; + if method_id.len() > MAX_METHOD_ID_LEN { + return Err(Error::Did( + "did:key method-specific id too long".to_string(), + )); + } let (base, bytes) = multibase::decode(method_id).map_err(|e| Error::Did(format!("multibase: {e}")))?; if base != multibase::Base::Base58Btc { @@ -465,6 +476,23 @@ mod tests { assert!(matches!(err, Error::Did(_))); } + #[test] + fn verify_rejects_oversized_method_id_before_decoding() { + let sk = fresh(); + let mut att = dummy_attestation(&sk, sample_cert_hash()); + // A well-formed prefix on an absurdly long id must fail the length + // bound, not the decoder: base58 decode cost is quadratic in input + // length, so reaching it at all is the defect. + att.signer = format!("did:key:z{}", "1".repeat(256)); + let err = att.verify_signature(sample_cert_hash()).unwrap_err(); + // Match the bound's own message: any other Did error (a decode or + // length mismatch downstream) means the oversized input was decoded. + match err { + Error::Did(m) => assert!(m.contains("too long"), "unexpected: {m}"), + e => panic!("expected Error::Did, got {e:?}"), + } + } + #[test] fn verify_rejects_bad_signature_base64() { let sk = fresh();