Skip to content

Instantly share code, notes, and snippets.

@cfm
Last active November 19, 2024 17:14
Show Gist options
  • Save cfm/c63561609d2bf621d877dbbef052ab1a to your computer and use it in GitHub Desktop.
Save cfm/c63561609d2bf621d877dbbef052ab1a to your computer and use it in GitHub Desktop.
securedrop-protocol-trace

Current as of: https://github.com/freedomofpress/securedrop-protocol/commit/e12ad2b57810c3eb17235a3a1ac2b5c6f915c9be

  • Compare with KEM variant

Goal

In contrast to (e.g.) Tamarin1, conduct a non-attack-oriented data-flow analysis, to trace how the intended participants in the protocol obtain and reconstruct protocol values. This is useful for multiple purposes:

  1. Validate the specification: Does the graph look right? Is everything accounted for? Are all values used, derived, transmitted, received, and reconstructed as expected?

    • For example, a value conceptualized with the wrong location or scope will show up all by itself in a separate subgraph.
  2. Learn about the protocol: Test intuitions about how protocol values are related across locations and scopes within the protocol (see next section).

  3. Check dependencies: As we consider moving to a key-encapsulation mechanism (KEM) for at least the message-level security of the protocol, it becomes important to understand how the derivation and use of specific protocol values may change.

Insights

We care about the scope or lifetime of values (long-term, per-message, per-request) for two reasons. First, this is where the three-party nature of the protocol becomes visible temporally as well as spatially:

Party Long-term Per-message Per-request
Source
Server
Journalist

Second, some values are reconstructed rather than transmitted, but always within the same scope.

Legend

See trace.yml for the trace format.

In trace.md, a Mermaid graph depicts:

  • a subgraph for each location or participant in the protocol;
    • a subgraph for each scope or lifetime at this location;
      • nodes for each value used in the protocol;
        • a list of all the values used to reconstruct this value locally; and
  • edges to show how values are connected, both
    • locally, within locations and scopes; and
    • remotely, transmitted and received on the network.

Footnotes

  1. Can we do this with the Tamarin models in progres? Felix Linker: "Theoretically yes, but the tool was not built to do this and you might fight it a lot."

