Skip to content

v0.3.0-alpha.1 (P1.2): DNS TXT cross-verification at _agentpin.{domain} (Rust) - #3

Merged
jaschadub merged 1 commit into
devfrom
feature/v0.3.0-dns-txt
May 1, 2026
Merged

v0.3.0-alpha.1 (P1.2): DNS TXT cross-verification at _agentpin.{domain} (Rust)#3
jaschadub merged 1 commit into
devfrom
feature/v0.3.0-dns-txt

Conversation

@jaschadub

Copy link
Copy Markdown
Contributor

Summary

Adds an OPTIONAL second-channel verification mechanism mirroring SchemaPin v1.4-alpha.1's `_schemapin.{domain}` record exactly. The wire format is the same parser shape with the version tag changed; AgentPin spec § 4.8.3 had already reserved this slot in v0.1, this PR ships the implementation.

Independent of PR #2 (v0.3.0 A2A AgentCard types) — both ship in alpha.1; either can land first.

Wire format

```
_agentpin.example.com. 3600 IN TXT "v=agentpin1; kid=acme-2026-04; fp=sha256:a1b2c3..."
```

FieldRequiredNotes
`v`yesMust be `agentpin1` (cleanly rejects `v=schemapin1` records).
`fp`yes`sha256:` JWK thumbprint.
`kid`noOptional key id; when present, the matching JWK MUST also carry the same `kid`.

Whitespace around `;` and `=` tolerated. Unknown fields ignored for forward compatibility.

API additions

ItemAvailability
`dns::DnsTxtRecord`always
`dns::parse_txt_record(value)`always
`dns::verify_dns_match(discovery, txt)`always
`dns::txt_record_name(domain)`always
`dns::fetch_dns_txt(domain)`behind `dns` Cargo feature (hickory-resolver)

Multi-key match semantics: AgentPin discovery docs may carry several keys for rotation. A published TXT record need only match one of them. When the TXT carries an explicit `kid`, the matching key MUST also carry the same `kid` (defends against the vanishingly-unlikely case of two keys sharing a SHA-256 fingerprint).

Cargo features

FeatureDefaultBrings in
`fetch` (existing)off`reqwest`, `tokio`, `async-trait`
`dns` (NEW)off`hickory-resolver`, `tokio`, `async-trait`

Verifier semantics

StateEffect
Absent recordNo effect — DNS TXT is purely additive
Present + matchVerification succeeds; absence of mismatch is the trust signal
Present + mismatchHard failure (`Error::Discovery`)
Present + malformedHard failure (`Error::Discovery`)

The mismatch case is fail-closed because a publisher who intentionally publishes a TXT record has signaled DNS is part of their trust chain — divergence between DNS and `.well-known` indicates compromise of one channel and there's no way for the verifier to tell which is authentic.

QA

CheckResult
`cargo test -j2 --all-features`141 lib tests pass (was 130; +11 new DNS tests covering all parse paths, multi-key match, kid-disambiguation, mismatch fail, txt_record_name behaviour, SchemaPin-record rejection sanity)
`cargo build -j2 --all-features`clean
`cargo clippy --all-features -j2 -- -D warnings`clean
`cargo fmt --check`clean
`python3 -m json.tool context7.json`valid

Backward compatibility

Purely additive — v0.2.0 verifiers ignore TXT records entirely; v0.3.0 publishers can adopt at their own pace.

Reader ↓ / Publisher →No TXTTXT matchingTXT mismatching
v0.2.0 verifier (no DNS check)worksworks (TXT ignored)works (TXT ignored)
v0.3.0 verifier without dns_txtworksworksworks (check skipped)
v0.3.0 verifier with dns_txtn/aworksDOMAIN_MISMATCH

Threat model

Defends against:

  • HTTPS-origin compromise (compromised hosting account, expired domain not removed from CDN, ACME ownership-validation bypass).
  • TLS cert mis-issuance (rogue or coerced CA issues a cert for the publisher's domain to an attacker).
  • CDN cache-poisoning of the static `.well-known` asset.

Does NOT defend against joint compromise of HTTPS + DNS or targeted DNS hijack at the verifier (use DNSSEC, DoH/DoT, or pinned recursive resolvers in high-stakes deployments).

Versions

All three SDK manifests bumped to `0.3.0-alpha.1` (CI enforces consistency). JS and Python ports of the `dns` module follow in subsequent alphas; the version tag aligns the alpha cycle.

Spec / docs

  • `CHANGELOG.md` — new `0.3.0-alpha.1` entry covering the DNS TXT surface (this PR's content).
  • `SKILL.md` — frontmatter version + stable_version, description expanded.
  • `context7.json` — description expanded with the DNS TXT surface.

Test plan

  • All cargo features build and test green
  • Clippy + fmt clean
  • context7.json valid
  • Cross-language interop test — deferred to JS/Python ports
  • Real DNS integration test against a controlled fixture — deferred (mirrors the SchemaPin v1.4-alpha.1 deferral for the same reason)

Adds an OPTIONAL second-channel verification mechanism mirroring SchemaPin
v1.4-alpha.1's _schemapin.{domain} record exactly. The wire format is the
same parser shape with the version tag changed; AgentPin spec § 4.8.3 had
already reserved this slot in v0.1, this PR ships the implementation.
Wire format:
_agentpin.example.com. 3600 IN TXT "v=agentpin1; kid=...; fp=sha256:..."
New `dns` module (always available; no DNS dependencies):
- DnsTxtRecord struct (version, kid, fingerprint).
- parse_txt_record(value) — whitespace-tolerant, case-insensitive on `fp`,
ignores unknown fields for forward compatibility, requires `v=agentpin1`
and `fp=sha256:<hex>`. Cleanly rejects SchemaPin's v=schemapin1 records.
- verify_dns_match(discovery, txt) — returns Ok(()) when the TXT `fp`
matches the JWK thumbprint of *any* key in discovery.public_keys.
AgentPin discovery docs may carry several keys for rotation; a published
TXT record need only match one. When the TXT carries an explicit `kid`,
the matching key MUST also carry the same `kid` (defends against the
vanishingly-unlikely case of two keys sharing a SHA-256 fingerprint).
- txt_record_name(domain) — `_agentpin.{domain}` with trailing-dot trim.
- fetch_dns_txt(domain) — async lookup behind the new `dns` Cargo feature
(uses hickory-resolver). Returns Ok(None) when no _agentpin record
exists; mismatching/malformed records return Err.
Verifier semantics:
- Absent record → no effect (purely additive)
- Present + match → verification succeeds (absence of mismatch = signal)
- Present + miss → hard fail (Error::Discovery) — fail-closed because a
publisher who *intentionally* published a TXT record signaled DNS is
part of their trust chain. Divergence between DNS and .well-known
indicates compromise of one channel; better to refuse than to guess.
Cargo features:
- `fetch` (existing) → reqwest + tokio + async-trait
- `dns` (NEW) → hickory-resolver + tokio + async-trait
QA:
- 141 lib tests pass (was 130; +11 new DNS tests covering parse paths,
multi-key match, kid-disambiguation, mismatch fail, txt_record_name).
- cargo build / test --all-features green.
- cargo clippy --all-features -- -D warnings clean.
- cargo fmt --check clean.
Versions bumped to 0.3.0-alpha.1 across Rust, JS, Python (CI requires
all three SDK manifests to match). JS/Python ports of the dns module
follow in subsequent alphas; the version tag aligns the alpha cycle.
Spec / docs:
- CHANGELOG: new 0.3.0-alpha.1 entry.
- SKILL.md: frontmatter version + stable_version, description expanded.
- context7.json: description expanded with DNS TXT surface.
Backward compatibility:
- v0.2.0 verifiers ignore TXT records entirely.
- v0.3.0 publishers can adopt at their own pace without breaking older
verifiers.
- All existing 130 tests still pass unchanged.
Threat model — defends against:
- HTTPS-origin compromise (compromised hosting account, expired domain
not removed from CDN, ACME ownership-validation bypass).
- TLS cert mis-issuance (rogue or coerced CA issues a cert for the
publisher's domain to an attacker).
- CDN cache-poisoning of the static .well-known asset.
Does NOT defend against joint compromise of HTTPS + DNS or targeted
DNS hijack at the verifier (use DNSSEC, DoH/DoT, or pinned recursive
resolvers in high-stakes deployments).
@jaschadub
jaschadubforce-pushed the feature/v0.3.0-dns-txt branch from 72fec52 to 39bd8c3CompareMay 1, 2026 20:31
@jaschadub
jaschadub merged commit 3c5bd97 into devMay 1, 2026
3 of 7 checks passed
@jaschadub
jaschadub deleted the feature/v0.3.0-dns-txt branch May 16, 2026 04:37
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jaschadub