AlphaProof Code: Verifying Signal’s Security at the Machine-Code Level

Paper › LibsignalBinary/Specifications › LibsignalTripleRatchetRepr.leanTier 2 · definitions

LibsignalTripleRatchetRepr.lean🔗

Tier 2 · definitions used by the computational proof and symbolic proof 4 headline theorems depend on it 84 of 659 lines trusted trust banner B3, trusted in part source ↗

Read the definitions shown. Only the declarations that the headline statements use are shown, with the namespace, section, open and variable commands needed to read them. The collapsed runs are theorems, which Lean checks, and definitions that no headline statement uses.

Declarations on this page (18)

Memory layouts of libsignal's session state, messages and results in the compiled image

Trusted in part. The theorems of the default library (Computational/Theorems/ outside CliElfLib) use only ServiceIdParser, UuidParser, addrBytes, hyphenatedLength, kindByte, pniPrefix, serviceIdParse, tryParseHyphenated, utf8, uuidTryParse, hexNibble, decodeHex, decodeHex32, parseSimple, hyphen, parseHyphenated, urnPrefix from this file: read them. To trust the theorems of Computational/Theorems/Main.lean (CliElfLib), also read errTag. Everything else here is checked by Lean. See Computational/docs/TRUSTED_BASE.md.

This file gives the separation-logic predicates that say how the Rust values the triple-ratchet entry points (LibsignalBinary/Specifications/LibsignalTripleRatchet.lean) read and write are laid out in memory, relating bytes to the session model (LibsignalBinary/Specifications/CryptoGames/Ratchet/LibsignalSession.lean). It extends LibsignalBinary/Specifications/LibsignalSessionRepr.lean (whose predicates it reuses: chainAt, messageKeyAt, optChainKeyAt, VecGhost, VecPin, ...). The trusted part is the pure address-parsing model: uuidTryParse (uuid 1.26.1's Uuid::try_parse), serviceIdParse (libsignal's ServiceId::parse_from_service_id_string) and the per-address bytes addrBytes, from which serializeAddresses builds the sender / recipient address bytes.

Every layout fact below was read off the disassembly of the session image; the evidence is cited by instruction offset at each definition. Independently, a Rust replica of the types (110 const assertions compiled by rustc 1.98.1 for aarch64-unknown-linux-gnu) confirms every offset, size and niche, including the fields no instruction touches. The niche choices (-1 for a single-niche Option, -2 for Result<Vec<u8>, SPE>::Ok, 2^63 + i for the SPE) depend on the rustc version: rerun the replica on any toolchain bump. Kernel-checked guards: issStr_lengths, macFailedStr_length, pniPrefix_word.

Glossary: SPE = SignalProtocolError (48 bytes); SS = proto::storage::SessionStructure (the payload of SessionState, 344 bytes); OTR / TR = OutgoingTripleRatchet (200 bytes) / TripleRatchet; IK = identity key; srk = sender ratchet key; pq / SPQR = the Sparse Post-Quantum Ratchet and its serialized state Vec<u8>; ISS = InvalidSessionStructure; UUID = universally unique identifier; ACI = Account Identity; PNI = Phone Number Identity; UTF-8 = 8-bit Unicode Transformation Format; LE = little-endian; RNG = random number generator; prost = the Rust protobuf (Protocol Buffers) library; niche = an invalid bit pattern of a field reused as an enum discriminant; sret = the caller-provided result buffer; Box<[u8]> = an owned byte slice (ptr, len) (no capacity word); rodata = the image's read-only data (↦ᵣ, duplicable read-only points-to).

Niches

  • Option<Vec-like> (Option<Chain>, Option<chain::ChainKey>, Option<PendingPreKey>, Option<PendingKyberPreKey>, spqr::MessageKey): None = capacity word usize::MAX (noneCap). Evidence: OTR::apply_to_session_state 0xbd00: cmn x22, #1 (SS + 0x90); clear_unacknowledged_pre_key_message writes -1 to SS + 0xf8 and SS + 0x128.

  • SPE: word 0 = 2^63 + variant index for every non-dataful variant (speWord).

  • Result<Vec<u8>, SPE> (try_decrypt): Ok = word 0 -2 (okTagVec), the Vec at +8; Err = the SPE inline at +0. Result<T, SPE> for T = SignalMessage / OutgoingTripleRatchet: Ok = the payload at +0; Err = word 0 -1 (errTag), the SPE at +8.

What is not modelled (framed byte-exactly by the ghost)

alice_base_key, the registration ids, the pending pre-key's ids / timestamp, the whole pending_kyber_pre_key, and every Vec the session model does not determine (VecGhost).

