In questa sezione

Questa guida mostra due modi di usare a runtime una specifica Lean dimostrata, quando il codice da verificare non è Lean:

  • Un monitor a runtime riceve gli eventi di un sistema in esecuzione, uno alla volta. È dimostrato che accetta esattamente le tracce di eventi che un protocollo consente, quindi ogni violazione che segnala è reale.
  • Un oracolo di test è un’implementazione di riferimento dimostrata. Il codice scritto a mano, in qualsiasi linguaggio, viene verificato rispetto a esso invece di essere accettato sulla fiducia.

L’esempio di riferimento è lungo-rs/examples/oracle-monitor, una politica di controllo degli accessi con un protocollo di sessione.

Scrivi il protocollo

Un protocollo (Lungo.Trace.Protocol ε) è l’insieme delle tracce di eventi a cui un componente può partecipare, chiuso rispetto ai prefissi. Il suo lato eseguibile è un reducer (Lungo.Trace.Reducer ε σ): uno stato iniziale e un passo che accetta un evento, dando lo stato successivo, oppure lo rifiuta.

import Lungo

namespace Access

open Lungo Lungo.Trace

inductive Event where
  | signIn (user role : String)
  | access (user action : String)
  | signOut (user : String)

/-- Who is signed in, with their role. -/
structure Session where
  users : List (String × String)

/-- The session after `e`; `none` when `e` breaks the protocol. -/
def next (policy : List Rule) (s : Session) (e : Event) : Option Session := ...

/-- No one signed in: the session a host starts monitoring from. -/
def start : Session := ⟨[]⟩

/-- The session protocol under `policy`. -/
@[lungo_spec "lungo.protocol"]
noncomputable def Sessions (policy : List Rule) : Protocol Event :=
  (Reducer.mk start (next policy)).protocol

Un protocollo è un oggetto logico, non codice da eseguire, quindi Sessions è noncomputable. lungo non lo esporta mai, e @[lungo_spec] lo registra come specifica che i claim possono citare.

Costruisci il monitor

Un monitor (Lungo.Monitor ε σ) ha uno stato iniziale start e un passo step : σ → ε → Except String σ. Un passo dà lo stato successivo, oppure il motivo per cui l’evento è una violazione. Monitor.run elabora un’intera traccia, Monitor.Accepts m t dice che m accetta la traccia t dal suo stato iniziale, e Monitor.Sound m p dice che m accetta esattamente le tracce che il protocollo p consente.

Costruisci i monitor con questi combinatori. Ognuno ha il proprio teorema di correttezza:

Monitor Costruito da Corretto per Teorema
Monitor.ofPredicate ok reason un controllo per ogni evento Protocol.always (ok · = true) ofPredicate_sound
Monitor.ofReducer r reason un reducer, con un motivo per ogni rifiuto r.protocol ofReducer_sound
Monitor.both a b due monitor eseguiti affiancati p.both q, dati a.Sound p e b.Sound q both_sound

L’esempio monitora il proprio reducer:

/-- Why `e` breaks the protocol in session `s`. -/
def reason (s : Session) (e : Event) : String := ...

private def monitor (policy : List Rule) : Monitor Event Session :=
  Monitor.ofReducer ⟨start, next policy⟩ reason

/-- The session after `e`, or the violation it is. -/
def observe (policy : List Rule) (s : Session) (e : Event) : Except String Session :=
  (monitor policy).step s e

L’host ha bisogno dello stato iniziale e del passo come funzioni, quindi gli export sono start e observe. Il valore Monitor in sé non serve fuori da Lean, quindi è private.

Afferma che è corretto

Enuncia il claim sulle funzioni esportate, con la relazione lungo.monitors:

@[lungo_claim "lungo.monitors" subject start observe spec Sessions]
theorem observe_sound (policy : List Rule) :
    (Monitor.mk start (observe policy)).Sound (Sessions policy) :=
  Monitor.ofReducer_sound (Reducer.mk start (next policy)) reason

Un claim lungo.monitors deve enunciare Lungo.Monitor.Sound m p, con m che menziona ogni soggetto. lungo verifica questa forma quando l’attributo viene applicato, e di nuovo quando legge i record. Perché la build fallisca se il claim viene rimosso o si basa su sorry, richiedilo:

