1 Authentication
Virgil edited this page 2026-02-19 17:00:15 +00:00

Authentication

Back to Home

The auth package (forge.lthn.ai/core/go-crypt/auth) implements OpenPGP challenge-response authentication with support for both online (HTTP) and air-gapped (file-based courier) transport. It was ported from dAppServer's mod-auth/lethean.service.ts.

Overview

Authentication uses PGP key pairs for identity verification. Users register with a username and password, which generates a PGP keypair and stores credentials via an io.Medium storage backend (disk, memory, or any custom implementation).

Two authentication flows are supported:

  1. Online — Challenge-response over HTTP
  2. Air-gapped (Courier) — Challenge-response via files exchanged on a Medium, suitable for air-gapped systems

Authentication Flow

Online Flow

Client                              Server
  |                                    |
  |-- Register(username, password) --> |  Generate PGP keypair, store credentials
  |                                    |
  |-- CreateChallenge(userID) -------> |  Generate nonce, encrypt with user's public key
  |<-- Challenge (encrypted nonce) --- |
  |                                    |
  |  Decrypt nonce, sign with          |
  |  private key                       |
  |                                    |
  |-- ValidateResponse(signed) ------> |  Verify signature, create session
  |<-- Session (token, 24hr TTL) ----- |

Air-Gapped (Courier) Flow

The same cryptographic operations, but challenge and response are exchanged via files on a Medium rather than over HTTP:

Server                                Client
  |                                      |
  |-- WriteChallengeFile(path) --------> |  Write encrypted challenge to file
  |                                      |  (transport file via USB, etc.)
  |                                      |
  |                                      |  Decrypt, sign, write response to file
  |                                      |
  |<-- ReadResponseFile(path) ---------- |  Read response, validate, create session

Types

User

type User struct {
    PublicKey    string    `json:"public_key"`
    KeyID        string    `json:"key_id"`
    Fingerprint  string    `json:"fingerprint"`
    PasswordHash string    `json:"password_hash"` // LTHN hash
    Created      time.Time `json:"created"`
    LastLogin    time.Time `json:"last_login"`
}

Challenge

type Challenge struct {
    Nonce     []byte    `json:"nonce"`
    Encrypted string    `json:"encrypted"` // PGP-encrypted nonce (armored)
    ExpiresAt time.Time `json:"expires_at"`
}

Challenges have a default TTL of 5 minutes. After expiry, the challenge is rejected.

Session

type Session struct {
    Token     string    `json:"token"`
    UserID    string    `json:"user_id"`
    ExpiresAt time.Time `json:"expires_at"`
}

Sessions have a default TTL of 24 hours. Tokens are 32-byte cryptographically random hex strings.

Creating an Authenticator

import (
    "forge.lthn.ai/core/go-crypt/auth"
    "forge.lthn.ai/core/go/pkg/io"
)

// Using a disk-based Medium
medium := io.NewDiskMedium("/path/to/data")
authenticator := auth.New(medium)

// With custom TTLs
authenticator := auth.New(medium,
    auth.WithChallengeTTL(10 * time.Minute),
    auth.WithSessionTTL(48 * time.Hour),
)

Storage Layout

All user data is persisted through the io.Medium interface:

users/
  {userID}.pub      PGP public key (armored)
  {userID}.key      PGP private key (armored, password-encrypted)
  {userID}.rev      Revocation certificate (placeholder)
  {userID}.json     User metadata (encrypted with user's public key)
  {userID}.lthn     LTHN password hash

The userID is derived from the username using the LTHN hash algorithm (crypt/lthn).

API Reference

Registration

func (a *Authenticator) Register(username, password string) (*User, error)

Creates a new user account. Generates a PGP keypair (protected by the given password), hashes the username with LTHN to produce a userID, and persists all artifacts via the Medium.

user, err := authenticator.Register("alice", "strong-password")
if err != nil {
    log.Fatal(err)
}
fmt.Println(user.KeyID)        // LTHN hash of "alice"
fmt.Println(user.Fingerprint)  // LTHN hash of the public key

Challenge-Response (Online)

func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error)
func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Session, error)
// Server: create challenge
challenge, err := authenticator.CreateChallenge(userID)
// Send challenge.Encrypted to client

// Client: decrypt nonce, sign it with private key
// ...

// Server: validate the signed response
session, err := authenticator.ValidateResponse(userID, signedNonce)
fmt.Println(session.Token)     // Hex session token
fmt.Println(session.ExpiresAt) // 24 hours from now

Challenge-Response (Air-Gapped)

func (a *Authenticator) WriteChallengeFile(userID, path string) error
func (a *Authenticator) ReadResponseFile(userID, path string) (*Session, error)
// Server: write challenge to a file (for transport to air-gapped client)
err := authenticator.WriteChallengeFile(userID, "challenge.json")

// Client: (on air-gapped machine) decrypt, sign, write response to file
// ...

// Server: read response file and validate
session, err := authenticator.ReadResponseFile(userID, "response.pgp")

Password-Based Login

func (a *Authenticator) Login(userID, password string) (*Session, error)

A convenience method that bypasses the PGP challenge-response flow. Verifies the password against the stored LTHN hash and creates a session on success.

session, err := authenticator.Login(userID, "strong-password")

Session Management

func (a *Authenticator) ValidateSession(token string) (*Session, error)
func (a *Authenticator) RefreshSession(token string) (*Session, error)
func (a *Authenticator) RevokeSession(token string) error
// Check if a session is valid
session, err := authenticator.ValidateSession(token)

// Extend a session's expiry
session, err = authenticator.RefreshSession(token)

// Invalidate a session immediately
err = authenticator.RevokeSession(token)

User Deletion

func (a *Authenticator) DeleteUser(userID string) error

Removes a user and all associated keys from storage. The "server" user is protected and cannot be deleted, mirroring the original TypeScript implementation's safeguard.

err := authenticator.DeleteUser(userID)
// Revokes all active sessions for this user

Default Parameters

Parameter Value Configurable Via
Challenge TTL 5 minutes auth.WithChallengeTTL()
Session TTL 24 hours auth.WithSessionTTL()
Nonce size 32 bytes Not configurable
Session token 32 bytes (hex-encoded) Not configurable
Protected users "server" Not configurable

See Also

  • Home — Package overview and quick start
  • Encryption-and-Hashing — Symmetric encryption and PGP operations used by the auth package
  • Trust-Engine — Agent trust tiers and capability-based access control