Main definitions

@[expose] public section
namespace CryptoVerification.LibsignalTripleRatchetRepr
open Arm64 CryptoGames CryptoGames.HMAC CryptoGames.HKDF CryptoGames.LibsignalSession
open CryptoVerification.RustHeap CryptoVerification.LibsignalSessionRepr
1 declaration not shown (lines 101–103): theorems, which Lean checks, or definitions that no headline statement uses

CryptoVerification.LibsignalTripleRatchetRepr.noneCap

def errTagL105–107 ↗#

The Err tag word of Result<T, SPE> for the 120 / 200-byte payloads (-1; try_from 0x258a4: str x9, [x19], OTR::from_session_state 0xb93c, OTR::encrypt 0x34058).

def errTag : BitVec 64 := asm64% resultErrTag
2 declarations not shown (lines 109–116): theorems, which Lean checks, or definitions that no headline statement uses

CryptoVerification.LibsignalTripleRatchetRepr.okTagVec, CryptoVerification.LibsignalTripleRatchetRepr.speWord

def utf8L118–119 ↗#

A string literal's UTF-8 bytes.

def utf8 (s : String) : List (BitVec 8) := s.toUTF8.toList.map fun b => BitVec.ofNat 8 b.toNat
16 declarations not shown (lines 121–316): theorems, which Lean checks, or definitions that no headline statement uses

CryptoVerification.LibsignalTripleRatchetRepr.invalidSessionStructureStr, CryptoVerification.LibsignalTripleRatchetRepr.issStr_lengths, CryptoVerification.LibsignalTripleRatchetRepr.macFailedStr, CryptoVerification.LibsignalTripleRatchetRepr.macFailedStr_length, CryptoVerification.LibsignalTripleRatchetRepr.strRefAt, CryptoVerification.LibsignalTripleRatchetRepr.boxU8At, CryptoVerification.LibsignalTripleRatchetRepr.optBoxU8At, CryptoVerification.LibsignalTripleRatchetRepr.invalidMessageStrAt, CryptoVerification.LibsignalTripleRatchetRepr.speAt, CryptoVerification.LibsignalTripleRatchetRepr.vecResultAt, CryptoVerification.LibsignalTripleRatchetRepr.errResultAt, CryptoVerification.LibsignalTripleRatchetRepr.SignalMessageGhost.ctPtr, CryptoVerification.LibsignalTripleRatchetRepr.SignalMessageGhost.serPtr, CryptoVerification.LibsignalTripleRatchetRepr.SignalMessageGhost, CryptoVerification.LibsignalTripleRatchetRepr.SignalMessageGhost.pqCap, CryptoVerification.LibsignalTripleRatchetRepr.SignalMessageGhost.pqPtr, CryptoVerification.LibsignalTripleRatchetRepr.SignalMessageGhost.addrPtr, CryptoVerification.LibsignalTripleRatchetRepr.signalMessageAt, CryptoVerification.LibsignalTripleRatchetRepr.signalMessageResultAt, CryptoVerification.LibsignalTripleRatchetRepr.AddrGhost.device, CryptoVerification.LibsignalTripleRatchetRepr.AddrGhost.name, CryptoVerification.LibsignalTripleRatchetRepr.AddrGhost, CryptoVerification.LibsignalTripleRatchetRepr.protocolAddressAt

abbrev ServiceIdParserL318–321 ↗#

A ServiceId parser on name bytes: the ServiceId of a name, or none. The Bool is the kind (false = Aci, tag 0; true = Pni, tag 1), the vector the UUID. The session specs use serviceIdParse uuidParse (below), the model of ServiceId::parse_from_service_id_string.

abbrev ServiceIdParser := List (BitVec 8) → Option (Bool × List.Vector (BitVec 8) 16)

abbrev UuidParserL323–325 ↗#

A uuid::Uuid::try_parse on the input bytes: the 16 UUID bytes (Ok), or none (Err). The session specs are generic in it; the statement instantiates them at uuidTryParse.

abbrev UuidParser := List (BitVec 8) → Option (List.Vector (BitVec 8) 16)

uuid::Uuid::try_parse (uuid 1.26.1)

