OXYGENHUB

Identity and Signing

Andy McCall edited this page · 1 revision

Identity and Signing

Every change and every bookmark move in Oxygen is authenticated. Signatures are composite: an Ed25519 signature and an ML-DSA-65 (post-quantum, FIPS 204) signature over the same message. Both must verify, so neither can be stripped.

Identities

classDiagram
    class Identity {
        ed25519 public key
        ML-DSA-65 public key
        name, email
        created, expires
        previous: KeyId of the identity it replaces
        delegates: keys that may act for it
    }
    class Delegate {
        key: KeyId
        scope: changes, seal, refs
        expires
    }
    Identity "1" *-- "many" Delegate
    Identity --> Identity : previous

An identity's KeyId is the hash of the whole identity object. Any change to it (a new expiry, a delegate added) is therefore a new identity that names the old one as previous: a rotation. Old signatures stay valid under the old identity, which stays stored.

oxy identity create --name "Ada" --email [email protected]
oxy identity show
oxy identity rotate                  # fresh key pair
oxy identity renew --expires 365     # same keys, new expiry
oxy identity revoke 3f2a… --reason "laptop stolen"

Signing changes: now or at push

flowchart TD
    Commit[oxy commit] --> Q{core.sign}
    Q -->|change| Sign[Sign each change now]
    Q -->|seal, the default| Unsigned[Leave changes unsigned locally]
    Unsigned --> Push[oxy push]
    Push --> Seal["One seal: a signature over<br/>the hashes of every unsigned change"]
    Sign --> Push2[oxy push]
    Seal --> Ref[Sign the bookmark move]
    Push2 --> Ref

A composite signature is about 3.4 KB, often larger than the change it signs. A seal signs a whole batch of change hashes once, at push, instead.

Signing bookmark moves

Every published bookmark update carries an attestation: a signed statement binding the repository, the bookmark name, its old and new state, and a serial number that only goes up. A client refuses an older serial than one it has seen, so nobody can roll a bookmark back by replaying an old attestation.

sequenceDiagram
    participant You
    participant Server
    participant Other as Another clone
    You->>Server: push: changes, seal, attestation (main: S1 to S2, serial 8)
    Server->>Server: check signatures, policy, old state = S1
    Server-->>You: accepted
    Other->>Server: fetch
    Server-->>Other: main = S2 with its attestation
    Other->>Other: check attestation against the trust policy,<br/>serial 8 > last seen 7

Delegates: letting an agent sign for you

A delegate is another key that may act for your identity within a scope:

ScopeThe delegate may
changessign changes authored by you
sealseal changes authored by you
refsmove bookmarks and tags as you would

An AI agent gets changes alone, usually with an expiry: its signature proves which changes it made, but it can never move a bookmark. You land its work.

flowchart LR
    Agent["Agent key<br/>(its own home, never your secret)"] -->|signs| C[Change<br/>author: you]
    You[Your identity] -->|delegates: changes, expires 8h| Agent
    You -->|signs| Push[Bookmark move]
    C --> Push
oxy identity delegate add agent.identity --scope changes --expires 8h
oxy log -r 'delegated()'          # changes an agent signed for you
oxy verify                        # names the agent on each of them

Receipts from CI

A runner such as OxygenFlow records each job as a receipt (the pipeline, the files in scope, the environment, the outputs and the exit code) and signs an attestation binding it to the state it ran on. Anyone can check it later with oxy receipt verify.

Who may publish is decided by the Trust Policy.