TL;DR: Create a provenance by traversing each commit. Attest into OCI registry. Verify all commits in attestation using cosign.
Abstract
This issue proposes introducing new cosign.sigstore.dev/attestation/gitsign/v1
predicate type for using the in-toto/attestation model. Also proposes a new subcommand in gitsign itself: attestation generate
to generate an attestation file by traversing each commits.
Motivation
While working on the Cosign VULN_SPEC proposal a few months ago, we learned a lot from the both sigstore and in-toto community members and valuable comments. And no doubt this proposal will be our next opportunity for new learnings for sure. Gitsign is a brand-new tool and very promising. So with @developer-guy, we thought that this proposal would be worth creating and discussing further here. So created this one, as far as we can do best.
Proposal
The main idea is to generate a provenance for the entire commit history for the repository in the container image build-time (to prevent a slight time window in which it could have tampered). And storing as an attestation format in the OCI registry. As a consumer of the container image, verifying gitsign attestation means that I verify the attestation itself and each commit that has been made by committers using their public key that is either fetched from Rekor (PKCS7 cert) or GPG verify (PGP cert).
By doing so; rather than saying "I trust this repository and binaries thus all artifacts are signed", the statement becomes "all commits that have made by commiters are signed & verified by either using gitsign and GPG, so I trust each commit. and also container image, binaries and artifacts already signed.".
And example use-case for verifying is to use cosign to verify attestation and also verify all the commits by using gitsign under the hood.
Whis proposal requires collaborate with gitsign and cosign together. Implement all of this, we would do add some commands:
For gitsign: introduce new attestation generate
command:
$ gitsign attestation generate
For cosign: introduce new --gitsign true|false
flag:
$ COSIGN_EXPERIMENTAL=1 cosign verify-attestation \
--type cosign.sigstore.dev/attestation/gitsign/v1 \
--gitsign \
foo/bar:baz
New --gitsign
flag traverses each commits and verifies by running under the hood:
$ cosign verify-blob \
--cert commits[*].rekor.signature.publicKey \
--signature commits[*].rekor.signature.content \
commits[*].author.digest.sha1
The 30,000-foot view
Please note that this spec should be generic and open for extendibility. This is not intended to be specially built for just gitsign. Wnyone who wants to create their own git commit signing tool should be able to consume this. Here is how new cosign.sigstore.dev/attestation/gitsign/v1
predicate looks like (draft - open for feedback):
{
"_type": "https://in-toto.io/Statement/v0.1",
"subject": [],
"predicateType": "cosign.sigstore.dev/attestation/gitsign/v1",
"predicate": {
"invocation": {
"fulcio_url": "https://fulcio.sigstore.dev",
"rekor_url": "https://rekor.sigstore.dev",
"oidc": {
"client_id": "sigstore",
"issuer": "https://oauth2.sigstore.dev/auth",
"redirect_url": ""
},
"timestamp": 1627564731
},
"commits": [
{
"issuer": {
"o": "sigstore.dev",
"cn": "sigstore-intermediate"
},
"signature": {
"status": "G",
"format": "pkcs7",
"publicKey":"base64-encoded-pkcs7-cert"
},
"rekor": {
"signature": {
"algorithm": "ecdsa-with-SHA256",
"content": "MEYCIQC1gffaJyVGfmJNoX94n9vOj+1EKEAlolT9UH7Bb2MuwwIhAJ1FFee9bDQdLbjt4yjbYz5Ojd3uITilNU4KLGJqiSQr",
"cert": "base64-encoded-cert"
},
"digest": {
"sha256": "d00bf6f1a50835a372b137ea18306d6a1d554500caf821e375817173db760868"
}
},
"author": {
"name": "[email protected]",
"digest": {
"sha1": "6a2b1f12938059f2fbe68754657fad33a4fca372"
}
}
},
{
"signature": {
"status": "G",
"format": "pgp",
"publicKey": "base64-encoded-pgp-cert"
},
"author": {
"name": "[email protected]",
"digest": {
"sha1": "5ee8eca4f05ef4b79eb6dc21c09198d4d61e3495"
}
}
},
{
"signature": {
"status": "N"
},
"author": {
"name": "[email protected]",
"digest": {
"sha1": "adba2b44bd04f8f2c92fbf739af797b8dcd046b5"
}
}
}
]
}
}
-
author.name: (required) name of the author
-
author.digest.sha1: (required) commit
-
signature.status: (required) %G?
of a commit
-
signature.format: (optional) signed with? (gpg, gitsign, etc.)
-
signature.publiKey: (optional) certificate from ($ git cat-file commit HEAD)
-
rekor: (optional) (if keyless signed with gitsign)
-
rekor.signature.algorithm: (required) signature algorithm
-
rekor.signature.content: (required) Rekor.Body.HashedRekordObj.signature.content
-
rekor.signature.cert: (required) Rekor.Body.HashedRekordObj.signature.publicKey.content
-
rekor.digest.sha256: (required) Rekor.Body.HashedRekordObj.data.hash.value
-
issuer: (optional) (if keyless signed)
-
issuer.o: (required) organization
-
issuer.cn (required) commonName
Implementation
-
Define a new SLSA provenance predicate type either here in gitsign or in the in-toto repository.
-
We should add a new attestation generate
command in gitsign:
$ gitsign attestation generate -o gitsign.json
See git-log(1) page for more details about the %G?
signature status.
Pseudocode:
loop through each commits
check %G? for each commit
if commit no signed: // <sig>N</sig>
set signature.status = N
if commit signed with GPG: // <sig>G</sig>
set signature.status = G
set signature.format = GPG
set signature.publicKey = base64-encoded-pgp-cert
if commit signed with gitsign:
search on Rekor: rekor-cli search --artifact <COMMIT>
get certificate: $(rekor-cli get --uuid=$uuid --format=json | jq -r .Body.HashedRekordObj.signature.publicKey.content)
get signature: $(rekor-cli get --uuid=$uuid --format=json | jq -r .Body.HashedRekordObj.signature.content)
get issuer from the cert
get necessary rekor obj values from rekor API
append issuer and rekor objects
append author object
- Introduce a new
--gitsign
boolean flag in cosign verify-attestation
:
$ COSIGN_EXPERIMENTAL=1 cosign verify-attestation \
--type cosign.sigstore.dev/attestation/gitsign/v1 \
--gitsign \
foo/bar:baz
Cosign should first verify the attestation as is. If verification succeed, it should parse the .att
to load all cosign.sigstore.dev/attestation/gitsign/v1
predicate type to struct definitions.
The New --gitsign
flag traverses each commit and verifies by running under the hood:
$ cosign verify-blob \
--cert commits[*].rekor.signature.publicKey \
--signature commits[*].rekor.signature.content \
commits[*].author.digest.sha1
Pseudocode:
loop through each commits
verify the attestation
if verification fails
exit
if verification succeed
parse predicate to structs
traverse in commits[*]
if commit signed with gitsign
run $ cosign verify-blob --cert .rekor.signature.publicKey --signature .rekor.signature.content .author.digest.sha1
if commit signed with gpg
run $ git verify-commit .author.digest.sha1 (in cosign)
if commit unsigned
exit
if one of verification fails exit
End User Usage
- Generates a provenance:
$ gitsign attestation generate -o gitsign.json
- Attests and push to OCI:
$ COSIGN_EXPERIMENTAL=1 cosign attest \
--type cosign.sigstore.dev/attestation/gitsign/v1 \
--predicate gitsign.json \
foo/bar:baz
- Verifies the attestation:
$ COSIGN_EXPERIMENTAL=1 cosign verify-attestation \
--type cosign.sigstore.dev/attestation/gitsign/v1 \
--gitsign \
foo/bar:baz
Intended Users
- container image builders
- pipelines
- end users
User Story
This container image seems signed by a trusted author but still, I couldn't trust the source code because I don't know who contributed. There is always the possibility that committers could have impersonated someone else by using their name and email. What I want is to verify all the commits that include in the source code that makes up the final container image. So I can be sure that there is no unsigned commit. All commits have a signature and are verified.
Concerns
-
We should be careful here since our main goal here is to not just say "all commits are verified" but "all commits are verified and trusted". So we should not forget that.
-
Backward compatibility is another big issue for us. What if a repository started enforcing GPG after the 50th commit and switched to a brand-new gitsign tool after the 200th commit? How the whole concept will work here?
Would pass something -n | --number <INTEGER>
flag to gitsign attestation generate
tackle this problem? So we can only get commits after n th.
Alternative Methods
-
Instead of generating an attestation and verifying, we can simply enforce signed commits only rule in the Git providers (GitHub, GitLab, etc.). So It would be easier to reject unsigned commits. E.g. server-side precommit hooks.
-
Similar to 1, instead of trusting the commits, we can trust the commit authors. By making an allowlist for the public keys that committers use, we can create restrictive policies to reject upcoming commits from the anonymous (i.e. open-source) contributors.
Related Proposals
- XREF: #94 by @wlynch
What distinguishes our proposal from the following above is that we do not create attestation on each commit, but instead keep a single one. Also this proposal does NOT store anything in refs
folder. We also want to verify everything as much as possible before using.
Open Questions
- Does this all make sense overall?
- Are all fields in
rekor
object really necessary? How would it be if we just pass UUID
instead?
- Should we verify each commit in history that made up the container image? (before running the container image)
- Instead of generating the attestation that only has one signature, shouldn't each commiter sign the same attestation with their PK or keyless somehow?
- Should we sign all commits individually?
- Should we store the final attestation in a version controlled in the repository itself? (i.e. in ref/, so anyone can verify all commits using gitsign itself either)
- Does this proposal uses Zero Trust ("trust nothing, verify everything") principles?
Waiting for your feedback!
cc @dlorenc @lukehinds @TomHennen @adityasaky @SantiagoTorres