authdsl

package module
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 5, 2026 License: MIT Imports: 10 Imported by: 0

README

authdsl

GoDoc

authdsl is a Go module for attribute-based authorisation using a human-readable policy DSL.

This module contains the DSL compiler, an Evaluator allowing testing of DSL statements, and a fully working authorisation engine that tests access based on policies written in the DSL.

Policies are versioned in epochs — immutable snapshots of group membership and compiled rules served by a remote control infrastructure.

An embedded engine.Engine evaluates requests against the active epoch with no lock contention on the hot path, and supports on-behalf-of (OBO) delegation chains and historic re-evaluation for compliance workflows.

When trust providers are configured, SubjectIdentity fields carry signed subject tokens (ED25519 or ML-DSA-87). EvaluateAsOfOBO uses split verification: the live caller is checked against today's trust providers while the historic inner chain is checked against the trust providers that were in effect at asOf, fetched from a dedicated Historic Trust Service advertised by the control server.

Architecture

graph TB
    subgraph proc["Your Application Process"]
        code(["Application Code"])
        subgraph sdk["authdsl module"]
            eng["engine.Engine
            ─────────────────────
            Epoch state machine
            Evaluate / EvaluateForSession
            EvaluateOBO / EvaluateOBOForSession
            EvaluateAsOfOBO"]

            re["remote.RemoteEngine
            ─────────────────────
            ED25519/ML-DSA-87 key pair
            ML-KEM-1024 response encryption
            Registration + polling
            Epoch lifecycle
            LoadHistoricTrustProviders"]
        end
        code -- "Request →\n← Decision" --> eng
        re -- "chan UpdateEpoch" --> eng
    end

    ctrl[["Control Server
    ─────────────────────
    POST /  register
    GET /   poll
    GET /at historic
    (advertises historic_trust_url)"]]

    data[["Data Server
    ─────────────────────
    GET /{id}           groups
    GET /{id}/policies/{p}
    GET /{id}/obo_policies/{p}
    GET /{id}/historic_auth_policies/{p}"]]

    hts[["Historic Trust Service
    ─────────────────────
    GET /historic_trust?t=...
    (trust providers at time t)"]]

    re -->|"signed + KEM-encrypted HTTPS"| ctrl
    re -->|"verified HTTPS"| data
    re -->|"signed HTTPS"| hts
    ctrl -. "retrieve_url" .-> data
    ctrl -. "historic_trust_url" .-> hts

authdsl.New creates and wires both components. remote.RemoteEngine handles all network communication, key management, and epoch delivery. engine.Engine is the evaluation surface exposed to your code — it never blocks on I/O during a request.

Quick Start

import (
    "context"
    "crypto/mlkem"
    "net/url"
    "time"

    "gitlab.com/possumco/authdsl"
    "gitlab.com/possumco/authdsl/engine"
)

// Load the server's ML-KEM-1024 encapsulation key (distributed out-of-band,
// alongside the control server's signing public key).
kemDecapKey, _ := mlkem.GenerateKey1024()   // on the server side
kemEncapKey := kemDecapKey.EncapsulationKey() // share this with clients

ctrlURL, _ := url.Parse("https://control.internal")
eng, err := authdsl.New(ctx, authdsl.Config{
    ControlConfig: authdsl.ControlConfig{
        URL:              *ctrlURL,
        PublicKey:        ctrlPub,    // ED25519 or ML-DSA-87 signing key
        KEMPublicKey:     kemEncapKey, // ML-KEM-1024 encapsulation key (required)
        ValidityDuration: 30 * time.Second,
    },
    ServiceConfig: authdsl.ServiceConfig{
        Class: "order-service",
        ID:    "ord-1",
    },
})
if err != nil {
    log.Fatal(err)
}
defer eng.Close()

// Wait for the first epoch.
for {
    if _, ok := eng.ActiveEpochID(); ok {
        break
    }
    time.Sleep(10 * time.Millisecond)
}

// Direct evaluation.
dec, err := eng.Evaluate(ctx, &engine.Request{
    SubjectIdentity: "alice",
    Process:         "document-store",
    Action:          "WRITE",
})
// dec == engine.DecisionAllow or engine.DecisionDeny

// On-behalf-of evaluation.
dec, err = eng.EvaluateOBO(ctx, &engine.OBORequest{
    SubjectIdentity: "agent-alice",
    Inner: engine.For(&engine.Request{
        SubjectIdentity: "customer-charlie",
        Process:         "customer.portal",
        Action:          "UPDATE",
    }),
})

// Historic re-evaluation.
dec, err = eng.EvaluateAsOfOBO(ctx, &engine.OBORequest{
    SubjectIdentity: "auditor-alice",
    Inner: engine.For(&engine.Request{
        SubjectIdentity: "trader-dave",
        Process:         "payments.ledger",
        Action:          "READ",
    }),
}, twoYearsAgo)

Capabilities

Policy DSL

Policies are written in a structured, human-readable language. A policy file contains ALIAS declarations (for reuse) followed by POLICY blocks, each of which is homogeneously ALLOW or DENY:

POLICY allow-admins ALLOW {
    group:admins TO PERFORM ANY_ACTION ON process:document-store ;
}

POLICY readers-read-only ALLOW {
    group:readers TO PERFORM READ ON process:document-store ;
}

POLICY support-obo ALLOW {
    group:support-staff TO PERFORM system.obo:ACT_AS FOR group:customers ;
}

Subjects, actions, and processes support boolean expressions (AND, OR, NOT), group membership (group:name), wildcards (ANY, ANY_ACTION, ANY_PROCESS), and UUID references. The WHERE clause adds attribute and context conditions (IS_OBO, IS_HISTORIC, OBO_DEPTH, TAG, SUBJECT.xxx, REQUEST.xxx). The DURING clause adds temporal constraints (FROM, UNTIL, BETWEEN, DAYS, HOURS).

Policies are compiled once — dsl.Compile(src) returns an immutable CompiledPolicy safe for concurrent use.

See tech_docs/dsl.md for the full language reference.

Evaluation

Five evaluation methods are available on engine.Engine:

Method Use case
Evaluate(ctx, *Request) Direct authorisation: is this subject allowed to perform this action?
EvaluateForSession(ctx, *Request) Direct authorisation + FixedEpoch: pin to the active epoch for session consistency
EvaluateOBO(ctx, *OBORequest) Delegation chain: can this agent act on behalf of that principal?
EvaluateOBOForSession(ctx, *OBORequest) Delegation chain + FixedEpoch: pin to the active epoch for OBO session consistency
EvaluateAsOfOBO(ctx, *OBORequest, asOf) Historic re-evaluation: was this action authorised at a past point in time?

All methods resolve group memberships from the epoch snapshot automatically — no group data needs to be set on the request by the caller. Evaluation never acquires a mutex on the hot path.

EvaluateForSession and EvaluateOBOForSession return a FixedEpoch alongside the decision. A FixedEpoch evaluates all subsequent requests against the epoch that was active when the session started, providing consistency even as the engine transitions to newer epochs. It is tied to the SubjectIdentity of the initial request; attempts to use it with a different subject return ErrInvalidRequest.

EvaluateAsOfOBO performs a three-phase check: authority verification against the live epoch, historic epoch load, and inner evaluation. The calling subject must hold a system.historic:EVALUATE grant in the current epoch before the historic epoch is ever fetched.

See tech_docs/eval.md for the full API reference.

Epochs

An epoch is a versioned, immutable snapshot of group memberships and policy rules. The engine always evaluates against the active epoch. When the remote control server announces a new epoch, the RemoteEngine fetches the data and delivers it to the engine, which atomically replaces the active state. In-flight evaluations against the previous epoch complete uninterrupted.

Each RemoteEngine instance:

  1. Generates an ED25519 or ML-DSA-87 key pair on startup.
  2. Registers with the control server (POST /), sending its service class, instance ID, hostname, and public key. The server responds with a process_id for the session.
  3. Polls the control server (GET /?last={n}) at the server-specified interval. When the epoch ID changes, the new epoch data is fetched from the data server.
  4. Signs every outgoing control request with its private key; verifies every control response with the server's public key. Control responses are also ML-KEM-1024 encrypted — each request includes a fresh KEM ciphertext; the server encrypts the response body with the derived shared key before signing.

Policy text is fetched lazily from the data server on the first evaluation for each process and cached in-memory for the epoch's lifetime.

For historic EvaluateAsOfOBO calls, a separate GET /at?t=<RFC3339Nano> request identifies the epoch that was active at the requested time, which is then fetched and cached with a configurable TTL (default 5 minutes).

See tech_docs/control_service.md, tech_docs/data_service.md, and tech_docs/engine.md for detailed specifications.

Encryption

Control server responses carry public key material that is later used to verify epoch signatures. To protect this material against harvest-now-decrypt-later attacks, every control response body is encrypted using a ML-KEM-1024 / AES-256-GCM hybrid scheme:

  1. The client generates a fresh ML-KEM-1024 ciphertext from the server's pre-configured encapsulation key and sends it in the X-KEM-Ciphertext request header.
  2. The server decapsulates the ciphertext to obtain the 32-byte shared key, encrypts the JSON response body with AES-256-GCM (12-byte nonce prepended), and signs the encrypted body with its ED25519 or ML-DSA-87 private key.
  3. The client verifies the signature over the encrypted body, then decrypts using the same shared key.

Each request uses a freshly encapsulated shared key, providing per-request forward secrecy: a future quantum adversary who recorded TLS traffic cannot recover the key material used for response signatures.

KEMPublicKey in ControlConfig is required and must be pre-distributed alongside the control server's signing public key.

See tech_docs/crypto.md for the full cryptographic specification.

Observability

Both engine and remote are instrumented with OpenTelemetry Go (API only — no SDK bundled). Register any TracerProvider before calling evaluation methods and spans will be emitted automatically:

import (
    "go.opentelemetry.io/otel"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(yourExporter))
otel.SetTracerProvider(tp)
defer tp.Shutdown(ctx)

Key span names: authdsl.Evaluate, authdsl.EvaluateOBO, authdsl.EvaluateAsOfOBO, authdsl.EvaluateForSession, authdsl.EvaluateOBOForSession, authdsl.FixedEpoch.Evaluate, authdsl.FixedEpoch.EvaluateOBO, authdsl.remote.fetchEpochData, authdsl.remote.LoadPolicy, authdsl.remote.LoadOBOPolicy, authdsl.remote.LoadHistoricAuthPolicy, authdsl.remote.updateEpoch, authdsl.remote.fetchEpochAt, authdsl.remote.LoadHistoricEpoch, authdsl.remote.LoadHistoricTrustProviders.

Spans carry authdsl.process, authdsl.action, authdsl.epoch_id, authdsl.decision, and authdsl.as_of attributes. Remote HTTP spans are SpanKindClient and capture full retrieval round-trip duration including signature verification.

See tech_docs/observability.md for the full span inventory and attribute reference.

Demos

Six self-contained demo programs are included:

Program What it shows
go run ./cmd/demo Live epoch transition: group membership changes mid-run
go run ./cmd/session Session consistency: FixedEpoch preserves epoch-1 decisions after transition to epoch 2
go run ./cmd/obo Single-hop and two-hop OBO delegation chains
go run ./cmd/historic Historic re-evaluation with authority checking
go run ./cmd/negotiation Algorithm negotiation: ED25519 and ML-DSA-87 trust providers both accepted (AlgorithmPolicyBoth)
go run ./cmd/mldsa_tokens Algorithm enforcement: AlgorithmPolicyMLDSA87Only rejects ED25519 tokens at evaluation time

Each demo starts its own in-process control and data servers so no external infrastructure is required.

Technical Documentation

Document Contents
tech_docs/dsl.md Full DSL grammar, all expression types, ALIAS, temporal constraints, namespaces, compile API, diagnostics
tech_docs/eval.md Evaluation API, Request/OBORequest types, group resolution, context flags, all error conditions
tech_docs/engine.md Epoch state machine, historic caching, concurrency model, shutdown
tech_docs/control_service.md Control server protocol, wire types, KEM-encrypted responses, request signing, key rotation, freshness checks
tech_docs/data_service.md Data server endpoints, epoch data structure, policy caching, response signing
tech_docs/crypto.md Key algorithms (ED25519, ML-DSA-87), ML-KEM-1024 encryption flow, wire key format, algorithm enforcement rules
tech_docs/wire_types.md Full wire type reference: RegistrationRequest, ControlResponse, EpochAtResponse, WireTrustKey, tagged key format
tech_docs/observability.md OpenTelemetry span inventory, attribute reference, TracerProvider wiring

Testing

go test ./... -race -count=1

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidControlConfig = errors.New("authdsl: invalid ControlConfig")
View Source
var ErrInvalidServiceConfig = errors.New("authdsl: invalid ServiceConfig")

Functions

func New

func New(ctx context.Context, config Config) (*engine.Engine, error)

New creates a new remote.RemoteEngine that connects to the controlURL to initiate epoch data loading for the engine.Engine. Once the current epoch has been loaded, the engine.Engine can start serving requests.

Types

type Config

type Config struct {
	ControlConfig ControlConfig
	ServiceConfig ServiceConfig
	// HTTPClient overrides the default HTTP client used for all remote calls.
	// Useful in tests and environments that require custom TLS configuration.
	HTTPClient *http.Client
}

type ControlConfig

type ControlConfig struct {
	// URL is the url to the control service (must use https scheme)
	URL url.URL
	// PublicKey is the ML-DSA-87 public key used to verify control server response
	// signatures (required).
	PublicKey engine.PublicVerifyKey
	// KEMPublicKey is the control server's ML-KEM-1024 encapsulation key.
	// The RemoteEngine generates a fresh ML-KEM-1024 ciphertext on every request
	// so the server can encrypt the response body; only the holder of the
	// corresponding private decapsulation key can recover the plaintext.
	// Required — must be a valid 1568-byte ML-KEM-1024 encapsulation key.
	KEMPublicKey *mlkem.EncapsulationKey1024
	// ValidityDuration is the maximum age of a response timestamp that will be
	// accepted.  Must be non-zero; a value of 30s is a reasonable default.
	ValidityDuration time.Duration
}

ControlConfig provides the initial information needed to bootstrap the authorisation engine

type ServiceConfig

type ServiceConfig struct {
	// Class defines the type of service being provided by the process
	Class string
	// ID is the unique id of this specific process
	ID string
	// contains filtered or unexported fields
}

ServiceConfig provides details of the process that is using the authorisation engine. This is sent to the ControlConfig such that it is aware of which service types and actual instances are using it, and also to filter the data the engine receives to prevent overloading the process with unnecessary policies etc.

Directories

Path Synopsis
cmd
demo command
Package main is a self-contained demonstration of authdsl using two in-process httptest servers to simulate the remote control infrastructure.
Package main is a self-contained demonstration of authdsl using two in-process httptest servers to simulate the remote control infrastructure.
eval command
Package main demonstrates eval.Evaluator for static in-process policy evaluation without any remote infrastructure.
Package main demonstrates eval.Evaluator for static in-process policy evaluation without any remote infrastructure.
historic command
Package main demonstrates EvaluateAsOfOBO — historic policy re-evaluation with mandatory authority checking.
Package main demonstrates EvaluateAsOfOBO — historic policy re-evaluation with mandatory authority checking.
mldsa_tokens command
Package main demonstrates an engine enforced to ML-DSA-87 tokens only.
Package main demonstrates an engine enforced to ML-DSA-87 tokens only.
negotiation command
Package main demonstrates algorithm negotiation with both ED25519 and ML-DSA-87 subject tokens accepted simultaneously.
Package main demonstrates algorithm negotiation with both ED25519 and ML-DSA-87 subject tokens accepted simultaneously.
obo command
Package main demonstrates OBO (on-behalf-of) delegation using authdsl.
Package main demonstrates OBO (on-behalf-of) delegation using authdsl.
session command
Package main extends the cmd/demo scenario to demonstrate EvaluateForSession and EvaluateOBOForSession.
Package main extends the cmd/demo scenario to demonstrate EvaluateForSession and EvaluateOBOForSession.
Package dsl provides a compiler and evaluator for the Policy DSL v3.0.0.
Package dsl provides a compiler and evaluator for the Policy DSL v3.0.0.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL