Every Dynadot request this provider has ever made was rejected with "the
X-Signature provided is not valid". The signature was correct. The header name
was not.
urllib capitalises header names when they go through add_header, which the
Request constructor does for everything passed to it, so X-Request-ID left as
X-request-id. Header names are case-insensitive under RFC 9110 and Dynadot's
gateway is not: it matches X-Request-ID exactly, did not find the rewritten
form, and computed the signature with an empty request id while we computed ours
with the UUID. A correct signature over a different string, reported as a bad
signature.
That message sends you to the algorithm and the credentials, which is where I
spent the evening. The header we were plainly sending is the last thing anyone
would suspect.
Assigning the mapping after construction bypasses add_header and preserves the
name. Confirmed against the live API: exact X-Request-ID is accepted, and
X-Request-Id one letter different is rejected.
Two tests, both of which fail against the previous code: the wire name keeps its
case, and the id in the signature is the id in the header. The second is the
invariant that actually broke, and it can break again without the first.
Co-authored-by: jw <jw@jwmacbookm5.local>
A 400 from Dynadot arrived as "HTTP 400" and nothing else. Dynadot explains the
refusal in the response body and we were discarding it, so the operator was left
with a number that does not distinguish "the domain is not in this account" from
"the signature is wrong" from "the caller address is not allowlisted". All three
are 400 and all three have different fixes.
The body is bounded to 400 characters and quoted as remote text. No credential
can appear in it: the key travels in an Authorization header and the signature
is computed rather than echoed.
Reading the body is best-effort. A stream that cannot be read must not cost the
status code as well, so the failure to read is swallowed and the code is still
raised, which is what the second test pins.
Co-authored-by: jw <jw@jwmacbookm5.local>
render_micron was a one-line alias to the Gemtext renderer, so every .mu file
AMPG produced was Gemtext. A Nomad Network browser renders those markers as
raw punctuation. Micron is a different markup: headings are > by depth, links
are inline as `[label`url] rather than standalone lines, emphasis is a toggle
pair, and literal blocks are fenced with `=.
Content is now escaped as well. Micron's control character is the backtick, and
>, <, #, and - are structural at column zero, so a vendor named ">Free Beer"
would have become a section heading on the published page. The line-start check
applies to the emitted line rather than each text chunk, since whether a
character is structural depends on it landing at column zero.
Separately, torrc and tunnels.conf are line-oriented with no quoting or escape
mechanism, so a newline in an interpolated value injects an arbitrary directive:
a tunnel_name of "web\nhost = attacker.example" silently repoints the tunnel.
tunnel_name and keys_file are now validated at the point of interpolation.
_validate_site_id covers the init path only, so a config loaded from disk never
passed through it. There is nothing to escape with here, so these reject rather
than sanitize.
Generated systemd units had no User= and no hardening, so an operator following
the install steps as root got a root-run daemon. Units now carry User=/Group=
when a service account is configured, a visible warning when not, and a baseline
of hardening directives chosen to not break a daemon whose state directory may
live anywhere.
Two tests asserted Gemtext markers inside a .mu file and so had encoded the bug.