Chain.lean
Declarations on this page (46)
- abbrev HkdfFn
- def spqrHkdfWith
- def asciiBytes
- def bytesLt
- inductive ChainPanic
- inductive ChainError
- abbrev ChainResult
- def ofPanic
- def ofError
- structure KeyHistory
- def KEY_SIZE
- def new
- def add
- def clear
- def remove
- def gcLoop
- def gc
- def getLoop
- def get
- structure ChainEpochDirection
- def chainNextInfo
- def new
- def nextKeyInternal
- def nextKey
- def keyAdvanceLoop
- def key
- def intoPb
- def fromPb
- def clearNext
- structure ChainEpoch
- structure Chain
-
def EPOCHS_
TO_ KEEP_ PRIOR_ TO_ SEND_ EPOCH - def chainStartInfo
- def chainAddEpochInfo
- def cedForDirection
- def new
- def addEpoch
- def epochIdx
- def clearSendNextPrefix
- def sendKey
- def recvKey
- def intoPb
- def linksFromPb
- def fromPb
- def chainFromVersionNegotiation
- def chainFrom
SPQR V1: symmetric key chain (Chain, src/ and src/)
Trusted in part. The theorems of Computational/ use only part of this file (see Computational/): the rest (the private self-test drivers run*, sendMany, recvAndCheckAll, testHkdf and testShaCompress, and the isPanic helpers of ChainError and ChainResult) is checked by Lean.
Panic-explicit specification of SPQR's symmetric per-epoch, per-direction hash chain (src/ and src/): HkdfFn, spqrHkdfWith, ChainPanic, ChainError, ChainResult, bounded skipped-key store KeyHistory (get, add, gc), directional chain state ChainEpochDirection (nextKey, key), and multi-epoch ChainEpoch / Chain (new, addEpoch, sendKey, recvKey, intoPb, fromPb, chainFromVersionNegotiation, chainFrom).
@[expose] public sectionnamespace CryptoVerification.LibsignalSpqrV1open CryptoGames.HKDF (hkdfExpand)Helper functions and HKDF parameter signature
abbrev HkdfFn
Signature of kdf::hkdf_to_vec / kdf::hkdf_to_slice: salt → ikm → info → okm_len → Bytes.
def spqrHkdfWith
HKDF-SHA256 instantiation of HkdfFn over compression function F (src/).
def asciiBytes
def asciiBytes (s : String) : Bytes :=
s.toList.map fun c => BitVec.ofNat 8 c.toNatdef bytesLt
Lexicographical < on byte slices.
inductive ChainPanic
Rust panic sites in src/.
inductive ChainPanic where
/-- `src/chain.rs:149`: `current_key < params.max_ooo_keys_or_default()` in `KeyHistory::gc`. -/
| gcKeyUnderflow
/-- `src/chain.rs:157`: `&self.data[i..i + 4]` out of bounds in `KeyHistory::gc`. -/
| gcSliceBounds
/-- `src/chain.rs:191`: `self.data.len() % KEY_SIZE != 0` in `KeyHistory::get`. -/
| keyHistoryUnaligned
/-- `src/chain.rs:192`: `at + max_ooo_keys_or_default()` `u32` overflow in `KeyHistory::get`. -/
| keyHistoryAddOverflow
/-- `src/chain.rs:230`: `next.len() != 32` in `ChainEpochDirection::next_key_internal`. -/
| nextKeyBadLen
/-- `src/chain.rs:231, 266, 271, 280`: `u32` addition overflow on `ctr`. -/
| ctrOverflow
/-- `src/chain.rs:361`: `self.current_epoch + 1` `u64` overflow in `Chain::add_epoch`. -/
| addEpochOverflow
/-- `src/chain.rs:361`: `epoch_secret.epoch != self.current_epoch + 1` in `Chain::add_epoch`. -/
| addEpochMismatch
/-- `src/chain.rs:405, 413, 421`: `self.links[epoch_index]` out of bounds. -/
| linksOutOfBounds
deriving DecidableEq, Repr, Inhabitedinductive ChainError
Error or panic outcome of a fallible chain operation.
inductive ChainError where
| err (e : Error)
| panic (p : ChainPanic)
deriving DecidableEq, Repr, Inhabited1 declaration not shown (lines 87–89): theorems, which Lean checks, or definitions that no headline statement uses
CryptoVerification.
abbrev ChainResult
abbrev ChainResult (α : Type) := Except ChainError αnamespace ChainResultdef ofPanic
def ofPanic (r : Except ChainPanic α) : ChainResult α :=
match r with
| .ok v => .ok v
| .error p => .error (.panic p)def ofError
def ofError (r : Except Error α) : ChainResult α :=
match r with
| .ok v => .ok v
| .error e => .error (.err e)1 declaration not shown (lines 107–110): theorems, which Lean checks, or definitions that no headline statement uses
CryptoVerification.
end ChainResult
KeyHistory (src/)
structure KeyHistory
Skipped-key table storing 36-byte entries [u8; 4][u8; 32] (4-byte BE index + 32-byte key).
structure KeyHistory where
data : Bytes := []
deriving DecidableEq, Repr, Inhabitednamespace KeyHistorydef KEY_SIZE
def KEY_SIZE : Nat := 4 + 32def new
def new : KeyHistory :=
{ data := [] }def add
def add (self : KeyHistory) (k : BitVec 32 × Bytes) (_params : ChainParamsPB) : KeyHistory :=
{ data := self.data ++ toBEBytes 4 k.1 ++ k.2 }def clear
def clear (_self : KeyHistory) : KeyHistory :=
{ data := [] }def remove
Swap-remove the 36-byte entry at byte offset myArrayIndex (src/).
def remove (self : KeyHistory) (myArrayIndex : Nat) (_params : ChainParamsPB) : KeyHistory :=
{ data :=
if myArrayIndex + KEY_SIZE < self.data.length then
let newEnd := self.data.length - KEY_SIZE
let moved := self.data.drop newEnd
let prefixBytes := self.data.take myArrayIndex
let restBytes := (self.data.take newEnd).drop (myArrayIndex + KEY_SIZE)
(prefixBytes ++ moved ++ restBytes).take newEnd
else
self.data.take myArrayIndex }1 declaration not shown (lines 146–150): theorems, which Lean checks, or definitions that no headline statement uses
CryptoVerification.
def gcLoop
Inner while i < self. loop of KeyHistory::gc (src/).
def gcLoop (self : KeyHistory) (trimHorizon : Bytes) (params : ChainParamsPB) (i : Nat) :
Except ChainPanic KeyHistory :=
if h : i < self.data.length then
if self.data.length < i + 4 then
.error .gcSliceBounds
else
let entryKey := (self.data.drop i).take 4
if bytesLt entryKey trimHorizon then
have _hlt : (self.remove i params).data.length < self.data.length :=
remove_length_lt self i params h
gcLoop (self.remove i params) trimHorizon params i
else
gcLoop self trimHorizon params (i + KEY_SIZE)
else
.ok self
termination_by self.data.length - i
decreasing_by
trimHorizon:Bytesparams:ChainParamsPBself:KeyHistoryi:ℕh:i < List.length self.datah✝¹:¬List.length self.data < i + 4entryKey:List (BitVec 8) := List.take 4 (List.drop i self.data)h✝:bytesLt entryKey trimHorizon = true_hlt:List.length (self.remove i params).data < List.length self.data⊢ List.length (self.remove i params).data - i < List.length self.data - i All goals completed! 🐙
trimHorizon:Bytesself:KeyHistoryi:ℕh:i < List.length self.datah✝¹:¬List.length self.data < i + 4entryKey:List (BitVec 8) := List.take 4 (List.drop i self.data)h✝:¬bytesLt entryKey trimHorizon = true⊢ List.length self.data - (i + KEY_SIZE) < List.length self.data - i trimHorizon:Bytesself:KeyHistoryi:ℕh:i < List.length self.datah✝¹:¬List.length self.data < i + 4entryKey:List (BitVec 8) := List.take 4 (List.drop i self.data)h✝:¬bytesLt entryKey trimHorizon = true⊢ List.length self.data - (i + (4 + 32)) < List.length self.data - i; All goals completed! 🐙def gc
KeyHistory::gc(&mut self, current_key: u32, params: &pqrpb::ChainParams) (src/). Panics with .gcKeyUnderflow at src/ if self. and current_key < max_ooo_keys_or_default().
def gc (self : KeyHistory) (currentKey : BitVec 32) (params : ChainParamsPB) :
Except ChainPanic KeyHistory :=
if self.data.length ≥ params.trimSize * KEY_SIZE then
let maxOoo := params.maxOooKeysOrDefault
if currentKey < maxOoo then
.error .gcKeyUnderflow
else
let trimHorizon := toBEBytes 4 (currentKey - maxOoo)
gcLoop self trimHorizon params 0
else
.ok selfdef getLoop
Inner for i in (0..self.KEY_SIZE) loop of KeyHistory::get (src/).
def getLoop (self : KeyHistory) (atIdx : BitVec 32) (want : Bytes) (params : ChainParamsPB)
(i : Nat) : ChainResult (KeyHistory × Bytes) :=
if _h : i < self.data.length then
if (self.data.drop i).take 4 == want then
let out := (self.data.drop (i + 4)).take 32
let self' := self.remove i params
.ok (self', out)
else
getLoop self atIdx want params (i + KEY_SIZE)
else
.error (.err (.keyAlreadyRequested atIdx))
termination_by self.data.length - i
decreasing_by
self:KeyHistorywant:Bytesi:ℕ_h:i < List.length self.datah✝:¬(List.take 4 (List.drop i self.data) == want) = true⊢ List.length self.data - (i + (4 + 32)) < List.length self.data - i; All goals completed! 🐙def get
KeyHistory::get (src/): retrieves and swap-removes the key for atIdx, checking 36-byte alignment, u32 overflow on atIdx + maxOoo, and the trim horizon currentCtr.
def get (self : KeyHistory) (atIdx currentCtr : BitVec 32) (params : ChainParamsPB) :
ChainResult (KeyHistory × Bytes) :=
if self.data.length % KEY_SIZE != 0 then
.error (.panic .keyHistoryUnaligned)
else
let maxOoo := params.maxOooKeysOrDefault
if atIdx.toNat + maxOoo.toNat ≥ 2 ^ 32 then
.error (.panic .keyHistoryAddOverflow)
else if atIdx + maxOoo < currentCtr then
.error (.err (.keyTrimmed atIdx))
else
let want := toBEBytes 4 atIdx
getLoop self atIdx want params 0end KeyHistory
ChainEpochDirection (src/)
structure ChainEpochDirection
Directional chain state within one epoch (src/).
structure ChainEpochDirection where
ctr : BitVec 32
next : Bytes
prev : KeyHistory
deriving DecidableEq, Repr, Inhabiteddef chainNextInfo
def chainNextInfo : Bytes :=
asciiBytes "Signal PQ Ratchet V1 Chain Next"namespace ChainEpochDirectiondef new
def new (k : Bytes) : ChainEpochDirection :=
{ ctr := 0#32
prev := KeyHistory.new
next := k }def nextKeyInternal
ChainEpochDirection::next_key_internal (src/): advances (next, ctr) via 64-byte HKDF (next' = out[..32], key = out[32..64]), panicking if next. or ctr == u32::MAX.
def nextKeyInternal (hkdf : HkdfFn) (next : Bytes) (ctr : BitVec 32) :
Except ChainPanic (Bytes × BitVec 32 × (BitVec 32 × Bytes)) :=
if next.length != 32 then
.error .nextKeyBadLen
else if ctr == 0xffffffff#32 then
.error .ctrOverflow
else
let ctr' := ctr + 1#32
let salt := List.replicate 32 0#8
let info := toBEBytes 4 ctr' ++ chainNextInfo
let genr8r := hkdf salt next info 64
let next' := genr8r.take 32
let key := (genr8r.drop 32).take 32
.ok (next', ctr', (ctr', key))1 declaration not shown (lines 261–276): theorems, which Lean checks, or definitions that no headline statement uses
CryptoVerification.
def nextKey
ChainEpochDirection::next_key(&mut self) -> (u32, Vec<u8>) (src/).
def nextKey (hkdf : HkdfFn) (self : ChainEpochDirection) :
Except ChainPanic (ChainEpochDirection × (BitVec 32 × Bytes)) := do
let (next', ctr', out) ← nextKeyInternal hkdf self.next self.ctr
return ({ self with next := next', ctr := ctr' }, out)def keyAdvanceLoop
Inner while at > self. loop of ChainEpochDirection::key (src/).
def keyAdvanceLoop (hkdf : HkdfFn) (self : ChainEpochDirection) (atIdx : BitVec 32)
(params : ChainParamsPB) : ChainResult ChainEpochDirection :=
if _h : self.ctr.toNat + 1 < atIdx.toNat then
match hnk : nextKeyInternal hkdf self.next self.ctr with
| .error p => .error (.panic p)
| .ok (next', ctr', k) =>
have _hctr' : ctr'.toNat = self.ctr.toNat + 1 :=
nextKeyInternal_ctr hkdf self.next next' self.ctr ctr' k hnk
let maxOoo := params.maxOooKeysOrDefault
if ctr'.toNat + maxOoo.toNat ≥ 2 ^ 32 then
.error (.panic .ctrOverflow)
else
let prev' :=
if ctr' + maxOoo ≥ atIdx then
self.prev.add k params
else
self.prev
keyAdvanceLoop hkdf { ctr := ctr', next := next', prev := prev' } atIdx params
else if self.ctr == 0xffffffff#32 then
-- Unreachable when `self.ctr < atIdx`, included for completeness of `self.ctr + 1` check.
.error (.panic .ctrOverflow)
else
.ok self
termination_by atIdx.toNat - self.ctr.toNat
decreasing_by
All goals completed! 🐙def key
ChainEpochDirection::key(&mut self, at: u32, params: &pqrpb::ChainParams) -> Result<Vec<u8>, Error> (src/).
def key (hkdf : HkdfFn) (self : ChainEpochDirection) (atIdx : BitVec 32)
(params : ChainParamsPB) : ChainResult (ChainEpochDirection × Bytes) :=
if atIdx < self.ctr then do
let (prev', out) ← self.prev.get atIdx self.ctr params
return ({ self with prev := prev' }, out)
else if atIdx == self.ctr then
.error (.err (.keyAlreadyRequested atIdx))
else
if atIdx - self.ctr > params.maxJumpOrDefault then
.error (.err (.keyJump self.ctr atIdx))
else
let maxOoo := params.maxOooKeysOrDefault
if self.ctr.toNat + maxOoo.toNat ≥ 2 ^ 32 then
.error (.panic .ctrOverflow)
else do
let self0 :=
if atIdx > self.ctr + maxOoo then
{ self with prev := self.prev.clear }
else
self
let self1 ← keyAdvanceLoop hkdf self0 atIdx params
let prevGc ← ChainResult.ofPanic (self1.prev.gc self1.ctr params)
let self2 := { self1 with prev := prevGc }
let (next', ctr', (_, outKey)) ←
ChainResult.ofPanic (nextKeyInternal hkdf self2.next self2.ctr)
return ({ self2 with next := next', ctr := ctr' }, outKey)def intoPb
def intoPb (self : ChainEpochDirection) : EpochDirectionPB :=
{ ctr := self.ctr
next := self.next
prev := self.prev.data }def fromPb
ChainEpochDirection::from_pb (src/), rejecting non-empty next of length ≠ 32.
def fromPb (pb : EpochDirectionPB) : Except Error ChainEpochDirection :=
if !pb.next.isEmpty && pb.next.length != 32 then
.error .stateDecode
else
.ok { ctr := pb.ctr, next := pb.next, prev := { data := pb.prev } }def clearNext
def clearNext (self : ChainEpochDirection) : ChainEpochDirection :=
{ self with next := [] }end ChainEpochDirection
ChainEpoch and Chain (src/)
structure ChainEpoch
Send and receive chains of a single epoch (src/).
structure ChainEpoch where
send : ChainEpochDirection
recv : ChainEpochDirection
deriving DecidableEq, Repr, Inhabitedstructure Chain
Full symmetric chain state (src/).
structure Chain where
dir : Direction
currentEpoch : Epoch
sendEpoch : Epoch
links : List ChainEpoch
nextRoot : Bytes
params : ChainParamsPB
deriving DecidableEq, Repr, Inhabited
def EPOCHS_
def EPOCHS_TO_KEEP_PRIOR_TO_SEND_EPOCH : Nat := 1def chainStartInfo
"Signal PQ Ratchet V1 Chain Start" (src/; two spaces before Start).
def chainStartInfo : Bytes :=
asciiBytes "Signal PQ Ratchet V1 Chain Start"def chainAddEpochInfo
def chainAddEpochInfo : Bytes :=
asciiBytes "Signal PQ Ratchet V1 Chain Add Epoch"namespace Chaindef cedForDirection
def cedForDirection (genr8r : Bytes) (dir : Direction) : ChainEpochDirection :=
ChainEpochDirection.new <|
match dir with
| .a2b => (genr8r.drop 32).take 32
| .b2a => (genr8r.drop 64).take 32def new
def new (hkdf : HkdfFn) (initialKey : Bytes) (dir : Direction) (params : ChainParamsPB) :
Except Error Chain :=
let salt := List.replicate 32 0#8
let genr8r := hkdf salt initialKey chainStartInfo 96
let epoch0 : ChainEpoch :=
{ send := cedForDirection genr8r dir
recv := cedForDirection genr8r dir.switch }
.ok
{ dir := dir
currentEpoch := 0#64
sendEpoch := 0#64
links := [epoch0]
nextRoot := genr8r.take 32
params := params }def addEpoch
Chain::add_epoch(&mut self, epoch_secret: EpochSecret) (src/).
-
Panics with .
addEpochOverflowifself.(current_epoch == u64::MAX src/).chain.rs:361 -
Panics with .
addEpochMismatchifepoch_secret.(epoch != self. current_epoch + 1 src/).chain.rs:361
def addEpoch (hkdf : HkdfFn) (self : Chain) (epochSecret : EpochSecret) :
Except ChainPanic Chain :=
if self.currentEpoch == 0xffffffffffffffff#64 then
.error .addEpochOverflow
else if epochSecret.epoch != self.currentEpoch + 1#64 then
.error .addEpochMismatch
else
let genr8r := hkdf self.nextRoot epochSecret.secret chainAddEpochInfo 96
let newLink : ChainEpoch :=
{ send := cedForDirection genr8r self.dir
recv := cedForDirection genr8r self.dir.switch }
.ok
{ self with
currentEpoch := epochSecret.epoch
nextRoot := genr8r.take 32
links := self.links ++ [newLink] }def epochIdx
def epochIdx (self : Chain) (epoch : Epoch) : Except Error Nat :=
if epoch > self.currentEpoch then
.error (.epochOutOfRange epoch)
else
let back := (self.currentEpoch - epoch).toNat
let linksLen := self.links.length
if back ≥ linksLen then
.error (.epochOutOfRange epoch)
else
.ok (linksLen - 1 - back)def clearSendNextPrefix
Clear send. on the first n elements of links (for i in 0..epoch_index in Chain::send_key, src/).
def clearSendNextPrefix : Nat → List ChainEpoch → List ChainEpoch
| 0, xs => xs
| _ + 1, [] => []
| n + 1, x :: xs => { x with send := x.send.clearNext } :: clearSendNextPrefix n xsdef sendKey
Derive the next sending key (index, key) in epoch, dropping older epochs when the send epoch advances. Chain::send_key(&mut self, epoch: Epoch) -> Result<(u32, Vec<u8>), Error> (src/).
def sendKey (hkdf : HkdfFn) (self : Chain) (epoch : Epoch) :
ChainResult (Chain × (BitVec 32 × Bytes)) :=
if epoch < self.sendEpoch then
.error (.err (.sendKeyEpochDecreased self.sendEpoch epoch))
else do
let epochIndex0 ← ChainResult.ofError (self.epochIdx epoch)
let (self1, epochIndex) :=
if self.sendEpoch != epoch then
let dropCount := epochIndex0 - EPOCHS_TO_KEEP_PRIOR_TO_SEND_EPOCH
let epochIndex' := epochIndex0 - dropCount
let links1 := self.links.drop dropCount
let links2 := clearSendNextPrefix epochIndex' links1
({ self with sendEpoch := epoch, links := links2 }, epochIndex')
else
(self, epochIndex0)
match self1.links[epochIndex]? with
| none => .error (.panic .linksOutOfBounds)
| some link => do
let (send', out) ← ChainResult.ofPanic (link.send.nextKey hkdf)
let links' := self1.links.set epochIndex { link with send := send' }
return ({ self1 with links := links' }, out)def recvKey
Derive the receiving key for message index of epoch; epoch 0, index 0 yields the empty key. Chain::recv_key(&mut self, epoch: Epoch, index: u32) -> Result<Vec<u8>, Error> (src/).
def recvKey (hkdf : HkdfFn) (self : Chain) (epoch : Epoch) (index : BitVec 32) :
ChainResult (Chain × Bytes) :=
if epoch == 0#64 && index == 0#32 then
.ok (self, [])
else do
let epochIndex ← ChainResult.ofError (self.epochIdx epoch)
match self.links[epochIndex]? with
| none => .error (.panic .linksOutOfBounds)
| some link => do
let (recv', out) ← link.recv.key hkdf index self.params
let links' := self.links.set epochIndex { link with recv := recv' }
return ({ self with links := links' }, out)def intoPb
def linksFromPb
def linksFromPb : List ChainEpochPB → Except Error (List ChainEpoch)
| [] => .ok []
| link :: rest => do
let sendPb ←
match link.send with
| some s => .ok s
| none => .error .stateDecode
let send ← ChainEpochDirection.fromPb sendPb
let recvPb ←
match link.recv with
| some r => .ok r
| none => .error .stateDecode
let recv ← ChainEpochDirection.fromPb recvPb
let tail ← linksFromPb rest
return { send := send, recv := recv } :: taildef fromPb
Chain::from_pb (src/).
def fromPb (pb : ChainPB) : Except Error Chain := do
let dir ←
match Direction.fromI32? pb.direction with
| some d => .ok d
| none => .error .stateDecode
let links ← linksFromPb pb.links
let params ←
match pb.params with
| some p => .ok p
| none => .error .stateDecode
return {
dir := dir
currentEpoch := pb.currentEpoch
sendEpoch := pb.sendEpoch
nextRoot := pb.nextRoot
links := links
params := params
}end Chain
chain_ and chain_from (src/)
def chainFromVersionNegotiation
chain_ (src/).
def chainFromVersionNegotiation (hkdf : HkdfFn) (vn : VersionNegotiationPB) :
Except Error Chain := do
let dir ←
match Direction.fromI32? vn.direction with
| some d => .ok d
| none => .error .stateDecode
let params ←
match vn.chainParams with
| some p => .ok p
| none => .error .chainNotAvailable
Chain.new hkdf vn.authKey dir paramsdef chainFrom
chain_from (src/).
def chainFrom (hkdf : HkdfFn) (pb : Option ChainPB) (vn : Option VersionNegotiationPB) :
Except Error Chain :=
match pb with
| some pb => Chain.fromPb pb
| none =>
match vn with
| none => .error .chainNotAvailable
| some vn => chainFromVersionNegotiation hkdf vn23 declarations not shown (lines 568–887): theorems, which Lean checks, or definitions that no headline statement uses
CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification., CryptoVerification.
end CryptoVerification.LibsignalSpqrV1