Upstream: ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.26.1/src/parser.rs. Uuid::try_parse(&str) (:110–112) = try_parse_ascii(input.as_bytes()) (:136–144), which maps try_parse (:147–163) to Ok(Uuid::from_bytes(bytes)) (the 16 bytes, unchanged) or to Err(Error(ErrorKind::ParseOther)) on every failure. try_parse dispatches on the length:

  • 32 bytes: parse_simple (:188–225), 32 hex digits;

  • 36 bytes: parse_hyphenated (:228–278), the whole input;

  • 38 bytes {…}: parse_hyphenated of the 36 bytes inside the braces (:156);

  • 45 bytes urn:uuid:…: parse_hyphenated of the 36 bytes after the prefix (:157);

  • any other length or shape: Err (:161). decode_hex32 (:283–322) maps '0'..='9' to 0..9 and 'a'..='f' / 'A'..='F' (via | 0x20) to 10..15, and rejects every other byte; byte j of the UUID is (nibble 2j << 4) | nibble 2j+1.

libsignal only reaches the 36-byte (hyphenated) path: try_parse_hyphenated (core/src/address.rs:264–271, tryParseHyphenated below) checks the length is hyphenatedLength = 36 before it calls Uuid::try_parse. The other shapes are modelled so that uuidTryParse is the whole function, as compiled into the image.

def hexNibbleL346–351 ↗#

decode_hex32's per-byte map (src/parser.rs:289–305): digit = c - '0' if < 10, else alpha = (c | 0x20) - 'a' if < 6, as alpha + 10; any other byte is bad (none).

def hexNibble (c : BitVec 8) : Option (BitVec 8) := if c - 0x30#8 < 10#8 then some (c - 0x30#8) else if (c ||| 0x20#8) - 0x61#8 < 6#8 then some ((c ||| 0x20#8) - 0x61#8 + 10#8) else none

def decodeHexL353–361 ↗#

decode_hex32's byte assembly (src/parser.rs:310–321) on a list of hex digits: each pair (hi, lo) gives (hi << 4) | lo; none if any digit is bad.

def decodeHex : List (BitVec 8) → Option (List (BitVec 8)) | hi :: lo :: rest => do let h ← hexNibble hi let l ← hexNibble lo let r ← decodeHex rest return ((h <<< 4) ||| l) :: r | _ => some []

def decodeHex32L363–365 ↗#

decode_hex32(hex: &[u8; 32]) (src/parser.rs:283–322): the 16 bytes, or none.

def decodeHex32 (hex : List (BitVec 8)) : Option (List.Vector (BitVec 8) 16) := (decodeHex hex).bind fun bs => if h : bs.length = 16 then some ⟨bs, h⟩ else none

def parseSimpleL367–369 ↗#

parse_simple(s, _) (src/parser.rs:188–225): 32 hex digits.

def parseSimple (s : List (BitVec 8)) : Option (List.Vector (BitVec 8) 16) := if s.length = 32 then decodeHex32 s else none

def hyphenL371–372 ↗#

The byte '-'.

def hyphen : BitVec 8 := 0x2d#8

def parseHyphenatedL374–381 ↗#

parse_hyphenated(s) (src/parser.rs:228–278): 36 bytes, '-' at positions 8, 13, 18 and 23 (:244–247), the other 32 bytes hex digits, gathered in order (:251–272).

def parseHyphenated (s : List (BitVec 8)) : Option (List.Vector (BitVec 8) 16) := if s.length = 36 ∧ s[8]? = some hyphen ∧ s[13]? = some hyphen ∧ s[18]? = some hyphen ∧ s[23]? = some hyphen then decodeHex32 (s.take 8 ++ (s.drop 9).take 4 ++ (s.drop 14).take 4 ++ (s.drop 19).take 4 ++ s.drop 24) else none

def urnPrefixL383–384 ↗#

The prefix urn:uuid: (src/parser.rs:157).

def urnPrefix : List (BitVec 8) := utf8 "urn:uuid:"

def uuidTryParseL386–395 ↗#

uuid::Uuid::try_parse (uuid 1.26.1, src/parser.rs:110–112, 136–163; the shapes are listed above): some u = Ok(Uuid::from_bytes(u)), none = Err(ParseOther).

def uuidTryParse : UuidParser := fun input => if input.length = 32 then parseSimple input else if input.length = 36 then parseHyphenated input else if input.length = 38 ∧ input.head? = some 0x7b#8 ∧ input.getLast? = some 0x7d#8 then parseHyphenated ((input.drop 1).take 36) else if input.length = 45 ∧ urnPrefix.isPrefixOf input then parseHyphenated (input.drop urnPrefix.length) else none
1 declaration not shown (lines 397–400): theorems, which Lean checks, or definitions that no headline statement uses

_private.0.CryptoVerification.LibsignalTripleRatchetRepr.uuidOf

def pniPrefixL402–403 ↗#

The prefix "PNI:" of a PNI's string form (core/src/address.rs:273).

def pniPrefix : List (BitVec 8) := utf8 "PNI:"
1 declaration not shown (lines 405–409): theorems, which Lean checks, or definitions that no headline statement uses

