qurl-conformance 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qurl_conformance-0.1.1/PKG-INFO +9 -0
- qurl_conformance-0.1.1/README.md +1 -0
- qurl_conformance-0.1.1/pyproject.toml +17 -0
- qurl_conformance-0.1.1/qurl_conformance/__init__.py +22 -0
- qurl_conformance-0.1.1/qurl_conformance/_data/issuer_signature_vectors.json +44 -0
- qurl_conformance-0.1.1/qurl_conformance/_data/qv2_conformance_vectors.json +376 -0
- qurl_conformance-0.1.1/qurl_conformance/_data/relay_knock_golden.json +31 -0
- qurl_conformance-0.1.1/qurl_conformance.egg-info/PKG-INFO +9 -0
- qurl_conformance-0.1.1/qurl_conformance.egg-info/SOURCES.txt +10 -0
- qurl_conformance-0.1.1/qurl_conformance.egg-info/dependency_links.txt +1 -0
- qurl_conformance-0.1.1/qurl_conformance.egg-info/top_level.txt +1 -0
- qurl_conformance-0.1.1/setup.cfg +4 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qurl-conformance
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: qURL v2 cross-language conformance vectors
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
qURL v2 cross-language conformance vectors (Python accessor).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qURL v2 cross-language conformance vectors (Python accessor).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qurl-conformance"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "qURL v2 cross-language conformance vectors"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
|
|
13
|
+
[tool.setuptools]
|
|
14
|
+
packages = ["qurl_conformance"]
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.package-data]
|
|
17
|
+
qurl_conformance = ["_data/*.json"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""qURL v2 cross-language conformance vectors (Python accessor)."""
|
|
2
|
+
import json
|
|
3
|
+
from importlib import resources
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _load(name: str):
|
|
7
|
+
return json.loads((resources.files(__package__) / "_data" / name).read_text(encoding="utf-8"))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def qv2_vectors():
|
|
11
|
+
"""Return the parsed qv2_conformance_vectors.json."""
|
|
12
|
+
return _load("qv2_conformance_vectors.json")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def issuer_signature_vectors():
|
|
16
|
+
"""Return the parsed issuer_signature_vectors.json."""
|
|
17
|
+
return _load("issuer_signature_vectors.json")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def relay_knock_vectors():
|
|
21
|
+
"""Return the parsed relay_knock_golden.json."""
|
|
22
|
+
return _load("relay_knock_golden.json")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"algorithm": "ECC_NIST_P256 / ECDSA_SHA_256, wire = raw r||s (64 bytes), low-S",
|
|
3
|
+
"description": "qURL v2 issuer-signature golden vectors: P-256 raw r||s low-S wire signatures over the exact claims bytes. These are VERIFY fixtures (ECDSA's nonce is random, so signatures are re-verified by consumers, never reproduced).",
|
|
4
|
+
"domain_separation_prefix": "NHP-QURL-V2-ISSUER",
|
|
5
|
+
"issuer": {
|
|
6
|
+
"jwk": {
|
|
7
|
+
"crv": "P-256",
|
|
8
|
+
"kty": "EC",
|
|
9
|
+
"x": "pDu9mdM6E96ncBm5qjKn16Rjv6sWoHRQQz2ElwKSg5Y",
|
|
10
|
+
"y": "EAywr6H7hG-4JiHdmASr92F3K3XN7ZgWokc2Ah_R6ek"
|
|
11
|
+
},
|
|
12
|
+
"kid": "qurl-issuer-vector-key",
|
|
13
|
+
"spki_der_b64": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpDu9mdM6E96ncBm5qjKn16Rjv6sWoHRQQz2ElwKSg5YQDLCvofuEb7gmId2YBKv3YXcrdc3tmBaiRzYCH9Hp6Q"
|
|
14
|
+
},
|
|
15
|
+
"vectors": [
|
|
16
|
+
{
|
|
17
|
+
"claims_b64": "eyJ2IjoyLCJpc3MiOiJxdXJsLXNlcnZpY2UiLCJraWQiOiJxdXJsLWlzc3Vlci12ZWN0b3Ita2V5IiwiaWF0IjoxNzgxOTEwMDAwLCJuYmYiOjE3ODE5MTAwMDAsImV4cCI6MTc4MTkxMDMwMCwianRpIjoicXVybF8wMUpWRUNUT1JGSVhUVVJFMDAwMCIsImNlbGxfcHVibGljX2tleV9iNjQiOiJSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVRIiwiY2VsbF9pZCI6InZlY3Rvci1jZWxsIiwicmVsYXlfdXJsIjoiaHR0cHM6Ly9yZWxheS5leGFtcGxlLmNvbSIsInJlc291cmNlX3B1YmxpY19rZXlfYjY0IjoiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFUDRpR2pVLVpoUzVyRmNNbjk0Y3BTdlB1MXJGTU1vWlFMMTB4Q2ZTRDZpNUhTcWwzclFaN0NYUlpDdUtwOTFMSFAwNEROUGcwdkluLU1xbm4tRnM5cHciLCJxdXJsX3VzZXJfcHVibGljX2tleV9iNjQiOiJWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZVIn0",
|
|
18
|
+
"expect": "accept",
|
|
19
|
+
"name": "accept_valid_low_s",
|
|
20
|
+
"reason": "valid 64-byte low-S raw r||s signature over the exact claims bytes",
|
|
21
|
+
"sig_b64": "a6UIAEY71V27RoEFV8xLiKSXpfCjwYBys9xSpDt1fwQ8m1u-UPS4DqVapOcpPevgLs60yT6MrIeMqP0lz01Ovw",
|
|
22
|
+
"sig_encoding": "raw_r_s",
|
|
23
|
+
"signing_input_b64": "TkhQLVFVUkwtVjItSVNTVUVSAGV5SjJJam95TENKcGMzTWlPaUp4ZFhKc0xYTmxjblpwWTJVaUxDSnJhV1FpT2lKeGRYSnNMV2x6YzNWbGNpMTJaV04wYjNJdGEyVjVJaXdpYVdGMElqb3hOemd4T1RFd01EQXdMQ0p1WW1ZaU9qRTNPREU1TVRBd01EQXNJbVY0Y0NJNk1UYzRNVGt4TURNd01Dd2lhblJwSWpvaWNYVnliRjh3TVVwV1JVTlVUMUpHU1ZoVVZWSkZNREF3TUNJc0ltTmxiR3hmY0hWaWJHbGpYMnRsZVY5aU5qUWlPaUpTUlZKRlVrVlNSVkpGVWtWU1JWSkZVa1ZTUlZKRlVrVlNSVkpGVWtWU1JWSkZVa1ZTUlZKRlVrVlJJaXdpWTJWc2JGOXBaQ0k2SW5abFkzUnZjaTFqWld4c0lpd2ljbVZzWVhsZmRYSnNJam9pYUhSMGNITTZMeTl5Wld4aGVTNWxlR0Z0Y0d4bExtTnZiU0lzSW5KbGMyOTFjbU5sWDNCMVlteHBZMTlyWlhsZllqWTBJam9pVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGVURScFIycFZMVnBvVXpWeVJtTk5iamswWTNCVGRsQjFNWEpHVFUxdldsRk1NVEI0UTJaVFJEWnBOVWhUY1d3emNsRmFOME5ZVWxwRGRVdHdPVEZNU0ZBd05FUk9VR2N3ZGtsdUxVMXhibTR0Um5NNWNIY2lMQ0p4ZFhKc1gzVnpaWEpmY0hWaWJHbGpYMnRsZVY5aU5qUWlPaUpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWlZJbjA"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"claims_b64": "eyJ2IjoyLCJpc3MiOiJxdXJsLXNlcnZpY2UiLCJraWQiOiJxdXJsLWlzc3Vlci12ZWN0b3Ita2V5IiwiaWF0IjoxNzgxOTEwMDAwLCJuYmYiOjE3ODE5MTAwMDAsImV4cCI6MTc4MTkxMDMwMCwianRpIjoicXVybF8wMUpWRUNUT1JGSVhUVVJFMDAwMCIsImNlbGxfcHVibGljX2tleV9iNjQiOiJSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVRIiwiY2VsbF9pZCI6InZlY3Rvci1jZWxsIiwicmVsYXlfdXJsIjoiaHR0cHM6Ly9yZWxheS5leGFtcGxlLmNvbSIsInJlc291cmNlX3B1YmxpY19rZXlfYjY0IjoiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFUDRpR2pVLVpoUzVyRmNNbjk0Y3BTdlB1MXJGTU1vWlFMMTB4Q2ZTRDZpNUhTcWwzclFaN0NYUlpDdUtwOTFMSFAwNEROUGcwdkluLU1xbm4tRnM5cHciLCJxdXJsX3VzZXJfcHVibGljX2tleV9iNjQiOiJWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZVIn0",
|
|
27
|
+
"expect": "reject",
|
|
28
|
+
"name": "reject_high_s",
|
|
29
|
+
"reason": "high_s",
|
|
30
|
+
"sig_b64": "a6UIAEY71V27RoEFV8xLiKSXpfCjwYBys9xSpDt1fwTDZKRArwtH8lqlWxjWwhQfjhhF5GiK8f1nEM2dLRXWkg",
|
|
31
|
+
"sig_encoding": "raw_r_s",
|
|
32
|
+
"signing_input_b64": "TkhQLVFVUkwtVjItSVNTVUVSAGV5SjJJam95TENKcGMzTWlPaUp4ZFhKc0xYTmxjblpwWTJVaUxDSnJhV1FpT2lKeGRYSnNMV2x6YzNWbGNpMTJaV04wYjNJdGEyVjVJaXdpYVdGMElqb3hOemd4T1RFd01EQXdMQ0p1WW1ZaU9qRTNPREU1TVRBd01EQXNJbVY0Y0NJNk1UYzRNVGt4TURNd01Dd2lhblJwSWpvaWNYVnliRjh3TVVwV1JVTlVUMUpHU1ZoVVZWSkZNREF3TUNJc0ltTmxiR3hmY0hWaWJHbGpYMnRsZVY5aU5qUWlPaUpTUlZKRlVrVlNSVkpGVWtWU1JWSkZVa1ZTUlZKRlVrVlNSVkpGVWtWU1JWSkZVa1ZTUlZKRlVrVlJJaXdpWTJWc2JGOXBaQ0k2SW5abFkzUnZjaTFqWld4c0lpd2ljbVZzWVhsZmRYSnNJam9pYUhSMGNITTZMeTl5Wld4aGVTNWxlR0Z0Y0d4bExtTnZiU0lzSW5KbGMyOTFjbU5sWDNCMVlteHBZMTlyWlhsZllqWTBJam9pVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGVURScFIycFZMVnBvVXpWeVJtTk5iamswWTNCVGRsQjFNWEpHVFUxdldsRk1NVEI0UTJaVFJEWnBOVWhUY1d3emNsRmFOME5ZVWxwRGRVdHdPVEZNU0ZBd05FUk9VR2N3ZGtsdUxVMXhibTR0Um5NNWNIY2lMQ0p4ZFhKc1gzVnpaWEpmY0hWaWJHbGpYMnRsZVY5aU5qUWlPaUpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWlZJbjA"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"claims_b64": "eyJ2IjoyLCJpc3MiOiJxdXJsLXNlcnZpY2UiLCJraWQiOiJxdXJsLWlzc3Vlci12ZWN0b3Ita2V5IiwiaWF0IjoxNzgxOTEwMDAwLCJuYmYiOjE3ODE5MTAwMDAsImV4cCI6MTc4MTkxMDMwMCwianRpIjoicXVybF8wMUpWRUNUT1JGSVhUVVJFMDAwMCIsImNlbGxfcHVibGljX2tleV9iNjQiOiJSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVRIiwiY2VsbF9pZCI6InZlY3Rvci1jZWxsIiwicmVsYXlfdXJsIjoiaHR0cHM6Ly9yZWxheS5leGFtcGxlLmNvbSIsInJlc291cmNlX3B1YmxpY19rZXlfYjY0IjoiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFUDRpR2pVLVpoUzVyRmNNbjk0Y3BTdlB1MXJGTU1vWlFMMTB4Q2ZTRDZpNUhTcWwzclFaN0NYUlpDdUtwOTFMSFAwNEROUGcwdkluLU1xbm4tRnM5cHciLCJxdXJsX3VzZXJfcHVibGljX2tleV9iNjQiOiJWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZVIn0",
|
|
36
|
+
"expect": "reject",
|
|
37
|
+
"name": "reject_wrong_length_der",
|
|
38
|
+
"reason": "wrong_length",
|
|
39
|
+
"sig_b64": "MEQCIGulCABGO9Vdu0aBBVfMS4ikl6Xwo8GAcrPcUqQ7dX8EAiA8m1u-UPS4DqVapOcpPevgLs60yT6MrIeMqP0lz01Ovw",
|
|
40
|
+
"sig_encoding": "der",
|
|
41
|
+
"signing_input_b64": "TkhQLVFVUkwtVjItSVNTVUVSAGV5SjJJam95TENKcGMzTWlPaUp4ZFhKc0xYTmxjblpwWTJVaUxDSnJhV1FpT2lKeGRYSnNMV2x6YzNWbGNpMTJaV04wYjNJdGEyVjVJaXdpYVdGMElqb3hOemd4T1RFd01EQXdMQ0p1WW1ZaU9qRTNPREU1TVRBd01EQXNJbVY0Y0NJNk1UYzRNVGt4TURNd01Dd2lhblJwSWpvaWNYVnliRjh3TVVwV1JVTlVUMUpHU1ZoVVZWSkZNREF3TUNJc0ltTmxiR3hmY0hWaWJHbGpYMnRsZVY5aU5qUWlPaUpTUlZKRlVrVlNSVkpGVWtWU1JWSkZVa1ZTUlZKRlVrVlNSVkpGVWtWU1JWSkZVa1ZTUlZKRlVrVlJJaXdpWTJWc2JGOXBaQ0k2SW5abFkzUnZjaTFqWld4c0lpd2ljbVZzWVhsZmRYSnNJam9pYUhSMGNITTZMeTl5Wld4aGVTNWxlR0Z0Y0d4bExtTnZiU0lzSW5KbGMyOTFjbU5sWDNCMVlteHBZMTlyWlhsZllqWTBJam9pVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGVURScFIycFZMVnBvVXpWeVJtTk5iamswWTNCVGRsQjFNWEpHVFUxdldsRk1NVEI0UTJaVFJEWnBOVWhUY1d3emNsRmFOME5ZVWxwRGRVdHdPVEZNU0ZBd05FUk9VR2N3ZGtsdUxVMXhibTR0Um5NNWNIY2lMQ0p4ZFhKc1gzVnpaWEpmY0hWaWJHbGpYMnRsZVY5aU5qUWlPaUpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWlZJbjA"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
{
|
|
2
|
+
"artifact": "qurl-v2-conformance-vectors",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"description": "Language-agnostic qURL v2 conformance vectors: the single wire-truth every qURL v2 verifier re-runs against its own implementation. Each class names the verifier entry point it targets and the input shape it consumes; consumers feed the input through their real parser/validator and assert the declared accept/reject outcome and reject_class.",
|
|
5
|
+
"source_of_truth": "layervai/qurl-conformance",
|
|
6
|
+
"notes": [
|
|
7
|
+
"Every committed input is deterministic: keys are fixed fill bytes or the exact keys carried by the composed signature fixture, never freshly minted.",
|
|
8
|
+
"claims_json / secret_json values are RAW JSON TEXT (ASCII), fed directly to parseClaims/parseSecret -- NOT base64. Duplicate keys and other JSON-layer faults survive because they live inside a JSON string value, not as object members a re-serializer would normalize away.",
|
|
9
|
+
"strict_base64 inputs are the base64url string VERBATIM (the fault is in the encoding layer); a consumer feeds them straight to its strict base64url decoder.",
|
|
10
|
+
"fragment inputs are full fragment bodies fed to ParseFragment, which does NOT verify the issuer signature -- the fragment class pins wire SHAPE only; signature accept/reject is the signature class.",
|
|
11
|
+
"The signature class is COMPOSED from issuer_signature_vectors.json (already byte-shared by the Go and TS verifiers); it is referenced here, not duplicated, so there is exactly one copy of the signature bytes.",
|
|
12
|
+
"KNK/ACK Noise handshake packet vectors live in the separate sibling artifact relay_knock_golden.json, not this one -- they belong to the Noise handshake layer, which the qURL v2 claims/signature/fragment layer does not import; the two families are split by layer."
|
|
13
|
+
],
|
|
14
|
+
"signature_class": {
|
|
15
|
+
"entry_point": "qv2.VerifyRawIssuerSignature(pub, claimsB64, rawSig)",
|
|
16
|
+
"composes": "issuer_signature_vectors.json",
|
|
17
|
+
"comment": "Run the composed file's vectors through the issuer-signature verifier. accept => verifies; reject => fails with the declared reject_class (high_s | wrong_length). The composed file pins the P-256 raw r||s low-S wire encoding and the 0x00 domain separator across KMS sign output, Go verify, and WebCrypto verify.",
|
|
18
|
+
"tamper_derivation": {
|
|
19
|
+
"reject_class": "tamper",
|
|
20
|
+
"comment": "Language-agnostic payload-tamper reject DERIVED from the composed file's accept vector -- so every consumer (Go, other language implementations, the Go SDK) synthesizes the SAME negative without a third copy of signature bytes. Procedure: take the accept vector's valid 64-byte low-S signature UNCHANGED, and verify it against a claims input formed by flipping the FIRST character of the accept vector's claims_b64 between base64url 'A' and 'B' (i.e. 'A' -> 'B', any other char -> 'A'). The FIRST symbol encodes the top 6 bits of decoded byte 0 -- always fully significant, never a don't-care tail bit -- so the transformed claims_b64 stays CANONICAL base64url AND decodes to DIFFERENT bytes. That portability matters: a consumer may hash the base64 string (qURL v2's Go verifier does), decode-then-hash, or strict-decode before verifying; only a change that is both canonical and decoded-differing yields the SAME tamper rejection for all three. (A last-char flip would only touch a don't-care padding bit when len(claims_b64) mod 4 != 0, decoding to identical bytes and producing a non-canonical string -- not portable.) The signature stays structurally well-formed, so it passes the length/range/low-S gates and fails ONLY at the curve check: a verifier MUST reject it with its signature-invalid sentinel (Go: bare ErrSignature, NOT ErrSignatureHighS/ErrSignatureLength). This is the most basic signature negative -- a valid signature over the wrong message -- and is intentionally specified here as a derivation rather than stored bytes.",
|
|
21
|
+
"derive_from": "accept_vector",
|
|
22
|
+
"claims_transform": "flip_first_base64url_char_A_B"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"classes": {
|
|
26
|
+
"claims_parse": {
|
|
27
|
+
"entry_point": "strict claims parser (raw JSON -> Claims)",
|
|
28
|
+
"input": "claims_json",
|
|
29
|
+
"vectors": [
|
|
30
|
+
{
|
|
31
|
+
"name": "accept_valid_full",
|
|
32
|
+
"expect": "accept",
|
|
33
|
+
"reason": "schema-valid claims object with every required field, valid key lengths, and coherent iat<=exp / nbf<=exp bounds",
|
|
34
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "accept_cell_id_absent",
|
|
38
|
+
"expect": "accept",
|
|
39
|
+
"reason": "cell_id is the one optional claim; absent entirely must still parse",
|
|
40
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "reject_duplicate_key",
|
|
44
|
+
"expect": "reject",
|
|
45
|
+
"reject_class": "parse",
|
|
46
|
+
"reason": "duplicate v key; a strict parser must reject (lenient last-wins is a signature-bypass hazard). The duplicate survives storage because it is inside a JSON string value.",
|
|
47
|
+
"claims_json": "{\"v\":2,\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "reject_unknown_field",
|
|
51
|
+
"expect": "reject",
|
|
52
|
+
"reject_class": "parse",
|
|
53
|
+
"reason": "extra is not in the claim allowlist",
|
|
54
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"extra\":\"x\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "reject_null_required",
|
|
58
|
+
"expect": "reject",
|
|
59
|
+
"reject_class": "parse",
|
|
60
|
+
"reason": "jti is null; a null in a scalar field must be rejected, not coerced to the zero value",
|
|
61
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":null,\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "reject_missing_required",
|
|
65
|
+
"expect": "reject",
|
|
66
|
+
"reject_class": "parse",
|
|
67
|
+
"reason": "jti member dropped entirely",
|
|
68
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "reject_float_time",
|
|
72
|
+
"expect": "reject",
|
|
73
|
+
"reject_class": "parse",
|
|
74
|
+
"reason": "exp is fractional; integer time fields must reject a float",
|
|
75
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300.0,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "reject_string_time",
|
|
79
|
+
"expect": "reject",
|
|
80
|
+
"reject_class": "parse",
|
|
81
|
+
"reason": "exp is a string; a string-where-int must be rejected",
|
|
82
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":\"1781910300\",\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "reject_array_for_scalar",
|
|
86
|
+
"expect": "reject",
|
|
87
|
+
"reject_class": "parse",
|
|
88
|
+
"reason": "jti is an array; an array-where-scalar must be rejected",
|
|
89
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":[\"x\"],\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "reject_wrong_version",
|
|
93
|
+
"expect": "reject",
|
|
94
|
+
"reject_class": "parse",
|
|
95
|
+
"reason": "v is 1; the version is pinned to 2 and not negotiated from the payload",
|
|
96
|
+
"claims_json": "{\"v\":1,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "reject_nbf_after_exp",
|
|
100
|
+
"expect": "reject",
|
|
101
|
+
"reject_class": "parse",
|
|
102
|
+
"reason": "nbf > exp; the clock-free ordering bound nbf<=exp rejects a structurally incoherent window",
|
|
103
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781920000,\"exp\":1781910300,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "reject_short_cell_key",
|
|
107
|
+
"expect": "reject",
|
|
108
|
+
"reject_class": "parse",
|
|
109
|
+
"reason": "cell_public_key_b64 decodes to 3 bytes, not the required 32; each key field is length-checked against its own size",
|
|
110
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"AAAA\",\"cell_id\":\"vector-cell\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "reject_top_level_array",
|
|
114
|
+
"expect": "reject",
|
|
115
|
+
"reject_class": "parse",
|
|
116
|
+
"reason": "top-level value is an array, not a single object",
|
|
117
|
+
"claims_json": "[{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}]"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "reject_trailing_data",
|
|
121
|
+
"expect": "reject",
|
|
122
|
+
"reject_class": "parse",
|
|
123
|
+
"reason": "a second concatenated JSON value follows the object; a lenient parser would ignore it",
|
|
124
|
+
"claims_json": "{\"v\":2,\"iss\":\"qurl-service\",\"kid\":\"qurl-issuer-vector-key\",\"iat\":1781910000,\"nbf\":1781910000,\"exp\":1781910300,\"jti\":\"qurl_01JVECTORFIXTURE0000\",\"cell_public_key_b64\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\"relay_url\":\"https://relay.example.com\",\"resource_public_key_b64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcOtuxu2qhc3gt1E7BiEU0CLqEDlXDwzZq0JnESgMAwERX6y_XXF5Cn5SKITWIZQmUhCZ0pHHlVn7SmFUTAnTGQ\",\"qurl_user_public_key_b64\":\"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU\"}{}"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"secret_parse": {
|
|
129
|
+
"entry_point": "strict secret parser (raw JSON -> Secret)",
|
|
130
|
+
"input": "secret_json",
|
|
131
|
+
"vectors": [
|
|
132
|
+
{
|
|
133
|
+
"name": "accept_valid",
|
|
134
|
+
"expect": "accept",
|
|
135
|
+
"reason": "exactly one required field carrying a 32-byte X25519 private key in base64url",
|
|
136
|
+
"secret_json": "{\"qurl_user_private_key_b64\":\"CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk\"}"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "reject_unknown_field",
|
|
140
|
+
"expect": "reject",
|
|
141
|
+
"reject_class": "parse",
|
|
142
|
+
"reason": "extra field x not in the secret allowlist",
|
|
143
|
+
"secret_json": "{\"qurl_user_private_key_b64\":\"CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk\",\"x\":1}"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "reject_missing_required",
|
|
147
|
+
"expect": "reject",
|
|
148
|
+
"reject_class": "parse",
|
|
149
|
+
"reason": "empty object; the one required field is absent",
|
|
150
|
+
"secret_json": "{}"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "reject_duplicate_key",
|
|
154
|
+
"expect": "reject",
|
|
155
|
+
"reject_class": "parse",
|
|
156
|
+
"reason": "the private-key field appears twice; the DUPLICATE is the sole fault -- both values are the valid 32-byte key, so a length-check alone cannot reject this. A verifier that does not detect duplicate JSON keys (lenient last-wins) MUST still reject it, which is the signature-bypass hazard this isolates for cross-language consumers.",
|
|
157
|
+
"secret_json": "{\"qurl_user_private_key_b64\":\"CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk\",\"qurl_user_private_key_b64\":\"CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk\"}"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "reject_short_private_key",
|
|
161
|
+
"expect": "reject",
|
|
162
|
+
"reject_class": "key_length",
|
|
163
|
+
"reason": "valid base64url but decodes to 3 bytes, not the 32-byte X25519 scalar; the PoP secret is length-checked at parse, not deferred",
|
|
164
|
+
"secret_json": "{\"qurl_user_private_key_b64\":\"AQID\"}"
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
"strict_base64": {
|
|
169
|
+
"entry_point": "strict base64url decoder (string -> bytes)",
|
|
170
|
+
"input": "value_b64",
|
|
171
|
+
"comment": "Encoding-layer rejections a browser's lenient atob would silently accept. Two cases are load-bearing because they decode leniently to the SAME bytes as a canonical string, so the strict decoder must reject them to keep one string per byte slice (a signature-bypass guard and the cross-language agreement point on which STRINGS are well-formed): non-canonical trailing bits, and embedded CR/LF, which Go's base64 decoder skips in every mode (including Strict). A verifier that only strict-decodes, without a re-encode/round-trip canonicality check, will WRONGLY accept the embedded-whitespace vectors.",
|
|
172
|
+
"vectors": [
|
|
173
|
+
{
|
|
174
|
+
"name": "accept_canonical_32_byte_key",
|
|
175
|
+
"expect": "accept",
|
|
176
|
+
"reason": "canonical unpadded base64url of 32 zero bytes (43 chars)",
|
|
177
|
+
"value_b64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"name": "reject_non_canonical_trailing_bits",
|
|
181
|
+
"expect": "reject",
|
|
182
|
+
"reject_class": "encoding",
|
|
183
|
+
"reason": "final char carries non-zero don't-care trailing bits; lenient decode yields the same 32 zero bytes but strict must reject",
|
|
184
|
+
"value_b64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "reject_padded",
|
|
188
|
+
"expect": "reject",
|
|
189
|
+
"reject_class": "encoding",
|
|
190
|
+
"reason": "trailing '=' padding is not in the unpadded base64url alphabet",
|
|
191
|
+
"value_b64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"name": "reject_non_alphabet_char",
|
|
195
|
+
"expect": "reject",
|
|
196
|
+
"reject_class": "encoding",
|
|
197
|
+
"reason": "'+' is a standard-base64 char, not base64url; a '.' (the fragment separator) fails the same way",
|
|
198
|
+
"value_b64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "reject_length_1_mod_4",
|
|
202
|
+
"expect": "reject",
|
|
203
|
+
"reject_class": "encoding",
|
|
204
|
+
"reason": "a single base64url char cannot encode a byte (length 1 mod 4)",
|
|
205
|
+
"value_b64": "A"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"name": "reject_embedded_lf",
|
|
209
|
+
"expect": "reject",
|
|
210
|
+
"reject_class": "encoding",
|
|
211
|
+
"reason": "an embedded LF inside an otherwise-canonical 43-char key; a browser's lenient atob silently skips '\\n' and recovers the same 32 bytes, so the strict decoder must reject it to keep one string per byte slice",
|
|
212
|
+
"value_b64": "AAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAA"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"name": "reject_embedded_cr",
|
|
216
|
+
"expect": "reject",
|
|
217
|
+
"reject_class": "encoding",
|
|
218
|
+
"reason": "an embedded CR inside an otherwise-canonical 43-char key; lenient atob silently skips '\\r' and recovers the same 32 bytes, so strict must reject it",
|
|
219
|
+
"value_b64": "AAAAAAAAAAAAAAAAAAAAA\rAAAAAAAAAAAAAAAAAAAAAA"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"name": "reject_embedded_crlf",
|
|
223
|
+
"expect": "reject",
|
|
224
|
+
"reject_class": "encoding",
|
|
225
|
+
"reason": "an embedded CRLF pair inside an otherwise-canonical 43-char key; lenient atob silently skips '\\r\\n' and recovers the same 32 bytes, so strict must reject it",
|
|
226
|
+
"value_b64": "AAAAAAAAAAAAAAAAAAAAA\r\nAAAAAAAAAAAAAAAAAAAAAA"
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
"fragment": {
|
|
231
|
+
"entry_point": "ParseFragment(body)",
|
|
232
|
+
"input": "fragment",
|
|
233
|
+
"comment": "ParseFragment pins wire SHAPE and strict-parses the claims/secret parts; it does NOT verify the issuer signature, so the sig part need only be a base64url 64-byte blob. The accept fragment composes the signature fixture's accept claims part with a valid secret part and the fixture's accept sig.",
|
|
234
|
+
"vectors": [
|
|
235
|
+
{
|
|
236
|
+
"name": "accept_valid_fragment",
|
|
237
|
+
"expect": "accept",
|
|
238
|
+
"reason": "qv2 prefix, four dot-separated parts, strict-valid claims and secret, base64url 64-byte sig",
|
|
239
|
+
"fragment": "qv2.eyJ2IjoyLCJpc3MiOiJxdXJsLXNlcnZpY2UiLCJraWQiOiJxdXJsLWlzc3Vlci12ZWN0b3Ita2V5IiwiaWF0IjoxNzgxOTEwMDAwLCJuYmYiOjE3ODE5MTAwMDAsImV4cCI6MTc4MTkxMDMwMCwianRpIjoicXVybF8wMUpWRUNUT1JGSVhUVVJFMDAwMCIsImNlbGxfcHVibGljX2tleV9iNjQiOiJSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVRIiwiY2VsbF9pZCI6InZlY3Rvci1jZWxsIiwicmVsYXlfdXJsIjoiaHR0cHM6Ly9yZWxheS5leGFtcGxlLmNvbSIsInJlc291cmNlX3B1YmxpY19rZXlfYjY0IjoiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFUDRpR2pVLVpoUzVyRmNNbjk0Y3BTdlB1MXJGTU1vWlFMMTB4Q2ZTRDZpNUhTcWwzclFaN0NYUlpDdUtwOTFMSFAwNEROUGcwdkluLU1xbm4tRnM5cHciLCJxdXJsX3VzZXJfcHVibGljX2tleV9iNjQiOiJWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZVIn0.eyJxdXJsX3VzZXJfcHJpdmF0ZV9rZXlfYjY0IjoiQ1FrSkNRa0pDUWtKQ1FrSkNRa0pDUWtKQ1FrSkNRa0pDUWtKQ1FrSkNRayJ9.a6UIAEY71V27RoEFV8xLiKSXpfCjwYBys9xSpDt1fwQ8m1u-UPS4DqVapOcpPevgLs60yT6MrIeMqP0lz01Ovw"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"name": "reject_wrong_prefix",
|
|
243
|
+
"expect": "reject",
|
|
244
|
+
"reject_class": "fragment",
|
|
245
|
+
"reason": "prefix is qv1, not the literal qv2",
|
|
246
|
+
"fragment": "qv1.eyJ2IjoyLCJpc3MiOiJxdXJsLXNlcnZpY2UiLCJraWQiOiJxdXJsLWlzc3Vlci12ZWN0b3Ita2V5IiwiaWF0IjoxNzgxOTEwMDAwLCJuYmYiOjE3ODE5MTAwMDAsImV4cCI6MTc4MTkxMDMwMCwianRpIjoicXVybF8wMUpWRUNUT1JGSVhUVVJFMDAwMCIsImNlbGxfcHVibGljX2tleV9iNjQiOiJSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVRIiwiY2VsbF9pZCI6InZlY3Rvci1jZWxsIiwicmVsYXlfdXJsIjoiaHR0cHM6Ly9yZWxheS5leGFtcGxlLmNvbSIsInJlc291cmNlX3B1YmxpY19rZXlfYjY0IjoiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFY090dXh1MnFoYzNndDFFN0JpRVUwQ0xxRURsWER3elpxMEpuRVNnTUF3RVJYNnlfWFhGNUNuNVNLSVRXSVpRbVVoQ1owcEhIbFZuN1NtRlVUQW5UR1EiLCJxdXJsX3VzZXJfcHVibGljX2tleV9iNjQiOiJWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZVIn0.eyJxdXJsX3VzZXJfcHJpdmF0ZV9rZXlfYjY0IjoiQ1FrSkNRa0pDUWtKQ1FrSkNRa0pDUWtKQ1FrSkNRa0pDUWtKQ1FrSkNRayJ9.zvNKPXCqjQEbjUtqfqqTL2IN7GeLHVUTODPlwTvyAew53yasEYsqmoUMEDFXzR1UAe2JQ3kETR5MG0HGd1JpJQ"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"name": "reject_too_few_parts",
|
|
250
|
+
"expect": "reject",
|
|
251
|
+
"reject_class": "fragment",
|
|
252
|
+
"reason": "only three dot-separated tokens (prefix + claims + secret); the sig part is missing",
|
|
253
|
+
"fragment": "qv2.eyJ2IjoyLCJpc3MiOiJxdXJsLXNlcnZpY2UiLCJraWQiOiJxdXJsLWlzc3Vlci12ZWN0b3Ita2V5IiwiaWF0IjoxNzgxOTEwMDAwLCJuYmYiOjE3ODE5MTAwMDAsImV4cCI6MTc4MTkxMDMwMCwianRpIjoicXVybF8wMUpWRUNUT1JGSVhUVVJFMDAwMCIsImNlbGxfcHVibGljX2tleV9iNjQiOiJSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVRIiwiY2VsbF9pZCI6InZlY3Rvci1jZWxsIiwicmVsYXlfdXJsIjoiaHR0cHM6Ly9yZWxheS5leGFtcGxlLmNvbSIsInJlc291cmNlX3B1YmxpY19rZXlfYjY0IjoiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFY090dXh1MnFoYzNndDFFN0JpRVUwQ0xxRURsWER3elpxMEpuRVNnTUF3RVJYNnlfWFhGNUNuNVNLSVRXSVpRbVVoQ1owcEhIbFZuN1NtRlVUQW5UR1EiLCJxdXJsX3VzZXJfcHVibGljX2tleV9iNjQiOiJWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZVIn0.eyJxdXJsX3VzZXJfcHJpdmF0ZV9rZXlfYjY0IjoiQ1FrSkNRa0pDUWtKQ1FrSkNRa0pDUWtKQ1FrSkNRa0pDUWtKQ1FrSkNRayJ9"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"name": "reject_empty_sig_part",
|
|
257
|
+
"expect": "reject",
|
|
258
|
+
"reject_class": "fragment",
|
|
259
|
+
"reason": "four parts but the sig part is empty",
|
|
260
|
+
"fragment": "qv2.eyJ2IjoyLCJpc3MiOiJxdXJsLXNlcnZpY2UiLCJraWQiOiJxdXJsLWlzc3Vlci12ZWN0b3Ita2V5IiwiaWF0IjoxNzgxOTEwMDAwLCJuYmYiOjE3ODE5MTAwMDAsImV4cCI6MTc4MTkxMDMwMCwianRpIjoicXVybF8wMUpWRUNUT1JGSVhUVVJFMDAwMCIsImNlbGxfcHVibGljX2tleV9iNjQiOiJSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVSRVJFUkVRIiwiY2VsbF9pZCI6InZlY3Rvci1jZWxsIiwicmVsYXlfdXJsIjoiaHR0cHM6Ly9yZWxheS5leGFtcGxlLmNvbSIsInJlc291cmNlX3B1YmxpY19rZXlfYjY0IjoiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFY090dXh1MnFoYzNndDFFN0JpRVUwQ0xxRURsWER3elpxMEpuRVNnTUF3RVJYNnlfWFhGNUNuNVNLSVRXSVpRbVVoQ1owcEhIbFZuN1NtRlVUQW5UR1EiLCJxdXJsX3VzZXJfcHVibGljX2tleV9iNjQiOiJWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZVIn0.eyJxdXJsX3VzZXJfcHJpdmF0ZV9rZXlfYjY0IjoiQ1FrSkNRa0pDUWtKQ1FrSkNRa0pDUWtKQ1FrSkNRa0pDUWtKQ1FrSkNRayJ9."
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
"relay_allowlist": {
|
|
265
|
+
"entry_point": "ValidateRelayURL(url, NewRelayAllowlist(entries))",
|
|
266
|
+
"input": "entries + url",
|
|
267
|
+
"comment": "Pins the relay_url HTTPS + allowlist contract acted on only AFTER signature verification. Covers match PRECEDENCE (exact host:port vs bare-host-any-port), the userinfo embedded-credentials bypass, the http rejection, and fail-closed on an empty/absent allowlist. There is no entry-ORDER semantic in the matcher; these vectors pin precedence/coverage, not ordering of the entries slice.",
|
|
268
|
+
"vectors": [
|
|
269
|
+
{
|
|
270
|
+
"name": "accept_bare_host_default_port",
|
|
271
|
+
"expect": "accept",
|
|
272
|
+
"reason": "a bare-host entry matches a no-explicit-port URL: with no port, host == hostname, so the exact host[:port] lookup already hits (the bare-host-any-port fallback path is exercised separately by accept_bare_host_any_port)",
|
|
273
|
+
"entries": ["relay.example.com", "relay2.example.com:8443"],
|
|
274
|
+
"url": "https://relay.example.com"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"name": "accept_bare_host_any_port",
|
|
278
|
+
"expect": "accept",
|
|
279
|
+
"reason": "a bare-host entry matches any explicit port",
|
|
280
|
+
"entries": ["relay.example.com", "relay2.example.com:8443"],
|
|
281
|
+
"url": "https://relay.example.com:9000"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"name": "accept_exact_host_port",
|
|
285
|
+
"expect": "accept",
|
|
286
|
+
"reason": "an explicit host:port entry matches the same host:port",
|
|
287
|
+
"entries": ["relay.example.com", "relay2.example.com:8443"],
|
|
288
|
+
"url": "https://relay2.example.com:8443"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"name": "reject_exact_host_port_other_port",
|
|
292
|
+
"expect": "reject",
|
|
293
|
+
"reject_class": "relay_url",
|
|
294
|
+
"reason": "an explicit host:port entry does NOT match a different port for that host (precedence: the :8443 entry is literal)",
|
|
295
|
+
"entries": ["relay.example.com", "relay2.example.com:8443"],
|
|
296
|
+
"url": "https://relay2.example.com:9999"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"name": "reject_http_scheme",
|
|
300
|
+
"expect": "reject",
|
|
301
|
+
"reject_class": "relay_url",
|
|
302
|
+
"reason": "scheme must be https",
|
|
303
|
+
"entries": ["relay.example.com"],
|
|
304
|
+
"url": "http://relay.example.com"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"name": "reject_off_allowlist",
|
|
308
|
+
"expect": "reject",
|
|
309
|
+
"reject_class": "relay_url",
|
|
310
|
+
"reason": "host is on no allowlist entry",
|
|
311
|
+
"entries": ["relay.example.com"],
|
|
312
|
+
"url": "https://evil.example.com"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"name": "reject_userinfo_bypass",
|
|
316
|
+
"expect": "reject",
|
|
317
|
+
"reject_class": "relay_url",
|
|
318
|
+
"reason": "classic allowlist bypass: the real host is evil.example.com with the allowlisted host smuggled into userinfo; userinfo-bearing URLs are rejected outright",
|
|
319
|
+
"entries": ["allowed.example.com"],
|
|
320
|
+
"url": "https://allowed.example.com@evil.example.com"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"name": "reject_userinfo_real_host_allowlisted",
|
|
324
|
+
"expect": "reject",
|
|
325
|
+
"reject_class": "relay_url",
|
|
326
|
+
"reason": "decisive case: even when the REAL post-@ host equals the allowlisted host, the userinfo guard (not the allowlist) must reject it",
|
|
327
|
+
"entries": ["allowed.example.com"],
|
|
328
|
+
"url": "https://allowed.example.com@allowed.example.com"
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"name": "reject_empty_allowlist_fail_closed",
|
|
332
|
+
"expect": "reject",
|
|
333
|
+
"reject_class": "relay_url",
|
|
334
|
+
"reason": "an empty allowlist rejects every URL (fail closed)",
|
|
335
|
+
"entries": [],
|
|
336
|
+
"url": "https://relay.example.com"
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
},
|
|
340
|
+
"server_id": {
|
|
341
|
+
"entry_point": "PubKeyFingerprint(decode(cell_public_key_b64))",
|
|
342
|
+
"input": "cell_public_key_b64 -> server_id",
|
|
343
|
+
"comment": "The relay routing id the JS/headless agent derives from the cell public key for POST /relay/{serverId}: base64url(SHA-256(rawCellPubKey)[:8]), 11 chars, no padding. The consumer DECODES cell_public_key_b64 to raw bytes, recomputes the fingerprint with its OWN canonical implementation, and asserts it equals server_id -- so the routing contract is recomputed, never a trusted stored value. The fill-0x42 / fill-0x70 samples reuse the existing fingerprint golden inputs/outputs (crypto_fingerprint_test.go <-> fingerprint.test.ts) so this class cannot fork that contract.",
|
|
344
|
+
"vectors": [
|
|
345
|
+
{
|
|
346
|
+
"name": "cell_fill_0x44_fixture_cell",
|
|
347
|
+
"expect": "accept",
|
|
348
|
+
"reason": "the exact cell key carried by the composed signature fixture (32 bytes of 0x44)",
|
|
349
|
+
"cell_public_key_b64": "REREREREREREREREREREREREREREREREREREREREREQ",
|
|
350
|
+
"server_id": "uzkUFcBeOdc"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"name": "cell_fill_0x42_golden",
|
|
354
|
+
"expect": "accept",
|
|
355
|
+
"reason": "reuses the fill-0x42 fingerprint golden vector input/output",
|
|
356
|
+
"cell_public_key_b64": "QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI",
|
|
357
|
+
"server_id": "Ql7U5KNrMOo"
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"name": "cell_fill_0x70_substitution",
|
|
361
|
+
"expect": "accept",
|
|
362
|
+
"reason": "reuses the fill-0x70 golden vector whose 8-byte SHA-256 prefix exercises both base64url + -> - and / -> _ substitutions",
|
|
363
|
+
"cell_public_key_b64": "cHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHA",
|
|
364
|
+
"server_id": "p8u_3-Ocffc"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"name": "cell_seq_1to32_golden",
|
|
368
|
+
"expect": "accept",
|
|
369
|
+
"reason": "reuses the seq-1to32 fingerprint golden vector input/output (cell key = 32 bytes of 0x01..0x20), shared with the relay-knock PubKeyFingerprint golden so the routing-id contract cannot fork",
|
|
370
|
+
"cell_public_key_b64": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA",
|
|
371
|
+
"server_id": "riFsLvUkejc"
|
|
372
|
+
}
|
|
373
|
+
]
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"artifact": "qurl-relay-knock-golden-vectors",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"description": "NHP relay-knock Noise-handshake golden packets (X25519 / AES-256-GCM / BLAKE2s).",
|
|
5
|
+
"source_of_truth": "github.com/layervai/qurl-conformance",
|
|
6
|
+
"notes": [
|
|
7
|
+
"knock: a conformant initiator MUST reproduce knock.packet_hex byte-for-byte from the listed inputs.",
|
|
8
|
+
"ack: the server reply is generated at origin by the NHP server seal with a RANDOM server ephemeral key, so it is NOT reproducible by a client; a consumer can only decrypt it and MUST recover type=ACK, counter, timestamp_nanos, and body_hex. Re-hosted here verbatim as a frozen golden value.",
|
|
9
|
+
"Origin: the NHP cross-language handshake fixtures; re-hosted and pinned here."
|
|
10
|
+
],
|
|
11
|
+
"knock": {
|
|
12
|
+
"server_static_priv_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
|
|
13
|
+
"server_static_pub_hex": "07a37cbc142093c8b755dc1b10e86cb426374ad16aa853ed0bdfc0b2b86d1c7c",
|
|
14
|
+
"device_static_priv_hex": "4142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60",
|
|
15
|
+
"device_static_pub_hex": "64b101b1d0be5a8704bd078f9895001fc03e8e9f9522f188dd128d9846d48466",
|
|
16
|
+
"ephemeral_priv_hex": "8182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0",
|
|
17
|
+
"timestamp_nanos": "1700000000000000000",
|
|
18
|
+
"counter": "1",
|
|
19
|
+
"preamble_hex": "11223344",
|
|
20
|
+
"body_hex": "7b2274657374223a226a732d6167656e74206b6e6f636b227d",
|
|
21
|
+
"packet_hex": "112233441123336d01000000000000000000000000000001883186b800b41d5cf0429695da9b3cc4f328ebcd184a6e482fa578c103f06c770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e033f9d754b03a03eac48c26963c36f336bd3f1cd4ebf20c39cb3179646bf3b8ac43e2e886508ebded4a9d25e693f13d6f8bbd76bde5ac81b3cf11ca8bfc60dac9f5d290dfb7e979e019974fa54fbf0f501cae15125de39e22f6fd6d4be21f53724f2edb234b305275e5958b30dbee3212980ea4ca98b63f436c12da56e6587097ba12762e2f4d61dcb8023603f82f1d6d"
|
|
22
|
+
},
|
|
23
|
+
"ack": {
|
|
24
|
+
"server_static_pub_hex": "07a37cbc142093c8b755dc1b10e86cb426374ad16aa853ed0bdfc0b2b86d1c7c",
|
|
25
|
+
"agent_static_priv_hex": "4142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60",
|
|
26
|
+
"timestamp_nanos": "1781494443173070000",
|
|
27
|
+
"counter_hex": "1122334455667788",
|
|
28
|
+
"body_hex": "7b22657272436f6465223a2230222c22726573486f7374223a7b22725f6a736167656e74223a2231302e302e302e37227d2c226f706e54696d65223a3930302c226167656e7441646472223a223230332e302e3131332e39222c226163546f6b656e73223a7b22725f6a736167656e74223a22746f6b2d616263313233227d7d",
|
|
29
|
+
"packet_hex": "455e3ec8455c3e4b01000002000000001122334455667788345bdbe28c304e7dae8ca4e672fbca9b48d9ec7d673566ce09c9b7ef662707670000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a332e8cfa2e4eff64b7636eed1d047c275f7b0aced2debd8138daecf49e29bd92e89bc40a2cd98f2bf9f29d0c451b186b825b5d2d55a6668c06eed5f504087255b267fac40a7eac3f612fd2d62f6740740c53495dfbd7fc12d42acea3eab519b3015e532128a9bd3d75a3e46313f2b4ce17d50551f9eb1fbbbb331f2ccb5aa8ad1d04a191feabdebc03b0984b9098823085259197e4ba00cffc238599766960438fd6225d945f8af6edece5d9feffb9bda0ec3ec5d3f65b0e0eb9d35c36c500809edbb2854a4cf25ea206f96f33383ae71919ac3ad64e7178936e09906169d2197e42e52ad8e1676ccb5c5"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qurl-conformance
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: qURL v2 cross-language conformance vectors
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
qURL v2 cross-language conformance vectors (Python accessor).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
qurl_conformance/__init__.py
|
|
4
|
+
qurl_conformance.egg-info/PKG-INFO
|
|
5
|
+
qurl_conformance.egg-info/SOURCES.txt
|
|
6
|
+
qurl_conformance.egg-info/dependency_links.txt
|
|
7
|
+
qurl_conformance.egg-info/top_level.txt
|
|
8
|
+
qurl_conformance/_data/issuer_signature_vectors.json
|
|
9
|
+
qurl_conformance/_data/qv2_conformance_vectors.json
|
|
10
|
+
qurl_conformance/_data/relay_knock_golden.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qurl_conformance
|