in-toto attestations
When we sign an artifact, like a blob, the signature proves that we were in
possesion of the private key. When we verify, we use the signature, the public
key, and the blob, and we are verifying that this was in fact the case. But it
does not say anything else about the artifact, we don't know what
was actually
signed.
By providing, and signing a document specifying statements
about the artifact
we can say things about the artifact as well. Statements could be anything which
we will address later in this document. A signed Statement is called an
Attestation.
An attestation is authenticated metadata about software artifacts and follows the Software-chain Levels for Software Artifacts attestation model.
An attestation can be json object, and the outer-most element is the Envelope
:
{
"payloadType": "application/vnd.in-toto+json",
"payload": "<Base64(Statement)>",
"signatures": [{ "sig": "<Base64(Signature)>" }]
}
Notice that the payload
is a base64 encoded Statement
. This format follows
the DSSE format.
The payloadType
could be JSON, CBOR, or ProtoBuf.
The structure of the Statement
, in payload element above, looks something
like this (before it is base64 encoded):
{
"_type": "https://in-toto.io/Statement/v0.1",
"subject": [
{
"name": "<NAME>",
"digest": { "<ALGORITHM>": "<HEX_VALUE>" }
}
],
"predicateType": "<URI>",
"predicate": {}
}
The subjects bind this attestation to a set of software artifacts, notice that this is an array of objects.
Each software artifact is given a name and a digest. The digest contains the
name of the hashing algorithm used, and the digest (the outcome of the hash
function).
The name could be a file name but it can also be left unspecified using _
.
This leads us to the predicate
fields, which like shown above has one field
for the type of the predicate (predicateType), and an object as the content of
the predicate.
The predicate can contain pretty much any metadata related to the Statement
object's subjects. The predicateType
provides a way of knowing how to
interpret the predicate
field.
Examples of predicate types are SLSA Provenance, in-toto Link , SPDX , Software Supply Chain Attribute Integrity (SCAI).
NPM also uses this for it to publish attestations:
{
"_type": "https://in-toto.io/Statement/v0.1",
"subject": [
{
"name": "pkg:npm/@scope/package-foo@1.4.3",
"digest": { "sha512": "41o0P/CEffYGDqvo2pHQXRBOfFOxvYY3WkwkQTy..." }
}
],
"predicateType": "https://github.com/npm/attestation/tree/main/specs/publish/v0.1",
"predicate": {
"name": "@scope/package-foo",
"version": "1.4.3",
"registry": "https://registry.npmjs.org"
}
}
The digest
in this case is the sha512sum of the published tar file.
So, we mentioned that the predicate type is used by the consumer of the
predicate so it knows how to interpret the contents of the predicate. But who
is the consumer?
Most often this would be a Policy Engine. The Policy engine would be passed
the contents of the Statement related to the predicate, and rules written in
the Policy Engine's language would process the predicate as input. The outcome
would be a true/false result (remember that a predicate is a statement/function
that returns true or false).
To try to make this a little more concrete lets take a look at an example that creates an attestation.
For this example I'm going to use a GitHub Action named slsa-github-generator which can generate SLSA provenance attestations for github native projects. This generator can generate SLSA provenance for SLSA level 3.
The example project I'm going to use is tuf-keyid but the actual project is not important in this case, any Rust project could have been used.
So we need to set up a GitHub Action which can been seen in provenance.yaml.
After that workflow has run it will produce an attestation and a binary which we will use to verify.
First, we need to download the binary from the workflow run (we should really be able to be download this from the releases page too, but I've not been able to get that to work just yet):
$ unzip tuf-keyid.zip
$ file tuf-keyid
tuf-keyid: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ceaa62d49b024798ebd7fe7d021f3ade5925b1f9, for GNU/Linux 3.2.0, with debug_info, not stripped
And then we need to download the attestation file:
$ curl -L https://github.com/danbev/tuf-keyid/releases/download/v0.2.0/tuf-keyid.intoto.jsonl --output tuf-keyid.intoto.jsonl
Lets inspect the attestation file:
$ cat tuf-keyid.intoto.jsonl | jq
{
"payloadType": "application/vnd.in-toto+json",
"payload": "...",
"signatures": [
{
"keyid": "",
"sig": "MEUCIHwmJopmrXWqi+rKIeTlWW0r027hLL1nO7xEj0mW8czsAiEAhdc6SDlhWo3m0YOtsUSoIYSlvw3Xu7ts3S8btHzdMpw=",
"cert": "-----BEGIN CERTIFICATE-----\nMIIDtjCCAzygAwIBAgIUCeak2sfkfZbS0IMRSbK4+BHcUzAwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjMwMTI2MTAxMzQxWhcNMjMwMTI2MTAyMzQxWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEZMurC3H80wzo+Xn7uifeTDV/AAFnye8uFwEj\n5VmxJb30VzuEw8gD8/Dj4V79bIW9sePcZjvREhFWak+PhUZVMqOCAlswggJXMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUncOT\nSyRyKgylBYlUHwPF+EyemfkwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wgYQGA1UdEQEB/wR6MHiGdmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1l\nd29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2Vu\nZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92MS40LjAwOQYKKwYB\nBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50\nLmNvbTASBgorBgEEAYO/MAECBARwdXNoMDYGCisGAQQBg78wAQMEKDUxM2IwZTA2\nMGM3NmExZGVkN2IxYTQxNzMxNjUxMDM4MzhmOGRkZTcwFQYKKwYBBAGDvzABBAQH\nUHVibGlzaDAeBgorBgEEAYO/MAEFBBBkYW5iZXYvdHVmLWtleWlkMB4GCisGAQQB\ng78wAQYEEHJlZnMvdGFncy92MC4yLjAwgYoGCisGAQQB1nkCBAIEfAR6AHgAdgDd\nPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynujgAAAYXtkZ+vAAAEAwBHMEUC\nIQDgO+S94sXq3wcfg344IV8FRhynvsJsVFEfHmwOHGqAVgIgArfX+7pnaLrplJ0u\nXB6tlWaCxQJ7GAo9YByqXCa0b2gwCgYIKoZIzj0EAwMDaAAwZQIxAOYkXbpLbSqC\njdORW6lWGWB/Ts2aOhK7VAHaQCRgRHQGiZx4Pe/LCwqkQF/1W2BAEQIwLB9Ic2jt\nIiEjtw8xKFDQAfnUleNUtZ51LXgXEkdpIX9cnj4UdR6k4gu/wul16Bd8\n-----END CERTIFICATE-----\n"
}
]
}
If we look back at the beginning of this document we will see that this format
matches the Envelope
of the attestation, and we have the payloadType
,
a payload
, and signatures
.
The certificate can be inspected using:
$ cat tuf-keyid.intoto.jsonl | jq -r '.signatures[].cert' | openssl x509 --text
Recall that the payload is a base64 encoded Statement
. Let's decode the
Statement
and take a closer a look at it:
$ cat tuf-keyid.intoto.jsonl | jq -r '.payload' | base64 -d | jq
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v0.2",
"subject": [
{
"name": "tuf-keyid",
"digest": {
"sha256": "470c549740f98fe1b1977d48e014031ed5183785fd459df7e04605daefe8e293"
}
}
],
"predicate": {
"builder": {
"id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v1.4.0"
},
"buildType": "https://github.com/slsa-framework/slsa-github-generator/generic@v1",
"invocation": {
"configSource": {
"uri": "git+https://github.com/danbev/tuf-keyid@refs/tags/v0.2.0",
"digest": {
"sha1": "513b0e060c76a1ded7b1a4173165103838f8dde7"
},
"entryPoint": ".github/workflows/provenance.yaml"
},
"parameters": {},
"environment": {
"github_actor": "danbev",
"github_actor_id": "432351",
"github_base_ref": "",
"github_event_name": "push",
"github_event_payload": {
"after": "13c69c54cbd04d1920cc5e42441f0a693a371494",
"base_ref": null,
"before": "0000000000000000000000000000000000000000",
"commits": [],
"compare": "https://github.com/danbev/tuf-keyid/compare/v0.2.0",
"created": true,
"deleted": false,
"forced": false,
"head_commit": {
"author": {
"email": "daniel.bevenius@gmail.com",
"name": "Daniel Bevenius",
"username": "danbev"
},
"committer": {
"email": "daniel.bevenius@gmail.com",
"name": "Daniel Bevenius",
"username": "danbev"
},
"distinct": true,
"id": "513b0e060c76a1ded7b1a4173165103838f8dde7",
"message": "Add content(releases) write permission\n\nSigned-off-by: Daniel Bevenius <daniel.bevenius@gmail.com>",
"timestamp": "2023-01-26T11:09:02+01:00",
"tree_id": "5030177fa47fc8b8252c26e8556083b4abc5df71",
"url": "https://github.com/danbev/tuf-keyid/commit/513b0e060c76a1ded7b1a4173165103838f8dde7"
},
"pusher": {
"email": "daniel.bevenius@gmail.com",
"name": "danbev"
},
"ref": "refs/tags/v0.2.0",
"repository": {
"allow_forking": true,
"archive_url": "https://api.github.com/repos/danbev/tuf-keyid/{archive_format}{/ref}",
"archived": false,
"assignees_url": "https://api.github.com/repos/danbev/tuf-keyid/assignees{/user}",
"blobs_url": "https://api.github.com/repos/danbev/tuf-keyid/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/danbev/tuf-keyid/branches{/branch}",
"clone_url": "https://github.com/danbev/tuf-keyid.git",
"collaborators_url": "https://api.github.com/repos/danbev/tuf-keyid/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/danbev/tuf-keyid/comments{/number}",
"commits_url": "https://api.github.com/repos/danbev/tuf-keyid/commits{/sha}",
"compare_url": "https://api.github.com/repos/danbev/tuf-keyid/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/danbev/tuf-keyid/contents/{+path}",
"contributors_url": "https://api.github.com/repos/danbev/tuf-keyid/contributors",
"created_at": 1674117641,
"default_branch": "main",
"deployments_url": "https://api.github.com/repos/danbev/tuf-keyid/deployments",
"description": "A command line tool to print the key id for a TUF public key in JSON format.",
"disabled": false,
"downloads_url": "https://api.github.com/repos/danbev/tuf-keyid/downloads",
"events_url": "https://api.github.com/repos/danbev/tuf-keyid/events",
"fork": false,
"forks": 0,
"forks_count": 0,
"forks_url": "https://api.github.com/repos/danbev/tuf-keyid/forks",
"full_name": "danbev/tuf-keyid",
"git_commits_url": "https://api.github.com/repos/danbev/tuf-keyid/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/danbev/tuf-keyid/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/danbev/tuf-keyid/git/tags{/sha}",
"git_url": "git://github.com/danbev/tuf-keyid.git",
"has_discussions": false,
"has_downloads": true,
"has_issues": true,
"has_pages": false,
"has_projects": true,
"has_wiki": true,
"homepage": null,
"hooks_url": "https://api.github.com/repos/danbev/tuf-keyid/hooks",
"html_url": "https://github.com/danbev/tuf-keyid",
"id": 590801502,
"is_template": false,
"issue_comment_url": "https://api.github.com/repos/danbev/tuf-keyid/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/danbev/tuf-keyid/issues/events{/number}",
"issues_url": "https://api.github.com/repos/danbev/tuf-keyid/issues{/number}",
"keys_url": "https://api.github.com/repos/danbev/tuf-keyid/keys{/key_id}",
"labels_url": "https://api.github.com/repos/danbev/tuf-keyid/labels{/name}",
"language": "Rust",
"languages_url": "https://api.github.com/repos/danbev/tuf-keyid/languages",
"license": null,
"master_branch": "main",
"merges_url": "https://api.github.com/repos/danbev/tuf-keyid/merges",
"milestones_url": "https://api.github.com/repos/danbev/tuf-keyid/milestones{/number}",
"mirror_url": null,
"name": "tuf-keyid",
"node_id": "R_kgDOIzbqXg",
"notifications_url": "https://api.github.com/repos/danbev/tuf-keyid/notifications{?since,all,participating}",
"open_issues": 0,
"open_issues_count": 0,
"owner": {
"avatar_url": "https://avatars.githubusercontent.com/u/432351?v=4",
"email": "daniel.bevenius@gmail.com",
"events_url": "https://api.github.com/users/danbev/events{/privacy}",
"followers_url": "https://api.github.com/users/danbev/followers",
"following_url": "https://api.github.com/users/danbev/following{/other_user}",
"gists_url": "https://api.github.com/users/danbev/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/danbev",
"id": 432351,
"login": "danbev",
"name": "danbev",
"node_id": "MDQ6VXNlcjQzMjM1MQ==",
"organizations_url": "https://api.github.com/users/danbev/orgs",
"received_events_url": "https://api.github.com/users/danbev/received_events",
"repos_url": "https://api.github.com/users/danbev/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/danbev/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/danbev/subscriptions",
"type": "User",
"url": "https://api.github.com/users/danbev"
},
"private": false,
"pulls_url": "https://api.github.com/repos/danbev/tuf-keyid/pulls{/number}",
"pushed_at": 1674727856,
"releases_url": "https://api.github.com/repos/danbev/tuf-keyid/releases{/id}",
"size": 21,
"ssh_url": "git@github.com:danbev/tuf-keyid.git",
"stargazers": 1,
"stargazers_count": 1,
"stargazers_url": "https://api.github.com/repos/danbev/tuf-keyid/stargazers",
"statuses_url": "https://api.github.com/repos/danbev/tuf-keyid/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/danbev/tuf-keyid/subscribers",
"subscription_url": "https://api.github.com/repos/danbev/tuf-keyid/subscription",
"svn_url": "https://github.com/danbev/tuf-keyid",
"tags_url": "https://api.github.com/repos/danbev/tuf-keyid/tags",
"teams_url": "https://api.github.com/repos/danbev/tuf-keyid/teams",
"topics": [],
"trees_url": "https://api.github.com/repos/danbev/tuf-keyid/git/trees{/sha}",
"updated_at": "2023-01-19T10:26:19Z",
"url": "https://github.com/danbev/tuf-keyid",
"visibility": "public",
"watchers": 1,
"watchers_count": 1,
"web_commit_signoff_required": false
},
"sender": {
"avatar_url": "https://avatars.githubusercontent.com/u/432351?v=4",
"events_url": "https://api.github.com/users/danbev/events{/privacy}",
"followers_url": "https://api.github.com/users/danbev/followers",
"following_url": "https://api.github.com/users/danbev/following{/other_user}",
"gists_url": "https://api.github.com/users/danbev/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/danbev",
"id": 432351,
"login": "danbev",
"node_id": "MDQ6VXNlcjQzMjM1MQ==",
"organizations_url": "https://api.github.com/users/danbev/orgs",
"received_events_url": "https://api.github.com/users/danbev/received_events",
"repos_url": "https://api.github.com/users/danbev/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/danbev/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/danbev/subscriptions",
"type": "User",
"url": "https://api.github.com/users/danbev"
}
},
"github_head_ref": "",
"github_ref": "refs/tags/v0.2.0",
"github_ref_type": "tag",
"github_repository_id": "590801502",
"github_repository_owner": "danbev",
"github_repository_owner_id": "432351",
"github_run_attempt": "1",
"github_run_id": "4014167952",
"github_run_number": "13",
"github_sha1": "513b0e060c76a1ded7b1a4173165103838f8dde7"
}
},
"metadata": {
"buildInvocationID": "4014167952-1",
"completeness": {
"parameters": true,
"environment": false,
"materials": false
},
"reproducible": false
},
"materials": [
{
"uri": "git+https://github.com/danbev/tuf-keyid@refs/tags/v0.2.0",
"digest": {
"sha1": "513b0e060c76a1ded7b1a4173165103838f8dde7"
}
}
]
}
}
So that gives us a concrete example of an attestation and in this case it is a SLSA Provenance.
Notice that the digest
in the subject array is the sha256sum of the tuf-keyid
binary:
$ cat tuf-keyid.intoto.jsonl | jq -r '.payload' | base64 -d | jq -r '.subject[].digest.sha256'
470c549740f98fe1b1977d48e014031ed5183785fd459df7e04605daefe8e293
$ sha256sum tuf-keyid
470c549740f98fe1b1977d48e014031ed5183785fd459df7e04605daefe8e293 tuf-keyid
Alright, so next step if to verify the binary that we produced, using the attestation.
There is project named slsa-verifier which can be used to verify the artifact.
Installing slsa-verifier
:
$ go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@v2.0.1
Let's try verifying the attestation using slsa-verifier
and using a local
build of the binary, that is, a local build on my laptop:
$ slsa-verifier verify-artifact --provenance-path tuf-keyid.intoto.jsonl \
--source-uri github.com/danbev/tuf-keyid \
~/work/rust/tuf-keyid/target/release/tuf-keyid
Verified signature against tlog entry index 11978552 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77a217b8f07bccab3dc8caa1c7badf65f104a762647e5e355db23ccc13a22e275dd
FAILED: SLSA verification failed: expected hash '32dcff46ec4be5462a66aeb5d82366da3b870d36796f3d1fe6fec6245f21ce6f' not found: artifact hash does not match provenance subject
And now let's see what happens when we try with the binary that was produced by the GitHub action:
$ slsa-verifier verify-artifact --provenance-path tuf-keyid.intoto.jsonl \
--source-uri github.com/danbev/tuf-keyid \
tuf-keyid
Verified signature against tlog entry index 11978552 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77a217b8f07bccab3dc8caa1c7badf65f104a762647e5e355db23ccc13a22e275dd
Verified build using builder https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v1.4.0 at commit 513b0e060c76a1ded7b1a4173165103838f8dde7
PASSED: Verified SLSA provenance
slsa-verifier
can also print out the predicate information after validation
, using --print-provenance
, which could then be passed to a Policy Engine:
$ slsa-verifier verify-artifact --provenance-path tuf-keyid.intoto.jsonl \
--source-uri github.com/danbev/tuf-keyid \
--print-provenance \
tuf-keyid