Skip to content

Assumptions on the Chain of Trust


For a security architecture to verify anything, it needs to parse data. After parsing, the (hopefully) cryptographically sound algorithms can do their job and prove that e.g. the kernel’s signature is valid and that it can be loaded. But who verifies the parsing?

What is the Chain of Trust and why does it work?

The Chain of Trust is a concept in which each part of a system verifies the authenticity of another element, with trust flowing from a root node. In embedded systems, this is usually something you can physically control, but can also be something you simply choose to trust (such as a root CA for certificates).

An example:

  • ROM verifies the bootloader
  • bootloader verifies the kernel (secure boot)
  • kernel verifies modules and the rootfs
  • … and somewhere up the stack the chain dissolves into “???” — which is exactly where most products stop caring.

The elegance is its transitivity. You only have to trust the anchor. Every verified stage verifies the next. Whether the anchor is trustworthy enough depends on the attack model you design your system for.

What are the implicit assumptions?

The chain is stated as “A verifies B.” Stated — and silently assumed:

  • A itself always behaves correctly while handling B (the verifier is sound), and
  • A treats B as potentially malicious, treating the input as adversarial and non-conformant.

The second clause can be awkward: verifying B requires parsing B first. The verifier is the only component in the system whose job is parsing untrusted bytes, written to spec, to then be potentially attacked on it.

There needs to be a strong discipline around reading but not interpreting. A hash over malicious bytes has a fixed control flow. A parser does the exact opposite. The parser’s control flow is being driven by what it is consuming.

In cryptography, this concept exists similarly: Encrypt-then-MAC computes the tag over the ciphertext, so it is checked before anything needs to be decrypted. MAC-then-encrypt forces a decryption first, bringing us to the same parse-before-verify mistake. It is one of the reasons why padding oracles were so effective.

What are real failure modes?

  • The verifier’s own input handling breaks. Such as secure-boot image-header parsers that crash or accept on malformed lengths. Cryptographically correct but compromised before the crypto was even invoked. Real examples are “fusée gelée”, in which the boot ROM trusted an attacker-controlled USB length field before verification; and U-Boot’s FIT verified-boot bypasses, where the image structure was parsed unsafely.
  • The assumption layer fails! No verifier needed at all. cloudcutter (2022): a provisioning service accepted an unauthenticated config message; one unchecked string copy let the attacker overwrite the device’s cloud identity. The chain started too late.
  • Other ways chains fail: rollback and replay, leftover recovery paths, post-verification actuation bugs, fault injection, signer compromise.

The pattern across the first two is identical: The chain breaks at the elements’ own input handling or on the assumption. The cryptography (hopefully) stays sound.

What did we learn and what are the consequences?

While choosing an adequate cryptographic element for verifying the next element in your chain of trust is important, you cannot neglect earlier touchpoints in this interaction, such as parsing.

  • Fuzz the verifier itself too, whether self-written, or a library, no excuses!
  • Keep the pre-verification logic as simple and boring as possible to minimize attack surface: Sanity check wire-dependent bounds or avoid them by using ring buffers or fixed bounded buffers.
  • Run verdicts in a context with low authority (such as a separate process or an MPU-isolated region). If something goes wrong, this reduces the blast radius.

The chain of trust verifies in one direction, but we need to equally verify the other direction, the data flow, as an open and attractive attack surface.