oidclogin

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package oidclogin implements `behalf login` — the verified identity root (D5, Q17, Q21, Q22) — and its offline re-verification.

The flow is ordinary OAuth 2.1 authorization-code + PKCE (S256) on a loopback redirect against any conformant OIDC provider, with one deviation: the OIDC nonce is the RFC 7638 JWK thumbprint of a freshly generated device Ed25519 key. OIDC Core requires the AS to echo the nonce into the signed ID token, so the IdP signs the thumbprint back without knowing it is doing anything unusual. The offline predicate is three checks (D5, Q17):

  1. the IdP signed the ID token (against the JWKS snapshot taken at login),
  2. nonce == jkt(device_pubkey),
  3. the root delegation statement is signed by that device key.

Everything above the root is asserted; skipping login yields permanently-asserted records (Q21).

Index

Constants

View Source
const (
	CheckIdPSignature = "idp-signature"             // check 1
	CheckNonceBinding = "nonce-binding"             // check 2
	CheckRootDelegSig = "root-delegation-signature" // check 3
)

Check names (the D5 predicate, in order).

View Source
const DefaultRootMaxDepth = 4

DefaultRootMaxDepth is del_max_depth minted on the root delegation: the deepest chain the D5 prototype exercised plus one for the proxy hop.

View Source
const DefaultRootTTL = 90 * 24 * time.Hour

DefaultRootTTL is the root delegation statement's validity. The ID token's own exp is minutes; the root delegation is the standing local authority anchor, so it gets its own expiry, recorded verbatim in the hop and re-minted by the next login.

View Source
const LoginStateFile = "login.json"

LoginStateFile is the login index file name under the state dir.

View Source
const OtelConventionsVersion = "1.29.0"

OtelConventionsVersion is the gen_ai.* conventions version in force at capture (Q8, Q49).

View Source
const PayloadTypeStatement = "application/vnd.behalf.root-delegation+json"

PayloadTypeStatement is the DSSE payloadType for root delegation statements.

View Source
const RootParHash = "0000000000000000000000000000000000000000000000000000000000000000"

RootParHash is par_hash at depth 0. The AAT draft gives the root no parent, but the frozen hop schema requires the field; all-zeros is the explicit no-parent sentinel. Exported so internal/aat mints and checks the same sentinel rather than defining a second one that could drift.

View Source
const SpoolFile = "spool.jsonl"

SpoolFile is the local receipt spool under the state dir. Each line is a signed spool entry; the log service consumes it when it lands.

View Source
const StatementSchemaVersion = "behalf.sh/root-delegation/v1"

StatementSchemaVersion is the root delegation statement's projection key.

View Source
const VerificationMethodRoot = "oidc-nonce-binding"

VerificationMethodRoot names the D5 three-check root predicate on the hop's verification object (Q17).

Variables

View Source
var ErrBlobMissing = errors.New("oidclogin: blob missing from store")

ErrBlobMissing marks a blob that is not in the store — the customer deleted it (or never had it). Distinct from ErrBlobTampered because a verifier must tell "deleted" apart from "altered" (Q22, Q36, D7).

View Source
var ErrBlobTampered = errors.New("oidclogin: blob content does not match its digest")

ErrBlobTampered marks a blob whose bytes no longer hash to their name.

View Source
var ErrNoLogin = errors.New("oidclogin: no login recorded")

ErrNoLogin means no login has ever completed in this state dir. Records emitted from here carry asserted attribution forever (Q21): immutability means no retro-upgrade.

Functions

func SubDigest

func SubDigest(issuer, sub string) string

SubDigest returns the pseudonymous principal identifier: lowercase-hex SHA-256 over issuer, a single 0x0A, and the raw OIDC sub (Q40). The raw sub never leaves the customer-held ID-token blob.

Types

type Check

type Check struct {
	Name   string
	Status CheckStatus
	Detail string
}

Check is one predicate check's result.

type CheckStatus

type CheckStatus string

CheckStatus is one check's disposition.

const (
	CheckPass    CheckStatus = "pass"
	CheckFail    CheckStatus = "fail"
	CheckSkipped CheckStatus = "skipped"
)

Check dispositions.

type Config

