fix: update hash format validation to -01 spec (plain base64url, no prefix)

Go ValidateHashFormat was still validating the old -00 format
(algorithm:base64url with sha-256/sha-384/sha-512 prefix). Updated to
validate plain base64url without prefix per -01 spec and RFC 9449.
Python was already updated but uncommitted. Both refimpls now match.
This commit is contained in:
2026-04-11 17:51:29 +02:00
parent 884d2dc836
commit ba38569319
5 changed files with 38 additions and 46 deletions

View File

@@ -14,9 +14,6 @@ DEFAULT_MAX_PRED_LENGTH = 100
_UUID_RE = re.compile(
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
)
_ALLOWED_HASH_ALGS = frozenset(("sha-256", "sha-384", "sha-512"))
def _json_depth(obj: Any, depth: int = 0) -> int:
if depth > EXT_MAX_DEPTH:
return depth
@@ -44,22 +41,22 @@ def valid_uuid(s: str) -> bool:
def validate_hash_format(s: str) -> None:
"""Raise ValueError if s is non-empty and not algorithm:base64url (sha-256, sha-384, sha-512)."""
"""Raise ValueError if s is non-empty and not plain base64url per RFC 9449 / ECT spec.
The ECT spec (draft-nennemann-wimse-ect-01) and RFC 9449 specify
``base64url(SHA-256(data))`` — a plain base64url string without any
algorithm prefix. This matches how ACT handles hashes.
"""
if not s:
return
idx = s.find(":")
if idx <= 0:
raise ValueError("ect: inp_hash/out_hash must be algorithm:base64url (e.g. sha-256:...)")
alg = s[:idx].lower()
if alg not in _ALLOWED_HASH_ALGS:
raise ValueError("ect: inp_hash/out_hash must be algorithm:base64url (e.g. sha-256:...)")
encoded = s[idx + 1:]
if not encoded:
raise ValueError("ect: inp_hash/out_hash must be algorithm:base64url (e.g. sha-256:...)")
pad = 4 - len(encoded) % 4
if pad != 4:
encoded += "=" * pad
# Reject strings containing non-base64url characters.
# base64url alphabet: A-Z a-z 0-9 - _ (no padding '=' expected)
if not re.fullmatch(r"[A-Za-z0-9_-]+", s):
raise ValueError("ect: inp_hash/out_hash must be plain base64url (no prefix)")
# Verify it actually decodes.
pad = 4 - len(s) % 4
padded = s + "=" * pad if pad != 4 else s
try:
base64.urlsafe_b64decode(encoded)
base64.urlsafe_b64decode(padded)
except Exception:
raise ValueError("ect: inp_hash/out_hash must be algorithm:base64url (e.g. sha-256:...)") from None
raise ValueError("ect: inp_hash/out_hash must be plain base64url (no prefix)") from None

View File

@@ -47,17 +47,18 @@ def test_validate_hash_format_empty():
def test_validate_hash_format_ok():
# sha-256:base64url (minimal valid)
validate_hash_format("sha-256:YQ")
validate_hash_format("sha-384:YQ")
validate_hash_format("sha-512:YQ")
# Plain base64url per RFC 9449 / ECT spec (no algorithm prefix)
validate_hash_format("YQ")
validate_hash_format("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
validate_hash_format("abc123-_XYZ")
def test_validate_hash_format_bad():
with pytest.raises(ValueError, match="algorithm:base64url|inp_hash"):
validate_hash_format("md5:abc")
with pytest.raises(ValueError, match="algorithm:base64url|inp_hash"):
validate_hash_format("no-colon")
# Invalid base64 that triggers decode error (e.g. binary)
with pytest.raises(ValueError, match="algorithm:base64url|inp_hash"):
validate_hash_format("sha-256:YQ\x00") # null in payload
# Colon is not valid base64url — rejects old prefixed format
with pytest.raises(ValueError, match="plain base64url"):
validate_hash_format("sha-256:YQ")
with pytest.raises(ValueError, match="plain base64url"):
validate_hash_format("not valid!!")
# Null byte in payload
with pytest.raises(ValueError, match="plain base64url"):
validate_hash_format("YQ\x00")