Witnessing Sigstore’s transparency log from the Ethereum blockchain

I love Sigstore. It’s a nice piece of public infrastructure that issues code signing certificates, attests to software build processes, and keeps record of them:

go build -o myapp main.go
cosign sign-blob --bundle myapp.sig.json myapp

Note that you don’t have to provide your own keys to cosign. Here’s what it does:

  • Get an identity token. If you run this locally, cosign will prompt you to sign in with an OAuth2 identity provider; if this is part of a GitHub Actions workflow, it will automatically request an OIDC token from GitHub.
  • Send the identity token to Sigstore’s hosted Fulcio CA to request a short-lived code signing certificate, and sign the requested file.
  • Submit the signature and the signing certificate to Sigstore’s hosted Rekor transparency log.

The signing event is then logged in Rekor and becomes publicly visible. An example: https://search.sigstore.dev/?logIndex=124482029

Rekor is an immutable, append-only log and is supposed to be tamper-resistant - there should be no way for the log operator to modify a past log entry without being caught. This is made possible through Merkle trees, a cryptographic construction that allows various types of statements on the tree to be proved and verified efficiently. Specifically, Rekor generates inclusion proofs and consistency proofs on its Merkle trees. However, a Rekor instance alone is not enough for security - it has to be continuously monitored and verified (”witnessed”) by third parties.

Split-view and witnesses

In the event a Rekor log instance gets compromised, an attacker can perform the “split-view” attack: present different views (signed tree heads) to different clients, therefore altering log entries without breaking consistency proofs. To protect clients from split-views, there need to be multiple third-parties who run “witnesses” to periodically fetch the signed tree head, co-sign them, and serve the co-signed tree head to the public. Clients will then query multiple semi-trusted witnesses for co-signatures, and only treat a log entry response from Rekor as valid if:

  • The log entry has an inclusion proof to be part of the Merkle tree with the root hash / “tree head” provided by Rekor
  • The tree head is signed by Rekor and co-signed by some witnesses
  • No witness returned a different tree head

If two different tree heads are observed, a client or witness can publish both on a public forum as proof of misbehavior and hold the log operator accountable.

However, we can see that the above process has a 1-of-n trust assumption - the client has to be able to reach at least one honest witness who happens to see a different tree head. If an attacker has control to the client’s network, they can block network access to all honest witnesses and only expose a malicious one. Even with quorum rules and a built-in list of “required witnesses”, you still need to answer questions like “what if the government of country X requires org A, B, C to comply”.

The alternative is to solve the hard problem of trust and build a Sybil-resistant witnessing system - and here we are, it’s a blockchain.

Let’s use a blockchain!

Blockchain is a controversial topic. The main type of applications that run on top of public blockchains is cryptocurrencies, and that’s often linked to shady activities. Actually, this article from Sigstore is a nice summary of “why not blockchains”, and I agree with a lot of it (fees, complexity to be production-ready, 2022-era centralized API access).

But there are also applications like the Ethereum name service (ENS) and some of decentralized finance (DeFi) that attempt to build really new and useful things. From a technical perspective, blockchain is still the only kind of technology that allows to establish computational truths with little or no trust.

I would use the Ethereum blockchain for anything serious, as it seems to be the only smart contract blockchain that has a sufficient level of security, with 1,060,822 validators as of August 24, 2024 running a battle-tested consensus mechanism. Witnessing Rekor from Ethereum could be the best of both worlds: you get the computational- and cost-efficiency of a transparency log, and also the security of a proper public blockchain.

Here’s the Rekor witness I wrote (try it out!): https://github.com/losfair/rekor-evm. The “chain of proofs” works like following:

  • User submits a log entry to Rekor
  • Rekor tree head is submitted to a smart contract on Scroll, a Ethereum layer 2 network secured by zero-knowledge proofs, along with a Merkle consistency proof that shows the current tree is created by appending one or more entries to the previous tree, and a signature from the operator (Sigstore). We submit to Scroll instead of directly to Ethereum to save costs - at the time of writing, Scroll is ~20-100x cheaper than Ethereum mainnet.
  • Scroll’s state trie root hash is submitted to its smart contract on Ethereum, along with a zero-knowledge proof of correct state transition.
  • Ethereum state is validated through re-execution by all validators.

A client that wants to verify a Rekor log entry will first fetch the current Ethereum state trie root hash from the consensus network in a verifiable way, and then query various untrusted services (Ethereum/Scroll/Rekor API) to collect evidence showing that the log entry is correctly rolled up to the final Ethereum state trie root. Now, we have proved that the log entry we see is the same one as everyone else.

mkdir sigcheck
cd sigcheck
wget https://github.com/losfair/rekor-evm/releases/download/v20240824.1/sigcheck-v20240824.1.tar.gz
tar xzf sigcheck-v20240824.1.tar.gz

# verify the signature without querying witnesses
cosign verify-blob --bundle sigcheck.sig.json \
    --certificate-identity-regexp '^https://github\.com/losfair/rekor-evm/\.github/workflows/ci\.yml@refs/tags/' \
    --certificate-oidc-issuer https://token.actions.githubusercontent.com \
    sigcheck

# start the sigcheck API server on localhost:2915
./sigcheck

# wait for the first "buffering new finalized block" log line
# then, in another terminal, let's verify the `sigcheck` binary we just downloaded (recursively!)
LOG_INDEX="$(jq --raw-output .rekorBundle.Payload.logIndex < sigcheck.sig.json)"
curl "https://sigstore-scroll-witness.deno.dev/proof/$LOG_INDEX" > proof.json
PROVED_BODY_HASH="$(curl http://localhost:2915/verify -d @proof.json -H "content-type: application/json" | \
    jq -c -j --sort-keys .body | sha256sum)"
PROVIDED_BODY_HASH="$(jq -j .rekorBundle.Payload.body < sigcheck.sig.json | base64 -d | sha256sum)"
[ "$PROVED_BODY_HASH" == "$PROVIDED_BODY_HASH" ] && echo "OK"
?
?

Replies

Copy and paste this URL into the search field of your favourite Mastodon app or web interface to interact with this post:

https://su3.io/posts/witnessing-sigstore-from-ethereum

Loading

Built with Fresh

© 2022 Heyang Zhou · 

RSS