CryptoVerification.LibsignalTripleRatchetRepr.pniPrefix_word

def hyphenatedLengthL411–412 ↗#

uuid::fmt::Hyphenated::LENGTH (uuid 1.26.1, src/fmt.rs:366).

def hyphenatedLength : Nat := 36

def tryParseHyphenatedL414–418 ↗#

try_parse_hyphenated (core/src/address.rs:264–271): Uuid::try_parse(input).ok() if the input has exactly hyphenatedLength bytes, else None without calling it.

def tryParseHyphenated (uuidParse : UuidParser) (input : List (BitVec 8)) : Option (List.Vector (BitVec 8) 16) := if input.length = hyphenatedLength then uuidParse input else none

def serviceIdParseL420–430 ↗#

ServiceId::parse_from_service_id_string (core/src/address.rs:263–280) over a Uuid::try_parse: if the input starts with "PNI:" (input.strip_prefix("PNI:"), a byte-prefix test), the PNI of the rest; otherwise the ACI of the whole input; each None unless the parsed part has exactly 36 bytes and uuidParse accepts it. In the image this and Uuid::try_parse are in-image code (libsignal-core and uuid are whole crates), verified against serviceIdParse uuidTryParse.

def serviceIdParse (uuidParse : UuidParser) : ServiceIdParser := fun input => if pniPrefix.isPrefixOf input then (tryParseHyphenated uuidParse (input.drop pniPrefix.length)).map fun u => (true, u) else (tryParseHyphenated uuidParse input).map fun u => (false, u)

def kindByteL432–436 ↗#

ServiceIdKind's byte: the tag of the 17-byte Option<ServiceId> that the in-image parse_from_service_id_string writes (Aci = 0, +0x50: strb wzr, [x19]; Pni = 1, +0xb4..+0xb8: mov w8, #1; strb w8, [x19]; None = 2, +0x98..+0x9c), stored unchanged by serialize_addresses (core/src/address.rs's ServiceIdKind order).

def kindByte (pni : Bool) : BitVec 8 := if pni then 1#8 else 0#8

def addrBytesL438–441 ↗#

The 18 bytes kind ‖ uuid ‖ device of one address (ServiceId::service_id_fixed_width_binary, core/src/address.rs:212–216, then the device).

def addrBytes (sid : Bool × List.Vector (BitVec 8) 16) (device : BitVec 8) : List (BitVec 8) := kindByte sid.1 :: (sid.2.toList ++ [device])
16 declarations not shown (lines 443–657): theorems, which Lean checks, or definitions that no headline statement uses

CryptoVerification.LibsignalTripleRatchetRepr.serializeAddresses, CryptoVerification.LibsignalTripleRatchetRepr.tapeAt, CryptoVerification.LibsignalTripleRatchetRepr.tapeAfter, CryptoVerification.LibsignalTripleRatchetRepr.coinsOf, CryptoVerification.LibsignalTripleRatchetRepr.drawsCoins, CryptoVerification.LibsignalTripleRatchetRepr.decryptDraws, CryptoVerification.LibsignalTripleRatchetRepr.SenderChainGhost.priv, CryptoVerification.LibsignalTripleRatchetRepr.SenderChainGhost, CryptoVerification.LibsignalTripleRatchetRepr.SenderChainGhost.srk, CryptoVerification.LibsignalTripleRatchetRepr.SenderChainGhost.mkPtr, CryptoVerification.LibsignalTripleRatchetRepr.SenderChainGhost.ck, CryptoVerification.LibsignalTripleRatchetRepr.SenderChainGhost.mkCap, CryptoVerification.LibsignalTripleRatchetRepr.optSenderChainAt, CryptoVerification.LibsignalTripleRatchetRepr.TailVecGhost.tail, CryptoVerification.LibsignalTripleRatchetRepr.TailVecGhost, CryptoVerification.LibsignalTripleRatchetRepr.TailVecGhost.vec, CryptoVerification.LibsignalTripleRatchetRepr.optTailVecAt, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.kyber, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.sender, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.pq, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.remoteIk, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.chains, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.root, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.chainsCap, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.localIk, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.alice, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.chainsPtr, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.regIds, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.pending, CryptoVerification.LibsignalTripleRatchetRepr.sessionStateAt, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.Keeps, CryptoVerification.LibsignalTripleRatchetRepr.SessionGhost.AfterTryDecrypt, CryptoVerification.LibsignalTripleRatchetRepr.otrAt, CryptoVerification.LibsignalTripleRatchetRepr.otrResultAt

end CryptoVerification.LibsignalTripleRatchetRepr