type Config struct {
	// Issuer is the OIDC issuer URL (its /.well-known/openid-configuration
	// must be conformant). Required.
	Issuer string
	// ClientID is the OAuth public client id. Required.
	ClientID string
	// Dir is the resolved state directory (identity.ResolveDir). Required.
	Dir string
	// Scopes defaults to [openid, email, profile].
	Scopes []string
	// NoBrowser suppresses opening the system browser; the auth URL is
	// delivered via OnAuthURL for the caller to print (or, in tests, to
	// drive programmatically).
	NoBrowser bool
	// OnAuthURL, if set, receives the authorization URL once the loopback
	// listener is accepting the redirect.
	OnAuthURL func(url string)
	// HTTPClient overrides the client used for discovery, JWKS snapshot
	// and token exchange. Nil means http.DefaultClient.
	HTTPClient *http.Client
	// Now overrides the clock (tests). Nil means time.Now.
	Now func() time.Time
	// Entropy overrides the ULID entropy source that mints the root
	// delegation's `jti`, the root receipt's `receipt_id` and its `run_id`.
	// Nil means crypto/rand, which is what a real login uses. A
	// deterministic recording injects a fixed stream so the login it
	// performs is byte-reproducible (see cmd/behalf-record).
	Entropy io.Reader
	// DeviceKey, if set, is bound as the human's device key instead of a
	// freshly generated one.
	//
	// DEMO AND TEST MATERIAL ONLY. A real login generates a fresh key per
	// login (that is what makes the nonce bind THIS login to THIS key), and
	// the CLI never sets this. It exists for the same reason
	// cmd/behalf-record pins the emitter key: a recording signed by a random
	// key is not reproducible and cannot be named in a checked-in alias map.
	// The login it performs is otherwise the real flow — real PKCE, real
	// token exchange, a real IdP-signed ID token echoing jkt(DeviceKey).
	DeviceKey *identity.Key
}

Config configures Login.

type LoginState

type LoginState struct {
	SchemaVersion   string `json:"schema_version"` // behalf.sh/login-state/v1
	Issuer          string `json:"issuer"`
	ClientID        string `json:"client_id"`
	SubDigest       string `json:"sub_digest"`
	DeviceJKT       string `json:"device_jkt"`
	IDTokenDigest   string `json:"id_token_digest"`
	JWKSDigest      string `json:"jwks_digest"`
	StatementDigest string `json:"statement_digest"`
	ReceiptID       string `json:"receipt_id"`
	LoggedInAt      string `json:"logged_in_at"` // RFC 3339
}

LoginState is the persisted index of the last completed login.

func LoadLoginState

func LoadLoginState(stateDir string) (*LoginState, error)

LoadLoginState reads login.json from stateDir. Returns ErrNoLogin (wrapped) if it does not exist.

type Report

type Report struct {
	State   State
	Checks  []Check
	Reasons []string
	Login   *LoginState
	// DeviceJKT is the thumbprint of the key this login bound — read from
	// the signed statement where it survives, else from login.json (which is
	// an index, not evidence). A chain root whose cnf.jwk is not this key was
	// not minted by this human's device key.
	DeviceJKT string
	// StatementDigest addresses the signed root delegation statement in the
	// customer-held store: the evidence behind a verified depth-0 hop.
	StatementDigest string
	// Issuer is the OIDC issuer that signed the ID token.
	Issuer string
}

Report is what VerifyRoot found. Reasons explains every non-verified outcome in plain language.

func VerifyRoot

func VerifyRoot(stateDir string) (*Report, error)

VerifyRoot runs the offline root predicate against stateDir. It returns ErrNoLogin (wrapped) when no login has ever completed there.

type Result

type Result struct {
	StateDir        string
	Issuer          string
	ClientID        string
	SubDigest       string
	DeviceJKT       string
	IDTokenDigest   string
	JWKSDigest      string
	StatementDigest string
	ReceiptID       string
	SealedReceipt   []byte // the sealed receipt payload bytes, as spooled
}

Result is what a completed login produced and persisted.

func Login

func Login(ctx context.Context, cfg Config) (*Result, error)

Login runs the browser (or --no-browser) OIDC flow, persists the JWKS snapshot, the full raw ID token and the device-key-signed root delegation statement as customer-held blobs (Q22), appends the root delegation receipt to the local spool, and records login.json.

type State

type State string

State is the root verification outcome.

const (
	StateVerified State = "verified"
	StateDegraded State = "degraded"
	StateBroken   State = "broken"
)

The three outcomes. A fourth condition — no login ever — is not a State: VerifyRoot returns ErrNoLogin and the caller renders the asserted-forever warning (Q21).

type Statement

type Statement struct {
	SchemaVersion string   `json:"schema_version"`
	JTI           string   `json:"jti"` // per-hop token id, behalf extension (Q11)
	Issuer        string   `json:"issuer"`
	SubDigest     string   `json:"sub_digest"` // sha256(issuer "\n" sub), never the raw sub (Q40)
	NonceJKT      string   `json:"nonce_jkt"`  // = jkt(device_pubkey) = the OIDC nonce (D5)
	IDTokenDigest string   `json:"id_token_digest"`
	JWKSDigest    string   `json:"jwks_digest"`
	DeviceJWK     dsse.JWK `json:"device_jwk"`
	DelegatedAt   string   `json:"delegated_at"` // RFC 3339
	Exp           int64    `json:"exp"`          // unix seconds
}

Statement is the root delegation statement payload. Field order is serialization order; it is sealed once and the bytes are frozen (the span rule, export-format-v1.md §1.2).

Jump to

Keyboard shortcuts

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