trace.md: Makefile trace.py trace.yml
echo '```mermaid' > $@
./trace.py < trace.yml >> $@
echo '```' >> $@
graph TD
subgraph journalist
subgraph journalist.long-term
journalist.long-term.J_SK>**J_SK**]
journalist.long-term.J_PK[**J_PK** ←<br>J_SK]
journalist.long-term.JC_SK>**JC_SK**]
journalist.long-term.JC_PK[**JC_PK** ←<br>JC_SK]
end
subgraph journalist.per-message
journalist.per-message.JE_SK>**JE_SK**]
journalist.per-message.JE_PK[**JE_PK** ←<br>JE_SK]
journalist.per-message.message_id[**message_id** ←<br>enc_m_id<br>kmid]
journalist.per-message.message_ciphertext[**message_ciphertext**]
journalist.per-message.ME_PK[**ME_PK**]
journalist.per-message.k((**k** ←<br>JE_SK<br>ME_PK))
journalist.per-message.m[**m** ←<br>JE_SK<br>ME_PK<br>k<br>message_ciphertext]
journalist.per-message.S_PK[**S_PK** ←<br>JE_SK<br>ME_PK<br>k<br>m<br>message_ciphertext]
journalist.per-message.SC_PK[**SC_PK** ←<br>JE_SK<br>ME_PK<br>k<br>m<br>message_ciphertext]
journalist.per-message.message[**message** ←<br>JE_SK<br>ME_PK<br>k<br>m<br>message_ciphertext]
end
subgraph journalist.per-request
journalist.per-request.pmgdh[**pmgdh**]
journalist.per-request.enc_m_id[**enc_m_id**]
journalist.per-request.kmid[**kmid** ←<br>JC_SK<br>pmgdh]
end
end
subgraph source
subgraph source.long-term
source.long-term.J_PK[**J_PK** ←<br>J_SK]
source.long-term.JC_PK[**JC_PK** ←<br>JC_SK]
source.long-term.JE_PK[**JE_PK** ←<br>JE_SK]
source.long-term.S_SK>**S_SK**]
source.long-term.S_PK[**S_PK** ←<br>S_SK]
source.long-term.SC_SK>**SC_SK**]
source.long-term.SC_PK[**SC_PK** ←<br>SC_SK]
end
subgraph source.per-message
source.per-message.ME_SK>**ME_SK**]
source.per-message.ME_PK[**ME_PK** ←<br>ME_SK]
source.per-message.k((**k** ←<br>JE_PK<br>JE_SK<br>ME_SK))
source.per-message.message>**message**]
source.per-message.m[**m** ←<br>SC_PK<br>SC_SK<br>S_PK<br>S_SK<br>message]
source.per-message.message_ciphertext[**message_ciphertext** ←<br>JE_PK<br>JE_SK<br>ME_SK<br>SC_PK<br>SC_SK<br>S_PK<br>S_SK<br>k<br>m<br>message]
source.per-message.message_gdh((**message_gdh** ←<br>JC_PK<br>JC_SK<br>ME_SK))
end
end
subgraph server
subgraph server.per-message
server.per-message.message_ciphertext[**message_ciphertext** ←<br>JE_PK<br>JE_SK<br>ME_SK<br>SC_PK<br>SC_SK<br>S_PK<br>S_SK<br>k<br>m<br>message]
server.per-message.message_gdh[**message_gdh** ←<br>JC_PK<br>JC_SK<br>ME_SK]
server.per-message.ME_PK[**ME_PK** ←<br>ME_SK]
server.per-message.message_id>**message_id**]
end
subgraph server.per-request
server.per-request.RE_SK>**RE_SK**]
server.per-request.RE_PK[**RE_PK** ←<br>RE_SK]
server.per-request.kmid[**kmid** ←<br>JC_PK<br>JC_SK<br>ME_SK<br>RE_SK<br>message_gdh]
server.per-request.pmgdh((**pmgdh** ←<br>ME_PK<br>ME_SK<br>RE_SK))
server.per-request.enc_m_id[**enc_m_id** ←<br>JC_PK<br>JC_SK<br>ME_SK<br>RE_SK<br>kmid<br>message_gdh<br>message_id]
end
end
journalist.long-term.J_SK --x journalist.long-term.J_PK
journalist.long-term.JC_SK --x journalist.long-term.JC_PK
journalist.per-message.JE_SK --x journalist.per-message.JE_PK
journalist.per-request.kmid --> journalist.per-message.message_id
journalist.per-request.enc_m_id --> journalist.per-message.message_id
server.per-message.message_ciphertext --> journalist.per-message.message_ciphertext
server.per-message.ME_PK --> journalist.per-message.ME_PK
journalist.per-message.ME_PK --o journalist.per-message.k
journalist.per-message.JE_SK --x journalist.per-message.k
journalist.per-message.k --> journalist.per-message.m
journalist.per-message.message_ciphertext --> journalist.per-message.m
journalist.per-message.m --> journalist.per-message.S_PK
journalist.per-message.m --> journalist.per-message.SC_PK
journalist.per-message.m --> journalist.per-message.message
server.per-request.pmgdh --> journalist.per-request.pmgdh
server.per-request.enc_m_id --> journalist.per-request.enc_m_id
journalist.per-request.pmgdh --> journalist.per-request.kmid
journalist.long-term.JC_SK --x journalist.per-request.kmid
journalist.long-term.J_PK --> source.long-term.J_PK
journalist.long-term.JC_PK --> source.long-term.JC_PK
journalist.per-message.JE_PK --> source.long-term.JE_PK
source.long-term.S_SK --x source.long-term.S_PK
source.long-term.SC_SK --x source.long-term.SC_PK
source.per-message.ME_SK --x source.per-message.ME_PK
source.per-message.ME_SK --x source.per-message.k
journalist.per-message.JE_PK --o source.per-message.k
source.per-message.message --> source.per-message.m
source.long-term.S_PK --o source.per-message.m
source.long-term.SC_PK --o source.per-message.m
source.per-message.k --> source.per-message.message_ciphertext
source.per-message.m --> source.per-message.message_ciphertext
journalist.long-term.JC_PK --o source.per-message.message_gdh
source.per-message.ME_SK --x source.per-message.message_gdh
source.per-message.message_ciphertext --> server.per-message.message_ciphertext
source.per-message.message_gdh --> server.per-message.message_gdh
source.per-message.ME_PK --> server.per-message.ME_PK
server.per-request.RE_SK --x server.per-request.RE_PK
source.per-message.message_gdh --> server.per-request.kmid
server.per-request.RE_SK --x server.per-request.kmid
source.per-message.ME_PK --o server.per-request.pmgdh
server.per-request.RE_SK --x server.per-request.pmgdh
server.per-request.kmid --> server.per-request.enc_m_id
server.per-message.message_id --> server.per-request.enc_m_id
Loading
#!/usr/bin/env python3
# This is a tool for visual derivation analysis---non-attack-oriented taint or
# generic data-flow analysis---of a network protocol. It depicts values and
# their dependencies within and across locations and scopes, with some checks
# to validate reuse.
#
# Standard input: YAML like `trace.yml`.
#
# Standard output: Mermaid graph diagram.
# - Visual dependencies (graph edges) indicate:
# - local derivations (within subgraphs);
# - values received (across subgraphs).
# - Listed dependencies are local, transitive, and cumulative: what's this value
# made of?
#
# Exceptions:
# - `ValueError` if a key is reused across scopes within the same location.
import yaml
import sys
data = yaml.safe_load(sys.stdin.read())
print("graph TD")
# The `nodes` dictionary is overloaded between global `key`s and fully-specified
# paths like `{location}.{scope}.{key}`.
nodes = {}
# Edges are direct derivation relationships: `x --> y` means that `y` is derived
# from `x`.
edges = []
# Iterate over locations in the trace; each will be a subgraph.
for location, scopes in data.items():
print(f"subgraph {location}")
# Iterate over scopes in this location; each will be a subgraph.
for scope, keys in scopes.items():
print(f"subgraph {location}.{scope}")
# Iterate over keys in this scope; each will be a node.
for x in keys:
# Save the key's definition at its fully-specified path so that we
# can trace its dependencies.
path = f"{location}.{scope}.{x}"
nodes[path] = keys[x]
nodes[path]["deps"] = set()
# Iterate over the key's dependencies; each will be an edge.
x_from = keys[x].get("from", [])
for y_path in x_from:
# Build the dependency's fully-specified path so that we can
# refer to it globally.
y_location, y_scope, y = y_path.split(".")
# Identities were received from another location, which will be
# indicated with a graph edge.
if y != x:
nodes[path]["deps"].add(y)
elif y_location == location and y_scope != scope:
raise ValueError(
f"{path} is reused across local scopes at {y_path}"
)
# Otherwise, dependencies accumulate transitively within a
# location.
nodes[path]["deps"] |= nodes.get(y_path, {}).get("deps", set())
arrow = "-->"
# Mark "encryptions" by contact with a public key.
if "PK" in y and y != x:
nodes[path]["encrypted"] = True
arrow = "--o"
# Mark "decryptions" by contact with a secret key.
elif "SK" in y and y != x:
nodes[path]["decrypted"] = True
arrow = "--x"
edges.append(f"{y_path} {arrow} {location}.{scope}.{x}")
# Output the node's definition, including all local dependencies for
# this path.
deps = ""
if len(nodes[path]["deps"]) > 0:
deps = " ←<br>" + "<br>".join(sorted(nodes[path]["deps"]))
if "encrypted" in nodes[path] and "decrypted" in nodes[path]:
print(f"{path}((**{x}**{deps}))")
elif len(x_from) == 0:
print(f"{path}>**{x}**{deps}]")
else:
print(f"{path}[**{x}**{deps}]")
print("end") # scope subgraph
print("end") # location subgraph
# Output all the edges at the end, since they belong to the global graph.
print("\n".join(edges))
journalist: # Location (or role): Where is this value (who has it)?
long-term: # Scope: What is the lifetime of this value? How long/far is it it applicable?
# key: name of value
# from: list of other `{location}.{scope}.{key}` paths on which this path
# (this key, in this scope, at this location) depends
J_SK: {}
J_PK:
from:
- journalist.long-term.J_SK
JC_SK: {}
JC_PK:
from:
- journalist.long-term.JC_SK
per-message:
JE_SK: {}
JE_PK:
from:
- journalist.per-message.JE_SK
message_id:
from:
- journalist.per-request.kmid
- journalist.per-request.enc_m_id
message_ciphertext:
from:
- server.per-message.message_ciphertext
ME_PK:
from:
- server.per-message.ME_PK
k:
from:
- journalist.per-message.ME_PK
- journalist.per-message.JE_SK
m:
from:
- journalist.per-message.k
- journalist.per-message.message_ciphertext
S_PK:
from:
- journalist.per-message.m
SC_PK:
from:
- journalist.per-message.m
message:
from:
- journalist.per-message.m
per-request:
pmgdh:
from:
- server.per-request.pmgdh
enc_m_id:
from:
- server.per-request.enc_m_id
kmid:
from:
- journalist.per-request.pmgdh
- journalist.long-term.JC_SK
source:
long-term:
J_PK:
from:
- journalist.long-term.J_PK
JC_PK:
from:
- journalist.long-term.JC_PK
JE_PK:
from:
- journalist.per-message.JE_PK
S_SK: {}
S_PK:
from:
- source.long-term.S_SK
SC_SK: {}
SC_PK:
from:
- source.long-term.SC_SK
per-message:
ME_SK: {}
ME_PK:
from:
- source.per-message.ME_SK
k:
from:
- source.per-message.ME_SK
- journalist.per-message.JE_PK
message: {}
m:
from:
- source.per-message.message
- source.long-term.S_PK
- source.long-term.SC_PK
message_ciphertext:
from:
- source.per-message.k
- source.per-message.m
message_gdh:
from:
- journalist.long-term.JC_PK
- source.per-message.ME_SK
server:
per-message:
message_ciphertext:
from:
- source.per-message.message_ciphertext
message_gdh:
from:
- source.per-message.message_gdh
ME_PK:
from:
- source.per-message.ME_PK
message_id: {}
per-request:
RE_SK: {}
RE_PK:
from:
- server.per-request.RE_SK
kmid:
from:
- source.per-message.message_gdh
- server.per-request.RE_SK
pmgdh:
from:
- source.per-message.ME_PK
- server.per-request.RE_SK
enc_m_id:
from:
- server.per-request.kmid
- server.per-message.message_id
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment