Ken Muse

Retrieving Properties From a Gitsigned Commit


In the last post, I showed how to use gitsign to sign and verify commits. I also mentioned that many of the attested properties were added to the certificate used to sign the commit. What if you you want to use those properties in your worn workflows? For example, what if you want to determine if a commit was created by a specific workflow? In this post, I will show how to retrieve those properties from a signed commit.

Extracting the signed data

To be able to read the properties, you need to extract the data from the signed commit. This signature is stored in PKCS#7 format, which is a standard for signing data. This includes the signature and the certificate used to sign the data. The certificate contains the properties. While there’s a few ways to get to this data, I want to walk you through extracting it using Bash so that you understand what is in the commit signature and how the data is structured.

To extract the PKCS#7 data, you start by retrieving the signed commit. This can be done using cat-file {commit}. This returns the commit object in a format similar to the following:

 1tree 6405bf9be28a7721d34c9cbef7b422955ec9a9aa
 2parent 111786ccdf4c12ef1d27a309049391140d8befa5
 3author Actions <41898282+github-actions[bot]@users.noreply.github.com> 1744669357 +0000
 4committer Actions <41898282+github-actions[bot]@users.noreply.github.com> 1744669357 +0000
 5gpgsig -----BEGIN SIGNED MESSAGE-----
 6 MIIIEQYJKoZIhvcNAQcCoIIIAjCCB/4CAQExDTALBglghkgBZQMEAgEwCwYJKoZI
 7 hvcNAQcBoIIGszCCBq8wggY1oAMCAQICFAeE7d1xYeQLfcgupCzPx9O3r/7eMAoG
 8 CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln
 9 ...
10 PCRDWi8h3box75q7JTW0+AALNQIgbXR2Fb7dQSDd24ioa9OyHlbSe9wRbRbPaMVF
11 znKT0W4=
12 -----END SIGNED MESSAGE-----
13
14Signed commit

Next, we need to extract the signed message body. For this, we can use sed to extract the lines between the -----BEGIN SIGNED MESSAGE----- and -----END SIGNED MESSAGE-----. This looks like:

1sed -n '/-BEGIN/, /-END/p'

Since the content is indented, we also need to remove the leading spaces:

1sed 's/^ //g'

The message starts with gpgsig, so we need to remove that as well:

1sed 's/gpgsig //g'

The last step is to convert the header and footer to indicate this is a PKCS7 message. This is done by replacing SIGNED MESSAGE with PKCS7. This can be done with another sed command:

1sed 's/SIGNED MESSAGE/PKCS7/g'

You now have the PKCS#7 data chunk.

Extracting the details

With the extracted PKCS#7 data, you can now extract the details using openssl. We can combine all of the previous steps in Bash by piping (|) the commands. Putting all of this together, the complete command to extract and display the signature and certificate details looks like this:

1git cat-file commit HEAD | sed -n '/-BEGIN/, /-END/p' | sed 's/^ //g' | sed 's/gpgsig //g' | sed 's/SIGNED MESSAGE/PKCS7/g' | openssl pkcs7 -print -print_certs -text

Deciphering the details

Within the certificate and signature details, you will find a list of “extensions”. These are properties – more specifically, “Object Identifiers” (OIDs) – that contain additional details about the certificate key usages and constraints. With attestation, these OIDs are used to also store the attested values. Because a certificate is signed by a trusted authority, these values can be trusted as having the same validity as the certificate itself.

Sigstore has registered as a private enterprise with IANA and was assigned the private enterprise number 57264. A Private Enterprise Number (PEN) is a unique identifier assigned to an organization by IANA. It gets its name from the OID arc that is the basis of the OID 1.3.6.1.4.1:

    ISO   Org   DoD   Internet   Private  Enterprise 
     1  .  3  .  6  .    1     .    4    .    1

Having a PEN allows Sigstore to define its own OIDs, with the arc (prefix) 1.3.6.1.4.1.57264. The OIDs we care about are the ones they assigned for Fulcio (1.3.6.1.4.1.57264.1), their certificate authority for OpenID Connect (OIDC) identities. Within that, there are OIDs that will be included in the certificate that provide details about the build source. Some of the more relevant OIDs you’ll find:

OIDNameDetails
2.5.29.17Subject Alternative NameFor GitHub Actions, this is typically https://github.com/<owner>/<repo>/.github/workflows/<workflow-file>@r<ref>
1.3.6.1.4.1.57264.1.8Issuer v2Issuer of the OIDC token, formatted to RFC 5280 as a DER-encoded string
1.3.6.1.4.1.57264.1.9Build Signer URIThe URL of the workflow or reusable workflow responsible for the signing
1.3.6.1.4.1.57264.1.10Build Signer DigestReference to the commit SHA of the build signer
1.3.6.1.4.1.57264.1.11Runner EnvironmentThe environment for the build, typically github-hosted or self-hosted
1.3.6.1.4.1.57264.1.12Source Repository URIThe URI to the repository that is the source of the build
1.3.6.1.4.1.57264.1.13Source Repository DigestThe SHA that the build is baased upon
1.3.6.1.4.1.57264.1.14Source Repository RefThe reference that the build is based on, such as refs/heads/main
1.3.6.1.4.1.57264.1.15Source Repository IdentifierA unique key for the repository, such as GitHub’s repository ID number
1.3.6.1.4.1.57264.1.16Source Repository Owner URIThe URL of the source code owner (organization)
1.3.6.1.4.1.57264.1.17Source Repository Owner IDThe unique identifier for the owner
1.3.6.1.4.1.57264.1.18Build Config URLThe URL of the workflow containing the top-level build instructions
1.3.6.1.4.1.57264.1.19Build Config DigestThe SHA for the commit containing the top-level build
1.3.6.1.4.1.57264.1.20Build TriggerThe build trigger event
1.3.6.1.4.1.57264.1.21Run Invocation URIThe URI that uniquely identifies the build execution
1.3.6.1.4.1.57264.1.22Source Repository VisibilityThe visibility of the repository (private, public, internal)

A more complete list — including the mappings to GitHub, GitLab, Buildkite, and Codefresh claims — is available in the Fulcio repo.

Extracting the values

Now that you know what is in the certificate, you can extract the values using openssl. Doing this is similar to the previous steps, but requires a few more steps to extract the values from Bash. First, we need to extract just the X.509 certificate from the PKCS#7 data:

1CERTIFICATE=$( git cat-file commit HEAD | sed -n '/-BEGIN/, /-END/p' | sed 's/^ //g' | sed 's/gpgsig //g' | sed 's/SIGNED MESSAGE/PKCS7/g' | openssl pkcs7 -print_certs | openssl x509)

This will give us the encoded certificate (everything from -----BEGIN CERTIFICATE----- to -----END CERTIFICATE-----). We can To read a specific OID, we need to first find its position within the certificate. The easiest way to do this is to use openssl asn1parse to parse the raw ASN1 data.

For this example, I want to read the workflow details. That’s the build config, OID 1.3.6.1.4.1.57264.1.18. If I run the following command, I can see the OID and its position:

1echo -e "${CERTIFICATE}" | openssl asn1parse

The output will include lines like this:

1 1158:d=4  hl=2 l=  99 cons: SEQUENCE          
2 1160:d=5  hl=2 l=  10 prim: OBJECT            :1.3.6.1.4.1.57264.1.18
3 1172:d=5  hl=2 l=  85 prim: OCTET STRING      [HEX DUMP]:0C5368747470733A2F2F6769746875622E636F6D2F6B656E6D7573652D6F72672F61747465737465722F2E6769746875622F776F726B666C6F77732F626C616E6B2E796D6C40726566732F68656164732F6D61696E

This tells me that the data is encoded as an OCTET STRING, starting at position 1172. It’s 5 layers deep in the tree and consists of 85 bytes of data. The only value we really need is the offset to the start of the string. I can use this command to automatically extract the value:

1OID=1.3.6.1.4.1.57264.1.18
2OFFSET=$(echo -e "${CERTIFICATE}" | openssl asn1parse | awk "/${OID}/{getline; print}" | cut -d ':' -f1 | tr -d " ")

This retrieves the line after the OID, extracts the first value before the colon, and trims the spaces.

I can then use asn1parse to parse the string value at that offset:

1echo -e "${CERTIFICATE}" | openssl asn1parse -strparse $OFFSET

This returns the parsed string data:

10:d=0  hl=2 l=  83 prim: UTF8STRING        :https://github.com/kenmuse-org/attest/.github/workflows/publish.yml@refs/heads/main

And with a slight modification, I can extract just final string value:

1echo -e "${CERTIFICATE}" | openssl asn1parse -strparse $OFFSET | cut -d ':' -f4- 

Putting it all together

Now that you know how to extract the values, you can put it all together in a single script. This will extract the value of a specific OID and print it to the console.

1OID=1.3.6.1.4.1.57264.1.18
2CERTIFICATE=$( git cat-file commit HEAD | sed -n '/-BEGIN/, /-END/p' | sed 's/^ //g' | sed 's/gpgsig //g' | sed 's/SIGNED MESSAGE/PKCS7/g' | openssl pkcs7 -print_certs | openssl x509)
3OFFSET=$(echo -e "${CERTIFICATE}" | openssl asn1parse | awk "/${OID}/{getline; print}" | cut -d ':' -f1 | tr -d " ")
4VALUE=$(echo -e "${CERTIFICATE}" | openssl asn1parse -strparse $OFFSET | cut -d ':' -f4- )
5
6echo $VALUE

It may seem like a lot of steps, but hopefully it helps you understand how the data is structured and how to extract parts using Bash. Being able to extract the values allows you to use them in your validation process. For example, you could validate that the expected workflow was used to create the commit, or that the code was built using a push to the main branch. This allows you to use the attestations for even more complex validations. This gives you additional ways to validate the integrity of the code you are using and the process used to create it.

And if you’re interested, I wrote a short script in Python that can report values for an OID in the current commit. You just need to specify the OID you want on the command line. This has a bit more complexity since I’m manually parsing the ASN.1 string values Download