Skip to main content

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 — IdentityManager drives 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 KeyProvider interface: file-backed with LocalKeyProvider, or externalized entirely with AwsKmsKeyProvider or 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.

Async verification. Use await manager.async_verify_domain(domain) and await http_signer.async_verify_signed_http_request(request) in middleware. Fresh identity-cache hits, coalesced waits, and default stale-status refreshes use no worker threads; policy and signature checks still run on every request. Async-led cold verification uses native TXT, JWKS, status and C2SP bundle, tile and predecessor reads. Optional AsyncDNSResolver, AsyncHTTPSFetcher and AsyncLogReader capabilities are awaited; keep synchronous methods for synchronous callers. Sync-only adapters, substantial C2SP validation, and atomic checkpoint-store operations use bounded workers. Network reads remain native async. Mixed sync/async calls share whichever evidence leader starts first; sync-led work retains synchronous adapters in workers. Injected resources remain caller-owned. Core verification, registry, and C2SP readers bound gzip/deflate output during decompression and reject other codings. Shared evidence has its own 30-second budget. Caller cancellation or deadlines stop only that caller's wait. Reuse one manager for the application lifetime; use async with manager or await manager.aclose() at shutdown to drain work and close owned resources. verification_budget(5) limits a request to five seconds; expiry raises VerificationError with DEADLINE_EXCEEDED and transient=True. Synchronous APIs remain available.

Use await manager.async_refresh_domain(domain) for a full verification that bypasses the identity cache and revalidates HTTP evidence. Concurrent explicit refreshes share work, but do not reuse ordinary lookups. Peer binding, acceptance, and freshness are enforced for each caller.

Signed httpx.AsyncClient requests offload synchronous signing (including KMS). create_signed_async_http_client(transport=...) accepts a custom outbound transport, including httpx.MockTransport; the returned client owns and closes it. Other blocking SDK operations still need asyncio.to_thread or an executor. See the runnable async verification example for configuration, request deadlines, error handling, and cleanup.

Async registry I/O. Every registry operation has an async_ variant, including async_register_agent(), async_get_agent_status(), and async_wait_for_status(). These use pooled native async HTTP with the same validation and error handling as synchronous calls:

from dnsid import registry_client_from_environment

async with registry_client_from_environment() as registry:
    status = await registry.async_get_agent_status("agent.example")

Reuse the client on one event loop and finish requests before closing it. Use await registry.aclose() if you do not use async with. Cancellation cannot undo mutations already accepted by the server; reconcile the result and reuse the same request and idempotency key when retrying. Manager-level publication and rotation workflows still need a worker.

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. Configuration-selected signers must match verified local publication before return; initial unpublished setup requires explicitly injected providers.

RegistryClient.get_organization_onboarding() explicitly reads existing account proof/delegation status; it does not create an identity or promise permanent replay. Pass expected_org_id and expected_gi to compare saved account bindings. The returned bindings_ready is true only for matching, verified governance proof and verified entity delegation. Pending proofs remain pending; no entity JWKS URL is inferred from this response.

Initial issuance requires one durable coordinator and one retry owner. The SDK provides preparation, signing, and submission primitives, not a complete issuance coordinator. See integration requirements.

To move an established signer, use explicit rotate_operational_key() with a target_key_provider and stable old/target provider references. To change the signing algorithm, explicitly pass target_algorithm="ES256" (or "EdDSA") and configure the target provider for that algorithm. The target may have no active key if it supports pending-key generation. The generated key must match before preparation. Recovery reuses the saved references and exact signed bytes, and validates both providers before submission or activation. Reopen their saved state; do not create replacement stores. If the manager already uses the target, pass the saved source as previous_key_provider to recovery methods. A removed source key is valid only with saved acceptance and the exact target already active; a distinct source must have no active signing key, but may retain unrelated historical keys. Custom providers must raise NoActiveSigningKeyError only for an absent active key, not a key-access failure, and override generation_algorithm() if generation does not use an active key. 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 Published identity and network access
examples/async-verification/ Async verification with deadlines, error handling, and cleanup Published identity and network access
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

Apache License 2.0

Metadata

Release files for dnsid 0.29.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dnsid 0.29.0
File Size Uploaded Provenance
dnsid-0.29.0.tar.gz 660.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for dnsid 0.29.0
File Interpreter ABI Platform
dnsid-0.29.0-py3-none-any.whl Python 3 none any Details

Total release size: 937.3 kB

Release files / dnsid-0.29.0.tar.gz

Download URL dnsid-0.29.0.tar.gz
Size 660.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d5d76705023ad9a8d264054101de4a79066217c357bab055f3b7f3cd8f19cc65
BLAKE2b-256 checksum
How to use checksums
61273c26b3a2f358dc77b025627b51620d30ffda975401f0a46dbd3daf67b58d
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 20, 2026.

Transparency log

Release files / dnsid-0.29.0-py3-none-any.whl

Download URL dnsid-0.29.0-py3-none-any.whl
Size 276.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fd9d931ae8d2d2d8b66641eadb577c4b22f69c3a136a419499b022e19c7afffc
BLAKE2b-256 checksum
How to use checksums
e18fd3bc0632a2e1fac433a340a54da0fb4c53fbd696e7c6d8caa287d77b478c
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 20, 2026.

Transparency log

Security for dnsid 0.29.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.

Select from 2 files

Attestation for dnsid-0.29.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 20, 2026.

Transparency log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page