# lungo.toml
[assurance]
require-claims = ["Access.observe"]

(Builder::require_claims("Access.observe") in build.rs.)

Passagli gli eventi dall’host

L’host conserva la sessione e chiama observe con ogni evento. Che cosa fare di una violazione (registrarla, rifiutare la richiesta, avvisare qualcuno) lo decide l’host; la dimostrazione garantisce soltanto che si tratta di una violazione.

Rust

let mut session = access::start();
for event in events {
    match access::observe(policy.clone(), session.clone(), event) {
        Ok(next) => session = next,
        Err(reason) => eprintln!("violation: {reason}"),
    }
}

Python

session = access.start()
for event in events:  # access.EventSignIn("ada", "editor"), access.EventAccess("ada", "write"), …
    verdict = access.observe(policy, session, event)
    if isinstance(verdict, lungo_py.Ok):
        session = verdict.value
    else:
        log.warning("violation: %s", verdict.error)

Go

session, err := access.Start()
if err != nil {
	return err
}
for _, e := range events { // access.EventSignIn{User: "ada", Role: "editor"}, …
	verdict, err := access.Observe(policy, session, e)
	if err != nil {
		return err
	}
	if verdict.Ok {
		session = verdict.Value
	} else {
		log.Printf("violation: %s", verdict.Error)
	}
}

TypeScript

let session = p.start();
for (const e of events) { // { kind: "signIn", user: "ada", role: "editor" }, …
  const verdict = p.observe(policy, session, e);
  if (verdict.ok) session = verdict.value;
  else console.warn(`violation: ${verdict.error}`);
}

Swift

var session = try start()
for e in events { // .signIn(user: "ada", role: "editor"), …
    switch try observe(policy, session, e) {
    case .ok(let next): session = next
    case .error(let reason): print("violation: \(reason)")
    }
}

C

access_start e access_observe restituiscono la sessione e il valore Except. Leggi quest’ultimo con lungo_value_is_ok e lungo_value_get_except.

Usa una funzione dimostrata come oracolo di test

Un oracolo è una specifica che si può eseguire. Nell’esempio, evaluate decide una richiesta rispetto a una politica, ed è dimostrato che decide esattamente la relazione Permits:

@[lungo_spec "lungo.relation"]
def Permits (policy : List Rule) (r : Request) : Prop :=
  (∃ rule ∈ policy, rule.covers r = true ∧ rule.effect = .allow) ∧
    ∀ rule ∈ policy, rule.covers r = true → rule.effect ≠ .deny

@[lungo_role "lungo.oracle"]
def evaluate (policy : List Rule) (r : Request) : Bool := ...

@[lungo_claim "lungo.decides" subject evaluate spec Permits]
theorem evaluate_decides (policy : List Rule) (r : Request) :
    evaluate policy r = true ↔ Permits policy r := ...

@[lungo_role "lungo.oracle"] registra lo scopo dell’export. Il ruolo compare nella voce dell’export in assurance.json e nella sua rappresentazione in ogni linguaggio, per esempio __meta::declaration("Access.evaluate").unwrap().assurance.roles in Rust.

Il codice di produzione che deve essere veloce, come un valutatore indicizzato per ruolo e azione, è scritto a mano e verificato rispetto all’oracolo su input generati:

for seed in 0..300 {
    let rules = policy(seed, (seed % 8) as usize);
    let index = Index::new(&rules);
    let lean = List::from(rules);
    for role in ROLES {
        for action in ACTIONS {
            let oracle = access::evaluate(lean.clone(), Request { role: role.into(), action: action.into() });
            assert_eq!(index.permits(role, action), oracle, "seed {seed}: {role} {action}");
        }
    }
}

Il test Python dell’esempio fa lo stesso con un valutatore Python. Un valutatore che ignora le regole di divieto fallisce il test entro pochi seed. Poiché è dimostrato che l’oracolo decide Permits, concordare con esso significa concordare con la specifica su ogni input verificato, non con un’altra implementazione che potrebbe avere lo stesso difetto.

Vedi anche