Documentation
¶
Overview ¶
Package zkauthn @notice Groth16 authentication and anonymous authorization over BN254.
@dev The operator that verifies proofs also performs each circuit's trusted setup. That operator can already bypass its own verifier, so single-party setup does not weaken the soundness property against users. No setup output is shipped by kal.
Index ¶
- Constants
- func CircuitInfo(kind Circuit) (constraints int, id [sha256.Size]byte, err error)
- func KnowledgeValid(w KnowledgeWitness) bool
- func MembershipValid(w MembershipWitness) bool
- func ProofSize() int
- func Setup(kind Circuit, pkw, vkw io.Writer) error
- type Circuit
- type Claim
- type ClaimKind
- type Credential
- type Field
- type KnowledgeCircuit
- type KnowledgeRequest
- type KnowledgeWitness
- type MembershipCircuit
- type MembershipPublic
- type MembershipRequest
- type MembershipWitness
- type MerklePath
- type Options
- type ProofSink
- type ProvingKey
- type Secret
- type VerifiedClaim
- type VerifyingKey
- type ZK
- func (z *ZK) Claim(ctx context.Context, db orm.DB, name string) (Claim, error)
- func (z *ZK) ClaimChallenge(ctx context.Context, db orm.DB) (string, error)
- func (z *ZK) EnrollKnowledge(ctx context.Context, db orm.DB, currentPassword string) (Secret, error)
- func (z *ZK) EnsureClaim(ctx context.Context, db orm.DB, claim Claim) error
- func (z *ZK) IssueCredential(ctx context.Context, db orm.DB, issuedTo string, attribute uint64) (*Credential, error)
- func (z *ZK) KnowledgeChallenge(ctx context.Context, db orm.DB) (string, error)
- func (z *ZK) Login(ctx context.Context, db orm.DB, req MembershipRequest) (*authz.Principal, error)
- func (z *ZK) LoginChallenge(ctx context.Context, db orm.DB) (string, error)
- func (z *ZK) Path(ctx context.Context, db orm.DB, index uint32) (*MerklePath, error)
- func (z *ZK) ProveClaim(ctx context.Context, db orm.DB, req MembershipRequest) error
- func (z *ZK) RevokeCredential(ctx context.Context, db orm.DB, index uint32) error
- func (z *ZK) RevokeCredentialsForUser(ctx context.Context, db orm.DB, userID string) error
- func (z *ZK) RootGrace() time.Duration
- func (z *ZK) VerifyKnowledge(ctx context.Context, db orm.DB, req KnowledgeRequest) error
Constants ¶
const ( // MerkleDepth @notice The fixed credential-tree depth and therefore part of the circuit. MerkleDepth = 32 // SecretSize @notice Bytes in a generated ZK secret; every 31-byte value is canonical BN254. SecretSize = 31 // KnowledgeConstraints pins the compiled R1CS cost under gnark v0.15.0. KnowledgeConstraints = 332 // MembershipConstraints pins the compiled R1CS cost under gnark v0.15.0. // // @dev v0.15's two-input MiMC absorbs two field blocks at 330 constraints each, so the // 32 tree compressions account for ~21k constraints. The handout's 20k estimate assumed // one permutation per two-to-one compression; the measured API semantics are pinned here. MembershipConstraints = 23001 KnowledgeCircuitID = "27262b1163cbc35c4fb83ec828234b98dc324f9f351681cf781ccf7529c1fe3f" MembershipCircuitID = "5f8c74c31dfa7fe1575ce175856d4ed02f5e657d2fb8a7d0208279ef63f6370f" )
const ( // ChallengeTTL @notice Lifetime of a server-issued proof challenge. ChallengeTTL = time.Minute )
Variables ¶
This section is empty.
Functions ¶
func CircuitInfo ¶
CircuitInfo @notice The measured size and serialized identity of a compiled circuit.
func KnowledgeValid ¶
func KnowledgeValid(w KnowledgeWitness) bool
KnowledgeValid @notice Plain-Go oracle for KnowledgeCircuit.
func MembershipValid ¶
func MembershipValid(w MembershipWitness) bool
MembershipValid @notice Plain-Go oracle for MembershipCircuit.
func ProofSize ¶
func ProofSize() int
ProofSize @notice The exact compressed proof length accepted before deserialization.
Types ¶
type Claim ¶
type Claim struct {
Name string
Audience Field
Threshold uint64
Kind ClaimKind
// AllowsLogin @notice Whether proving this claim may mint a session. False by default: Kind
// distinguishes how long a nullifier lives, not what a proof is good for, so without this a
// claim written for an @auth(proves:) step-up is also a login endpoint.
AllowsLogin bool
}
Claim @notice Server policy supplying the audience and threshold for a schema claim name.
type ClaimKind ¶
type ClaimKind string
ClaimKind @notice Whether a nullifier is a recurring pseudonym or a one-shot allowance.
type Credential ¶
type Credential struct {
Secret Secret
Attribute uint64
LeafIndex uint32
Path MerklePath
}
Credential @notice A membership credential returned once when the operator issues it.
type Field ¶
Field @notice A canonical, big-endian BN254 scalar-field encoding.
func ChallengeField ¶
ChallengeField @notice Converts a server challenge token into its canonical circuit input.
func KnowledgeCommitment ¶
KnowledgeCommitment @notice Computes the public commitment a client proves knowledge of.
func MembershipCommitment ¶
MembershipCommitment @notice Computes the raw credential leaf bound to secret and attribute.
func NewAudience ¶
NewAudience @notice Derives a canonical audience field from deployment and policy labels.
@dev SHA-256 output is reduced deliberately into BN254. Audience collision resistance is therefore the field size (~254 bits), and the versioned prefix prevents reuse by another protocol in the same process.
type KnowledgeCircuit ¶
type KnowledgeCircuit struct {
Commitment frontend.Variable `gnark:",public"`
Challenge frontend.Variable `gnark:",public"`
Secret frontend.Variable
}
KnowledgeCircuit @notice Proves knowledge of a secret committed for the current session.
@dev Flattened form:
- h = MiMC(DOM_KNOWLEDGE, Secret)
- assert h == Commitment
- c2 = Challenge * Challenge
Statement 3 deliberately creates an otherwise-unused constraint. Without it Challenge is present in the public witness but not bound to the proof, so a proof replays under every server challenge.
type KnowledgeRequest ¶
KnowledgeRequest @notice Proof bytes plus the server challenge for MFA step-up.
type KnowledgeWitness ¶
KnowledgeWitness @notice Typed inputs for creating a knowledge proof.
type MembershipCircuit ¶
type MembershipCircuit struct {
Root frontend.Variable `gnark:",public"`
Audience frontend.Variable `gnark:",public"`
Threshold frontend.Variable `gnark:",public"`
Nullifier frontend.Variable `gnark:",public"`
Challenge frontend.Variable `gnark:",public"`
Secret frontend.Variable
Attribute frontend.Variable
Path [MerkleDepth + 1]frontend.Variable
Index frontend.Variable
}
MembershipCircuit @notice Proves membership, one numeric threshold and an audience pseudonym.
@dev Flattened form:
- decompose Attribute into exactly 64 bits
- leaf = MiMC(DOM_LEAF, Secret, Attribute)
- assert leaf == Path[0]
- verify Path at Index recomputes Root
- assert Threshold <= Attribute
- n = MiMC(DOM_NULLIFIER, Secret, Audience)
- assert n == Nullifier
- c2 = Challenge * Challenge
Path[0] is private prover input. Statement 3 is what proves knowledge of the credential rather than mere knowledge of any public leaf. Statement 8 binds freshness just as it does in KnowledgeCircuit.
type MembershipPublic ¶
MembershipPublic @notice The public witness supplied by the verifier after policy lookup.
type MembershipRequest ¶
type MembershipRequest struct {
Proof []byte `json:"proof"`
Root []byte `json:"root"`
Nullifier []byte `json:"nullifier"`
Challenge string `json:"challenge"`
Claim string `json:"claim"`
}
MembershipRequest @notice Attacker-supplied proof fields; policy values are deliberately absent.
type MembershipWitness ¶
type MembershipWitness struct {
Secret Secret
Attribute uint64
Path [MerkleDepth + 1]Field
Index uint32
Root Field
Audience Field
Threshold uint64
Nullifier Field
Challenge Field
}
MembershipWitness @notice Typed inputs for creating a membership or threshold proof.
func MembershipWitnessFor ¶
func MembershipWitnessFor(credential Credential, path MerklePath, claim Claim, nullifier Field, challenge Field) MembershipWitness
MembershipWitnessFor @notice Builds client prover inputs from a credential, path and policy.
type MerklePath ¶
type MerklePath struct {
Root Field
Path [MerkleDepth + 1]Field
Index uint32
}
MerklePath @notice The current root and sibling path for one credential leaf.
func SingleLeafPath ¶
func SingleLeafPath(commitment Field) (MerklePath, error)
SingleLeafPath @notice Builds the sparse-tree path for the first and only credential.
@dev This is primarily a setup smoke-test and example helper. Live clients must call Path, because every later enrolment changes siblings and the root.
type Options ¶
type Options struct {
KnowledgeVK *VerifyingKey
MembershipVK *VerifyingKey
Sessions *session.Sessions
// Hasher @notice Required. Re-verifies the password that gates replacing a commitment.
Hasher *authn.Hasher
ProofSink ProofSink
CookieName string
Schema string
RootGrace time.Duration
// MFAWindow @notice How recently MFA must have been satisfied for an account with no
// password to replace its commitment. Zero means authz.DefaultMFAWindow.
MFAWindow time.Duration
MaxConcurrentVerifications int64
}
Options @notice Dependencies and production defaults for New.
type ProofSink ¶
type ProofSink func(context.Context, VerifiedClaim) error
ProofSink adds a verified claim to the request-local authorization holder.
type ProvingKey ¶
type ProvingKey struct {
// contains filtered or unexported fields
}
ProvingKey @notice A parsed Groth16 proving key and its matching compiled circuit.
func LoadProvingKey ¶
func LoadProvingKey(kind Circuit, r io.Reader) (*ProvingKey, error)
LoadProvingKey @notice Safely parses a proving key for the named compiled circuit.
func (*ProvingKey) ProveKnowledge ¶
func (p *ProvingKey) ProveKnowledge(w KnowledgeWitness) ([]byte, error)
ProveKnowledge @notice Creates a compressed Groth16 knowledge proof.
func (*ProvingKey) ProveMembership ¶
func (p *ProvingKey) ProveMembership(w MembershipWitness) ([]byte, error)
ProveMembership @notice Creates a compressed Groth16 membership proof.
type Secret ¶
type Secret [SecretSize]byte
Secret @notice A generated 248-bit credential that is always a canonical field element.
type VerifiedClaim ¶
VerifiedClaim @notice A claim whose proof and server policy were verified for this request.
type VerifyingKey ¶
type VerifyingKey struct {
// contains filtered or unexported fields
}
VerifyingKey @notice A safely parsed, hash-pinned Groth16 verifying key.
func LoadVerifyingKey ¶
LoadVerifyingKey @notice Hash-pins and safely parses a BN254 verifying key.
func (*VerifyingKey) VerifyKnowledge ¶
func (v *VerifyingKey) VerifyKnowledge(proof []byte, commitment, challenge Field) error
VerifyKnowledge @notice Verifies proof against server-supplied commitment and challenge.
func (*VerifyingKey) VerifyMembership ¶
func (v *VerifyingKey) VerifyMembership(proof []byte, public MembershipPublic) error
VerifyMembership @notice Verifies proof against server-supplied policy public inputs.
type ZK ¶
type ZK struct {
// contains filtered or unexported fields
}
ZK @notice Knowledge MFA, membership credentials, anonymous login and proof-backed claims.
func (*ZK) Claim ¶
Claim @notice Reads the server policy clients must use as membership public inputs.
func (*ZK) ClaimChallenge ¶
ClaimChallenge @notice Issues a 60-second membership challenge bound to the current session.
func (*ZK) EnrollKnowledge ¶
func (z *ZK) EnrollKnowledge(ctx context.Context, db orm.DB, currentPassword string) (Secret, error)
EnrollKnowledge @notice Generates a knowledge secret and returns it exactly once. Replacing an existing commitment requires re-authentication.
@dev A session cookie alone used to be enough to overwrite the commitment, so an attacker who stole one enrolled their own secret, proved it, set mfa_at and satisfied every @auth(mfa: true) field — the second factor defeated by the first, which is the definition of not being one. First enrolment stays open; replacement takes the other factor the account still has:
existing commitment | account has a password | required none | — | an authenticated session yes | yes | the current password, re-verified yes | no | MFAAt within the MFA window
The third row exists because re-enrolment is the documented recovery path for a lost secret, so it must be reachable without the old factor. An account with neither cannot self-serve, which is the operator's problem in the same way an unaccepted invite is.
Replacement revokes every other session rather than rotating like ChangePassword: a routine password change should keep other devices signed in, but replacing a second factor under a possibly-stolen session is the reset case.
@param currentPassword the account's current password; ignored on first enrolment @return Secret the new secret, returned once and never stored
func (*ZK) EnsureClaim ¶
EnsureClaim @notice Idempotently installs the server policy for one schema claim name.
func (*ZK) IssueCredential ¶
func (z *ZK) IssueCredential(ctx context.Context, db orm.DB, issuedTo string, attribute uint64) (*Credential, error)
IssueCredential @notice Generates, inserts and returns one linked membership credential.
@dev issuedTo is normally the named auth_users id so revocation can name a person. Empty deliberately stores NULL for deployments that prefer an unlinked issuance registry.
func (*ZK) KnowledgeChallenge ¶
KnowledgeChallenge @notice Issues a 60-second challenge bound to the current session.
func (*ZK) Login ¶
Login @notice Verifies recurring membership and issues an ordinary pseudonymous kal session.
func (*ZK) LoginChallenge ¶
LoginChallenge @notice Issues a 60-second challenge usable only for anonymous membership login.
func (*ZK) Path ¶
Path @notice Returns the current Merkle path for an active credential index.
@dev A path and its root are one logical snapshot, and a proof built across a mutation verifies against no published root at all (gotcha 54). That reasoning is right; taking the deployment-wide writer lock to get it was the wrong tool. Every member fetches a path before proving, so path reads serialized across the whole deployment, each holding a connection for 33 round-trips while excluding writers it never conflicts with.
Instead: read the root, walk, read it again. Equal roots mean nothing was published across the walk and the path is a real snapshot. One retry, because a second collision means genuine write pressure rather than an unlucky interleaving, and spinning here is what the lock was for.
ponytail: optimistic, not locked — two extra root reads per path against a deployment-wide serialization point. If issuance ever becomes hot enough that the retry fires routinely, the upgrade is a repeatable-read transaction, not the advisory lock.
func (*ZK) ProveClaim ¶
ProveClaim @notice Verifies membership for the current session and records its claim.
@dev Recurring claims persist for the session. One-shot claims are atomically burned in Postgres and exist only in the request-local holder populated through ProofSink.
func (*ZK) RevokeCredential ¶
RevokeCredential @notice Removes one active leaf and publishes the resulting root atomically.
func (*ZK) RevokeCredentialsForUser ¶
RevokeCredentialsForUser @notice Revokes every live credential linked to a named user.
func (*ZK) VerifyKnowledge ¶
VerifyKnowledge @notice Verifies step-up, timestamps MFA and rotates the session credential.
@dev Three phases, and the split is the point. Phase 1 reads and phase 2 verifies, both outside any transaction: pairing arithmetic queued behind a semaphore used to run with an open transaction, a pooled connection and the challenge row's lock all held, so CPU pressure on an unauthenticated endpoint turned into connection-pool exhaustion for every other query. Phase 3 opens the transaction and burns the challenge with the single-use UPDATE, which is still the only thing deciding freshness. Do not collapse these back together for atomicity — phases 1 and 2 write nothing.