GET requests included.
One header holds a signature. You sign a canonical string: six lines of text built from the request, in a fixed format.
Clock skew is how far your clock can differ from ours.
Headers
- Write the timestamp as an unpadded decimal. It must be within ±30 s of our clock.
- Encode the raw signature bytes as standard base64, with padding.
The canonical string
- The literal
NOVIG-V3. - The same value as
Novig-Timestamp. A leading zero changes the string. - The method, in uppercase ASCII.
- The path, never the full URL. Start at the leading
/and include the mount prefix, verbatim. - The canonical query, or an empty string. The line is always present.
- The SHA-256 of the raw body bytes, in lowercase hex.
/v3/keys and /v3/keys/ are different paths.
With no body, line 6 is the hash of zero bytes: e3b0c442…7852b855.
A literal {} body gives a different hash.
Hash the exact bytes you send, and send them with
Content-Type: application/json. Re-serialized JSON changes the hash.GET /v3/keys at timestamp 1755000000000:
1
NOVIG-V32
17550000000003
GET4
/v3/keys5
6
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Joined by
LF. No trailing newline.The canonical query
Build line 5 from the raw query string in six steps:- Split the raw query on
&. - Split each pair at the first
=. A pair with no=has an empty value. - URI-decode both parts, reading
%XXas UTF-8. A bare+stays a literal+. - Re-encode both parts. Keep
ALPHA DIGIT - . _ ~as is, and write everything else as%XXin uppercase hex. - Sort bytewise by name, then by value, keeping repeats.
Z(0x5A) sorts beforea(0x61). - Join the pairs with
&.
Our edge strips
Expires, Key-Pair-Id, Policy, and Signature from the query. A request that signs one of them fails, and nothing shows why.Algorithms
Both algorithms sign the canonical string itself.
A P-256 signature is DER-encoded as
SEQUENCE { r, s }.
WebCrypto’s ECDSA returns raw
r‖s, not DER, and a raw signature never verifies. Convert it to DER, or sign with Ed25519.Sign in Rust
request.sign(&key) signs a built reqwest request, using the method, path, query, and body it will send.
It signs with Ed25519, and its canonical string matches all test vectors.
sign.rs
Test vectors
A test vector is a sample request with its expected canonical string. Use them to check your signer.signing-vectors.json
vectors and two test keypairs, for Ed25519 and P-256. They cover every case in the table below.
Test your signature
POST /v3/echo returns your request body byte for byte.
Like every route, echo requires a valid signature.
A 200 proves your host, key, clock, and canonical string are all correct.
Echo doesn’t return the canonical string.
When echo returns 401, sign a test vector and diff your canonical string against its string_to_sign.
The first byte that differs is the bug.
For a full walkthrough, see Quickstart.
