mirror of
https://github.com/hanzoai/hips.git
synced 2026-10-30 15:04:16 +00:00
The confidential-tier proposal specifies the report-data field and reaches the same conclusion about a boot-only binding. There is one such field and it has one specification, so this document stops proposing a second. What its B1 does not carry is a durable identity: the session keys it binds are ephemeral by requirement, so a report says a confidential environment holds these keys and nothing that reaches the bonded node. The join is the epoch statement naming the same session terms, so a verifier follows report -> receipt key <- statement -> address, checking two links separately. Adding the address to B1 would be stronger and is offered to that proposal rather than assumed here. States plainly that this binds identity to session, not key to silicon. Co-authored-by: Hanzo Dev <[email protected]>
1828 lines
92 KiB
TeX
1828 lines
92 KiB
TeX
% hip-node-identity-wallet.tex
|
|
% Hanzo Improvement Proposal — Node Identity: the Wallet as the Node.
|
|
% Number assigned when this lands in HIPs/.
|
|
\documentclass[11pt,a4paper]{article}
|
|
|
|
\usepackage[utf8]{inputenc}
|
|
\usepackage[T1]{fontenc}
|
|
\usepackage[margin=1in]{geometry}
|
|
\usepackage{amsmath,amssymb,amsthm}
|
|
\usepackage{booktabs}
|
|
\usepackage{longtable}
|
|
\usepackage{array}
|
|
\usepackage{enumitem}
|
|
\usepackage{xcolor}
|
|
\usepackage{microtype}
|
|
\usepackage[colorlinks=true,linkcolor=black,citecolor=black,urlcolor=blue]{hyperref}
|
|
|
|
\newtheorem{definition}{Definition}[section]
|
|
\newtheorem{claim}[definition]{Claim}
|
|
\newtheorem{remark}[definition]{Remark}
|
|
|
|
% Long identifiers are the norm here, so let the typewriter font break.
|
|
\newcommand{\code}[1]{\texttt{\hyphenchar\font=`\-\relax #1}}
|
|
\sloppy
|
|
\emergencystretch=3em
|
|
\newcommand{\Held}{\mathrm{Held}}
|
|
\newcommand{\Ran}{\mathrm{Ran}}
|
|
\newcommand{\Conf}{\mathrm{Conf}}
|
|
\newcommand{\Vrfy}{\mathsf{Vrfy}}
|
|
\newcommand{\Sign}{\mathsf{Sign}}
|
|
\newcommand{\addr}{\mathsf{addr}}
|
|
\newcommand{\sha}[1]{\mathrm{SHA\text{-}#1}}
|
|
\newcommand{\kec}{\mathrm{Keccak256}}
|
|
|
|
\setlist[itemize]{itemsep=2pt,topsep=4pt}
|
|
\setlist[description]{itemsep=3pt,topsep=4pt}
|
|
|
|
\title{\textbf{Node Identity: the Wallet as the Node}\\[6pt]
|
|
\large A Hanzo Improvement Proposal}
|
|
|
|
\author{Hanzo AI}
|
|
\date{5 September 2026}
|
|
|
|
\begin{document}
|
|
\maketitle
|
|
|
|
\begin{center}
|
|
\begin{tabular}{@{}ll@{}}
|
|
\toprule
|
|
Number & assigned on merge into \code{HIPs/} \\
|
|
Type & Standards Track \\
|
|
Category & Core \\
|
|
Status & Draft \\
|
|
Requires & HIP-0024 (Hanzo sovereign L1), HIP-0026 (IAM), HIP-0027 (KMS),\\
|
|
& HIP-0095 (QoS challenge), HIP-0096 (compute rewards),\\
|
|
& HIP-0097 (\code{did:hanzo:}), HIP-0121 (BYO compute fleet),\\
|
|
& HIP-0134 (one process, one socket, one identity),\\
|
|
& HIP-1325 (\code{/v1/node}) \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
\begin{abstract}
|
|
\noindent
|
|
A Hanzo node is a physical host that boots a Linux microVM, joins one k3s
|
|
cluster, and runs the Hanzo cloud. This proposal specifies the identity of such
|
|
a node: an account address on the Hanzo sovereign L1, held by the node's
|
|
\code{hanzod}, used to sign what the node says about itself, named in the node's
|
|
on-chain registration, joined by signature to the node's hardware report,
|
|
presented to the cluster at admission, and paid the node's rewards.
|
|
|
|
The proposal is written around one distinction, because every claim the system
|
|
can make about a node depends on it. A signature establishes that some party
|
|
able to evaluate the signing function chose to sign a message. It does not
|
|
establish which machine that party was, nor what software that machine was
|
|
running. An attestation rooted in a hardware vendor's certificate chain
|
|
establishes the second of those, conditionally, and only for platforms that have
|
|
the hardware, and only if the key is inside the report. We name the difference
|
|
the \emph{possession--platform gap} (\S\ref{sec:gap}), state what falls on each
|
|
side of it, and specify the custody, join, staking, reward and lifecycle rules
|
|
that follow from where a given node falls.
|
|
|
|
Four properties are load-bearing. First, a node key that can be copied is an
|
|
identity that can be forged; custody is therefore specified as a ladder with a
|
|
derived assurance class, not as a claim an operator makes. Second, the identity
|
|
key (on the box, signing constantly) and the stake owner (off the box, moving
|
|
money) are two roles and two keys, because a design in which the compromised key
|
|
can also withdraw the bond has no recovery path. Third, forfeiture on-chain is
|
|
restricted to self-proving faults --- principally equivocation, which is exactly
|
|
what an uncoordinated copy of a key produces --- so that slashing needs no quorum
|
|
and no oracle. Fourth, a hardware report binds a platform to an image and to the
|
|
ephemeral keys of a session; nothing in it reaches a durable identity, so the link
|
|
from a bonded node to attested hardware has to be made by a signature and is
|
|
specified here.
|
|
|
|
Section~\ref{sec:status} and Appendix~\ref{app:fleet} separate what is
|
|
implemented from what is proposed. Measurement, the SHA-384 register fold, and
|
|
the SEV-SNP and TDX report request paths exist in \code{hanzoai/vm}
|
|
(\code{crates/vm-measure}); a second, incompatible measurement and a host-signed
|
|
evidence type exist in \code{hanzoai/node} (\code{hanzo-libs/hanzo-attest}); the
|
|
wallet custody backends exist in \code{hanzoai/cloud} (\code{apps/wallet}) and
|
|
\code{hanzoai/cli} (\code{hanzo wallet}); the validator manager exists in
|
|
\code{luxfi/standard}. The identity binding between them, the epoch statement,
|
|
cluster admission by address, and forfeiture do not exist and are proposed here.
|
|
\end{abstract}
|
|
|
|
\tableofcontents
|
|
|
|
% =====================================================================
|
|
\section{Scope}
|
|
|
|
This document specifies the identity of a node in the Hanzo decentralized AI
|
|
cloud: how the identity is created, how it is held, what it signs, what a
|
|
verifier may conclude from what it signs, how it enters the cluster, how it
|
|
stakes, how it is paid, and how it ends.
|
|
|
|
It does not specify consensus, the scheduler, the workload API, or the
|
|
verification of AI work. Verification of advertised capability and delivered
|
|
service is HIP-0095 (QoS challenges) and HIP-0096 (compute rewards); this
|
|
document states only where the identity boundary meets those and what identity
|
|
does not supply.
|
|
|
|
Throughout, \emph{observed} marks a statement about code or hardware that was
|
|
read or measured, with the location given. \emph{Proposed} marks a change this
|
|
document asks for. Statements with neither marker are definitions or
|
|
consequences of the two.
|
|
|
|
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as
|
|
in RFC 2119.
|
|
|
|
% =====================================================================
|
|
\section{Terms}
|
|
|
|
\begin{description}
|
|
\item[Node] A host that runs \code{hanzod} and offers compute to the network.
|
|
Physical, and heterogeneous: Appendix~\ref{app:fleet} records three, on three
|
|
architectures, with three different attestation capabilities.
|
|
|
|
\item[Machine] A microVM the node starts. A node serves many. The vocabulary
|
|
\emph{machine} $>$ \emph{cluster} $>$ \emph{vm} $>$ \emph{sandbox} is fixed
|
|
elsewhere in the platform; in this document \emph{machine} means a microVM the
|
|
node runs and \emph{host} means the physical box.
|
|
|
|
\item[Monitor] The \code{hanzo-vm} binary that builds and runs a machine.
|
|
Virtualization.framework on macOS, cloud-hypervisor or KVM on Linux.
|
|
|
|
\item[Node microVM] The one machine on each host that joins the k3s cluster and
|
|
runs the Hanzo cloud. It is the uniformity layer: it is why an Apple-Silicon
|
|
Mac and an x86 workstation present the same surface to the cluster. Exactly one
|
|
per node.
|
|
|
|
\item[Launch] What the hypervisor was handed: kernel, kernel command line,
|
|
initramfs where present, source root image, and shape. \emph{Observed} as the
|
|
launch log in \code{vm-measure} (\code{crates/vm-cli/src/measure.rs},
|
|
\code{launch}).
|
|
|
|
\item[Workload] What the started system was told to run, extended into a second
|
|
log by whatever drives the machine. \emph{Observed} as the second log in
|
|
\code{vm-measure}; \code{hanzo up} is the intended extender.
|
|
|
|
\item[Register] The fold of one log:
|
|
$R_{i+1} = \sha{384}(R_i \,\|\, e_i)$ from 48 zero bytes, where
|
|
$e_i = \sha{384}(\mathit{name}_i \,\|\, \mathtt{0x00} \,\|\, \sha{384}(\mathit{content}_i))$.
|
|
Written $L$ for the launch register and $W$ for the workload register.
|
|
\emph{Observed}, \code{crates/vm-measure/src/lib.rs}.
|
|
|
|
\item[Measured state] $\mu = (L, W)$. Used in \S\ref{sec:proves} where the
|
|
argument does not depend on which of the two registers is meant.
|
|
|
|
\item[Bind] The 64 bytes a hardware report carries as its caller-supplied field
|
|
(REPORT\_DATA on SEV-SNP, the report data on TDX). \emph{Observed} as
|
|
$\beta = \sha{512}(L \,\|\, W)$, \code{Measurement::bind}.
|
|
|
|
\item[Node key] The secp256k1 key whose address is the node's identity. Held by
|
|
\code{hanzod}. Signs the node's statements about itself.
|
|
|
|
\item[Node address] $A_n = \addr(pk_n)$, the low 20 bytes of $\kec$ over the
|
|
uncompressed public key. This is the identity.
|
|
|
|
\item[Owner key] The key that registers the node, posts the bond, ends the
|
|
validation, and withdraws. Address $A_o$. Held by the operator, off the node.
|
|
|
|
\item[Assurance class] What a node can \emph{prove} about the platform holding
|
|
its key. Derived from evidence, never declared. \S\ref{sec:class}.
|
|
\end{description}
|
|
|
|
% =====================================================================
|
|
\section{What signatures, measurements and attestations establish}
|
|
\label{sec:proves}
|
|
|
|
This section is the foundation. Everything later --- staking, forfeiture, reward
|
|
attribution, cluster admission, fleet enumeration --- inherits exactly the
|
|
assurance established here and no more.
|
|
|
|
Let $\Sigma = (\mathsf{Gen}, \Sign, \Vrfy)$ be a signature scheme existentially
|
|
unforgeable under chosen-message attack. Fix a keypair $(sk, pk)$ and write
|
|
$A = \addr(pk)$. Over a run of the system define three predicates:
|
|
|
|
\begin{description}
|
|
\item[$\Held(P, sk, t)$] party $P$ can evaluate $\Sign_{sk}(\cdot)$ at time $t$.
|
|
\item[$\Ran(h, I, t)$] host $h$ was executing image $I$ at time $t$.
|
|
\item[$\Conf(sk, h)$] $sk$ is confined to $h$: no party other than $h$ can
|
|
evaluate $\Sign_{sk}(\cdot)$.
|
|
\end{description}
|
|
|
|
\subsection{A signature}
|
|
|
|
\begin{claim}[What verification gives]\label{cl:sig}
|
|
If $\Vrfy_{pk}(m, \sigma) = 1$ then, under EUF-CMA, there exist a party $P$ and a
|
|
time $t$ with $\Held(P, sk, t)$, and $P$ chose the message $m$.
|
|
\end{claim}
|
|
|
|
\begin{claim}[What it does not give]\label{cl:indep}
|
|
The conclusion of Claim~\ref{cl:sig} is independent of $\Ran$ and $\Conf$. For
|
|
any host $h$ and image $I$ a verifier believes in, there is a run in which the
|
|
conclusion holds and $\Ran(h, I, t)$ is false.
|
|
\end{claim}
|
|
|
|
\noindent The proof is the attack: copy $sk$ to any machine and sign $m$ there.
|
|
Nothing in $\sigma$ distinguishes the two runs, because nothing in $\sigma$
|
|
depends on where the signing happened.
|
|
|
|
Claim~\ref{cl:indep} holds for every message, including a message that is
|
|
\emph{about} a machine. Signing a measured state does not make the signature
|
|
evidence about a boot. It makes it evidence that the key-holder asserted a
|
|
number.
|
|
|
|
\subsection{A measurement}
|
|
|
|
$L$ and $W$ are functions of files and text on a host, computed by the host,
|
|
before and during the machine's life. Two parties holding the same inputs compute
|
|
the same registers; a third party holding only a register can check a claim about
|
|
those inputs. That is the entire content.
|
|
|
|
\begin{claim}\label{cl:meas}
|
|
A register binds inputs. It does not witness execution. A host that folds one set
|
|
of files into the register and hands the monitor a different set produces a value
|
|
that verifies against the first set and describes nothing that ran.
|
|
\end{claim}
|
|
|
|
This is stated in the implementation itself (\code{crates/vm-measure/src/lib.rs},
|
|
module documentation: ``computed on the host, a measurement is a statement about
|
|
what was launched, not evidence against the host that launched it. A host that
|
|
can substitute the kernel can also substitute the program that hashes it''). We
|
|
restate it because it is the property most often lost in transit: a measurement
|
|
in a registry looks like a fact about a running system and is not one.
|
|
|
|
\subsection{An attestation}
|
|
\label{sec:att}
|
|
|
|
\begin{claim}[What a hardware report gives]\label{cl:att}
|
|
Let $Q$ be a report over 64 caller-supplied bytes $\beta$, verifying under vendor
|
|
root $R$, and carrying the platform's own launch measurement $\lambda$. Then
|
|
there exists a platform $h$ whose attestation key $R$ certifies, such that $h$
|
|
was launched in state $\lambda$ and $h$ reported the value $\beta$.
|
|
\end{claim}
|
|
|
|
Four things do not follow:
|
|
|
|
\begin{enumerate}[label=(\alph*)]
|
|
\item \textbf{A measured state is not behavior.} A register covers what was
|
|
loaded. Code loaded later, a kernel module, a \code{kexec}, a writable root
|
|
filesystem, or an interpreter reading a script from the network all change
|
|
behavior without changing $L$. A measured platform running a program that
|
|
fetches and executes arbitrary bytes measures as honestly as one that does not.
|
|
|
|
\item \textbf{$\beta$ is not $\Conf$.} Reporting a value inside a report binds it
|
|
to the platform's \emph{statement}, not to the platform. $\Conf(sk_n, h)$
|
|
follows only if $sk_n$ was generated inside $h$ and is non-extractable, and only
|
|
if the platform reports that fact in a form the verifier checks. A key generated
|
|
elsewhere and copied in attests exactly as well.
|
|
|
|
\item \textbf{A report says nothing about a key it does not name.} If $\beta$ is
|
|
a function of the image alone, the report is a statement about the image alone,
|
|
and any node holding that image can present it. \S\ref{sec:bind} develops this,
|
|
because it is the state of the implemented code.
|
|
|
|
\item \textbf{The host is in the trust base unless the VM is confidential.}
|
|
Guest-side measurement of a machine whose memory the hypervisor can read and
|
|
write establishes $\Ran$ only relative to an honest host. On AMD SEV-SNP, Intel
|
|
TDX or Arm CCA the host is removed from that base. \emph{Observed:} none of the
|
|
three hosts in Appendix~\ref{app:fleet} runs a confidential VM ---
|
|
\code{kvm\_amd.sev\_snp = N} on the x86 host, no Realm support on the aarch64
|
|
host, and Virtualization.framework on the Mac. Guest measurement on this fleet
|
|
therefore defends against a remote attacker and not against the node operator.
|
|
\end{enumerate}
|
|
|
|
Point (d) matters most for third-party nodes, where the node operator is the
|
|
adversary of interest. On the current fleet the only hardware root that speaks
|
|
about the \emph{host} is the TPM (\S\ref{sec:class}).
|
|
|
|
\subsection{The possession--platform gap}
|
|
\label{sec:gap}
|
|
|
|
\begin{definition}[Possession--platform gap]
|
|
The gap between $\Held$ and $\Conf$: a signature establishes that \emph{someone}
|
|
could sign, and never that \emph{this platform} is the only one that could.
|
|
\end{definition}
|
|
|
|
Every downstream property inherits it. If the node key can be copied, then
|
|
``which node produced this statement'' has no answer, and with it neither does
|
|
``which node earned this reward'', ``which node's stake answers for this fault'',
|
|
nor ``how many nodes are in the fleet''. Fleet enumeration by wallet is only as
|
|
meaningful as the injectivity of the map from wallets to machines, and a copied
|
|
key breaks injectivity silently: two machines answer as one node, and the console
|
|
shows one row.
|
|
|
|
Closing the gap requires two properties, both of which must be reported and
|
|
checked, not assumed:
|
|
|
|
\begin{enumerate}[label=(\roman*)]
|
|
\item the key is generated inside a platform and is non-extractable from it, and
|
|
\item the platform can produce, under a root the verifier trusts, a statement
|
|
binding that key to a measured state.
|
|
\end{enumerate}
|
|
|
|
No software arrangement supplies (i). Custody in a key-management service does
|
|
not supply it either --- it \emph{moves} the gap, which is a real improvement and
|
|
a different property; see \S\ref{sec:ladder}. The implemented report path supplies
|
|
neither, because the value it reports does not mention the key; see
|
|
\S\ref{sec:bind}.
|
|
|
|
\subsection{What an unattested but signed join does not prove}
|
|
\label{sec:notprove}
|
|
|
|
A node that signs its measured state with its node key and presents that to the
|
|
network proves that the holder of $sk_n$ asserted $\mu$ at a stated time and that
|
|
the assertion has not been altered since. It does not prove any of the following.
|
|
Each is a distinct failure, and none is covered by adding more signatures of the
|
|
same kind:
|
|
|
|
\begin{enumerate}[label=\textbf{N\arabic*.},leftmargin=2.6em]
|
|
\item \textbf{Not that the machine ran the measured image.} The registers are
|
|
folded over files chosen by the same party that signs. A node that measures
|
|
image $I$ and boots image $I'$ produces a statement that verifies
|
|
(Claim~\ref{cl:meas}).
|
|
\item \textbf{Not that the statement came from the host it describes.} The
|
|
signing key is portable; the statement can be produced anywhere, including on a
|
|
laptop, and relayed.
|
|
\item \textbf{Not that the key is on any particular host at all.} $\Conf$ is not
|
|
established (Claim~\ref{cl:indep}).
|
|
\item \textbf{Not that the identity is one machine.} Without an epoch-unique
|
|
statement (\S\ref{sec:equiv}), $N$ machines holding a copy of $sk_n$ join as one
|
|
node and are indistinguishable from one machine.
|
|
\item \textbf{Not that the advertised inventory is real.} CPU, memory and GPU
|
|
counts are numbers the node reports. The shape inside the launch log is what
|
|
\code{hanzod} \emph{asked the monitor for}, which is what it can honestly sell,
|
|
and is not a statement about the physical host.
|
|
\item \textbf{Not that the machine still runs what it reported.} The launch
|
|
register is fixed before start. Nothing in the statement constrains the machine
|
|
afterwards.
|
|
\item \textbf{Not that the node will do the work correctly.} Identity is
|
|
orthogonal to service. HIP-0095 and HIP-0096 own that.
|
|
\item \textbf{Not that the signer is authorized.} Verification of a signature is
|
|
not authorization. Whether the network accepts $A_n$ is a registration question
|
|
(\S\ref{sec:stake}), and the implementation is right to separate them
|
|
(\code{hanzo-attest/src/evidence.rs}: ``Deciding that a signer is acceptable is
|
|
a policy question \dots\ not here'').
|
|
\end{enumerate}
|
|
|
|
\subsection{Five controls, none substitutable}
|
|
|
|
\begin{center}
|
|
\small
|
|
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{0.15\textwidth}>{\raggedright\arraybackslash}p{0.35\textwidth}>{\raggedright\arraybackslash}p{0.40\textwidth}@{}}
|
|
\toprule
|
|
\textbf{Control} & \textbf{Establishes} & \textbf{Does not establish} \\
|
|
\midrule
|
|
Signature &
|
|
Authorship of a statement under a key &
|
|
Which machine; which image; uniqueness \\[3pt]
|
|
Custody &
|
|
Who can cause a signature to exist &
|
|
That the statement is true \\[3pt]
|
|
Attestation &
|
|
That a vendor-rooted platform reported a launch state, and reported the 64 bytes it was handed &
|
|
That the state is the behavior; that the host is outside the trust base, unless the VM is confidential; anything about a key the 64 bytes do not name \\[3pt]
|
|
Stake &
|
|
A price for a fault, once the fault is established &
|
|
Detection \\[3pt]
|
|
Challenge (HIP-0095) &
|
|
Detection of false capability and service claims &
|
|
Prevention; identity \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
Layering is not redundancy here: each control's ``does not establish'' column is
|
|
covered by a different row, and removing any row leaves a hole no other row fills.
|
|
A system with attestation and no stake detects faults it cannot price. A system
|
|
with stake and no attestation prices faults it cannot detect.
|
|
|
|
% =====================================================================
|
|
\section{The credentials that exist, and the one that replaces them}
|
|
\label{sec:three}
|
|
|
|
\subsection{Three, observed}
|
|
|
|
A node today presents three unrelated credentials, none of which is a wallet, and
|
|
no two of which name the same subject.
|
|
|
|
\begin{center}
|
|
\small
|
|
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{0.16\textwidth}>{\raggedright\arraybackslash}p{0.24\textwidth}>{\raggedright\arraybackslash}p{0.24\textwidth}>{\raggedright\arraybackslash}p{0.28\textwidth}@{}}
|
|
\toprule
|
|
\textbf{Credential} & \textbf{Names} & \textbf{Custody, observed} & \textbf{What its holder gets} \\
|
|
\midrule
|
|
\code{IDENTITY\_SEC\-RET\_KEY} (ed25519) &
|
|
the fabric node, by a free-form string &
|
|
plaintext \code{KEY=value} file, or the environment variable of the same name &
|
|
the ability to sign as that node, from anywhere \\[3pt]
|
|
IAM bearer token &
|
|
the operator's org, not the node &
|
|
an owner-only credential file under the CLI's data directory,
|
|
\code{\dots/hanzo/credentials} &
|
|
every cloud call the org can make \\[3pt]
|
|
\code{K3S\_TOKEN} &
|
|
nothing --- it is shared by every agent &
|
|
root-owned \code{0600} file on each agent &
|
|
membership of the cluster under any node name \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
\emph{Observed, in order.} \code{hanzoai/node},
|
|
\code{hanzo-bin/hanzo-node/src/utils/keys.rs}: \code{generate\_or\_load\_keys}
|
|
parses \code{IDENTITY\_SECRET\_KEY} out of a \code{KEY=value} file, falls back to
|
|
the environment variable, and otherwise generates an ephemeral key;
|
|
\code{src/runner.rs}:342--352 writes the secret back with \code{fs::write}, which
|
|
creates at the process umask rather than owner-only. \code{hanzoai/cloud},
|
|
\code{cli/link.go}: ``One identity: the IAM token \code{hanzo login} mints
|
|
authorizes every cloud call; the server derives the tenant from the token'' ---
|
|
correct for the operator, and it is the operator that is authenticated, not the
|
|
machine. On the measured fleet, the x86 host joined the cluster from
|
|
\code{/etc/systemd/system/k3s-agent.service.env}, mode \code{0600}, root-owned,
|
|
carrying \code{K3S\_TOKEN} and \code{K3S\_URL}; the control-plane holds the same
|
|
value at \code{/var/lib/rancher/k3s/server/node-token}.
|
|
|
|
\subsection{What each is worth}
|
|
|
|
The ed25519 key is at rung L0 of the custody ladder (\S\ref{sec:ladder}): a file
|
|
read is a forged identity, and by Claim~\ref{cl:indep} no statement it signs says
|
|
where it was signed. The IAM token authenticates a person or an org and is the
|
|
right credential for the act of linking a machine; it is the wrong credential for
|
|
a machine, because it is not per-machine, it carries the operator's authority, and
|
|
a node holding one can act as the operator. The k3s token is a fleet-wide shared
|
|
secret: it is not an identity at all, it is a capability to become a member, and
|
|
its holder chooses the name it joins under.
|
|
|
|
\begin{claim}
|
|
No composition of the three yields node identity. Two of them do not name a node,
|
|
and the one that does is at L0.
|
|
\end{claim}
|
|
|
|
\subsection{The replacement}
|
|
|
|
One key, one address, four uses:
|
|
|
|
\begin{enumerate}[label=(\alph*)]
|
|
\item the node signs its measured state with it, and at class \code{platform} or
|
|
above the hardware report commits to it (\S\ref{sec:bind});
|
|
\item it is the address bound to a bonded registration on the settlement L1, and
|
|
the address a forfeiture is charged against (\S\ref{sec:stake});
|
|
\item it is the address AI-work rewards pay (\S\ref{sec:reward});
|
|
\item it is the row key of the fleet view and the node's name in the cluster
|
|
(\S\ref{sec:enum}, \S\ref{sec:admit}).
|
|
\end{enumerate}
|
|
|
|
The IAM token stays where it belongs --- the operator, at the moment of linking,
|
|
authorizing the registration. It MUST NOT be the node's durable credential, and
|
|
\code{hanzod} MUST NOT hold an org-scoped bearer token that can act as the
|
|
operator. The k3s shared token is retired in favour of a short-lived credential
|
|
issued against $A_n$ (\S\ref{sec:admit}). This is HIP-0134's rule --- one process,
|
|
one socket, one identity --- applied to the machine rather than to the process.
|
|
|
|
% =====================================================================
|
|
\section{The identity}
|
|
\label{sec:identity}
|
|
|
|
\subsection{Two roles, one identity}
|
|
\label{sec:roles}
|
|
|
|
A single key cannot do both of the jobs a node needs done. The node key signs
|
|
constantly, from a box that runs other people's workloads and is, for third-party
|
|
nodes, outside our physical control. The bond must be movable only by someone we
|
|
still trust after that box is compromised. If they are the same key, compromise
|
|
of the node key also lets the attacker withdraw the bond, so the economic control
|
|
intended to price the compromise is unavailable in exactly the case it was posted
|
|
against.
|
|
|
|
Therefore:
|
|
|
|
\begin{description}
|
|
\item[Node key ($A_n$)] Lives with \code{hanzod}. Signs epoch statements and
|
|
machine records. \emph{Is the node identity.} Has no authority to move value.
|
|
\item[Owner key ($A_o$)] Lives with the operator, off the node. Registers the
|
|
node, posts and withdraws the bond, ends the validation, changes the reward
|
|
recipient. Never on the box.
|
|
\end{description}
|
|
|
|
This is one identity with an explicit controller, not two identities. $A_n$ is
|
|
what the fleet is enumerated by, what evidence is signed with, what work is
|
|
attributed to, and what rewards are paid to. $A_o$ appears once per lifecycle
|
|
event.
|
|
|
|
\emph{Observed:} the vendored validator manager sets
|
|
\code{\_posValidatorInfo[validationID].owner = \_msgSender()} at registration
|
|
(\code{luxfi/standard}, \code{contracts/validators/PoSValidatorManager.sol}:440)
|
|
and authorizes end-of-validation, reward withdrawal and recipient changes against
|
|
that one address (lines 196, 261, 303). There is no second address field. The
|
|
registration therefore knows $A_o$ and does not know $A_n$; supplying the binding
|
|
is \hyperref[sec:req]{IR-4}.
|
|
|
|
\begin{remark}
|
|
For a Hanzo-operated node, $A_o$ SHOULD be a \code{KindTreasury} or \code{KindMPC}
|
|
wallet --- threshold custody, no single holder --- and MUST NOT be a key present
|
|
on any node. \emph{Observed:} both custody kinds exist in \code{hanzoai/cloud},
|
|
\code{apps/wallet/custody.go}, and a \code{TierValidator} wallet tier already
|
|
exists in the same file.
|
|
\end{remark}
|
|
|
|
\subsection{Derivation}
|
|
|
|
$A_n$ is an EVM-runtime account address: the low 20 bytes of $\kec$ over the
|
|
uncompressed secp256k1 public key. \emph{Observed} in \code{hanzoai/cli},
|
|
\code{src/commands/wallet.rs} (\code{address\_from\_vk}), with the canonical path
|
|
\code{m/44\textquotesingle/60\textquotesingle/0\textquotesingle/0/0} for locally
|
|
derived keys.
|
|
|
|
The node key MUST be generated for the node and MUST NOT be derived from an
|
|
operator seed that also derives other nodes' keys. Derivation from a shared
|
|
mnemonic makes every node's key recoverable from one secret, which converts a
|
|
single custody failure into a fleet-wide one. Where a fleet seed is used for
|
|
operational reasons, each node's key MUST come from a distinct hardened account
|
|
index and the seed MUST NOT be present on any node.
|
|
|
|
\subsection{Why secp256k1, and not the ed25519 key that exists}
|
|
\label{sec:curve}
|
|
|
|
The choice is forced by where the identity has to be checked, not by taste.
|
|
|
|
\begin{enumerate}[label=(\roman*)]
|
|
\item \textbf{Forfeiture is verified on-chain.} An equivocation proof
|
|
(\S\ref{sec:equiv}) is two signature verifications inside a contract. The EVM
|
|
has \code{ecrecover} as a precompile at roughly 3000 gas; it has no ed25519
|
|
verifier. An ed25519 identity would make the one fault this design can prove
|
|
unprovable where it must be priced.
|
|
\item \textbf{Recovery removes a field.} \code{ecrecover} returns the address, so
|
|
a statement carries a signature and no public key, and a statement is
|
|
self-addressing: the identity is a function of the message and the signature.
|
|
With ed25519 the verifier needs the key alongside, and needs a mapping from key
|
|
to identity that is one more thing to get wrong.
|
|
\item \textbf{One key pays and signs.} The address that receives rewards is an
|
|
account address. Using a different curve for statements would reintroduce the
|
|
two-identifier reconciliation this proposal exists to remove.
|
|
\end{enumerate}
|
|
|
|
Signatures MUST be canonical: $s \le N/2$ where $N$ is the curve order (EIP-2),
|
|
and $v \in \{27, 28\}$. \emph{Observed:} the low-$S$ ceiling is already computed
|
|
and enforced in \code{hanzoai/cloud}, \code{apps/wallet/anchor.go}
|
|
(\code{secp256k1HalfN}), for the treasury anchor signer, so the rule is one the
|
|
platform already applies.
|
|
|
|
\subsection{The one object}
|
|
|
|
A node record is:
|
|
\[
|
|
\bigl(A_n,\; A_o,\; \text{chainID},\; L,\; W,\; \text{class},\;
|
|
\text{validationID},\; \text{org},\; \text{status}\bigr)
|
|
\]
|
|
and nothing else identifies a node anywhere in the system. \code{admin.hanzo.ai}
|
|
enumerates by $A_n$. The scheduler selects by $A_n$ and \code{class}. Settlement
|
|
pays $A_n$. The cluster names the node after $A_n$. The registry keys on $A_n$.
|
|
|
|
\emph{Observed gap:} \code{hanzo-attest}'s \code{Record} carries
|
|
\code{node: String} --- a free-form name --- and its \code{Signer} is an ed25519
|
|
public key (\code{evidence.rs}). Replacing both is \hyperref[sec:req]{IR-1}.
|
|
|
|
\emph{Relationship to HIP-0097.} HIP-0097 specifies \code{did:hanzo:<name>} as the
|
|
DID method for every Hanzo subject. $A_n$ is the node's key material and its
|
|
settlement address; a \code{did:hanzo:} name MAY resolve to a DID Document whose
|
|
verification method is $A_n$. Two names for one subject is acceptable only in that
|
|
direction --- the DID resolves to the address; the address is not derived from the
|
|
DID --- because settlement, admission and staking cannot depend on a resolver
|
|
being reachable.
|
|
|
|
% =====================================================================
|
|
\section{Custody}
|
|
\label{sec:custody}
|
|
|
|
\subsection{The ladder}
|
|
\label{sec:ladder}
|
|
|
|
Custody options differ in one measurable respect: what an attacker must obtain in
|
|
order to sign as $A_n$.
|
|
|
|
\begin{center}
|
|
\small
|
|
\begin{tabular}{@{}l>{\raggedright\arraybackslash}p{0.30\textwidth}>{\raggedright\arraybackslash}p{0.44\textwidth}@{}}
|
|
\toprule
|
|
& \textbf{Custody} & \textbf{To forge the identity, obtain} \\
|
|
\midrule
|
|
L0 & Plaintext file or environment variable & A file read as any user that can read it. \emph{This is the current state of \code{hanzod}.} \\
|
|
L1 & OS keychain, owner-only file & Code execution as the node's user. \\
|
|
L2 & Held by KMS/MPC; node never sees the key & A credential that authorizes KMS to sign --- which is a bearer token on the node. \\
|
|
L3 & Sealed to a platform: generated inside, non-extractable, released only under a policy over the measured state & Physical compromise of the platform, or a break in its root of trust. \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
L2 is where the interesting mistake lives. Moving the key into KMS is a genuine
|
|
improvement --- the key is not on the box, so a stolen disk is not a stolen
|
|
identity, and every signature is centrally logged and revocable. But it does not
|
|
close the possession--platform gap; it \emph{relocates} it. The question ``which
|
|
machine holds the key'' becomes ``which principal can make KMS sign'', and on a
|
|
node that principal is authenticated by a credential that sits on the node. Under
|
|
L2 the identity is exactly as strong as the custody of that credential --- which,
|
|
absent hardware, is L0 or L1 again.
|
|
|
|
\begin{claim}[The recursion]\label{cl:recursion}
|
|
Software custody delegation does not increase assurance about \emph{location}. It
|
|
replaces custody of a signing key with custody of an authorization credential.
|
|
The chain terminates only at a key that some hardware refuses to export.
|
|
\end{claim}
|
|
|
|
This is not an argument against KMS. It is an argument for what KMS custody must
|
|
be combined with, and it produces the requirements below.
|
|
|
|
\subsection{How a node key comes into existence}
|
|
\label{sec:gen}
|
|
|
|
Generation is where the assurance class is decided, because a key cannot be moved
|
|
up the ladder afterwards: a key that once existed outside a platform has an
|
|
unknowable custody history, and no later ceremony makes it non-extractable.
|
|
Exactly one of three procedures applies, chosen by what the host has:
|
|
|
|
\begin{description}
|
|
\item[L2, the default.] The operator, authenticated by IAM, asks the wallet
|
|
service for a new wallet of kind \code{kms} or \code{mpc} in tier
|
|
\code{validator}. The key is generated inside KMS, sealed under the KMS envelope,
|
|
and never returned; the service returns $A_n$. \code{hanzod} is issued a signing
|
|
credential scoped by C3. \emph{Observed:} this is the existing
|
|
\code{/v1/wallet/*} path in \code{hanzoai/cloud}, \code{apps/wallet}, where a
|
|
secp256k1 key is generated, sealed, and every signature recovers to the wallet
|
|
address.
|
|
|
|
\item[L3, where the hardware exists.] The key is generated inside the enclave, is
|
|
not exportable, and its use is conditioned on the measured state by G1--G4. The
|
|
enclave's session keys are attested against the measured state, and the epoch
|
|
statement signed by $sk_n$ names those keys (\S\ref{sec:field}), which is what
|
|
lets a verifier record class \code{confidential} against this identity.
|
|
|
|
\item[L1, development only.] The key is generated locally and held in the OS
|
|
keychain. \emph{Observed:} \code{hanzo wallet}'s local custody, where ``the
|
|
private key / mnemonic lives in the OS keychain, never on disk, never printed''.
|
|
A node at L1 MUST be recorded at class \code{self} and MUST NOT be bonded on
|
|
mainnet.
|
|
\end{description}
|
|
|
|
Three procedures are forbidden, and each is a way an operator might reasonably
|
|
think they were being careful:
|
|
|
|
\begin{itemize}
|
|
\item Generating the key elsewhere and copying it to the node --- the key exists in
|
|
two places from the first minute, and \S\ref{sec:equiv} will eventually charge
|
|
for it.
|
|
\item Deriving it from a fleet mnemonic --- one secret then recovers every node
|
|
(\S\ref{sec:identity}).
|
|
\item Backing it up. A node key has no backup: the recovery path for a lost node
|
|
key is succession under $A_o$ (\S\ref{sec:succ}), which costs one transaction,
|
|
whereas a backup is a second copy that equivocates the day it is restored.
|
|
\end{itemize}
|
|
|
|
\subsection{Requirements}
|
|
|
|
\begin{enumerate}[label=\textbf{C\arabic*.},leftmargin=2.6em]
|
|
\item The node key MUST NOT exist in plaintext at rest anywhere: not in an
|
|
environment variable, not in a file, not in a container image, not in a
|
|
Kubernetes Secret that is not itself KMS-sealed.
|
|
\item The node key SHOULD be held at L2 --- generated inside KMS/MPC, never
|
|
returned --- with \code{hanzod} holding only $A_n$ and a signing capability.
|
|
\emph{Observed:} this is exactly the cloud-custody model already implemented in
|
|
\code{hanzo wallet} (``the private key / mnemonic lives in the OS keychain,
|
|
never on disk, never printed'' for local custody; server-side keys for cloud
|
|
custody) and in \code{cloud/apps/wallet/custody.go}, where \code{KindKMS} seals
|
|
a real secp256k1 key under the KMS envelope and every \code{Sign} recovers to
|
|
the wallet address.
|
|
\item Where the node key is at L2, the credential that authorizes signing MUST be
|
|
scoped to (a) this node's wallet only, (b) the epoch-statement and
|
|
machine-record message shapes only, and (c) a bounded rate. A node's signing
|
|
credential MUST NOT be able to sign an arbitrary transaction, because a
|
|
credential that can sign anything is the owner key by another name.
|
|
\item On a platform with a TPM 2.0, the credential in C3 MUST be sealed to a PCR
|
|
policy, so that a credential lifted from the disk is unusable on a different
|
|
platform or after a change to the boot chain.
|
|
\item On a platform with a hardware enclave (SEV-SNP, TDX, CCA), the key SHOULD be
|
|
generated inside the enclave and its release conditioned on the enclave
|
|
measurement, reaching L3.
|
|
\item Secrets MUST be stored only in Hanzo KMS. Where a digest suffices, the
|
|
digest is stored and the value is not. No node key, signing credential or
|
|
cluster token appears in a manifest, a repository or a report.
|
|
\item A signing operation MUST be refused when the node record's status is revoked.
|
|
Revocation that only stops scheduling leaves a compromised node able to make
|
|
statements the network still verifies.
|
|
\end{enumerate}
|
|
|
|
\emph{Observed} for the KMS envelope this relies on: secrets are sealed as
|
|
AES-256-GCM over a per-secret data key with the data key wrapped by ML-KEM
|
|
(\code{luxfi/kms}, \code{pkg/store/secrets.go}, \code{ModeStandard = "aead+mlkem"}),
|
|
the additional authenticated data binds path, name, environment and version, and
|
|
\code{pkg/store/crypto\_test.go:TestAADBindingPreventsSwap} demonstrates that
|
|
swapping one secret's wrapped data key into another fails. Transport is a ZAP
|
|
handshake combining X25519 and ML-KEM-768 under HKDF-SHA256
|
|
(\code{pkg/zap/handshake.go}).
|
|
|
|
\subsection{Release conditioned on the measured state}
|
|
|
|
At L3 the release policy is the point of the exercise, so it is specified rather
|
|
than left to configuration:
|
|
|
|
\begin{enumerate}[label=\textbf{G\arabic*.},leftmargin=2.6em]
|
|
\item The key material is generated inside the platform and is not exportable.
|
|
\item A report whose report-data field commits to the measured state and to keys
|
|
the epoch statement names (\S\ref{sec:field}) is the only evidence that reaches
|
|
the platform. Its freshness term MUST be one the node could not have chosen in
|
|
advance.
|
|
\item Release --- or use --- MUST be denied when the measured state is not on the
|
|
allowed list for the node's declared image. Denial MUST be the default on any
|
|
error, including an unreachable verifier.
|
|
\item The allowed list is a set of register values, published with the release,
|
|
and changed by a release of the image --- not by an operator edit on the node.
|
|
\S\ref{sec:threat} records the publisher as a trust anchor.
|
|
\end{enumerate}
|
|
|
|
\subsection{Assurance class}
|
|
\label{sec:class}
|
|
|
|
A node's assurance class is derived from evidence the network verified. It is a
|
|
field on the node record, it is visible in the console and to the scheduler, and a
|
|
node MUST NOT be able to set it.
|
|
|
|
\begin{center}
|
|
\small
|
|
\begin{tabular}{@{}l>{\raggedright\arraybackslash}p{0.22\textwidth}>{\raggedright\arraybackslash}p{0.50\textwidth}@{}}
|
|
\toprule
|
|
\textbf{Class} & \textbf{Root} & \textbf{Requires} \\
|
|
\midrule
|
|
\code{self} & the node's own key &
|
|
An epoch statement verifying to $A_n$. Trust in it is trust in the node operator.
|
|
\emph{Implementable today.} \\[3pt]
|
|
\code{platform} & TPM 2.0 &
|
|
A quote over PCRs committing to $A_n$ and a freshness input, \emph{and} an
|
|
enforced verified boot chain. Without enforcement the PCR values are
|
|
attacker-chosen and the class is \code{self}. \emph{Proposed.} \\[3pt]
|
|
\code{confidential} & SEV-SNP, TDX, CCA &
|
|
A vendor-rooted report, joined to $A_n$ by an epoch statement naming the session
|
|
keys the report attests (\S\ref{sec:field}); removes the host from the trust base.
|
|
\emph{Request path implemented, no hardware on the current fleet.} \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
\emph{Observed} on the current fleet (Appendix~\ref{app:fleet}): both Linux hosts
|
|
have TPM 2.0 with a SHA-256 PCR bank and a firmware event log at
|
|
\code{/sys/kernel/security/tpm0/binary\_bios\_measurements}. They differ in one
|
|
respect that decides the class: the aarch64 host has SecureBoot enabled, the x86
|
|
host has it disabled. The x86 host also reports \code{sev\_snp = N}, so it cannot
|
|
reach \code{confidential} on the running configuration. The Apple-Silicon host
|
|
exposes no TPM and no confidential-VM facility to a Linux guest --- it is
|
|
\code{self}, and this is a property of the platform, not a deficiency to be worked
|
|
around.
|
|
|
|
\begin{remark}[Why the Mac is in the fleet anyway]
|
|
The microVM is the uniformity layer: it is what lets a Mac present the same k3s
|
|
node as a Linux box. Uniformity of \emph{interface} is not uniformity of
|
|
\emph{assurance}, and the design does not pretend otherwise. The Mac is a
|
|
first-class node at class \code{self}, and the scheduler is what decides which
|
|
workloads a \code{self} node may receive. One identity primitive, one enumerable
|
|
field for the difference.
|
|
\end{remark}
|
|
|
|
Given the hardware actually present, \emph{a TPM 2.0 attester and verifier is
|
|
worth more than SEV-SNP or TDX support}, because two of three hosts have the
|
|
hardware today and zero have SNP or TDX. That is \hyperref[sec:req]{IR-5}.
|
|
|
|
% =====================================================================
|
|
\section{Binding the key to the platform}
|
|
\label{sec:bind}
|
|
|
|
Both shipping confidential-computing platforms offer the same primitive: a guest
|
|
asks its firmware for a report over 64 caller-supplied bytes, and the report also
|
|
carries the platform's own measurement of how that guest was started. What goes
|
|
into those 64 bytes decides what the report is evidence of. This section states
|
|
what the implementation puts there, what that does and does not establish, and the
|
|
smallest change that makes a report evidence about a node rather than about an
|
|
image.
|
|
|
|
\subsection{What is implemented}
|
|
|
|
\emph{Observed}, \code{hanzoai/vm}, \code{crates/vm-measure}. A measurement is two
|
|
logs, each folded into a SHA-384 register by the extend rule that Intel TDX
|
|
applies to a runtime measurement register, so a register built by the launcher
|
|
reproduces an RTMR that started at zero and received the same events in the same
|
|
order. The two registers are then bound into the 64 bytes a hardware report
|
|
carries as its caller-supplied field:
|
|
\[
|
|
\beta \;=\; \sha{512}\bigl(L \,\|\, W\bigr).
|
|
\]
|
|
\code{attest::status} asks the platform for a report over $\beta$ when
|
|
\code{/dev/sev-guest} or \code{/dev/tdx\_guest} is present, and records the raw
|
|
report beside the measurement. The verifier's obligation is stated in the crate as
|
|
one line: the report's caller field must equal the measurement's bind.
|
|
|
|
The design is right in its choice of digest --- SHA-384 is what both TDX registers
|
|
and the SEV-SNP launch measurement use, so nothing is re-hashed at the boundary to
|
|
hardware --- and right in separating launch from workload, because the two are
|
|
extended by different programs at different times and map onto separate registers
|
|
on TDX.
|
|
|
|
\begin{remark}[What the launch register covers]
|
|
The launch log's events are the kernel, the command line, the initramfs where
|
|
present, the source root image and the shape --- what the hypervisor was handed.
|
|
The hypervisor itself is not among them. On a confidential VM that is correct: the
|
|
platform's own launch measurement covers the firmware and the VMM-supplied state,
|
|
so measuring the monitor from inside would duplicate a value the hardware already
|
|
signs. At class \code{self} nothing covers it, and a node that swaps its monitor
|
|
binary produces the same register. \code{hanzo-attest}'s encoding does include the
|
|
monitor's digest, which is the right choice for the class it serves. A single
|
|
measurement (IR-3) has to keep both behaviours: the monitor digest is measured
|
|
where no platform measurement exists, and omitted where one does.
|
|
\end{remark}
|
|
|
|
\subsection{Two properties $\beta$ does not have}
|
|
|
|
\begin{claim}[Transferability]\label{cl:transfer}
|
|
Let two nodes $h_1 \ne h_2$ run the same release, so
|
|
$(L_1, W_1) = (L_2, W_2)$ and therefore $\beta_1 = \beta_2$. A report obtained by
|
|
$h_1$ verifies, under the vendor root, as a report over $\beta_2$. A verifier
|
|
checking only ``the report's caller field equals the bind'' accepts $h_1$'s report
|
|
as evidence for $h_2$.
|
|
\end{claim}
|
|
|
|
\noindent The uniformity that makes the fleet operable is what makes the report
|
|
transferable: identical inputs are the goal, and identical inputs produce an
|
|
identical bind. The report does carry platform-specific material --- a chip
|
|
identifier on SEV-SNP, a TCB version --- but a verifier that does not bind those
|
|
to a node record is not using them, and there is no field in the node record for
|
|
them to bind to. Nothing in $\beta$ names the node.
|
|
|
|
\begin{claim}[Staleness]\label{cl:stale}
|
|
$\beta$ is a function of the release alone. A report over it is therefore valid
|
|
forever and replayable by anyone who obtains it once. Neither SEV-SNP nor TDX
|
|
places a timestamp in the report; freshness in both designs comes from the
|
|
caller-supplied field.
|
|
\end{claim}
|
|
|
|
Together these say that the implemented path, completed, would establish
|
|
\emph{that some platform of the right kind launched this image at some time} ---
|
|
which is worth having, and is not node identity.
|
|
|
|
\subsection{Who owns the field}
|
|
\label{sec:field}
|
|
|
|
The confidential-tier proposal in this repository
|
|
(\code{hip-confidential-tier.tex}) specifies the contents of the report-data field
|
|
and reaches Claims~\ref{cl:transfer} and~\ref{cl:stale} independently: ``a binding
|
|
over the boot log alone omits the keys the session runs on and omits any freshness
|
|
term, so the report is replayable by anyone holding a copy''. Its requirement B1
|
|
is
|
|
\[
|
|
\mathrm{rd} \;=\; \sha{512}\bigl(\text{``hanzo/rd/v1''} \,\|\, L \,\|\, W \,\|\,
|
|
pk_e \,\|\, pk_r \,\|\, \mathrm{sid} \,\|\, n_v \bigr),
|
|
\]
|
|
where $pk_e$ is the environment's key-exchange public key, $pk_r$ its receipt
|
|
verifying key, $\mathrm{sid}$ the session identifier, and $n_v$ a verifier nonce.
|
|
Its B3 requires $sk_e$ and $sk_r$ to be generated inside the environment and to
|
|
die with it.
|
|
|
|
There is one report-data field and it has one specification. This document does
|
|
not propose a second. What it observes is that B1 contains no durable identity:
|
|
$pk_e$ and $pk_r$ are ephemeral by construction, so a report under B1 establishes
|
|
that \emph{some} confidential environment holds \emph{these session keys}, and
|
|
nothing that ties that environment to the bonded, staked, paid node the rest of
|
|
this document is about. The bond, the reward and the fleet row hang off $A_n$; the
|
|
hardware evidence hangs off $pk_r$; today nothing joins the two.
|
|
|
|
\subsection{Joining identity to hardware}
|
|
|
|
\emph{Proposed.} The join is a signature, not a second field. The epoch statement
|
|
(\S\ref{sec:stmt}) carries the session terms, so that $sk_n$ endorses exactly the
|
|
keys the report attests:
|
|
\[
|
|
\text{report} \;\longrightarrow\; pk_r \;\longleftarrow\; \text{epoch statement}
|
|
\;\longrightarrow\; A_n .
|
|
\]
|
|
A verifier checks two links it can check separately: the report verifies under the
|
|
vendor root and its report-data equals B1 recomputed over $(L, W, pk_e, pk_r,
|
|
\mathrm{sid}, n_v)$; and the epoch statement naming those same $(pk_e, pk_r,
|
|
\mathrm{sid})$ recovers to $A_n$, which is bound to an active bonded registration.
|
|
|
|
\begin{claim}[What the composition establishes]\label{cl:bind}
|
|
Under B1 plus an epoch statement naming the same session terms: a platform
|
|
certified by $R$, launched in the recorded state, held the measurement log folding
|
|
to $(L,W)$ and the session keys $(pk_e, pk_r)$; and the holder of $sk_n$, bonded
|
|
under an active registration, endorsed those session keys for epoch $e$. Work
|
|
performed under $sk_r$ is therefore attributable to $A_n$, and $A_n$'s bond answers
|
|
for it.
|
|
\end{claim}
|
|
|
|
\begin{remark}[What it still does not establish]
|
|
$\Conf(sk_n, h)$ --- that the node key is on that platform. The composition binds
|
|
the identity to the \emph{session}, not the identity key to the \emph{silicon}: an
|
|
operator holding $sk_n$ off the box can endorse session keys held by a confidential
|
|
machine they also control. That is sufficient for settlement, which needs to know
|
|
whose bond answers for the work, and it is not the same statement as ``this node's
|
|
key lives in this enclave''. Reaching that requires generation inside the platform
|
|
and a non-extractability statement the verifier checks --- requirement (i) of
|
|
\S\ref{sec:gap} and requirement G1 --- and it would additionally require $A_n$ to
|
|
appear in the report-data preimage. Adding $A_n$ to B1 is therefore a strictly
|
|
stronger option, at the cost of one term in a field that already has four
|
|
claimants; it is offered as \hyperref[sec:req]{IR-2} for the confidential-tier
|
|
proposal to take or decline, not asserted here.
|
|
\end{remark}
|
|
|
|
Because $n_v$ is per-session and B2 fixes the report to a completed measurement
|
|
chain, a report is requested once per session rather than once per epoch. The
|
|
epoch statement is what carries a per-epoch freshness term for the identity
|
|
(\S\ref{sec:join}); the two freshness inputs sit at different layers and neither
|
|
substitutes for the other, which the confidential-tier proposal states for its own
|
|
pair of nonces and which holds here for the same reason.
|
|
|
|
% =====================================================================
|
|
\section{Joining}
|
|
\label{sec:join}
|
|
|
|
\subsection{Epochs come from the chain}
|
|
|
|
Let $H$ be the height of the settlement L1 and let $K$ be the epoch length in
|
|
blocks. The epoch is $e = \lfloor H / K \rfloor$.
|
|
|
|
Deriving the epoch from block height rather than wall-clock time removes clock
|
|
skew from the protocol: two verifiers agree on the current epoch because they
|
|
agree on the chain, and a node with a fast clock cannot publish into the future.
|
|
The block hash $B$ of any block in epoch $e$ is a value no node could have
|
|
predicted before that block, which is what makes the epoch statement fresh without
|
|
a round trip to a verifier. The hardware report has its own freshness term, a
|
|
verifier nonce, at its own layer (\S\ref{sec:field}).
|
|
|
|
$K$ MUST be chosen so that an epoch is minutes, not hours: short enough that a
|
|
node restarting with a new image is unavailable for a short time
|
|
(\S\ref{sec:equiv}), long enough that ordinary restarts do not dominate statement
|
|
volume.
|
|
|
|
\subsection{The epoch statement}
|
|
\label{sec:stmt}
|
|
|
|
A node publishes one statement per epoch. \emph{Proposed}, normatively:
|
|
|
|
\begin{center}
|
|
\small
|
|
\begin{tabular}{@{}llp{0.42\textwidth}@{}}
|
|
\toprule
|
|
\textbf{Field} & \textbf{Bytes} & \textbf{Meaning} \\
|
|
\midrule
|
|
tag & \code{field(hanzo.node.epoch.v1)} & Domain separation. \\
|
|
chainID & \code{u64} big-endian & The settlement L1. Mainnet 36963. \\
|
|
node & 20 & $A_n$. \\
|
|
epoch & \code{u64} big-endian & $e$. \\
|
|
block & 32 & $B$, a block hash in epoch $e$. \\
|
|
launch & 48 & $L$ of the node microVM. \\
|
|
workload & 48 & $W$ of the node microVM. \\
|
|
session & \code{field(\dots)} & $pk_e \| pk_r \| \mathrm{sid}$, or empty at class \code{self}. \\
|
|
evidence & \code{field(\dots)} & The raw hardware report, or empty. \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
\noindent where \code{field(x)} is the length of \code{x} as a big-endian
|
|
\code{u64} followed by \code{x} --- the encoding \code{hanzo-attest} and
|
|
\code{vm-measure} already use, so that no two distinct statements encode to the
|
|
same bytes. The signature is over $\kec$ of the encoding:
|
|
\[
|
|
\sigma \;=\; \Sign^{\mathrm{rec}}_{sk_n}\bigl(\kec(S)\bigr), \qquad
|
|
A_n \;=\; \mathtt{ecrecover}\bigl(\kec(S), \sigma\bigr).
|
|
\]
|
|
|
|
Six remarks, none decorative:
|
|
|
|
\begin{description}
|
|
\item[The statement carries no public key.] The address is recovered from the
|
|
signature (\S\ref{sec:curve}), so the statement is self-addressing and there is
|
|
no key-to-identity mapping to keep consistent.
|
|
\item[chainID] prevents a statement made for testnet from being replayed on
|
|
mainnet. \emph{Observed:} the sovereign L1 uses $\text{networkID} =
|
|
\text{chainID}$, mainnet 36963, testnet 36962, devnet 36964, localnet 1337
|
|
(\code{hanzoai/cli}, \code{src/commands/network.rs}).
|
|
\item[node] appears in the preimage as well as being recoverable, so that a
|
|
statement cannot be filed under a different registration even if the recovery is
|
|
performed loosely by some reader.
|
|
\item[epoch and block] make the statement unique to an epoch and unforgeable
|
|
ahead of it. Uniqueness is what makes equivocation provable
|
|
(\S\ref{sec:equiv}); the block hash is what makes the statement fresh, so that a
|
|
node cannot pre-sign a year of epochs.
|
|
\item[launch and workload] are the registers of the \emph{node microVM} --- the
|
|
one that joins the cluster --- not of a tenant machine. The epoch statement is
|
|
distinct from the per-machine record: a node serves many machines and files many
|
|
records; it has exactly one node microVM and files exactly one epoch statement
|
|
per epoch. Conflating them would make honest multi-tenancy look like
|
|
equivocation. $L$ is fixed at launch. $W$ is the workload register \emph{as of
|
|
the moment the statement is made}, and an extension after that moment changes
|
|
$W$ without producing a new statement --- which is the time-of-check to
|
|
time-of-use residue, stated again here because this is the field where a reader
|
|
is most likely to assume otherwise. A node MUST NOT publish a second statement to
|
|
report the new $W$; it waits for the next epoch, for the reason in
|
|
\S\ref{sec:equiv}.
|
|
\item[session] carries the environment's session keys and session identifier, so
|
|
that the signature under $sk_n$ endorses exactly the keys the hardware report
|
|
attests (\S\ref{sec:field}). Empty at class \code{self}, where there is no
|
|
report and no environment to hold keys.
|
|
\item[evidence] carries the report and not a class. A node states what it can
|
|
show; the verifier derives the class. A statement that asserted its own class
|
|
would be a node setting a field it MUST NOT set (\S\ref{sec:class}).
|
|
\end{description}
|
|
|
|
\emph{Observed gap:} the preimage implemented in \code{hanzo-attest} is
|
|
$(\text{tag}, \text{kind}, \text{measurement}, \text{timestamp})$
|
|
(\code{evidence.rs}, \code{preimage}). It carries no chain, no address and no
|
|
epoch, so it is presentable under any registration on any chain and equivocation
|
|
over it is undefined; its timestamp is the node's own clock. Replacing it is
|
|
\hyperref[sec:req]{IR-3}.
|
|
|
|
\subsection{What the network verifies}
|
|
|
|
On receiving a join or an epoch statement, the verifier MUST, in order, and
|
|
refusing at the first failure:
|
|
|
|
\begin{enumerate}[label=\textbf{V\arabic*.},leftmargin=2.6em]
|
|
\item \textbf{Shape.} $S$ parses; \code{chainID} is this chain; $e$ is the current
|
|
epoch or the immediately previous one; $\sigma$ is canonical ($s \le N/2$).
|
|
\item \textbf{Signature.} $\mathtt{ecrecover}(\kec(S), \sigma)$ equals the
|
|
\code{node} field.
|
|
\item \textbf{Freshness.} $B$ is the hash of a block this verifier knows, at a
|
|
height inside epoch $e$.
|
|
\item \textbf{Registration.} $A_n$ is bound to an active \code{validationID} whose
|
|
bond meets the minimum, the binding was written by $A_o$, and the record is not
|
|
revoked.
|
|
\item \textbf{Uniqueness.} No other statement exists for $(\text{chainID}, A_n, e)$.
|
|
A second, differing statement is not an error to discard --- it is evidence to
|
|
keep (\S\ref{sec:equiv}).
|
|
\item \textbf{Image.} $(L, W)$ is on the allowed list for the release the node
|
|
claims. A node running an unpublished image is admitted at class \code{self}
|
|
with no image assertion, or refused, according to network policy; it MUST NOT be
|
|
admitted as though it were running a published image.
|
|
\item \textbf{Class.} Any hardware evidence verifies under the vendor root, and its
|
|
report-data field equals the confidential-tier proposal's B1 recomputed over
|
|
$(L, W)$ from this statement and the session terms in this statement's
|
|
\code{session} field. A report whose session terms are not the ones $sk_n$
|
|
signed is a report about a different environment. The class recorded is the
|
|
highest class \emph{proved}, defaulting to \code{self}.
|
|
\item \textbf{Rate.} At most one accepted statement per $(A_n, e)$ enters the
|
|
record, and unverified statements are dropped before signature verification
|
|
exceeds a per-source budget. Signature verification is the expensive step and
|
|
MUST NOT be reachable in unbounded volume by an unregistered source.
|
|
\end{enumerate}
|
|
|
|
Every failure is a refusal. A verifier that cannot reach the vendor root records
|
|
class \code{self}; it MUST NOT record a higher class on an unverified claim, and
|
|
it MUST NOT refuse the join for that reason alone --- the two are different
|
|
decisions, and conflating them turns an availability failure into a fleet outage.
|
|
|
|
\subsection{What the network has after V1--V8}
|
|
|
|
Precisely: that the holder of $sk_n$, bound to an active bonded registration,
|
|
asserted image $(L, W)$ for epoch $e$ on this chain, within the freshness window
|
|
that $B$ defines; and that at class \code{platform} or above, a vendor-rooted
|
|
platform reported that state together with this address. Nothing in
|
|
\S\ref{sec:notprove} that is not covered by class \code{platform} or above becomes
|
|
true because V1--V8 passed.
|
|
|
|
\subsection{Admission to the cluster}
|
|
\label{sec:admit}
|
|
|
|
The chain says who may join. The cluster has to enforce it, and today it does not:
|
|
membership is granted by a shared bearer token (\S\ref{sec:three}), which is a
|
|
capability, not an identity, and which every agent holds in full.
|
|
|
|
\emph{Observed:} the mechanism for doing this correctly is already present and
|
|
unused. k3s issues Kubernetes bootstrap tokens --- \code{k3s token create} takes
|
|
\code{--ttl}, \code{--groups} and \code{--usages} --- and the cluster carries the
|
|
standard \code{system:node} role binding. What is missing is not a protocol. It is
|
|
the decision of whom to issue a token to, and the retirement of the shared secret
|
|
that makes the decision unnecessary today.
|
|
|
|
\emph{Proposed.} No new authentication is invented; the existing flow is driven by
|
|
the chain.
|
|
|
|
\begin{enumerate}[label=\textbf{A\arabic*.},leftmargin=2.6em]
|
|
\item The node's name in the cluster is derived from its address:
|
|
\code{n-}\textit{40 hex characters}, lowercase, 42 characters --- a valid
|
|
DNS-1123 label. One name, derived, not chosen.
|
|
\item \code{hanzod} presents its epoch statement to an admission service on the
|
|
control plane. The service performs V1--V8 and, on success, creates a bootstrap
|
|
token with a short \code{--ttl} --- minutes, against the 24-hour default --- for
|
|
that node's join and no other. The service holds the server token; the node
|
|
never sees it.
|
|
\item The kubelet uses the bootstrap token for TLS bootstrap only: it obtains a
|
|
client certificate with subject \code{system:node:n-\dots} in group
|
|
\code{system:nodes}, and the bootstrap token is then spent or expires.
|
|
\item The API server runs the Node authorizer and the \code{NodeRestriction}
|
|
admission plugin, so that a node's certificate authorizes it to read and write
|
|
its own \code{Node} object and the objects scheduled to it, and nothing else.
|
|
\item The k3s server token is a cluster secret, held in KMS, delivered to the
|
|
control plane and to no agent. \code{K3S\_TOKEN} on an agent is retired, and the
|
|
server token is rotated once it is (\code{k3s token rotate}) because every agent
|
|
has held it.
|
|
\item A revoked node's certificate is not renewed and its \code{Node} object is
|
|
cordoned and drained. Certificate lifetime bounds how long a revoked node remains
|
|
a member; it MUST be short relative to the unbonding period.
|
|
\end{enumerate}
|
|
|
|
This closes the loop the wallet-as-identity design opens: the address is not only
|
|
the name in the console and the account on the chain, it is what the cluster
|
|
admits. Everything above it --- workloads, scheduling, settlement --- then refers
|
|
to one identifier. It is \hyperref[sec:req]{IR-6}.
|
|
|
|
% =====================================================================
|
|
\section{Stake and forfeiture}
|
|
\label{sec:stake}
|
|
|
|
\subsection{What exists}
|
|
|
|
\emph{Observed}, in \code{luxfi/standard}, \code{contracts/validators/} --- the
|
|
vendored ACP-77 validator manager, renamed to Network/L1 nomenclature with the ABI
|
|
preserved:
|
|
|
|
\begin{itemize}
|
|
\item \code{initializeValidatorRegistration} is \code{payable}. The native value
|
|
sent stays in the manager and is the bond; \code{\_lock} records the amount and
|
|
\code{\_unlock} returns it with \code{payable(to).sendValue(value)}
|
|
(\code{NativeTokenStakingManager.sol}).
|
|
\item The registration is keyed by \code{validationID} (\code{bytes32}) and
|
|
records \code{owner = \_msgSender()}, a delegation fee, a minimum stake
|
|
duration, and an uptime counter (\code{PoSValidatorManager.sol}:440).
|
|
\item \code{submitUptimeProof} and the end-of-validation path condition rewards on
|
|
uptime; below the threshold, rewards are forfeit and the bond is still returned.
|
|
\item The registration input carries \code{nodeID} (\code{bytes}) and
|
|
\code{blsPublicKey}. The contract checks the BLS key's \emph{length} and that
|
|
\code{nodeID} is non-empty and not already registered
|
|
(\code{ValidatorManager.sol}). It does not verify possession of either key.
|
|
\item \textbf{There is no slashing.} A case-insensitive search for \code{slash}
|
|
across \code{contracts/validators/} returns nothing.
|
|
\end{itemize}
|
|
|
|
So the economic instrument that exists today is uptime-conditioned reward
|
|
forfeiture plus bond return. Any statement that a bad attestation ``slashes the
|
|
stake'' is, as of this writing, a proposal. This document treats it as one.
|
|
|
|
\subsection{Registration binds the identity}
|
|
|
|
\emph{Proposed.} A separate, minimal registry contract --- not a fork of the
|
|
vendored manager, whose ABI is preserved deliberately for in-place upgradability:
|
|
|
|
\begin{quote}\ttfamily
|
|
mapping(bytes32 validationID => address node) nodes;\\
|
|
mapping(address node => bytes32 validationID) registrations;\\
|
|
\\
|
|
function bind(bytes32 validationID, address node) external;\\
|
|
event Bound(bytes32 indexed validationID, address indexed node);
|
|
\end{quote}
|
|
|
|
\code{bind} is callable only by the \code{owner} of \code{validationID} as the
|
|
manager reports it; it refuses a node address bound to any registration, live or
|
|
retired (\S\ref{sec:decom}); and it refuses to rebind a live registration except
|
|
by the succession path of \S\ref{sec:succ}. One mapping pair, one function, one
|
|
event. Everything else --- weight, churn, uptime, delegation --- stays where it
|
|
already works.
|
|
|
|
\code{bind} SHOULD require a proof of possession of $A_n$: a signature by $sk_n$
|
|
over $(\text{chainID}, \text{validationID}, A_o)$, verified with
|
|
\code{ecrecover}. Without it, an operator can bind an address they do not hold,
|
|
which does not steal anything --- rewards pay an address the operator chose to
|
|
name --- but does let them file a registration against someone else's identity and
|
|
so pollute the injectivity the fleet view depends on.
|
|
|
|
\subsection{Forfeiture for self-proving faults}
|
|
\label{sec:forfeit}
|
|
|
|
\emph{Proposed.} On-chain forfeiture applies to faults that are
|
|
\emph{self-proving}: a verifier can establish them from the submitted evidence
|
|
alone, in constant time, without a vote, an oracle, or a trusted reporter. This
|
|
restriction is the whole design, and it is what keeps the mechanism from becoming
|
|
a governance surface.
|
|
|
|
\begin{quote}\ttfamily
|
|
function forfeit(bytes32 validationID, bytes calldata proof) external;\\
|
|
event Forfeited(bytes32 indexed validationID, uint256 amount, bytes32 fault);
|
|
\end{quote}
|
|
|
|
The contract verifies \code{proof} itself and burns or redistributes the bond
|
|
according to the network's reward policy. Anyone may submit; a submitter reward
|
|
paid from the forfeited amount is what makes detection someone's job. A
|
|
\code{validationID} already forfeited MUST reject a second submission, and a proof
|
|
whose epoch is older than the unbonding period MUST be refused, so that a bond can
|
|
eventually settle.
|
|
|
|
Faults that are \emph{not} self-proving --- poor service, wrong answers,
|
|
unavailability, a false inventory claim --- are handled by the mechanisms that
|
|
already exist for them: uptime-conditioned reward forfeiture in the manager, QoS
|
|
challenges (HIP-0095), and delisting by the scheduler. They MUST NOT be routed
|
|
through \code{forfeit}, because each would require a trusted reporter, and a
|
|
trusted reporter that can burn a stranger's bond is a larger risk than the
|
|
behavior it polices.
|
|
|
|
\subsection{Equivocation}
|
|
\label{sec:equiv}
|
|
|
|
\begin{definition}[Equivocation]
|
|
Two epoch statements $S_1, S_2$ with the same $(\text{chainID}, A_n, e)$,
|
|
differing in any other field, both recovering to $A_n$.
|
|
\end{definition}
|
|
|
|
The proof submitted to \code{forfeit} is $(S_1 \| \sigma_1 \| S_2 \| \sigma_2)$.
|
|
The contract computes two Keccak digests and two \code{ecrecover} calls, checks
|
|
that both recover to the node bound to \code{validationID}, that the triples
|
|
agree, and that the statements differ. That is constant work, roughly two
|
|
precompile calls, and it needs nothing off-chain.
|
|
|
|
\begin{claim}
|
|
Equivocation is publicly verifiable in two signature verifications and one
|
|
inequality, and an honest node running one node microVM cannot produce it.
|
|
\end{claim}
|
|
|
|
\paragraph{No supersede.} A node that restarts with a different image mid-epoch
|
|
MUST NOT publish a second statement; it waits for epoch $e+1$. An earlier draft of
|
|
this design permitted a \emph{supersede} statement naming the prior statement's
|
|
digest, to spare the node that wait. That is unsound: two machines holding the
|
|
same key can chain supersede statements at each other and never equivocate, which
|
|
removes the only fault the design can prove. The wait is the price of a detector
|
|
that cannot be talked out of firing, and $K$ is chosen (\S\ref{sec:join}) to make
|
|
the price small.
|
|
|
|
\paragraph{What it catches.} Uncoordinated duplicate use of the key: a backup
|
|
restored onto a second box, a stolen key run in parallel with the honest node, a
|
|
misconfigured redeployment. In each case two parties publish for the same epoch
|
|
without agreeing, the statements differ, and either party's statement together with
|
|
the other's is the proof. Detection requires no monitoring: the statements are the
|
|
evidence, and both are public.
|
|
|
|
\paragraph{What it does not catch.} A single adversary running $N$ coordinated
|
|
copies that agree to publish exactly one statement per epoch. Equivocation is
|
|
silent there, and this must be stated rather than assumed away. What bounds the
|
|
damage is the reward design rather than the detector:
|
|
|
|
\begin{claim}[A stolen node key does not move money]\label{cl:nomoney}
|
|
Rewards pay $A_n$, and the reward recipient is changed only by $A_o$
|
|
(\S\ref{sec:reward}). A party holding $sk_n$ and not $sk_o$ can make statements
|
|
attributed to the node and cannot redirect a payment. Coordinated copies therefore
|
|
earn into the honest operator's address.
|
|
\end{claim}
|
|
|
|
\noindent The residual gain to a coordinated adversary is the ability to serve
|
|
work under the node's identity --- which is bounded by the work being real work,
|
|
priced by HIP-0096 --- and to consume the node's standing, for which the remedy is
|
|
succession (\S\ref{sec:succ}). The custody requirements of \S\ref{sec:custody}
|
|
exist because this residue cannot be removed by a detector.
|
|
|
|
% =====================================================================
|
|
\section{Reward}
|
|
\label{sec:reward}
|
|
|
|
Rewards pay $A_n$ --- the identity address --- from two independent sources:
|
|
|
|
\begin{description}
|
|
\item[Validation rewards] From the validator manager, conditioned on uptime.
|
|
\emph{Observed:} the manager keeps a \code{\_rewardRecipients} mapping that
|
|
\emph{defaults to} \code{owner} (\code{PoSValidatorManager.sol}) and can be
|
|
pointed elsewhere by \code{changeValidatorRewardRecipient}.
|
|
\item[Work rewards] From compute settlement (HIP-0096), for AI work the node
|
|
actually served.
|
|
\end{description}
|
|
|
|
Two consequences follow from the observed divergence, and both must be stated
|
|
rather than assumed away:
|
|
|
|
\begin{enumerate}[label=\textbf{R\arabic*.},leftmargin=2.6em]
|
|
\item The reward recipient MUST default to the identity address. Divergence is
|
|
permitted --- an operator consolidating payouts is a legitimate need --- but it
|
|
is an explicit on-chain act by $A_o$, visible in the fleet view next to the node
|
|
it applies to.
|
|
\item Accounting, attribution and enumeration key on $A_n$, \textbf{never} on the
|
|
recipient. A recipient is where money goes; it is not who did the work, it is
|
|
not unique to a node, and treating it as an identifier silently merges distinct
|
|
nodes into one row.
|
|
\end{enumerate}
|
|
|
|
Paying the address the node signs with means ``which node earned this'' and ``which
|
|
node said this'' resolve through the same lookup. That is the practical content of
|
|
using one primitive for attestation, staking, payment and enumeration instead of
|
|
four identifiers that have to be reconciled. R1 is also what gives
|
|
Claim~\ref{cl:nomoney} its force: the money follows an address the node key cannot
|
|
change.
|
|
|
|
% =====================================================================
|
|
\section{Enumeration}
|
|
\label{sec:enum}
|
|
|
|
\code{admin.hanzo.ai} lists the fleet by $A_n$: one row per address, carrying
|
|
class, operator org, bond, validationID, current $(L, W)$, last epoch seen, reward
|
|
recipient where it diverges from $A_n$, and status. The surface is HIP-1325's
|
|
\code{/v1/node}; this document adds only the requirement that the row key is the
|
|
address and that no other identifier is offered as one.
|
|
|
|
Two properties of the view follow from earlier sections and belong in the
|
|
interface rather than in a footnote:
|
|
|
|
\begin{itemize}
|
|
\item \textbf{The count is a count of addresses, not of machines.} By
|
|
\S\ref{sec:gap} the two coincide only where custody makes the key unique to a
|
|
machine. A console that reports ``$n$ nodes'' is reporting $n$ registered
|
|
identities. Where class is \code{self} for most rows, that distinction is the
|
|
honest reading and the view SHOULD present class beside the count rather than
|
|
behind a detail pane.
|
|
\item \textbf{Recipient divergence is a visible fact.} R1 permits it; the view
|
|
shows it on the row, because a payout address that differs from the identity is
|
|
the one place where the single-primitive design admits a second address, and an
|
|
invisible second address is how reconciliation errors start.
|
|
\end{itemize}
|
|
|
|
% =====================================================================
|
|
\section{Lifecycle}
|
|
\label{sec:lifecycle}
|
|
|
|
\subsection{Rotation is succession}
|
|
\label{sec:succ}
|
|
|
|
An EVM address is a hash of the public key. There is no operation that changes the
|
|
key and keeps the address. A design that wanted stable identifiers across key
|
|
changes would need a level of indirection, and indirection here would be a
|
|
mistake: after a compromise, carrying the identifier forward carries the
|
|
compromised node's standing forward with it.
|
|
|
|
So rotation is \textbf{succession}: a new key, a new address, an explicit link.
|
|
|
|
\begin{enumerate}[label=\textbf{S\arabic*.},leftmargin=2.6em]
|
|
\item The operator generates $sk_n'$ at the target assurance class and obtains
|
|
$A_n'$.
|
|
\item $A_o$ calls \code{bind} to move the registration's node address from $A_n$ to
|
|
$A_n'$, emitting \code{Bound}. The bond does not move; the registration does not
|
|
end; no unbonding period is incurred. The prior binding is retained as retired,
|
|
so $A_n$ can never be bound again.
|
|
\item The outgoing key SHOULD sign a statement naming $A_n'$, which lets an
|
|
observer follow the chain without consulting the registry. It is corroboration,
|
|
not authority: the authority for succession is $A_o$, precisely because the case
|
|
that matters most is the one where $sk_n$ is in someone else's hands.
|
|
\item The node's cluster identity changes with its address: a new name, a new
|
|
bootstrap token, a new client certificate (\S\ref{sec:admit}). The old \code{Node}
|
|
object is drained and deleted.
|
|
\item Assurance class does not transfer. The successor is measured on what it can
|
|
prove.
|
|
\item Accrued reputation and service history transfer only when the succession is
|
|
\emph{planned} --- a hardware refresh or a key-hygiene rotation. After a
|
|
suspected compromise they MUST NOT transfer.
|
|
\end{enumerate}
|
|
|
|
Because $A_o$ is the authority, succession works when the node key is compromised,
|
|
which is the case it exists for. This is the concrete reason for the two-key split
|
|
in \S\ref{sec:roles}: with one key there is no step S2 an honest operator can take
|
|
that the attacker cannot.
|
|
|
|
\subsection{Revocation}
|
|
|
|
Revocation is immediate and off-chain-effective:
|
|
|
|
\begin{itemize}
|
|
\item $A_o$, or the network on a proven fault, marks $A_n$ revoked.
|
|
\item The scheduler stops placing work on the node at once. Running work is drained
|
|
under the workload's own policy.
|
|
\item Statements signed by $A_n$ after the revocation height are refused (V4), and
|
|
the node's KMS signing credential is revoked (C7), so the node cannot produce
|
|
them either.
|
|
\item The node's cluster certificate is not renewed, and its \code{Node} object is
|
|
cordoned and drained.
|
|
\item The bond does \textbf{not} unlock at revocation. It unlocks after the
|
|
unbonding period, so that a node cannot escape a forfeiture that is still being
|
|
submitted. Making revocation fast and unbonding slow is the whole point of
|
|
separating them.
|
|
\end{itemize}
|
|
|
|
\subsection{Decommission}
|
|
\label{sec:decom}
|
|
|
|
The orderly path, for a node leaving without fault: drain, stop accepting work,
|
|
publish a final epoch statement, and let $A_o$ call the manager's existing
|
|
end-of-validation path. Uptime is settled, rewards pay out, the bond returns to
|
|
$A_o$ after the unbonding period, and the record is retired.
|
|
|
|
$A_n$ MUST NOT be reused, and the registry retains the retired binding permanently
|
|
so that \code{bind} can enforce it. A retired address has an unknowable custody
|
|
history: whoever held the key still holds it, and nothing distinguishes a returning
|
|
node from an old copy.
|
|
|
|
\subsection{Compromise}
|
|
|
|
There is no procedure that un-leaks a key. What the procedure can do is stop the
|
|
attacker from profiting and stop the network from believing the wrong things.
|
|
Assume $sk_n$ is disclosed at unknown height $H_c$:
|
|
|
|
\begin{enumerate}[label=\textbf{K\arabic*.},leftmargin=2.6em]
|
|
\item \textbf{Revoke immediately} (previous section). Scheduling stops, cluster
|
|
membership lapses, the bond stays locked.
|
|
\item \textbf{Do not use the node key.} It is the attacker's key too. Every
|
|
recovery action goes through $A_o$.
|
|
\item \textbf{Treat statements as unattributable from $H_c$.} Not false ---
|
|
unattributable. Work attributed to $A_n$ after $H_c$ cannot be assigned to the
|
|
operator or the attacker on the evidence available.
|
|
\item \textbf{Expect equivocation, and let it settle.} If the attacker runs the key
|
|
while the honest node runs, the two statements collide and the bond is forfeit.
|
|
That is the mechanism working: the bond is what the operator posted against
|
|
exactly this.
|
|
\item \textbf{Succeed to $A_n'$} at a class at least as high as the one that failed.
|
|
If the failure was custody, the successor MUST be at a strictly higher class, or
|
|
the same failure is available again.
|
|
\item \textbf{Rotate the credential, not only the key.} At L2 the compromise may be
|
|
of the signing \emph{authorization}, not the key --- see
|
|
Claim~\ref{cl:recursion}. Revoke the node's KMS credential, and check the KMS
|
|
audit log for signatures in $[H_c, \text{now}]$ that the node cannot account for.
|
|
\end{enumerate}
|
|
|
|
\begin{remark}
|
|
If $A_o$ is compromised, this is not a node incident. The bond is movable and the
|
|
registration is controllable by the attacker. Recovery is at the operator level:
|
|
threshold custody for $A_o$ is what makes the event survivable, which is why
|
|
\S\ref{sec:roles} requires it rather than suggesting it.
|
|
\end{remark}
|
|
|
|
% =====================================================================
|
|
\section{Hanzo-operated and third-party nodes}
|
|
\label{sec:same}
|
|
|
|
They are the same object. A Hanzo node is a node whose owner key Hanzo holds.
|
|
There is no Hanzo-only field, no privileged registration path, no separate
|
|
enumeration, and no code path that branches on who operates the box. The console
|
|
lists them in one table; the scheduler selects from one pool; settlement pays them
|
|
through one mechanism; the cluster admits them by the same certificate flow.
|
|
|
|
Three differences remain. Each is a value on the record, and none is a difference
|
|
in the identity:
|
|
|
|
\begin{enumerate}[label=\textbf{D\arabic*.},leftmargin=2.6em]
|
|
\item \textbf{Who can authorize signing.} For a Hanzo node, $A_o$ and the node's
|
|
KMS credential live in Hanzo's KMS under Hanzo's IAM org; for a third party, in
|
|
theirs. This changes who can cause a signature, not what the signature means.
|
|
Both are subject to \S\ref{sec:custody} identically.
|
|
\item \textbf{Physical control, and therefore who the adversary is.} For a
|
|
third-party node, the operator is a possible adversary, and \S\ref{sec:att},
|
|
point~(d), applies at full strength: without a confidential VM, guest measurement
|
|
does not defend against the operator. For a Hanzo node the host is under our
|
|
control, which is a real assurance difference and is \emph{not} expressible as a
|
|
cryptographic proof to a third party. It is recorded as the operator org on the
|
|
node record and priced by policy, not asserted as a security property.
|
|
\item \textbf{Whose money is at risk.} Hanzo's own nodes are bonded by Hanzo, so a
|
|
forfeiture is an internal transfer rather than a loss to a counterparty. The
|
|
incentive differs; the mechanism does not.
|
|
\end{enumerate}
|
|
|
|
Scheduling policy MAY restrict workloads by assurance class and by operator org ---
|
|
for instance, requiring \code{confidential} for a tenant's sensitive inference, or
|
|
restricting a regulated workload to a named org. That is a constraint over fields,
|
|
evaluated by the scheduler, and it is the one place the difference is allowed to
|
|
appear. It is not a second identity model.
|
|
|
|
\begin{remark}
|
|
No field marks a node as trusted for skipping a verification step, and none should
|
|
be added. Such a field is a target: whoever obtains it obtains the assurance the
|
|
network attaches to our own hardware, without the hardware. Hanzo nodes reach their
|
|
class through the same evidence as everyone else, which is cheap for us because we
|
|
control the hosts and can enable SecureBoot on them.
|
|
\end{remark}
|
|
|
|
% =====================================================================
|
|
\section{Threat model}
|
|
\label{sec:threat}
|
|
|
|
Assets: the node key and the credential that authorizes it to sign; the bond; the
|
|
reward stream; cluster membership; tenant workloads and their data; the integrity
|
|
of the fleet view.
|
|
|
|
\begin{center}
|
|
\small
|
|
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{0.19\textwidth}>{\raggedright\arraybackslash}p{0.36\textwidth}>{\raggedright\arraybackslash}p{0.37\textwidth}@{}}
|
|
\toprule
|
|
\textbf{Adversary} & \textbf{Capability assumed} & \textbf{What this design does} \\
|
|
\midrule
|
|
Remote attacker &
|
|
Network access; no code on the node &
|
|
Statements are signed and epoch-bound, so replay across chains, epochs and
|
|
registrations fails (V1--V3). Cluster admission needs a token issued against a
|
|
verified statement (A2). \\[3pt]
|
|
Malicious tenant &
|
|
Code inside a machine on the node &
|
|
Out of scope for identity; the node key is not in a tenant machine, and C3 bounds
|
|
what a leaked signing credential can sign. \\[3pt]
|
|
Careless operator &
|
|
Copies a key, restores a backup onto a second box &
|
|
Equivocation forfeits the bond, with no monitoring required (\S\ref{sec:equiv}). \\[3pt]
|
|
Malicious node operator &
|
|
Full physical control of the host, root in the microVM &
|
|
At class \code{self}, only the bond and HIP-0095 constrain them; \S\ref{sec:att}(d)
|
|
is why. At \code{confidential} the host leaves the trust base. This is the
|
|
adversary the class field exists for. \\[3pt]
|
|
Thief of the node key &
|
|
Holds $sk_n$, not $sk_o$ &
|
|
Cannot redirect rewards (Claim~\ref{cl:nomoney}); is detected if they run in
|
|
parallel with the honest node; is evicted by succession under $A_o$. \\[3pt]
|
|
Thief of the owner key &
|
|
Holds $sk_o$ &
|
|
Not a node incident. The registration and bond are lost; threshold custody for
|
|
$A_o$ is the control (\S\ref{sec:roles}). \\[3pt]
|
|
Release publisher &
|
|
Chooses the allowed list of register values &
|
|
A trust anchor. A compromised publisher can allow an image of its choosing on
|
|
every node at once. The allowed list MUST be published under threshold control,
|
|
never a single key, and changes MUST be observable. \\[3pt]
|
|
Control-plane operator &
|
|
Issues bootstrap tokens &
|
|
Can admit a machine to the cluster. Admission is bounded by V1--V8 being performed
|
|
against the chain, so an issued token names a node the chain already accepts; the
|
|
control plane cannot invent a bonded identity. \\[3pt]
|
|
Chain adversary &
|
|
Reorgs, censors &
|
|
Epochs and freshness derive from the chain, so a deep reorg moves the epoch
|
|
boundary. $K$ MUST exceed the chain's finality depth; a statement referring to a
|
|
block that did not survive is refused at V3, not accepted with a warning. \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
% =====================================================================
|
|
\section{Residual risk}
|
|
|
|
What this design does not fix, stated plainly so nobody has to discover it later:
|
|
|
|
\begin{itemize}
|
|
\item \textbf{Class \code{self} is an honesty assumption.} Most of the fleet is at
|
|
\code{self} today. For those nodes, every property in \S\ref{sec:notprove}
|
|
stands, and the only controls are the bond and the challenge system. That is a
|
|
real security posture --- economic rather than cryptographic --- and it must be
|
|
described that way in anything a customer reads.
|
|
\item \textbf{Time-of-check to time-of-use.} A register describes a launch. A node
|
|
measured honestly at height $H$ can \code{kexec}, load a module, or write its
|
|
root filesystem at $H + 1$. Continuous attestation --- runtime measurement of
|
|
what is loaded, not only what was --- is out of scope here and is the natural
|
|
next specification. The workload register is where it would attach.
|
|
\item \textbf{The bond bounds the loss, not the damage.} A node that leaks a
|
|
tenant's data loses its bond; the tenant's data is still out. Confidentiality of
|
|
tenant workloads on non-confidential VMs is a scheduling policy question, not an
|
|
identity question.
|
|
\item \textbf{Sybil resistance is economic.} Nothing prevents one operator from
|
|
running many nodes, each correctly identified. That is a feature; it becomes a
|
|
problem only where a protocol counts nodes rather than weighing stake. No
|
|
protocol specified here counts nodes.
|
|
\item \textbf{Vendor roots are trust anchors.} At class \code{platform} and above
|
|
the verifier trusts a manufacturer's certificate chain and its revocation
|
|
behavior. That is a smaller assumption than trusting every node operator, and it
|
|
is not zero.
|
|
\item \textbf{The registration does not prove possession of the consensus key.}
|
|
\emph{Observed:} the manager checks the BLS public key's length and the
|
|
\code{nodeID}'s uniqueness, and verifies possession of neither. Uniqueness
|
|
prevents two registrations of one \code{nodeID} on the same manager; it does not
|
|
establish that the registrant holds the corresponding secrets. The primitive
|
|
already exists (\code{luxfi/keys}, \code{pop.go}, \code{ProofOfPossession}).
|
|
\item \textbf{A coordinated operator can run copies undetected.} \S\ref{sec:equiv}
|
|
says exactly what the detector does and does not see, and
|
|
Claim~\ref{cl:nomoney} bounds the gain. It does not remove it.
|
|
\end{itemize}
|
|
|
|
% =====================================================================
|
|
\section{Implementation status and interface requests}
|
|
\label{sec:status}
|
|
|
|
\subsection{Implemented}
|
|
|
|
\begin{center}
|
|
\small
|
|
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{0.28\textwidth}>{\raggedright\arraybackslash}p{0.64\textwidth}@{}}
|
|
\toprule
|
|
\textbf{Where} & \textbf{What} \\
|
|
\midrule
|
|
\code{hanzoai/vm},\newline \code{crates/vm-measure} &
|
|
SHA-384 event log and register fold matching the TDX RTMR extend rule; launch and
|
|
workload logs kept apart; \code{bind} as $\sha{512}(L \| W)$; SEV-SNP and TDX
|
|
platform detection, request ABIs and ioctl encodings, with tests pinning structure
|
|
sizes against the kernel headers. The launch log is built in
|
|
\code{crates/vm-cli/src/measure.rs}. Not exposed by the \code{hanzo-vm} 2.0.0
|
|
binary installed on the fleet. \\[3pt]
|
|
\code{hanzoai/node},\newline \code{hanzo-libs/hanzo-attest} &
|
|
A second measurement: BLAKE3 over a \code{Boot} struct; \code{Evidence} with a
|
|
domain-separated \code{Kind}; an ed25519 \code{Kind::Host} attester and verifier;
|
|
\code{Record} with \code{coherent()}; an in-memory registry. \\[3pt]
|
|
\code{hanzoai/cli},\newline \code{src/commands/wallet.rs} &
|
|
Two custody models, no plaintext secrets: cloud custody where the key never leaves
|
|
KMS/MPC, and local custody in the OS keychain. EVM address derivation at
|
|
\code{m/44\textquotesingle/60\textquotesingle/0\textquotesingle/0/0}. \\[3pt]
|
|
\code{hanzoai/cloud},\newline \code{apps/wallet} &
|
|
\code{/v1/wallet/*} with four custody backends (\code{kms}, \code{mpc},
|
|
\code{treasury}, \code{safe}), key rotation, audit emission, a \code{TierValidator}
|
|
tier, EIP-2 low-$S$ normalization, and fail-closed behavior when the MPC cluster
|
|
is absent. \\[3pt]
|
|
\code{luxfi/kms} &
|
|
AES-256-GCM with an ML-KEM-wrapped data key, AAD bound to path/name/environment/version,
|
|
with a test that a swapped data key fails; hybrid X25519 + ML-KEM-768 transport. \\[3pt]
|
|
\code{luxfi/standard},\newline \code{contracts/validators} &
|
|
ACP-77 validator manager: bonded registration, \code{owner}, uptime-conditioned
|
|
rewards, churn limits, end-of-validation. No slashing. \\[3pt]
|
|
\code{hanzoai/cloud},\newline \code{cli/link.go} &
|
|
\code{hanzo link}: starts \code{hanzod} via \code{hanzo node up} and registers the
|
|
host in the org's compute fleet, authorized by the IAM token. \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
\subsection{Interface requests}
|
|
\label{sec:req}
|
|
|
|
These are changes in code owned elsewhere. They are requested here, with the reason
|
|
and the minimal shape, and are not made by this document.
|
|
|
|
\begin{description}
|
|
\item[IR-1 --- \code{hanzod}: the identity key is the wallet key, and is never
|
|
plaintext.] \code{hanzo-bin/hanzo-node/src/utils/keys.rs} loads an ed25519 secret
|
|
from a plaintext \code{KEY=value} file or the environment;
|
|
\code{src/runner.rs}:342--352 writes it back with \code{fs::write}, at the umask.
|
|
Requested: the node identity becomes a secp256k1 key whose address is $A_n$,
|
|
obtained through the wallet custody path that already exists, so that
|
|
\code{hanzod} holds an address and a signing capability rather than a secret.
|
|
Rationale: \S\ref{sec:custody} C1--C2 and \S\ref{sec:curve}; with a copyable key
|
|
none of the properties in this document hold, and with an ed25519 key the
|
|
forfeiture in \S\ref{sec:forfeit} cannot be verified on-chain. If the change must
|
|
land in two steps, the first step is the file mode and the second is the custody.
|
|
|
|
\item[IR-2 --- the report-data preimage MAY carry the node address.] The
|
|
confidential-tier proposal's B1 binds the boot registers, the ephemeral session
|
|
keys, the session identifier and a verifier nonce. It carries no durable
|
|
identity, so a report under it establishes that some confidential environment
|
|
holds these session keys and nothing that reaches the bonded node
|
|
(\S\ref{sec:field}). The composition of \S\ref{sec:field} closes this without
|
|
touching the field, and is what this document specifies. Offered, not asserted:
|
|
adding $A_n$ as a term of B1 would make the platform itself report the identity,
|
|
which is strictly stronger and costs one term. The decision belongs to the
|
|
confidential-tier proposal, which owns that field; this document's requirement is
|
|
only that the epoch statement name the same session terms the report does.
|
|
|
|
\item[IR-3 --- one measurement, not two.] \code{hanzo-attest} folds a \code{Boot}
|
|
struct with BLAKE3; \code{vm-measure} folds an event log with SHA-384. They
|
|
measure the same kernel and the same root image to different numbers, and only
|
|
the second matches what TDX and SEV-SNP registers use. Requested:
|
|
\code{hanzo-attest} stops defining a measurement and signs \code{vm-measure}'s
|
|
registers, with the epoch-statement preimage of \S\ref{sec:stmt} replacing
|
|
\code{evidence::preimage}. A release manifest MUST publish the digest function
|
|
the measurement uses; today the monitor's local cache records SHA-384 while
|
|
\code{hanzo-attest} would need BLAKE3, and a verifier holding the release can
|
|
reproduce neither from the other.
|
|
|
|
\item[IR-4 --- a node-binding registry contract.] The vendored manager preserves
|
|
its ABI deliberately and should not be forked. Requested: the two-mapping,
|
|
one-function contract of \S\ref{sec:stake}, so that \code{validationID} and $A_n$
|
|
agree on-chain, with a proof of possession of $A_n$ at \code{bind}, and
|
|
\code{forfeit} taking the equivocation proof of \S\ref{sec:equiv}.
|
|
|
|
\item[IR-5 --- a TPM 2.0 attester and verifier, ahead of SNP and TDX.] On the
|
|
measured fleet, two of three hosts have TPM 2.0 with a SHA-256 PCR bank and a
|
|
firmware event log, and zero have SNP or TDX. Requested: a quote over PCRs whose
|
|
caller field carries $\sha{256}(A_n \| B)$, so the same binding rule as IR-2
|
|
applies to the one hardware root the fleet actually has.
|
|
|
|
\item[IR-6 --- cluster admission by address.] The shared \code{K3S\_TOKEN} grants
|
|
membership under any name to anyone holding it. The replacement needs no new
|
|
protocol: \code{k3s token create --ttl} and the \code{system:node} binding are
|
|
already there. Requested: an admission service that performs V1--V8 and mints a
|
|
minutes-long bootstrap token for a node name derived from $A_n$; the server token
|
|
moved to KMS, removed from every agent, and rotated; the Node authorizer and
|
|
\code{NodeRestriction} confirmed on the API server.
|
|
|
|
\item[IR-7 --- \code{hanzod} does not hold an operator token.] \code{hanzo link}
|
|
authorizes registration with the operator's IAM bearer, which is correct for the
|
|
act of linking. Requested: the token is not retained as the node's durable
|
|
credential, and any long-lived cloud call \code{hanzod} makes is authorized by
|
|
its own address, not by the operator's org.
|
|
\end{description}
|
|
|
|
\subsection{Operational requests}
|
|
|
|
\begin{itemize}
|
|
\item Enable SecureBoot on the x86 host. It has a TPM 2.0 and SecureBoot is
|
|
currently disabled, which caps it at class \code{self} even once IR-5 lands: an
|
|
unenforced boot chain means the PCR values are chosen by whoever controls the
|
|
boot.
|
|
\item Retire the local credential files under \code{\textasciitilde/.hanzo/} on
|
|
every host that runs a node. Owner-only file permissions are L1 by the ladder of
|
|
\S\ref{sec:ladder}; C1 requires L2 or better, and a node's credential is what C3
|
|
scopes.
|
|
\item Move the k3s server token into KMS and rotate it when IR-6 lands. Until then
|
|
it is a fleet-wide shared secret held on every agent.
|
|
\end{itemize}
|
|
|
|
% =====================================================================
|
|
\appendix
|
|
|
|
\section{The fleet, as measured}
|
|
\label{app:fleet}
|
|
|
|
Measured 5 September 2026 by direct inspection of the three hosts. Recorded because
|
|
the assurance classes in \S\ref{sec:class} are claims about real hardware and
|
|
should be checkable.
|
|
|
|
\begin{center}
|
|
\footnotesize
|
|
\begin{tabular}{@{}llll@{}}
|
|
\toprule
|
|
& \textbf{aarch64 host} & \textbf{x86 host} & \textbf{Apple-Silicon host} \\
|
|
\midrule
|
|
Machine & NVIDIA DGX Spark (P4242) & GMKtec NucBox EVO-X2 & MacBook Pro \\
|
|
CPU & NVIDIA GB10 & AMD Ryzen AI MAX+ 395 & Apple M4 Max \\
|
|
Arch & aarch64 & x86\_64 & arm64 \\
|
|
OS & Ubuntu 24.04.4 & Ubuntu 24.04.4 & macOS 26.5.2 \\
|
|
Kernel & 6.17.0-1031-nvidia & 7.0.0-31-generic & Darwin 25.5.0 \\
|
|
TPM & 2.0 & 2.0 & none exposed \\
|
|
Event log & \code{binary\_bios\_measurements} & \code{binary\_bios\_measurements} & --- \\
|
|
SecureBoot & enabled & \textbf{disabled} & n/a \\
|
|
SEV-SNP / TDX & n/a (aarch64) & \code{sev\_snp = N} & n/a \\
|
|
Monitor & KVM & cloud-hypervisor & Virtualization.framework \\
|
|
k3s & control-plane, v1.35.5+k3s1 & agent, v1.35.5+k3s1 & not joined \\
|
|
OS assets & 2.0.0 & 2.0.0 & 2.0.0 \\
|
|
Kernel image & 9,710,080 bytes & 23,587,632 bytes & 9,710,080 bytes \\
|
|
Class today & \code{self} (\code{platform} after IR-5) & \code{self} & \code{self} \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
\noindent
|
|
PCR~0, SHA-256 bank, read on each Linux host:
|
|
|
|
\begin{center}\small\ttfamily
|
|
aarch64\quad 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969\\
|
|
x86\quad\ \ \ \ \ 2af943023aa8bdf10890398588b1315325e085645fcdf29d6596fa73db2b6375
|
|
\end{center}
|
|
|
|
\noindent
|
|
Only the aarch64 value is meaningful as an assurance claim: with SecureBoot
|
|
disabled on the x86 host, its PCR values record a boot chain nothing enforced.
|
|
|
|
Both Linux hosts are joined to one cluster; the Apple-Silicon host is not currently
|
|
a member. The microVM is what would let it join on the same terms, which is the
|
|
architectural point of \S\ref{sec:class}: uniform interface, honestly non-uniform
|
|
assurance.
|
|
|
|
Three details of this table bear on the specification rather than on the hardware.
|
|
|
|
First, the OS assets are at version 2.0.0 on all three hosts and the kernel images
|
|
differ in size, because they are different kernels for different architectures.
|
|
The launch register is therefore per-architecture at a fixed asset version, and the
|
|
allowed list checked at V6 is a set indexed by (release, architecture), not by
|
|
release alone. A verifier that expects one value per release will refuse every host
|
|
but one.
|
|
|
|
Second, the monitor is not uniform either. The two Apple-Silicon hosts carry
|
|
\code{hanzo} 8.5.157 and \code{hanzo-vm} 2.0.0 on the path; the Linux hosts carry
|
|
the Go \code{hanzo} CLI (\code{v1.801.358} and \code{v1.801.498}) and no
|
|
\code{hanzo-vm} on the path, though both hold the 2.0.0 OS assets. The monitor
|
|
binary is one of the things a launch measurement should commit to
|
|
(\code{hanzo-attest} includes its digest; \code{vm-measure}'s launch log does not),
|
|
and on this fleet it varies.
|
|
|
|
Third, \code{hanzo-vm} 2.0.0 as installed exposes \code{run}, \code{init},
|
|
\code{upgrade}, \code{checkpoint} and \code{prune}. It does not expose a measure
|
|
verb: \code{vm-measure} exists in the repository and not in the binary the fleet
|
|
runs. Nothing on this fleet has produced a measurement yet.
|
|
|
|
\section{Two encodings, and the digest problem}
|
|
\label{app:encoding}
|
|
|
|
\emph{Observed.} Two crates measure the same assets to different numbers.
|
|
|
|
\paragraph{\code{vm-measure} (\code{hanzoai/vm}).} An ordered event log folded into
|
|
one register:
|
|
\[
|
|
e_i = \sha{384}\bigl(\mathit{name}_i \,\|\, \mathtt{0x00} \,\|\, \sha{384}(\mathit{content}_i)\bigr),
|
|
\quad R_0 = 0^{48}, \quad R_{i+1} = \sha{384}(R_i \,\|\, e_i).
|
|
\]
|
|
The launch log's events are, in order: \code{kernel} (file), \code{cmdline} (text),
|
|
\code{initrd} (file, when present), \code{root} (file, the source image and never
|
|
the per-instance copy), \code{shape} (text,
|
|
\code{cpus=\dots\ memory=\dots MB disk=\dots MB}). The workload log is extended by
|
|
whatever drives the machine. The name is inside its own event digest, so events
|
|
cannot trade places; the fold commits to order, so a prefix and a whole log are
|
|
different registers; paths never enter, so the same assets measure identically on
|
|
every host. The wire form carries both the value and the events and is refused on
|
|
read if the value does not fold from the events.
|
|
|
|
\paragraph{\code{hanzo-attest} (\code{hanzoai/node}).} A single BLAKE3 digest over a
|
|
canonical encoding of a \code{Boot} struct: the domain tag \code{hanzo.boot.v1},
|
|
then the monitor binary digest, the OS-asset version string, the kernel digest and
|
|
the root filesystem digest --- each length-prefixed with a big-endian \code{u64} ---
|
|
then \code{cpus}, \code{memory\_mb} and \code{disk\_mb} as big-endian \code{u64},
|
|
then the argv count and each argument length-prefixed.
|
|
|
|
\paragraph{The digest problem.} The kernel image on the Apple-Silicon host has three
|
|
different digests in three places that all describe ``the kernel'':
|
|
|
|
\begin{center}\footnotesize\ttfamily
|
|
\begin{tabular}{@{}ll@{}}
|
|
\toprule
|
|
SHA-384, \code{\textasciitilde/.hanzo/vm/digests.json} & 40c33bf220f5315d\dots{}b3b26f7 \\
|
|
BLAKE3, what \code{hanzo-attest} would fold & ee841563267f54a2\dots{}62638930 \\
|
|
SHA-256, what a release note would quote & 2f71e062b952e5f6\dots{}7bba3025 \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{center}
|
|
|
|
\noindent
|
|
A verifier who holds the published release must be able to recompute the value the
|
|
epoch statement commits to. That is possible only if the release publishes the
|
|
digest function the measurement uses. SHA-384 is the right one, because it is what
|
|
the hardware registers use and it is therefore the only choice that needs no
|
|
re-hashing at the boundary to a report. IR-3 is the request to make it the only one.
|
|
|
|
\end{document}
|