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:
| Scope | The delegate may |
|---|---|
changes | sign changes authored by you |
seal | seal changes authored by you |
refs | move 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.