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.
63 lines
2.1 KiB
Python
63 lines
2.1 KiB
Python
"""Validation helpers: ext size/depth, UUID, inp_hash/out_hash format."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import re
|
|
from typing import Any
|
|
|
|
EXT_MAX_SIZE = 4096
|
|
EXT_MAX_DEPTH = 5
|
|
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}$"
|
|
)
|
|
def _json_depth(obj: Any, depth: int = 0) -> int:
|
|
if depth > EXT_MAX_DEPTH:
|
|
return depth
|
|
if isinstance(obj, dict):
|
|
return max((_json_depth(v, depth + 1) for v in obj.values()), default=depth + 1)
|
|
if isinstance(obj, list):
|
|
return max((_json_depth(x, depth + 1) for x in obj), default=depth + 1)
|
|
return depth
|
|
|
|
|
|
def validate_ext(ext: dict[str, Any] | None) -> None:
|
|
"""Raise ValueError if ext exceeds EXT_MAX_SIZE or nesting depth EXT_MAX_DEPTH."""
|
|
if not ext:
|
|
return
|
|
raw = json.dumps(ext)
|
|
if len(raw.encode("utf-8")) > EXT_MAX_SIZE:
|
|
raise ValueError("ect: ext exceeds max size (4096 bytes)")
|
|
if _json_depth(ext) > EXT_MAX_DEPTH:
|
|
raise ValueError("ect: ext exceeds max nesting depth (5)")
|
|
|
|
|
|
def valid_uuid(s: str) -> bool:
|
|
"""Return True if s is a UUID string (RFC 9562)."""
|
|
return bool(_UUID_RE.match(s))
|
|
|
|
|
|
def validate_hash_format(s: str) -> None:
|
|
"""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
|
|
# 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(padded)
|
|
except Exception:
|
|
raise ValueError("ect: inp_hash/out_hash must be plain base64url (no prefix)") from None
|