An authorization verifier needs answers from several systems, but those systems do not answer the same question.
- KERI describes controller state and key rotation.
- A raw-key method derives a principal directly from key material.
- Ed25519 and P-256 verify signatures.
- SHA-256 commits bytes to fixed-size identifiers and digests.
- Iroh, HTTPS, TCP, Unix sockets, files, and in-memory channels move a proof between machines.
Calling all of them “authentication” hides their different outputs.
auths-proof gives each category a separate interface, then converts the
accepted results into one authority decision.
Agnostic does not mean unrestricted
The kernel does not hard-code one identity or transport system. It also does not accept any implementation that happens to satisfy a Rust trait.
An implementation is usable only when:
- it has an exact registry identifier;
- the verifier includes it in an immutable registry;
- the trusted context accepts that identifier;
- the trusted context’s configuration commitment equals the executable registry’s configuration commitment;
- the implementation stays within its declared work bound.
An unknown principal method or signature suite produces Indeterminate.
Malformed or contradictory supplied evidence produces Denied. Neither result
falls back to another method.
flowchart LR
subgraph I["Principal methods"]
RK["raw key"]
DK["did:key"]
K["KERI"]
DW["bundled did:web"]
W["WebAuthn"]
X["SPIFFE / X.509"]
end
subgraph S["Signature suites"]
E["Ed25519"]
P["P-256 / SHA-256"]
end
subgraph T["Proof exchange"]
IR["Iroh"]
H["HTTPS"]
TCP["TLS / TCP"]
U["Unix / file / memory"]
end
RK --> C["ControlEvidence"]
DK --> C
K --> C
DW --> C
W --> C
X --> C
E --> V["signed statement valid"]
P --> V
C --> A["authority kernel"]
V --> A
IR --> Q["ProofBundle + CanonicalAction"]
H --> Q
TCP --> Q
U --> Q
Q --> A
A --> M["Permission<br/>capability + resource"]
M --> O["VerifiedAction"]Principal methods establish control facts
Every principal adapter implements the same effect-free port:
pub trait PrincipalMethod {
fn id(&self) -> &PrincipalMethodId;
fn configuration_id(&self) -> AdapterConfigurationId;
fn maximum_work_units(&self) -> u64;
fn verify_control(
&self,
input: PrincipalControlInput<'_>,
) -> Result<ControlEvidence, PrincipalControlError>;
}
The input contains the exact principal, verification method, signature suite, control purpose, signing preimage, asserted signing time, bound evidence, and verifier-supplied evaluation time.
The output contains:
pub struct ControlEvidence {
verification_key: Vec<u8>,
claims: Vec<AssuranceClaim>,
consumed_evidence: Vec<EvidenceId>,
adapter: AdapterId,
adapter_version: u16,
work_units: u64,
signature_message: Option<Vec<u8>>,
}
This is the common boundary. A principal method does not grant a permission. It establishes which key should verify the statement, which assurance claims the evidence supports, and exactly which evidence objects it consumed.
Raw keys bind identity directly to key material
RawKeyMethod selects one evidence object, decodes its bounded descriptor, and
checks three equalities:
if descriptor.suite() != input.signature_suite.as_str() {
return Err(PrincipalControlError::SignatureSuiteMismatch);
}
let principal = descriptor
.principal()
.map_err(|_| PrincipalControlError::InvalidEvidence)?;
if &principal != input.principal {
return Err(PrincipalControlError::PrincipalMethodMismatch);
}
if input.verification_method.as_str() != principal.as_str() {
return Err(PrincipalControlError::VerificationMethodMismatch);
}
If those checks pass, it returns the public key plus
self-certifying-identifier and offline-verifiable assurance claims.
KERI replays controller history
DidKeriMethod receives a bounded KERI event log as evidence. It:
- decodes the event sequence under configured event, byte, attachment, and key limits;
- replays the log to its latest accepted state;
- derives the expected KERI principal from that state;
- parses the signed verification-method identifier into establishment sequence and key index;
- selects the key valid at that establishment state;
- checks that the descriptor’s signature suite matches the selected key.
When an immutable verifier checkpoint matches the replayed state and evaluation
time, the adapter can add claims such as controller-state-current-at,
revocation-checked-at, and witness-threshold-met.
Both adapters return ControlEvidence, but they do not claim equal assurance.
The verifier applies role-indexed assurance policy after the adapter returns.
Signature suites verify bytes selected by the method
Principal methods and signature algorithms are separate registries.
pub trait SignatureSuite {
fn id(&self) -> &SignatureSuiteId;
fn configuration_id(&self) -> AdapterConfigurationId;
fn verify(
&self,
input: SignatureInput<'_>,
) -> Result<(), SignatureError>;
fn work_units(&self) -> u64;
}
V1 includes exact implementations for:
ed25519-v1;p256-sha256-v1.
The P-256 implementation parses a SEC1 public key and fixed-width signature, rejects high-S signatures, then verifies the complete domain-separated signing preimage. Ed25519 verifies the same class of preimage with strict RFC 8032 semantics.
SHA-256 is not interchangeable with those signature suites. It supplies content digests, object identifiers, configuration commitments, and the hash inside the P-256 suite. KERI is not a signature suite either; it is a principal method that selects a key from controller history. Keeping these categories separate prevents a new identity method from changing the meaning of signature verification.
The registry is part of the trusted input
ImmutableRegistries rejects duplicate implementation identifiers and computes
one configuration identifier over every implementation ID and immutable
configuration value.
let registries = ImmutableRegistries::new(
&principal_methods,
&signature_suites,
)?;
let context = context.with_configuration(
registries.configuration_id(),
)?;
At verification time, two comparisons occur before principal control:
context.accepted_registries.manifest == executable_registry.manifest
context.configuration == executable_registry.configuration
The manifest identifies the protocol registry. The configuration identifier commits the actual adapters and their decision-affecting configuration, including KERI limits and checkpoints.
Two machines can use different admitted principal systems. They cannot produce an authorized result while pretending they executed the same configuration.
Transports carry proofs but do not grant authority
Proof exchange has its own semantic port:
pub trait ClientProofChannel {
type Error;
fn peer_observation(&self) -> &PeerObservation;
async fn receive_challenge(&mut self)
-> Result<ActionChallenge, Self::Error>;
async fn submit_action(&mut self, request: ActionSubmission)
-> Result<ActionResponse, Self::Error>;
}
The same sequence has adapters for Iroh, HTTPS, TCP, Unix sockets, files, and in-memory tests:
receive challenge
→ submit bounded action body and proof
→ receive response
Each channel reports what it actually observed:
pub enum PeerObservation {
IrohEndpoint([u8; 32]),
HttpsServerCertificate([u8; 32]),
MutualTlsCertificate([u8; 32]),
TcpEndpoint(String),
UnixPeerCredentials { uid: u32, gid: u32, pid: Option<u32> },
FileEnvelope { digest: [u8; 32], sequence: u64 },
AuthenticatedOpaque { kind: String, identifier: Vec<u8> },
ServerAuthenticated,
Unauthenticated,
}
An Iroh endpoint key, TLS certificate, Unix user ID, or authenticated HTTPS server is a transport observation. It is not automatically an Auths principal and does not create permission.
If an application requires channel binding, its profile commits the relevant peer identifier into the signed action and compares it with the observation. The relation is explicit rather than inferred from the transport.
Transport failures remain transport errors. They do not become Denied,
Indeterminate, or Authorized results. Conversely, an authenticated TLS or
Iroh channel cannot upgrade an invalid proof.
Permission keeps the same meaning across machines
After identity and signature checks succeed, the authority kernel works with a small model:
pub struct Permission {
capability: CapabilityId,
resource: ResourceId,
}
A GitHub integration might derive:
("issue.close", "github:auths-dev/auths-proof/issues/42")
A Kubernetes profile might derive:
("deployment.rollout", "k8s:prod/payments/api")
The strings differ because the domains differ. The operation does not:
- the profile canonicalizes the application request;
- it derives one capability-resource pair;
- the grant chain may preserve or narrow the permission set;
- the terminal action must request a member of that set;
- the verified canonical body is the body available for execution.
Identity systems answer who controlled the signed statement. Signature suites answer whether the signature matches its exact bytes. Transports answer how the proof arrived and what channel peer was observed. The authority model answers whether the resulting principal was delegated permission for this exact action.
That separation is the form of agnosticism implemented by the repository.
Part one introduces the motivation and verifier contract: Authority should only get smaller.
Part two follows the checks that keep the boundaries intact: The repository rejects reverse dependencies.