99 lines
2.8 KiB
Plaintext
99 lines
2.8 KiB
Plaintext
-- libertaria/identity.jan
|
|
-- Cryptographic identity for sovereign agents
|
|
-- Exit is Voice: Identity can be rotated, expired, or burned
|
|
|
|
module Identity exposing
|
|
( Identity
|
|
, create, rotate, burn
|
|
, is_valid, is_expired
|
|
, public_key, fingerprint
|
|
, sign, verify
|
|
)
|
|
|
|
import crypto.{ed25519, hash}
|
|
import time.{timestamp, duration}
|
|
|
|
-- Core identity type with cryptographic material and metadata
|
|
type Identity =
|
|
{ public_key: ed25519.PublicKey
|
|
, secret_key: ed25519.SecretKey -- Encrypted at rest
|
|
, created_at: timestamp.Timestamp
|
|
, expires_at: ?timestamp.Timestamp -- Optional expiry
|
|
, rotated_from: ?fingerprint.Fingerprint -- Chain of custody
|
|
, revoked: bool
|
|
}
|
|
|
|
-- Create new sovereign identity
|
|
-- Fresh keypair, no history, self-sovereign
|
|
fn create() -> Identity
|
|
let (pk, sk) = ed25519.generate_keypair()
|
|
let now = timestamp.now()
|
|
|
|
{ public_key = pk
|
|
, secret_key = sk
|
|
, created_at = now
|
|
, expires_at = null
|
|
, rotated_from = null
|
|
, revoked = false
|
|
}
|
|
|
|
-- Rotate identity: New keys, linked provenance
|
|
-- Old identity becomes invalid after grace period
|
|
fn rotate(old: Identity) -> (Identity, Identity)
|
|
assert not old.revoked "Cannot rotate revoked identity"
|
|
|
|
let (new_pk, new_sk) = ed25519.generate_keypair()
|
|
let now = timestamp.now()
|
|
let old_fp = fingerprint.of_identity(old)
|
|
|
|
let new_id =
|
|
{ public_key = new_pk
|
|
, secret_key = new_sk
|
|
, created_at = now
|
|
, expires_at = null
|
|
, rotated_from = some(old_fp)
|
|
, revoked = false
|
|
}
|
|
|
|
-- Old identity gets short grace period then auto-expires
|
|
let grace_period = duration.hours(24)
|
|
let expired_old = { old with expires_at = some(now + grace_period) }
|
|
|
|
(new_id, expired_old)
|
|
|
|
-- Burn identity: Cryptographic deletion
|
|
-- After burn, no messages can be signed, verification still works for history
|
|
fn burn(id: Identity) -> Identity
|
|
{ id with
|
|
secret_key = ed25519.zero_secret(id.secret_key)
|
|
, revoked = true
|
|
, expires_at = some(timestamp.now())
|
|
}
|
|
|
|
-- Check if identity is currently valid
|
|
fn is_valid(id: Identity) -> bool
|
|
not id.revoked and not is_expired(id)
|
|
|
|
-- Check if identity has expired
|
|
fn is_expired(id: Identity) -> bool
|
|
match id.expires_at with
|
|
| null -> false
|
|
| some(t) -> timestamp.now() > t
|
|
|
|
-- Get public key for sharing/verification
|
|
fn public_key(id: Identity) -> ed25519.PublicKey
|
|
id.public_key
|
|
|
|
-- Get fingerprint (short, unique identifier)
|
|
fn fingerprint(id: Identity) -> fingerprint.Fingerprint
|
|
fingerprint.of_key(id.public_key)
|
|
|
|
-- Sign message with this identity
|
|
fn sign(id: Identity, message: bytes.Bytes) -> signature.Signature
|
|
assert is_valid(id) "Cannot sign with invalid identity"
|
|
ed25519.sign(id.secret_key, message)
|
|
|
|
-- Verify signature against this identity's public key
|
|
fn verify(id: Identity, message: bytes.Bytes, sig: signature.Signature) -> bool
|
|
ed25519.verify(id.public_key, message, sig)
|