Skip to content

API tweak to allow further external verification #182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

jku
Copy link
Member

@jku jku commented Aug 11, 2022

This is a discussion starter for #157

Summary

It is currently not possible to use sigstore-python to verify the signatures made with a GitHub Actions certificate -- or rather it's not possible to verify any meaningful claims made by GitHub (like which project is responsible or what workflow was used). This is true for both the CLI tool and the API.

This change keeps CLI as is, but tweaks the API so that callers of verify() can do their own verification of the certificate contents after sigstore-python is happy with it.

Release Notes

  • Renamed current verify() to verify_email() -- this rename could be easily avoided if that's desirable
  • Refactored the generic verification code to verify_base() for re-use in both verify_email() and other code
  • Included (cryptography.X509) Certificate in the VerificationSuccess object: this enables calling code to continue verifying the contents without re-parsing the certificate

Documentation

The change allows an external developer to write (as an example) this code to verify GitHub specific claims outside the sigstore-python codebase:

from typing import Optional
from cryptography.x509 import ExtensionNotFound, ObjectIdentifier, SubjectAlternativeName, UniformResourceIdentifier
from sigstore._verify import Verifier, VerificationResult, VerificationFailure

GITHUB_ACTIONS_ISSUER = "https://token.actions.githubusercontent.com"
OIDC_GITHUB_WORKFLOW_SHA_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.3")
OIDC_GITHUB_WORKFLOW_REPOSITORY_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.5")

def verify_github(filename: str, expected_repo: str, expected_workflow: str, expected_tag: str, expected_sha: Optional[str]) -> VerificationResult:
    with open(filename, "rb") as f:
        input_ = f.read()
    with open(f"{filename}.crt", "rb") as f:
        cert = f.read()
    with open(f"{filename}.sig", "rb") as f:
        sig = f.read()
    
    verifier = Verifier.production()

    # Verify that signature is correct and issuer is GitHub
    result = verifier.verify_base(input_, cert, sig, GITHUB_ACTIONS_ISSUER)
    if not result:
        return result

    extensions = result.certificate.extensions
    san_ext = extensions.get_extension_for_class(SubjectAlternativeName)
    sans = san_ext.value.get_values_for_type(UniformResourceIdentifier)
    try:
        repository = extensions.get_extension_for_oid(OIDC_GITHUB_WORKFLOW_REPOSITORY_OID).value
        sha = extensions.get_extension_for_oid(OIDC_GITHUB_WORKFLOW_SHA_OID).value
    except ExtensionNotFound:
        return VerificationFailure(reason="Certificate does not contain a required GitHub extension")

    # Verify the signer identity
    expected_san = f"https://github.com/{expected_repo}/{expected_workflow}@refs/tags/{expected_tag}"
    if expected_san not in sans:
        return VerificationFailure(reason=f"Expected subject name '{expected_san}', got '{sans}'")
    
    # Verify the repository
    if repository.value != expected_repo.encode():
        return VerificationFailure(
            reason=f"Certificate's workflow repository does not match (got {repository.value})"
        )

    # Verify SHA
    if expected_sha:
        if sha.value != expected_sha.encode():
            return VerificationFailure(
                reason=f"Certificate's workflow SHA does not match (got {sha.value})"
            )

    # All good!
    return result

verify_github(
    "sigstore-0.6.3-py3-none-any.whl"
    "sigstore/sigstore-python",
    ".github/workflows/release.yml",
    "v0.6.3",
    "bff2e635da74627b62b31efd746f533bf97801ed"
)

* Rename current verify() as verify_email()
* Split the generic verification code to verify_base() for re-use
  in both verify_email() and other code
* Include Certificate in the VerificationSuccess: this enables
  calling code to continue verifying the contents without re-parsing the
  certificate

Signed-off-by: Jussi Kukkonen <jku@goto.fi>
@jku
Copy link
Member Author

jku commented Aug 11, 2022

I'm leaving this as draft PR for initial comments on direction: I'm happy to polish it as needed if the general idea seems acceptable.

file = _ASSETS / name
cert = _ASSETS / f"{name}.crt"
sig = _ASSETS / f"{name}.sig"
email = _ASSETS / f"{name}.email"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh btw, I didn't include these files since I didn't know if William wanted this email in a text file in the repo (it is in the certs already)

@woodruffw woodruffw added component:verification Core verification functionality component:api Public APIs labels Aug 12, 2022
@woodruffw
Copy link
Member

@jku the refactor I'm doing in #299 should cover a lot of the requirements here, including policy models for verifying GitHub Actions-specific claims. Could you take a look at it and let me know if it dovetails with your goals/API requirements? 🙂

@woodruffw
Copy link
Member

I think this is encompassed in #299, so I'm going to close it. But please let me know if I'm wrong @jku!

@woodruffw woodruffw closed this Nov 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component:api Public APIs component:verification Core verification functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants