dnsid-py
Python SDK for the DNSid Protocol — agent identity management and verification.
DNSid lets agents publish a cryptographically signed identity record in DNS and verify the identities of other agents before exchanging data.
Installation
pip install dnsid
For AWS KMS-backed signing keys, install the aws extra:
pip install "dnsid[aws]"
If the release is not yet available on PyPI, install directly from GitHub.
Replace main with a release tag
to pin a version; the [aws] extra works the same way:
pip install "dnsid @ git+https://github.com/dnsid-ai/dnsid-py@main"
Requires Python 3.11+. CI-tested against Python 3.11, 3.12, and 3.13. Dependencies: idna, httpx, cryptography, dnspython, http-sf. The package ships a py.typed marker (PEP 561), so mypy and other type checkers see its full strict-mode annotations.
See COMPATIBILITY.md for the full runtime and dependency compatibility matrix, including tested Python versions, dependency version ranges, optional extras, and platform support.
First verification
For this local signing identity, set DNSID_LOG_REF to its persisted,
registry-provided c2sp-tlog:public: reference on a supported DNSid-managed
log. A verification-only manager needs no local identity or reference (see the
Quickstart).
The .example domains below are placeholders; networked flows require published
DNSid identities. Local CLI examples use the .test zone.
import os
from pathlib import Path
from dnsid import DnsidConfig, IdentityConfig, IdentityManager, IdentityManagerDependencies, LocalKeyProvider
from dnsid.c2sp_tlog import create_dnsid_managed_verification_registry
manager = IdentityManager(
DnsidConfig(identity=IdentityConfig(
domain="billing-agent.example", governance_id="billing-agent.example",
log_ref=os.environ["DNSID_LOG_REF"], # persisted c2sp-tlog identity-instance reference
status_url="https://billing-agent.example/status",
)),
LocalKeyProvider.load(Path("~/.dnsid/keys.json").expanduser(), create_if_missing=True),
# Explicitly trust the SDK's pinned DNSid-managed public log roots.
deps=IdentityManagerDependencies(log_registry=create_dnsid_managed_verification_registry()),
)
# For a counterparty using a supported DNSid-managed public log:
vd = manager.verify_domain("payments-agent.example")
print(vd.domain, vd.cached_state())
Draft-01 verification fails closed if the domain's lr uses a different log
method or trust root; configure a matching reader (see the
transparency-log reference). The
Quickstart shows verification setup. The default
auto DNSSEC policy works with the built-in resolver; validated and
required need a DNSSEC-aware resolver injected through
IdentityManagerDependencies.dns_resolver.
What's in the SDK
- Core protocol —
IdentityManagerdrives every protocol operation: domain verification, TXT-record creation and publication, key rotation, and lifecycle-event signing. - Application profiles (not part of the core protocol): JWT/JWS (JOSE), RFC 9421 HTTP Message Signatures, Web Bot Auth, and OIDC.
- Key management — private keys stay behind the
KeyProviderinterface: file-backed withLocalKeyProvider, or externalized entirely withAwsKmsKeyProvideror your own KMS/HSM-backed implementation. - Transparency log — a ready-made C2SP tile-log
LogReader(dnsid.c2sp_tlog). - Registry client — operator-side registration and publication workflows.
Import surface. Everything is importable from the package root (from dnsid import IdentityManager, JoseProfile), mirroring the TypeScript SDK's umbrella package. The profile classes are also importable from their home submodules (dnsid.jose, dnsid.http_signatures) — both paths refer to the same classes. The transparency-log integration is the exception: import it from dnsid.c2sp_tlog.
Synchronous by design. Verification and most public operations are synchronous. Async support includes RegistryClient.async_wait_for_status() (which polls synchronously), signed httpx.AsyncClient transport plumbing, and async_retry_transient. Call blocking SDK operations from async code via asyncio.to_thread or an executor.
Documentation
- docs.dnsid.ai — hosted guides, concepts, and the product CLI documentation
- Quickstart Guide — installation, prerequisites, and core workflows (resolve, sign, verify)
- API Reference — generated from docstrings (also on docs.dnsid.ai); regenerate with
python scripts/gen_docs.py - Security — threat model, key-rotation/JWT-overlap guidance, and production operations notes
Quick start
Authenticating with JWT
from dnsid import JWTOptions
from dnsid.jose import JoseProfile
jose = JoseProfile.from_identity_manager(manager)
# Create a signed JWT to send to a counterparty
token = jose.create_jwt(JWTOptions(audience="payments-agent.example"))
# Verify a JWT received from a counterparty
vd = jose.verify_jwt(token)
Minting DNSid OIDC tokens from server-side code
Use OIDCProfile when a server-side agent needs a DNSid OIDC access token for
an outbound call, for example an AgentCore-hosted Org A agent calling an Org B
AgentCore Gateway/tool. Keep the signing key on the server; do not mint DNSid
OIDC tokens in browser, mobile, or other client-side code.
import httpx
import os
from pathlib import Path
from dnsid import (
DnsidConfig,
IdentityConfig,
IdentityManager,
LocalKeyProvider,
OIDCConfig,
OIDCProfile,
OIDCTokenExchangeOptions,
)
key_provider = LocalKeyProvider.load(Path("/var/lib/dnsid/agent-signing-key.jwk"))
manager = IdentityManager(
DnsidConfig(
identity=IdentityConfig(
domain="agent-a.example",
governance_id="agent-a.example",
log_ref=os.environ["DNSID_LOG_REF"], # this agent's persisted c2sp-tlog reference
status_url="https://agent-a.example/status",
),
),
key_provider,
)
with httpx.Client(timeout=5.0) as http:
oidc = OIDCProfile.from_identity_manager(
manager,
OIDCConfig(
default_issuer="https://oidc.dnsid.ai",
http_client=http,
timeout=5.0,
),
)
token_response = oidc.mint_oidc_token(
OIDCTokenExchangeOptions(
audience="https://gateway.example",
scope=["openid", "dnsid"],
)
)
headers = {"Authorization": f"Bearer {token_response.access_token}"}
The issuer is configurable. For local/proxy-style deployments that mirror the
CLI behavior, pass server_url="http://127.0.0.1:3001"; the SDK discovers the
issuer there but posts the assertion to server_url + "/token". For explicit
issuer mode, pass issuer, discovery_url, or token_endpoint as needed.
Minting a token only proves the agent could sign a DNSid assertion at exchange time. Relying parties must still verify the returned OIDC token and perform the DNSid subject status/revocation checks required by their trust policy.
Signing and verifying HTTP requests (RFC 9421)
from dnsid import HttpRequest, HttpSigningOptions
from dnsid.http_signatures import HttpSignatureProfile
http_sig = HttpSignatureProfile.from_identity_manager(manager)
# Sign an outbound request
req = HttpRequest(method="POST", url="https://payments-agent.example/charge", body=b'{"amount":100}')
signed = http_sig.create_signed_http_request(req)
# Verify an inbound request
vd = http_sig.verify_signed_http_request(signed)
Web Bot Auth (RFC 9421)
Sign and verify DNSid WBA requests, including signed-directory discovery:
from dnsid import (
HttpRequest, WBAHttpSigner, WBAHttpVerifier, WebBotAuthVerificationOptions,
)
request = HttpRequest(method="GET", url="https://target.example/api")
WBAHttpSigner(key_provider, "agent.example").sign(request)
verifier = WBAHttpVerifier(manager)
try:
identity = verifier.verify_web_bot_auth_signed_request(
request,
WebBotAuthVerificationOptions(signer_domain="agent.example"),
)
finally:
verifier.close()
The verifier requires a RefreshingIdentityResolver (implemented by
IdentityManager), not local signing keys. It verifies
both signatures, binds the directory key to the current DNSid ku signing key, and
applies the resolver's entity acceptance on each invocation. Supply peer_cert
in the options for the current application peer when fl=mtls applies; the
directory endpoint's certificate is not a substitute. Reuse the verifier across
requests to reuse its secure connection pool, then call close() at shutdown
once in-flight verifications finish. Injected transports remain caller-owned.
Discovery uses public-address-only HTTPS and rejects all redirects, including
same-host redirects. The complete operation has a default 30-second deadline;
timeout can lower it. Directories are fetched with Cache-Control: no-cache
on every invocation; injected caches must revalidate and cannot serve stale evidence.
For one-time semantics,
supply an atomic replay_policy
callback (see the API reference).
Without it, time validity limits reuse but does not detect it. Applications must
decide whether the covered components are sufficient: WBA requires @authority,
not @method or @target-uri. For operation binding, sign additional components
with WebBotAuthSigningOptions.additional_components and require them with
WebBotAuthVerificationOptions.required_components. The verifier checks exact
component coverage before discovery; its default adds no requirements.
Directories must be uncoded; any Content-Encoding field is rejected before
reading. Injected discovery transports must preserve bytes and enforce HTTPS,
public-address routing, and original HTTP freshness if they reuse evidence.
The verifier checks visible Date/Age metadata and rechecks positive max-age
deadlines before return, including after identity refresh and replay callbacks.
Fresh no-store, no-cache, and max-age=0 responses can serve their acquiring
invocation; HTTP-stale evidence cannot.
This is the narrower DNSid WBA profile targeting published
draft-ietf-webbotauth-httpsig-protocol-00, including its directory format,
not a claim of general WBA or partner interoperability. A covered Signature-Agent
is mandatory, even with trusted caller discovery. Compatibility flags
include_signature_agent=False and signature_agent=False now raise ArgumentError.
For directory, configure an ASCII HTTPS origin without a path (including /),
query, fragment, or credentials; use jwks_uri for a direct endpoint. Directory
signatures cannot have future created timestamps, even within clock skew.
Nested signing is unsupported. Only the current DNSid operational key is accepted;
rotation can invalidate outstanding old-key requests without an overlap period.
After both signatures verify, a current-key mismatch permits one full identity
refresh per domain per five seconds. Concurrent retries are suppressed; failed
attempts consume the allowance. Refresh never bypasses acceptance or revocation.
WBA discovery must not identify the live ku endpoint, including query/fragment
variants. Supply ku_url to WBAHttpSigner when locally known to reject invalid
jwks_uri configuration before signing. Verification checks authenticated ku.
Directory serving backdates signatures by min(5, ttl - 1) seconds and emits signed
max-age=N, must-revalidate, with at most 295 seconds at the default TTL. Disable
all CDN stale-serving overrides, stale-while-revalidate, and stale-if-error.
Use the shared managed rotation/recovery workflow and pause request/directory
signing during rotation; do not publish overlapping live ku keys. Disable
signing and use revocation/reissuance for suspected key compromise. Cache and
publication delays can delay new-key acceptance; short TTLs do not revoke a key.
Unrestricted WBA signing-key access also authorizes KEY_ROTATION, not just
crawler requests. Keeping ek off the signing fleet does not remove that authority.
Dedicated WBA-key delegation requires a separate security design before broad
multi-node deployment.
JWT bot bearer authentication
The legacy WebBotAuthProfile name is retained for compatibility. It implements
JWT bearer authentication, not RFC 9421 Web Bot Auth. Its request and client
helpers reject existing Authorization fields before token creation, including
empty values and client default credentials.
Authenticate outbound bot/agent HTTP requests with DNSid-issued JWTs, and
verify inbound bot identity from the Authorization: Bearer header.
from dnsid import WebBotAuthProfile, BotIdentity, BotAuthConfig
import httpx
# Configure the bot's identity metadata
bot = BotIdentity(
name="AcmeSearchBot",
version="2.1",
purpose="search-indexing",
operator_url="https://acme.example/bot-info",
)
# Create the bot auth profile from an IdentityManager
bot_auth = WebBotAuthProfile.from_identity_manager(manager, bot=bot)
# --- Signing outbound requests ---
# Option 1: Sign a single request
request = httpx.Request("GET", "https://target.example/api/data")
signed_request = bot_auth.sign_bot_request(request)
# signed_request now has Authorization: Bearer <dnsid-jwt> and User-Agent headers
# Option 2: Create a token directly (for use with any HTTP library)
token = bot_auth.create_bot_token("https://target.example/api/data")
headers = {"Authorization": f"Bearer {token}"}
# Option 3: Auto-signing httpx client
client = bot_auth.create_signed_bot_client()
response = client.get("https://target.example/api/data") # auto-signed
# --- Verifying inbound requests ---
# Verify a bot request (e.g. in a web framework middleware)
from dnsid import WebBotAuthProfile, BotAuthConfig
verifier = WebBotAuthProfile.from_identity_manager(
manager,
config=BotAuthConfig(require_bot_claim=True),
)
result = verifier.verify_bot_request(
headers={"Authorization": "Bearer <jwt-from-request>"},
expected_audience="https://my-server.example", # configured server origin, not request Host
)
print(result.domain) # "acmebot.example"
print(result.bot.name) # "AcmeSearchBot"
print(result.bot.purpose) # "search-indexing"
Publishing your agent's identity
Draft 01 publication uses two key providers: the agent (operational) provider
passed to IdentityManager, and an accountable-entity provider supplied via
IdentityManagerDependencies.entity_key_provider that signs the TXT record
and entity lifecycle events. The two current public keys must be distinct.
import os
from dnsid import DnsidConfig, IdentityConfig, IdentityManager, IdentityManagerDependencies, LocalKeyProvider
agent_key = LocalKeyProvider.generate()
entity_key = LocalKeyProvider.generate()
config = DnsidConfig(identity=IdentityConfig(
domain="billing-agent.example",
governance_id="billing-agent.example",
log_ref=os.environ["DNSID_LOG_REF"], # assigned/persisted identity-instance reference
status_url="https://billing-agent.example/status",
ku_url="https://billing-agent.example/.well-known/jwks.json",
ek_url="https://billing-agent.example/.well-known/entity-jwks.json",
))
manager = IdentityManager(config, agent_key, IdentityManagerDependencies(
entity_key_provider=entity_key,
))
# Serve these key sets over HTTPS at the configured ku and ek URLs.
ku_jwks = manager.get_key_set()
ek_jwks = manager.get_entity_key_set()
# Publish only after the registry has accepted the bilateral ISSUANCE.
txt_record = manager.create_txt_record() # then publish at _dnsid.billing-agent.example
The bundled C2SP registry is read-only: it cannot append the ISSUANCE.
Use the registry's C2SP preparation/submission workflow to obtain and validate
its assigned lr, sign and submit the bilateral entry, then publish the TXT
record. The code above builds the record but does not publish it or log the
issuance. Never invent a new lr at each startup.
Architecture
dnsid/
├── manager.py # IdentityManager — all protocol operations
├── models.py # All data types (DnsidConfig, IdentityConfig, VerificationConfig, TransportConfig, RegistryConfig, JWK, JWKS, DnsIdTxtRecord, VerifiedDomain, …)
├── interfaces.py # ABCs to implement: KeyProvider, Log, LogReader, DNSResolver, IdentityCache
├── registry.py # LogRegistry — plug in ledger method implementations
├── registry_client.py # RegistryClient — operator-side registry workflows
├── config_loading.py # load_environment / load_file / load_cli_directory → merge → construct
├── c2sp_tlog/ # C2SP tile-log LogReader binding (lr=c2sp-tlog:...)
├── web_bot_auth.py # Legacy WebBotAuthProfile — JWT bot bearer authentication
├── wba_signer.py # RFC 9421 WBA signing and directory serving
├── wba_verifier.py # DNSid WBA request and directory verification
├── _crypto.py # JWK thumbprint (RFC 7638) and signature verification
├── _https_client.py # HTTPS fetching with redirect enforcement and TLS cert capture
├── _default_resolver.py # Default DNS resolver (DoH or system via dnspython)
└── _utils.py # FQDN normalisation, base64 helpers, algorithm maps
AWS KMS key provider
AwsKmsKeyProvider keeps private key material in AWS KMS and exposes public
keys as JWKs. Install the aws extra, then construct a BotoKmsFacade around
a boto3 KMS client:
import boto3
from dnsid import AwsKmsConfig, AwsKmsKeyProvider, AwsKmsKeyState, BotoKmsFacade
facade = BotoKmsFacade(boto3.client("kms", region_name="us-east-1"))
# Load from a previously persisted state (or supply active_key_id directly)
state = AwsKmsKeyState(
active_key_id="alias/dnsid-current", # ARN, key ID, or alias
retained_key_ids=[],
pending_key_ids=[],
)
provider = AwsKmsKeyProvider.load(
facade,
AwsKmsConfig(
state=state,
algorithm="ECDSA_SHA_256", # or "ED25519_SHA_512"
),
)
load() resolves aliases to canonical KMS key IDs and warms the JWK cache.
Persisting state — AwsKmsKeyProvider owns the active/pending/retained
lifecycle but does not persist state itself. Save the snapshot after any
lifecycle change and restore it on the next process start:
# After generate_key(), activate(), or supersede():
snapshot = provider.state_snapshot()
save_to_database(snapshot) # your persistence layer
# On next start, load saved state back into AwsKmsKeyState and pass to load()
Key rotation:
new_kid = provider.generate_key() # creates a new KMS key in pending state
provider.activate(new_kid) # promotes to active; old active → retained
provider.supersede(old_kid) # removes a retained key (optionally schedules KMS deletion)
save_to_database(provider.state_snapshot())
IAM permissions required at runtime: kms:GetPublicKey, kms:Sign. Add
kms:CreateKey for generate_key() and kms:ScheduleKeyDeletion if
schedule_key_deletion_on_supersede=True.
Implementing KeyProvider
All private-key operations go through the KeyProvider interface — supply one that wraps your KMS or HSM to keep key material out of the process entirely:
from dnsid.interfaces import KeyProvider
from dnsid import JWK
class MyKeyProvider(KeyProvider):
def signing_key(self) -> JWK: ...
def jwk(self, kid: str) -> JWK: ...
def list_key_ids(self) -> list[str]: ...
def sign(self, payload: bytes) -> bytes: ...
def generate_key(self) -> str: ...
def activate(self, kid: str) -> None: ...
def supersede(self, kid: str) -> None: ...
C2SP transparency-log verification
Use an identity-instance reference such as
c2sp-tlog:public:https://log.dnsid.ai#<opaque-stream-id> for C2SP records.
The stream ID is not the domain and must remain stable across restarts. Use
the lr assigned by your registry/CLI; when provisioning a new self-managed
stream, generate_c2sp_tlog_stream_id() creates a fresh 128-bit ID to persist
with that identity. Never regenerate it when loading an existing identity.
The SDK ships a ready-made C2SP tile-log
LogReader, wire-compatible with the other DNSid SDKs. It verifies proofs
but does not write events. See interfaces.py
for the reader interface. The factory returns a registry ready for verify_domain:
from dnsid.c2sp_tlog import (
C2spTlogVerificationOptions,
create_c2sp_tlog_verification_registry,
)
registry = create_c2sp_tlog_verification_registry(
C2spTlogVerificationOptions(
policy_url="https://policy.example/dnsid-policy",
checkpoint_freshness_ms=5 * 60 * 1000,
max_clock_skew_ms=30 * 1000,
)
)
The policy URL is independently trusted caller configuration and is never
derived from an unverified identity record, its lr, or its log prefix. Pass
trusted policy bytes with policy_document instead when appropriate. The
factory uses verified per-domain stream bundles when configured with a trust
profile, falling back to its bounded complete scanner when the bundle endpoint
is unavailable or a valid newer bundle needs raw consistency evidence. The
complete scan must prove both the stored and bundle checkpoint roots.
For an explicit application decision to trust DNSid-managed logs, use the separately named managed factory:
from dnsid.c2sp_tlog import create_dnsid_managed_verification_registry
registry = create_dnsid_managed_verification_registry()
It selects SDK-embedded reviewed trust only for exact canonical public
references to https://log.dev.dnsid.ai, https://log.dnsid.ai, or
https://log.partners.dnsid.ai; unknown scopes and prefixes fail closed. Every
managed log prefers verified signed stream bundles with bounded raw-scan fallback. The generic factory never selects these roots.
Both factories use a restart-ephemeral in-memory checkpoint store
by default; inject a durable CheckpointStore when rollback protection must
survive restarts. If checkpoint_freshness_ms is omitted,
non-revocation checks fail closed. Custom resource fetchers must implement the
bounded-fetch capability contract documented in the API reference. Lower-level
policy parsing, custom source construction, registration, and checkpoint-store
injection remain available
for advanced deployments.
The SDK supplies only in-memory checkpoint storage, matching the Go and TypeScript
SDKs. Reuse the same registry/store to retain observed trust. Applications that
need cross-restart protection implement the CheckpointStore interface and inject
it through C2spTlogVerificationOptions(trusted_checkpoint_store=store, ...) or
DnsidManagedVerificationOptions(trusted_checkpoint_store=store), then inject the
registry into IdentityManagerDependencies(log_registry=registry). Deployment
files and SDK environment variables do not configure storage.
Checkpoint failures expose stable category values through VerificationError.
Its typed __cause__ retains continuity diagnostics or the storage failure.
Cancellation is cooperative through the invocation budget and prevents publication
when observed beforehand. Custom stores must preserve atomicity and cancellation,
fail closed on lost or corrupt state, and acknowledge durable writes only after
commit. A failed write may already have published; reconcile actual stored state
before recovery. Recovery requires independently authenticated authorization,
retained signed evidence, and fresh verification, not an automatic trust reset.
See checkpoint implementation checks.
The reader authenticates checkpoints against the pinned log and witness keys
(with anti-rollback via a checkpoint store), verifies Merkle inclusion, and
for records using dnsid-draft-01 or pre-RFC DNSid1 verifies the logged
lifecycle: bilateral ISSUANCE binding
of the ek/ku keys and operational-key continuity. Draft 01 verification
fails closed without a log reader capable of these checks. Current
non-revocation is operation policy, not part of verify_domain: callers
gating high-value or irreversible operations should call
result.log_reader.verify_non_revocation(domain, at) themselves (liveness is
otherwise covered by the su status check).
See the transparency-log reference
and examples/a2a/startup.py for a complete setup.
Registry client (authenticated)
Local (default). RegistryClient() talks to the local registry from
dnsid local up at http://127.0.0.1:7755 unless told otherwise:
dnsid local up --zone test # local registry, DNS, and CA in Docker
dnsid local run my-agent -- python app.py # registers my-agent if needed, runs with DNSID_* set
from dnsid import AgentRegistrationInput, registry_client_from_environment
# DNSID_REGISTRY_URL and DNSID_API_KEY when set; otherwise the local registry, no credential.
client = registry_client_from_environment()
client.register_agent(AgentRegistrationInput(domain="my-agent.test"))
client.verify_agent("my-agent.test")
To export the same variables into your shell instead of wrapping one command:
eval "$(dnsid local env my-agent)". If nothing is listening, calls fail with
no registry at 127.0.0.1:7755; run dnsid local up or set DNSID_REGISTRY_URL.
Hosted. Set DNSID_REGISTRY_URL and DNSID_API_KEY from the console; the
same code then talks to the hosted registry. Or pass them explicitly:
from dnsid import RegistryClient
client = RegistryClient("https://api.dnsid.ai", api_key="<console-issued key>")
# Legacy status reads need no credential:
RegistryClient("https://api.dnsid.ai").get_agent_status("agent.example")
base_url must be HTTPS, or HTTP on loopback. Constructors never read the
environment; only registry_client_from_environment() does.
Registration accepts an exact domain, root_domain, or governance_domain.
Public-key-only input gets a managed sandbox name. Assigned names require
public_key_jwk; the registry determines hosting even for an exact domain.
Explicit legacy selectors (environment, managed, zone_id, tier) are
preserved without injected defaults. Use register_live_agent() for managed Live.
Supply register_agent(input, idempotency_key="...") for retry-safe creation.
Retry with the same complete input and key. Errors retain registration_request,
registration_idempotency_key, and registration_response for recovery; a failure
does not prove no identity was created. Save the returned immutable id,
publication_config, and OIDC issuer. Publication authority comes from authenticated
agent detail, not the requested selectors; this configuration does not grant trust.
Hosted mutation calls require owner credentials: an organization session or API key,
passed as api_key and sent as an Authorization: Bearer header. The local
loopback registry does not require a credential. An agent
bearer token is not sufficient. This also applies to prepare_key_rotation()
and the other transparency-log preparation operations.
Self-managed agents prove key possession through a challenge handshake: after
verify_agent(), the status document exposes a single-use nonce
(get_agent_status(domain).raw["challenge"]); sign the base64url-decoded nonce
bytes with the active key and submit via
submit_challenge_signature(domain, nonce, signature), then poll with
wait_for_status(domain, target_state="VERIFIED"). See
examples/a2a/startup.py for the complete
register → verify → challenge → publish sequence.
Managed Live uses the separate register_live_agent() operation. Sign the exact
bytes obtained by base64url-decoding its challenge_message, and send proof to
its derived domain. A successful reissue_live_proof() response supersedes all
older challenges: only the latest challenge and message may be signed or submitted.
Live status requires owner/session/API-key authentication because it can expose
its proof challenge; only legacy status is public. Calling a hosted mutation without
api_key raises ArgumentError before any request is sent. The credential never
appears in repr()/str(), exceptions, or logs. See the
registry reference for the full method list and how to
inspect the registry-managed signed record (effective ku/su).
Registration groundwork
Deployment files can select an existing operational key with keySource.provider,
keySource.keyRef, and non-secret keySource.settings. Known cloud factories load
only the selected optional package; missing packages fail without file fallback.
Constructors never generate keys. Local file selection warns against production use.
RegistryClient.get_organization_onboarding() explicitly reads existing account
proof/delegation status; it does not create an identity or promise permanent replay.
Pending proofs are not verified bindings, and this response supplies no entity JWKS URL.
To move an established signer, use explicit rotate_operational_key() with a
target_key_provider and stable old/target provider references. Recovery reuses those
references and exact signed bytes. Activation requires verified current publication
and rotation continuity; application signing stays paused until durable reconciliation.
Changing deployment configuration alone neither rotates nor imports a private key.
Configuration reference
DnsidConfig is the single core configuration entry point:
| Section | Type | Default | Description |
|---|---|---|---|
identity |
IdentityConfig |
None |
Local identity publication settings. Omit for a verification-only manager (no key providers allowed). |
verification |
VerificationConfig |
defaults | Protocol verification policy and counterparty acceptance. Same defaults with or without identity. |
transport |
TransportConfig |
defaults | SDK-managed DNS and HTTPS settings. Never mutates injected dependencies. |
IdentityConfig fields:
| Field | Required | Default | Description |
|---|---|---|---|
domain |
yes | — | Agent FQDN (e.g. billing-agent.example) |
governance_id |
yes | — | Registrant domain (gi tag) |
log_ref |
yes | — | Persisted C2SP identity-instance reference (c2sp-tlog:<scope>:<log-prefix>#<stream-id>); use the reference assigned by your registry |
status_url |
yes | — | HTTPS URL for the agent's status endpoint |
policy_flags |
no | "" |
Comma-separated flags, e.g. mtls,logchk |
max_key_age |
no | "" |
Max signing key age: 24h, 7d, 30d, 90d |
ek_url |
no | "" |
Accountable-entity JWKS URL (ek tag); required for draft-01 publishing |
ku_url |
no | "" |
Agent operational JWKS URL; required for draft-01 publishing |
capabilities_url |
no | "" |
HTTPS URL for AGENTS.md or Agent Card |
publish_profile |
no | "" (= dnsid-draft-01) |
Wire profile emitted when creating records |
VerificationConfig fields:
| Field | Default | Description |
|---|---|---|
status_check_interval |
0 (spec-strict) |
Cache freshness interval for status re-checks; must be non-negative |
dnssec_mode |
auto |
auto, validated, or required |
trusted_entities |
None |
Counterparty allowlist of TrustedEntity(governance_id, entity_key_thumbprints=None). None makes no acceptance decision; [] denies all. Matching is exact on the verified gi (no wildcard/suffix); optional pins are RFC 7638 SHA-256 thumbprints of the current record-signing key. Denials raise a permanent VerificationError(COUNTERPARTY_NOT_ACCEPTED) carrying only the observed verified_governance_id / verified_entity_key_thumbprint. Acceptance runs on every verify_domain call, including cache hits; evidence is cached before acceptance and denials are never cached. |
DNSSEC policy is explicit: auto accepts VALID, UNSIGNED, and UNKNOWN
but always rejects FAILED; validated additionally rejects UNKNOWN; and
required accepts only VALID. Use a custom DNSResolver that can distinguish
all four states when selecting validated or required.
Configuration is validated and snapshotted at construction (invalid values,
duplicate normalized entities, or malformed pins raise ArgumentError before
any network work). Transport settings with no SDK-managed consumer are rejected:
dns_server when both dns_resolver and https_fetcher are injected,
ca_bundle_path when https_fetcher is injected. Constructors never read files
or environment variables; loading is a separate step, below.
TransportConfig fields:
| Field | Default | Description |
|---|---|---|
dns_server |
"" |
Custom DNS server (e.g. "8.8.8.8:53") |
ca_bundle_path |
"" |
Path to custom CA bundle for TLS verification |
private_address_hosts |
frozenset() |
Hostnames allowed to resolve to private addresses; exact, or .suffix for a whole zone |
RegistryConfig fields:
| Field | Default | Description |
|---|---|---|
registry_url |
"" |
DNSid registry base URL; absent lets RegistryClient default to the local registry (http://127.0.0.1:7755), set DNSID_REGISTRY_URL for hosted |
Loading configuration
Loaders parse; constructors default. Each source has one loader returning a
LoadedConfig with only the fields the source actually carries; merge_loaded_config
combines them field-wise (later wins, lists replace; log_trust and operational
key_source fields replace as groups);
construct_identity_manager fills deps.log_registry from log_trust and key providers from
key_source when the caller did not supply them, then calls the ordinary
IdentityManager constructor. The one-call constructors are exactly
construct_identity_manager(merge_loaded_config(load_…(), LoadedConfig(dnsid=overlay)), key_provider, deps).
from dnsid import (
identity_manager_from_environment, # DNSID_* variables
identity_manager_from_dnsid, # ~/.dnsid or a DNSid CLI identity directory
identity_manager_from_file, # JSON deployment file {"dnsid", "logTrust", "registry", "keySource"}
load_environment, load_file, load_cli_directory, merge_loaded_config, construct_identity_manager, LoadedConfig,
)
idm = identity_manager_from_environment() # under `dnsid local run`: keys, transport, log trust
idm = construct_identity_manager(merge_loaded_config(load_file("dnsid.json"), load_environment()))
| Variable | Maps to |
|---|---|
DNSID_DOMAIN, DNSID_GOVERNANCE_ID, DNSID_STATUS_URL, DNSID_LOG_REF, DNSID_EK_URL, DNSID_KU_URL, DNSID_PUBLISH_PROFILE, DNSID_CAPABILITIES_URL |
dnsid.identity.* |
DNSID_DNSSEC_MODE |
dnsid.verification.dnssec_mode (auto, validated, required) |
DNSID_DNS_SERVER, DNSID_CA_BUNDLE, DNSID_PRIVATE_HOSTS (comma-separated) |
dnsid.transport.* |
DNSID_LOG_POLICY_URL, DNSID_LOG_POLICY_FILE, DNSID_LOG_TRUST_PROFILE_FILE |
log_trust (exactly one variant; {"managed": true} is file/code only) |
DNSID_REGISTRY_URL |
registry.registry_url |
DNSID_API_KEY |
Read directly by registry_client_from_environment(); never included in loaded configuration |
DNSID_CONFIG_DIR, DNSID_KEY_STORE |
key_source.cli_directory, key_source.key_store_path |
Empty or whitespace-only values are absent. No loader defaults or derives a
value: without DNSID_DOMAIN the manager is verification-only; with
DNSID_DOMAIN but no DNSID_LOG_REF, construction fails with ArgumentError
(no placeholder log reference, no status_url derived from the registry URL).
Tooling variables (DNSID_PUBLIC_URL, DNSID_AGENT_PORT, DNSID_SERVER, …)
are ignored. In merge_loaded_config, default-valued overlays cannot clear
loaded identity strings, transport.dns_server/ca_bundle_path,
registry.registry_url (""), verification.status_check_interval (zero),
transport.private_address_hosts (empty), or key_source paths (None).
verification.dnssec_mode=None is also absent; set AUTO explicitly to
replace another mode. To clear a loaded field, edit the merged config before
construct_identity_manager. trusted_entities=[] is present and denies all.
Examples
For status intervals, entity-key pins, and log/bundle trust, see the trust policy example and full guide.
Runnable examples live in examples/. Each subdirectory is self-contained.
| Example | Description | Prerequisites |
|---|---|---|
| examples/local-key-provider/ | File-backed local key provider demo | None — fully self-contained |
| examples/published-signing/ | Load deployment config, verify an existing identity, and sign with AWS KMS or file custody | Published identity and its existing operational key |
| examples/a2a/ | Two agents (Alice + Bob) exchanging RFC 9421-signed A2A messages | Local registry (Docker + DNSid CLI) |
| examples/validate-domain/ | Verify a domain's DNSid identity | Network access — defaults to a hosted sandbox identity and the managed production log |
| examples/webbotauth/ | Sign and verify bot HTTP requests with WebBotAuthProfile |
None — fully self-contained |
| examples/oidc/ | Mint and inspect a DNSid OIDC assertion with OIDCProfile |
None offline; published identity + OIDC endpoint for opt-in exchange |
Self-contained example (no local registry needed)
The local-key-provider example runs entirely offline — it demonstrates key lifecycle, JWKS construction, and JWT signing without any network dependencies:
source .venv/bin/activate
python examples/local-key-provider/main.py
Local-registry examples
The a2a example requires the local registry managed by the dnsid CLI
(installation guide;
dnsid local up --zone test / dnsid local run), which provides local DNS, a
CA, and a registry. See
examples/a2a/README.md for full prerequisites and
step-by-step instructions.
Validate-domain example (no local registry needed)
Verifies the default hosted sandbox identity against DNSid's managed production log.
See the example README
for local verification of bob.test:
source .venv/bin/activate
python examples/validate-domain/main.py
Development
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# Run tests
pytest
# Lint
ruff check dnsid/
# Type check
mypy dnsid/
Security & trust
Official sources. Source: github.com/dnsid-ai/dnsid-py. Package: dnsid on PyPI. Releases: GitHub Releases on
this repository, each with a CycloneDX SBOM attached. Forks, mirrors, and similarly named packages
are not maintained by us. Report vulnerabilities per SECURITY.md; never in a public issue.
Software is not identity. This SDK ships no private keys or credentials; it bundles public log trust roots used only when the managed-log factory is selected. A DNSid identity is proven by control of a DNS zone, an agent private key, and the registry's published status. Possessing, forking, or modifying this code grants none of those: an unofficial build cannot mint or inherit anyone's identity.
What it does on the network. Only when you call it, and only to hosts you or the domain being verified chose:
- DNS TXT lookup of
_dnsid.<domain>through your system resolver (no hardcoded resolver) - HTTPS GET to the JWKS and status URLs published in that TXT record
- Opt-in only, never contacted unless you configure them:
https://api.dnsid.ai(registry client),https://log.dnsid.ai/log.dev.dnsid.ai/log.partners.dnsid.ai(C2SP transparency log, bundled public trust roots), cloud KMS endpoints - No telemetry, usage reporting, update checks, or crash reporting
Logging. None today. A standard-library logging logger under the dnsid namespace is declared for future
use; if it ever emits, it will carry key thumbprints and verification states, never private keys, tokens, or
signatures. Errors are raised to the caller.
Hosted endpoints. api.dnsid.ai and log.dnsid.ai are operated separately from this SDK under their
own terms. Nothing in this repository is an availability, uptime, or support commitment for them.
For your privacy notice. Using this SDK causes your system to make DNS and HTTPS requests to the domains you verify and to the JWKS/status hosts they publish. It sends them nothing about your users. If you enable the registry or transparency-log clients, requests also go to DNSid-operated endpoints; disclose that where your notice requires it.
License
Metadata
Release files for dnsid 0.28.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| dnsid-0.28.0.tar.gz | 585.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dnsid-0.28.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 841.3 kB
Release files / dnsid-0.28.0.tar.gz
| Download URL | dnsid-0.28.0.tar.gz |
|---|---|
| Size | 585.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
57fd84196989f5022df1e6ebec1ca11289dd1bec174a467b6750e58dfab71068
|
|
BLAKE2b-256 checksum How to use checksums |
e1111d7a1283cde1e57fda0e3f1d49e8442401424a6b476f85005daac38aca3c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / dnsid-0.28.0-py3-none-any.whl
| Download URL | dnsid-0.28.0-py3-none-any.whl |
|---|---|
| Size | 256.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e889708794e0b53ca075a30afe9a86257df388fad097d254378510e2c24e3787
|
|
BLAKE2b-256 checksum How to use checksums |
8e090785c40a99cd3e9c424802dacbd37bc4454f9f155e3c5c1608be26eae3b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logSecurity for dnsid 0.28.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for dnsid-0.28.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.