├── .github ├── ISSUE_TEMPLATE │ └── bug.md ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── e2e.yaml │ ├── release.yml │ ├── validate-release.yml │ └── verify.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTORS.md ├── COPYRIGHT.txt ├── LICENSE ├── Makefile ├── README.md ├── cmd └── gitsign-credential-cache │ ├── README.md │ └── main.go ├── contrib └── systemd │ ├── gitsign-credential-cache.service │ └── gitsign-credential-cache.socket ├── docs ├── cli │ ├── gitsign.md │ ├── gitsign_attest.md │ ├── gitsign_initialize.md │ ├── gitsign_show.md │ ├── gitsign_verify-tag.md │ ├── gitsign_verify.md │ ├── gitsign_version.md │ └── main.go ├── committer-verification.md ├── timestamp.md └── verification.md ├── e2e └── sign_test.go ├── go.mod ├── go.sum ├── hack └── presubmit.sh ├── images └── unverified.png ├── internal ├── attest │ ├── attest.go │ ├── attest_test.go │ └── testdata │ │ ├── bar.txt │ │ ├── foo.txt │ │ └── test.json ├── cache │ ├── api │ │ └── api.go │ ├── cache_test.go │ ├── client.go │ └── service │ │ └── service.go ├── cert │ └── verify.go ├── commands │ ├── attest │ │ ├── README.md │ │ └── attest.go │ ├── initialize │ │ └── initialize.go │ ├── root │ │ ├── root.go │ │ ├── sign.go │ │ └── verify.go │ ├── show │ │ ├── show.go │ │ ├── show_test.go │ │ └── testdata │ │ │ ├── fulcio-cert.in.txt │ │ │ ├── fulcio-cert.out.json │ │ │ ├── gpg.in.txt │ │ │ └── gpg.out.json │ ├── verify-tag │ │ └── verify_tag.go │ ├── verify │ │ └── verify.go │ └── version │ │ └── version.go ├── config │ ├── config.go │ ├── config_test.go │ └── testdata │ │ └── config.txt ├── e2e │ ├── offline_test.go │ └── testdata │ │ └── offline.commit ├── fork │ └── ietf-cms │ │ ├── LICENSE │ │ ├── README.md │ │ ├── main_test.go │ │ ├── sign.go │ │ ├── sign_test.go │ │ ├── signed_data.go │ │ ├── timestamp.go │ │ ├── timestamp │ │ ├── timestamp.go │ │ └── timestamp_test.go │ │ ├── timestamp_test.go │ │ ├── verify.go │ │ └── verify_test.go ├── fulcio │ ├── fulcioroots │ │ ├── fulcioroots.go │ │ └── fulcioroots_test.go │ └── identity.go ├── git │ ├── doc.go │ ├── git.go │ └── gittest │ │ └── testing.go ├── gitsign │ ├── gitsign.go │ └── gitsign_test.go ├── gpg │ └── status.go ├── io │ └── streams.go ├── rekor │ ├── client.go │ └── oid │ │ ├── oid.go │ │ ├── oid_test.go │ │ ├── pbcompat.go │ │ ├── pbcompat_test.go │ │ └── testdata │ │ ├── commit.txt │ │ └── tlog.json ├── signature │ ├── doc.go │ ├── sign.go │ └── sign_test.go ├── signerverifier │ └── cert.go ├── utils.go └── utils_test.go ├── main.go └── pkg ├── fulcio ├── fulcio.go └── fulcio_test.go ├── git ├── git_test.go ├── signature_test.go ├── verifier.go └── verify.go ├── gitsign └── signer.go ├── predicate └── commit.go ├── rekor ├── option.go └── rekor.go └── version ├── version.go └── version_test.go /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Something wrong with the project? Report it here! 4 | labels: 'bug' 5 | assignees: '' 6 | 7 | --- 8 | 9 | **Description** 10 | 11 | 14 | 15 | **Version** 16 | 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | 5 | - package-ecosystem: gomod 6 | directory: "/" 7 | schedule: 8 | interval: weekly 9 | open-pull-requests-limit: 10 10 | groups: 11 | gomod: 12 | update-types: 13 | - "patch" 14 | 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: weekly 19 | open-pull-requests-limit: 10 20 | groups: 21 | actions: 22 | update-types: 23 | - "minor" 24 | - "patch" 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | ci: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | with: 17 | persist-credentials: false 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 21 | with: 22 | go-version-file: 'go.mod' 23 | check-latest: true 24 | 25 | - name: Build 26 | run: make build-all 27 | 28 | - name: Unit Tests 29 | run: make unit-test 30 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | name: E2E 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | e2e: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | # The rest of these are sanity-check settings, since I'm not sure if the 15 | # org default is permissive or restricted. 16 | # See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token 17 | # for more details. 18 | actions: none 19 | checks: none 20 | contents: read 21 | deployments: none 22 | id-token: none 23 | issues: none 24 | packages: none 25 | pages: none 26 | pull-requests: none 27 | repository-projects: none 28 | security-events: none 29 | statuses: none 30 | 31 | steps: 32 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | with: 34 | persist-credentials: false 35 | 36 | - name: Set up Go 37 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 38 | with: 39 | go-version-file: 'go.mod' 40 | check-latest: true 41 | 42 | - name: Get test OIDC token 43 | uses: sigstore-conformance/extremely-dangerous-public-oidc-beacon@main 44 | 45 | - name: export OIDC token 46 | run: | 47 | echo "SIGSTORE_ID_TOKEN=$(cat ./oidc-token.txt)" >> $GITHUB_ENV 48 | 49 | - name: e2e unit tests 50 | run: | 51 | set -e 52 | make e2e-test 53 | 54 | - name: Install Gitsign 55 | run: | 56 | set -e 57 | 58 | # Setup repo + tool 59 | make install-gitsign 60 | export PATH="$PATH:$GOPATH/bin" 61 | echo "PATH=${PATH}" 62 | whereis gitsign 63 | mkdir /tmp/git 64 | cd /tmp/git 65 | git init -b main . 66 | git config --global user.email "test@example.com" 67 | git config --global user.name "gitsign" 68 | git config --global gpg.x509.program gitsign 69 | git config --global gpg.format x509 70 | git config --global commit.gpgsign true 71 | 72 | # Verify tool is on our path 73 | gitsign -h 74 | - name: Test Sign and Verify commit 75 | run: | 76 | set -e 77 | 78 | # Sign commit 79 | git commit --allow-empty -S --message="Signed commit" 80 | 81 | # Verify commit 82 | echo "========== git verify-commit ==========" 83 | git verify-commit HEAD 84 | 85 | echo "========== gitsign verify ==========" 86 | gitsign verify \ 87 | --certificate-github-workflow-repository="sigstore-conformance/extremely-dangerous-public-oidc-beacon" \ 88 | --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ 89 | --certificate-identity="https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main" 90 | 91 | # Extra debug info 92 | 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 -print_certs -text 93 | - name: Test Sign and Verify commit - offline verification 94 | env: 95 | GITSIGN_REKOR_MODE: "offline" 96 | run: | 97 | set -e 98 | 99 | # Sign commit 100 | git commit --allow-empty -S --message="Signed commit" 101 | 102 | # Verify commit 103 | echo "========== git verify-commit ==========" 104 | git verify-commit HEAD 105 | 106 | echo "========== gitsign verify ==========" 107 | gitsign verify \ 108 | --certificate-github-workflow-repository="sigstore-conformance/extremely-dangerous-public-oidc-beacon" \ 109 | --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ 110 | --certificate-identity="https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main" 111 | 112 | # Extra debug info 113 | 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 -print_certs -text 114 | 115 | - name: Debug log 116 | if: failure() 117 | run: cat "${GITSIGN_LOG}" 118 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | # run only on tags 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write # needed to write releases 14 | id-token: write # needed for keyless signing 15 | packages: write # needed for push images 16 | attestations: write 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | with: 20 | fetch-depth: 0 # this is important, otherwise it won't checkout the full tree (i.e. no previous tags) 21 | persist-credentials: false 22 | 23 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 24 | with: 25 | go-version-file: 'go.mod' 26 | check-latest: true 27 | 28 | - uses: imjasonh/setup-crane@31b88efe9de28ae0ffa220711af4b60be9435f6e # v0.4 29 | 30 | - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 31 | 32 | - uses: anchore/sbom-action/download-syft@e11c554f704a0b820cbf8c51673f6945e0731532 # v0.20.0 33 | 34 | - name: Set env 35 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> "$GITHUB_ENV" 36 | 37 | - name: Login to GitHub Containers 38 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 39 | with: 40 | registry: ghcr.io 41 | username: ${{ github.repository_owner }} 42 | password: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 45 | with: 46 | version: latest 47 | args: release --clean 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | KO_DOCKER_REPO: ghcr.io/sigstore/gitsign 51 | 52 | - name: get the digest 53 | id: digest 54 | run: | 55 | digest=$(crane digest ghcr.io/sigstore/gitsign:${RELEASE_VERSION}) 56 | echo "digest=${digest}" >> "$GITHUB_OUTPUT" 57 | 58 | - name: sign image 59 | run: | 60 | cosign sign "ghcr.io/sigstore/gitsign@${DIGEST_TO_SIGN}" 61 | env: 62 | DIGEST_TO_SIGN: ${{ steps.digest.outputs.digest }} 63 | COSIGN_YES: true 64 | 65 | - name: Generate build provenance attestation 66 | uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 67 | with: 68 | subject-name: ghcr.io/sigstore/gitsign 69 | subject-digest: ${{ steps.digest.outputs.digest }} 70 | push-to-registry: true 71 | -------------------------------------------------------------------------------- /.github/workflows/validate-release.yml: -------------------------------------------------------------------------------- 1 | name: validate-release 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | validate-release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | with: 16 | persist-credentials: false 17 | 18 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 19 | with: 20 | go-version-file: 'go.mod' 21 | check-latest: true 22 | 23 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 24 | with: 25 | path: ~/go/pkg/mod 26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | 30 | - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 31 | - uses: anchore/sbom-action/download-syft@e11c554f704a0b820cbf8c51673f6945e0731532 # v0.20.0 32 | - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 33 | with: 34 | version: latest 35 | args: release --clean --snapshot --skip=sign 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | license-check: 14 | name: license boilerplate check 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | with: 19 | persist-credentials: false 20 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 21 | with: 22 | go-version-file: 'go.mod' 23 | check-latest: true 24 | - name: Install addlicense 25 | run: go install github.com/google/addlicense@v1.0.0 26 | - name: Check license headers 27 | run: | 28 | set -e 29 | addlicense -l apache -c 'The Sigstore Authors' -v \ 30 | -ignore "*.yml" \ 31 | -ignore "*.yaml" \ 32 | -ignore "internal/fork/**" \ 33 | * 34 | git diff --exit-code 35 | 36 | golangci: 37 | name: lint 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 41 | with: 42 | persist-credentials: false 43 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 44 | with: 45 | go-version-file: 'go.mod' 46 | check-latest: true 47 | - name: golangci-lint 48 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 49 | 50 | generate-docs: 51 | name: generate-docs 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 55 | with: 56 | persist-credentials: false 57 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 58 | with: 59 | go-version-file: 'go.mod' 60 | check-latest: true 61 | - name: Check CLI docs are up to date 62 | run: ./hack/presubmit.sh 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swap files 2 | *.swp 3 | dist/* 4 | .vscode/* 5 | /gitsign 6 | /gitsign-credential-cache 7 | !/cmd/gitsign-credential-cache 8 | vendor 9 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | issues-exit-code: 1 4 | linters: 5 | enable: 6 | - asciicheck 7 | - errorlint 8 | - gocritic 9 | - gosec 10 | - importas 11 | - misspell 12 | - prealloc 13 | - revive 14 | - staticcheck 15 | - tparallel 16 | - unconvert 17 | - unparam 18 | - whitespace 19 | exclusions: 20 | generated: lax 21 | presets: 22 | - comments 23 | - common-false-positives 24 | - legacy 25 | - std-error-handling 26 | rules: 27 | - linters: 28 | - errcheck 29 | - gosec 30 | path: _test\.go 31 | - linters: 32 | - staticcheck 33 | text: 'SA1019: package golang.org/x/crypto/openpgp' 34 | paths: 35 | - internal/fork 36 | - third_party$ 37 | - builtin$ 38 | - examples$ 39 | issues: 40 | max-issues-per-linter: 0 41 | max-same-issues: 0 42 | formatters: 43 | enable: 44 | - gofmt 45 | - goimports 46 | exclusions: 47 | generated: lax 48 | paths: 49 | - internal/fork 50 | - third_party$ 51 | - builtin$ 52 | - examples$ 53 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: gitsign 2 | version: 2 3 | 4 | gomod: 5 | proxy: true 6 | 7 | builds: 8 | - id: gitsign 9 | mod_timestamp: '{{ .CommitTimestamp }}' 10 | env: 11 | - CGO_ENABLED=0 12 | flags: 13 | - -trimpath 14 | goos: 15 | - linux 16 | - darwin 17 | - freebsd 18 | - windows 19 | goarch: 20 | - amd64 21 | - arm64 22 | ldflags: 23 | - "-s -w" 24 | - "-extldflags=-zrelro" 25 | - "-extldflags=-znow" 26 | - "-buildid= -X github.com/sigstore/gitsign/pkg/version.gitVersion={{ .Version }}" 27 | 28 | - id: gitsign-credential-cache 29 | mod_timestamp: '{{ .CommitTimestamp }}' 30 | main: ./cmd/gitsign-credential-cache 31 | binary: gitsign-credential-cache 32 | env: 33 | - CGO_ENABLED=0 34 | flags: 35 | - -trimpath 36 | goos: 37 | - linux 38 | - darwin 39 | - freebsd 40 | - windows 41 | goarch: 42 | - amd64 43 | - arm64 44 | ldflags: 45 | - "-s -w" 46 | - "-extldflags=-zrelro" 47 | - "-extldflags=-znow" 48 | - "-buildid= -X github.com/sigstore/gitsign/pkg/version.gitVersion={{ .Version }}" 49 | 50 | nfpms: 51 | - id: default 52 | package_name: gitsign 53 | vendor: Sigstore 54 | homepage: https://github.com/sigstore/gitsign 55 | maintainer: Billy Lynch 56 | description: Keyless git commit signing using OIDC identity 57 | builds: 58 | - gitsign 59 | - gitsign-credential-cache 60 | formats: 61 | - apk 62 | - deb 63 | - rpm 64 | 65 | archives: 66 | - id: binary 67 | formats: 68 | - binary 69 | allow_different_binary_count: true 70 | 71 | kos: 72 | - id: gitsign 73 | repositories: 74 | - github.com/sigstore/gitsign 75 | tags: 76 | - 'v{{ .Version }}' 77 | ldflags: 78 | - "-s -w -extldflags=-zrelro -extldflags=-znow -buildid= -X github.com/sigstore/gitsign/pkg/version.gitVersion={{ .Version }}" 79 | main: . 80 | bare: true 81 | preserve_import_paths: false 82 | base_import_paths: false 83 | sbom: spdx 84 | # then it have a shell 85 | base_image: cgr.dev/chainguard/git:latest-dev 86 | platforms: 87 | - linux/amd64 88 | - linux/arm64 89 | - linux/arm 90 | 91 | checksum: 92 | name_template: 'checksums.txt' 93 | 94 | source: 95 | enabled: true 96 | 97 | sboms: 98 | - id: binaries 99 | artifacts: binary 100 | - id: packages 101 | artifacts: package 102 | 103 | signs: 104 | - cmd: cosign 105 | env: 106 | - COSIGN_YES=true 107 | certificate: '${artifact}.pem' 108 | signature: '${artifact}.sig' 109 | args: 110 | - sign-blob 111 | - '--output-certificate=${certificate}' 112 | - '--output-signature=${signature}' 113 | - '${artifact}' 114 | artifacts: binary 115 | output: true 116 | 117 | release: 118 | prerelease: allow 119 | draft: true # allow for manual edits 120 | github: 121 | owner: sigstore 122 | name: gitsign 123 | footer: | 124 | ### Thanks to all contributors! 125 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # The CODEOWNERS are managed via a GitHub team, but the current list is (in alphabetical order): 2 | # 3 | # lukehinds 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish 4 | to make via an [issue](https://github.com/sigstore/{project-name}/issues). 5 | 6 | ## Pull Request Process 7 | 8 | 1. Create an [issue](https://github.com/sigstore/{project-name}/issues) 9 | outlining the fix or feature. 10 | 2. Fork the {project-name} repository to your own github account and clone it locally. 11 | 3. Hack on your changes. 12 | 4. Update the README.md with details of changes to any interface, this includes new environment 13 | variables, exposed ports, useful file locations, CLI parameters and 14 | new or changed configuration values. 15 | 5. Correctly format your commit message see [Commit Messages](#Commit Message Guidelines) 16 | below. 17 | 6. Ensure that CI passes, if it fails, fix the failures. 18 | 7. Every pull request requires a review from the [core {project-name} team](https://github.com/orgs/github.com/sigstore/teams/{project-name}-codeowners) 19 | before merging. 20 | 8. If your pull request consists of more than one commit, please squash your 21 | commits as described in [Squash Commits](#Squash Commits) 22 | 23 | ## Commit Message Guidelines 24 | 25 | We follow the commit formatting recommendations found on [Chris Beams' How to Write a Git Commit Message article]((https://chris.beams.io/posts/git-commit/). 26 | 27 | Well formed commit messages not only help reviewers understand the nature of 28 | the Pull Request, but also assists the release process where commit messages 29 | are used to generate release notes. 30 | 31 | A good example of a commit message would be as follows: 32 | 33 | ``` 34 | Summarize changes in around 50 characters or less 35 | 36 | More detailed explanatory text, if necessary. Wrap it to about 72 37 | characters or so. In some contexts, the first line is treated as the 38 | subject of the commit and the rest of the text as the body. The 39 | blank line separating the summary from the body is critical (unless 40 | you omit the body entirely); various tools like `log`, `shortlog` 41 | and `rebase` can get confused if you run the two together. 42 | 43 | Explain the problem that this commit is solving. Focus on why you 44 | are making this change as opposed to how (the code explains that). 45 | Are there side effects or other unintuitive consequences of this 46 | change? Here's the place to explain them. 47 | 48 | Further paragraphs come after blank lines. 49 | 50 | - Bullet points are okay, too 51 | 52 | - Typically a hyphen or asterisk is used for the bullet, preceded 53 | by a single space, with blank lines in between, but conventions 54 | vary here 55 | 56 | If you use an issue tracker, put references to them at the bottom, 57 | like this: 58 | 59 | Resolves: #123 60 | See also: #456, #789 61 | ``` 62 | 63 | Note the `Resolves #123` tag, this references the issue raised and allows us to 64 | ensure issues are associated and closed when a pull request is merged. 65 | 66 | Please refer to [the github help page on message types](https://help.github.com/articles/closing-issues-using-keywords/) 67 | for a complete list of issue references. 68 | 69 | ## Squash Commits 70 | 71 | Should your pull request consist of more than one commit (perhaps due to 72 | a change being requested during the review cycle), please perform a git squash 73 | once a reviewer has approved your pull request. 74 | 75 | A squash can be performed as follows. Let's say you have the following commits: 76 | 77 | initial commit 78 | second commit 79 | final commit 80 | 81 | Run the command below with the number set to the total commits you wish to 82 | squash (in our case 3 commits): 83 | 84 | git rebase -i HEAD~3 85 | 86 | You default text editor will then open up and you will see the following:: 87 | 88 | pick eb36612 initial commit 89 | pick 9ac8968 second commit 90 | pick a760569 final commit 91 | 92 | # Rebase eb1429f..a760569 onto eb1429f (3 commands) 93 | 94 | We want to rebase on top of our first commit, so we change the other two commits 95 | to `squash`: 96 | 97 | pick eb36612 initial commit 98 | squash 9ac8968 second commit 99 | squash a760569 final commit 100 | 101 | After this, should you wish to update your commit message to better summarise 102 | all of your pull request, run: 103 | 104 | git commit --amend 105 | 106 | You will then need to force push (assuming your initial commit(s) were posted 107 | to github): 108 | 109 | git push origin your-branch --force 110 | 111 | Alternatively, a core member can squash your commits within Github. 112 | ## Code of Conduct 113 | 114 | {project-name} adheres to and enforces the [Contributor Covenant](http://contributor-covenant.org/version/1/4/) Code of Conduct. 115 | Please take a moment to read the [CODE_OF_CONDUCT.md](https://github.com/sigstore/{project-name}/blob/master/CODE_OF_CONDUCT.md) document. 116 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2021 The Sigstore Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2022 The Sigstore Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | GIT_VERSION ?= $(shell git describe --tags --always --dirty) 17 | 18 | ARCH ?= $(shell uname -m) 19 | 20 | CGO_FLAG=0 21 | # Fix `-buildmode=pie requires external (cgo) linking, but cgo is not enabled` 22 | ifeq ($(ARCH),riscv64) 23 | CGO_FLAG=1 24 | endif 25 | 26 | LDFLAGS=-buildid= -X github.com/sigstore/gitsign/pkg/version.gitVersion=$(GIT_VERSION) 27 | 28 | .PHONY: build-gitsign 29 | build-gitsign: 30 | CGO_ENABLED=$(CGO_FLAG) go build -trimpath -ldflags "$(LDFLAGS)" . 31 | 32 | .PHONY: build-credential-cache 33 | build-credential-cache: 34 | CGO_ENABLED=$(CGO_FLAG) go build -trimpath -ldflags "$(LDFLAGS)" ./cmd/gitsign-credential-cache 35 | 36 | .PHONY: build-all 37 | build-all: build-gitsign build-credential-cache 38 | 39 | .PHONY: install-gitsign 40 | install-gitsign: 41 | CGO_ENABLED=$(CGO_FLAG) go install -trimpath -ldflags "$(LDFLAGS)" github.com/sigstore/gitsign 42 | 43 | .PHONY: install-credential-cache 44 | install-credential-cache: 45 | CGO_ENABLED=$(CGO_FLAG) go install -trimpath -ldflags "$(LDFLAGS)" github.com/sigstore/gitsign/cmd/gitsign-credential-cache 46 | 47 | .PHONY: install-all 48 | install-all: install-gitsign install-credential-cache 49 | 50 | .PHONY: unit-test 51 | unit-test: 52 | go test -v ./... 53 | 54 | # These tests use live dependencies, and may otherwise modify state. 55 | .PHONY: e2e-test 56 | e2e-test: 57 | go test -tags e2e -v ./... 58 | -------------------------------------------------------------------------------- /cmd/gitsign-credential-cache/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | "net" 21 | "net/rpc" 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/coreos/go-systemd/v22/activation" 26 | "github.com/spf13/pflag" 27 | 28 | "github.com/sigstore/gitsign/internal/cache/service" 29 | "github.com/sigstore/gitsign/pkg/version" 30 | ) 31 | 32 | var ( 33 | // Action flags 34 | versionFlag = pflag.BoolP("version", "v", false, "print the version number") 35 | systemdFlag = pflag.Bool("systemd-socket-activation", false, "use systemd socket activation") 36 | ) 37 | 38 | func main() { 39 | pflag.Parse() 40 | 41 | if *versionFlag { 42 | v := version.GetVersionInfo() 43 | fmt.Printf("gitsign-credential-cache version %s\n", v.GitVersion) 44 | 45 | os.Exit(0) 46 | } 47 | 48 | var connChan = make(chan net.Conn) 49 | if *systemdFlag { 50 | // Stop if we're not running under systemd. 51 | if os.Getenv("LISTEN_PID") == "" { 52 | log.Fatalf("systemd socket activation requested but not running under systemd") 53 | } 54 | 55 | listeners, err := activation.Listeners() 56 | if err != nil { 57 | log.Fatalf("error getting systemd listeners: %v", err) 58 | } 59 | var validCount int 60 | for _, l := range listeners { 61 | if l == nil { 62 | continue 63 | } 64 | fmt.Println(l.Addr().String()) 65 | go connToChan(l, connChan) 66 | validCount++ 67 | } 68 | if validCount == 0 { 69 | log.Fatalf("no valid systemd listeners found") 70 | } 71 | } else { 72 | user, err := os.UserCacheDir() 73 | if err != nil { 74 | log.Fatalf("error getting user cache directory: %v", err) 75 | } 76 | 77 | dir := filepath.Join(user, "sigstore", "gitsign") 78 | if err := os.MkdirAll(dir, 0700); err != nil { 79 | log.Fatalf("error creating %s: %v", dir, err) 80 | } 81 | 82 | path := filepath.Join(dir, "cache.sock") 83 | if _, err := os.Stat(path); err == nil { 84 | os.Remove(path) 85 | } 86 | fmt.Println(path) 87 | 88 | l, err := net.Listen("unix", path) 89 | if err != nil { 90 | log.Fatalf("error opening socket: %v", err) 91 | } 92 | 93 | // Previously, we used syscall.Umask(0077) to ensure this was 94 | // permissioned only to the current user. Windows doesn't have this 95 | // syscall, so we're switching over to an explicit Chmod on the socket 96 | // path. 97 | // Also see https://github.com/golang/go/issues/11822 98 | if err := os.Chmod(path, 0700); err != nil { 99 | log.Fatalf("error setting socket permissions: %v", err) 100 | } 101 | 102 | go connToChan(l, connChan) 103 | } 104 | srv := rpc.NewServer() 105 | if err := srv.Register(service.NewService()); err != nil { 106 | log.Fatalf("error registering RPC service: %v", err) 107 | } 108 | for conn := range connChan { 109 | go srv.ServeConn(conn) 110 | } 111 | } 112 | 113 | func connToChan(l net.Listener, connChan chan net.Conn) { 114 | for { 115 | conn, err := l.Accept() 116 | if err != nil { 117 | log.Fatalf("error accepting connection: %v", err) 118 | } 119 | connChan <- conn 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /contrib/systemd/gitsign-credential-cache.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=GitSign credential cache 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart=%h/.local/bin/gitsign-credential-cache 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /contrib/systemd/gitsign-credential-cache.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=GitSign credential cache socket 3 | 4 | [Socket] 5 | ListenStream=%C/sigstore/gitsign/cache.sock 6 | DirectoryMode=0700 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /docs/cli/gitsign.md: -------------------------------------------------------------------------------- 1 | ## gitsign 2 | 3 | Keyless Git signing with Sigstore! 4 | 5 | ``` 6 | gitsign [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -a, --armor create ascii armored output 13 | -b, --detach-sign make a detached signature 14 | -h, --help help for gitsign 15 | --include-certs int -3 is the same as -2, but omits issuer when cert has Authority Information Access extension. -2 includes all certs except root. -1 includes all certs. 0 includes no certs. 1 includes leaf cert. >1 includes n from the leaf. Default -2. (default -2) 16 | -u, --local-user string use USER-ID to sign 17 | -s, --sign make a signature 18 | --status-fd int write special status strings to the file descriptor n. (default -1) 19 | -v, --verify verify a signature 20 | --version print Gitsign version 21 | ``` 22 | 23 | ### SEE ALSO 24 | 25 | * [gitsign attest](gitsign_attest.md) - add attestations to Git objects 26 | * [gitsign initialize](gitsign_initialize.md) - Initializes Sigstore root to retrieve trusted certificate and key targets for verification. 27 | * [gitsign show](gitsign_show.md) - Show source predicate information 28 | * [gitsign verify](gitsign_verify.md) - Verify a commit 29 | * [gitsign verify-tag](gitsign_verify-tag.md) - Verify a tag 30 | * [gitsign version](gitsign_version.md) - print Gitsign version 31 | 32 | -------------------------------------------------------------------------------- /docs/cli/gitsign_attest.md: -------------------------------------------------------------------------------- 1 | ## gitsign attest 2 | 3 | add attestations to Git objects 4 | 5 | ``` 6 | gitsign attest [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -f, --filepath string attestation filepath 13 | -h, --help help for attest 14 | --objtype string [commit | tree] - Git object type to attest (default "commit") 15 | --type string specify a predicate type URI 16 | ``` 17 | 18 | ### SEE ALSO 19 | 20 | * [gitsign](gitsign.md) - Keyless Git signing with Sigstore! 21 | 22 | -------------------------------------------------------------------------------- /docs/cli/gitsign_initialize.md: -------------------------------------------------------------------------------- 1 | ## gitsign initialize 2 | 3 | Initializes Sigstore root to retrieve trusted certificate and key targets for verification. 4 | 5 | ### Synopsis 6 | 7 | Initializes Sigstore root to retrieve trusted certificate and key targets for verification. 8 | 9 | The following options are used by default: 10 | - The current trusted Sigstore TUF root is embedded inside gitsign at the time of release. 11 | - Sigstore remote TUF repository is pulled from the CDN mirror at tuf-repo-cdn.sigstore.dev. 12 | 13 | To provide an out-of-band trusted initial root.json, use the -root flag with a file or URL reference. 14 | This will enable you to point gitsign to a separate TUF root. 15 | 16 | Any updated TUF repository will be written to $HOME/.sigstore/root/. 17 | 18 | Trusted keys and certificate used in gitsign verification (e.g. verifying Fulcio issued certificates 19 | with Fulcio root CA) are pulled form the trusted metadata. 20 | 21 | ``` 22 | gitsign initialize [flags] 23 | ``` 24 | 25 | ### Examples 26 | 27 | ``` 28 | gitsign initialize -mirror -out 29 | 30 | # initialize root with distributed root keys, default mirror, and default out path. 31 | gitsign initialize 32 | 33 | # initialize with an out-of-band root key file, using the default mirror. 34 | gitsign initialize -root 35 | 36 | # initialize with an out-of-band root key file and custom repository mirror. 37 | gitsign initialize -mirror -root 38 | ``` 39 | 40 | ### Options 41 | 42 | ``` 43 | -h, --help help for initialize 44 | --mirror string GCS bucket to a Sigstore TUF repository, or HTTP(S) base URL, or file:/// for local filestore remote (air-gap) (default "https://tuf-repo-cdn.sigstore.dev") 45 | --root string path to trusted initial root. defaults to embedded root 46 | ``` 47 | 48 | ### SEE ALSO 49 | 50 | * [gitsign](gitsign.md) - Keyless Git signing with Sigstore! 51 | 52 | -------------------------------------------------------------------------------- /docs/cli/gitsign_show.md: -------------------------------------------------------------------------------- 1 | ## gitsign show 2 | 3 | Show source predicate information 4 | 5 | ### Synopsis 6 | 7 | Show source predicate information 8 | 9 | Prints an in-toto style predicate for the specified revision. 10 | If no revision is specified, HEAD is used. 11 | 12 | This command is experimental, and its CLI surface may change. 13 | 14 | ``` 15 | gitsign show [revision] [flags] 16 | ``` 17 | 18 | ### Options 19 | 20 | ``` 21 | -h, --help help for show 22 | -r, --remote string git remote (used to populate subject) (default "origin") 23 | ``` 24 | 25 | ### SEE ALSO 26 | 27 | * [gitsign](gitsign.md) - Keyless Git signing with Sigstore! 28 | 29 | -------------------------------------------------------------------------------- /docs/cli/gitsign_verify-tag.md: -------------------------------------------------------------------------------- 1 | ## gitsign verify-tag 2 | 3 | Verify a tag 4 | 5 | ### Synopsis 6 | 7 | Verify a tag. 8 | 9 | verify-tag verifies a tag against a set of certificate claims. 10 | This should generally be used over git verify-tag, since verify-tag will 11 | check the identity included in the signature's certificate. 12 | 13 | ``` 14 | gitsign verify-tag [flags] 15 | ``` 16 | 17 | ### Options 18 | 19 | ``` 20 | --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. 21 | --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. 22 | --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon 23 | --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. 24 | --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run 25 | --certificate-identity string The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. 26 | --certificate-identity-regexp string A regular expression alternative to --certificate-identity. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. 27 | --certificate-oidc-issuer string The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. 28 | --certificate-oidc-issuer-regexp string A regular expression alternative to --certificate-oidc-issuer. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. 29 | -h, --help help for verify-tag 30 | --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log 31 | --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. 32 | ``` 33 | 34 | ### SEE ALSO 35 | 36 | * [gitsign](gitsign.md) - Keyless Git signing with Sigstore! 37 | 38 | -------------------------------------------------------------------------------- /docs/cli/gitsign_verify.md: -------------------------------------------------------------------------------- 1 | ## gitsign verify 2 | 3 | Verify a commit 4 | 5 | ### Synopsis 6 | 7 | Verify a commit. 8 | 9 | verify verifies a commit against a set of certificate claims. 10 | This should generally be used over git verify-commit, since verify will 11 | check the identity included in the signature's certificate. 12 | 13 | If no revision is specified, HEAD is used. 14 | 15 | ``` 16 | gitsign verify [commit] [flags] 17 | ``` 18 | 19 | ### Options 20 | 21 | ``` 22 | --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. 23 | --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. 24 | --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon 25 | --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. 26 | --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run 27 | --certificate-identity string The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. 28 | --certificate-identity-regexp string A regular expression alternative to --certificate-identity. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. 29 | --certificate-oidc-issuer string The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. 30 | --certificate-oidc-issuer-regexp string A regular expression alternative to --certificate-oidc-issuer. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. 31 | -h, --help help for verify 32 | --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log 33 | --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. 34 | ``` 35 | 36 | ### SEE ALSO 37 | 38 | * [gitsign](gitsign.md) - Keyless Git signing with Sigstore! 39 | 40 | -------------------------------------------------------------------------------- /docs/cli/gitsign_version.md: -------------------------------------------------------------------------------- 1 | ## gitsign version 2 | 3 | print Gitsign version 4 | 5 | ``` 6 | gitsign version [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for version 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gitsign](gitsign.md) - Keyless Git signing with Sigstore! 18 | 19 | -------------------------------------------------------------------------------- /docs/cli/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:generate go run . 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/sigstore/gitsign/internal/commands/root" 23 | "github.com/spf13/cobra" 24 | "github.com/spf13/cobra/doc" 25 | ) 26 | 27 | var ( 28 | dir string 29 | cmd = &cobra.Command{ 30 | Use: "gendoc", 31 | Short: "Generate help docs", 32 | Args: cobra.NoArgs, 33 | RunE: func(*cobra.Command, []string) error { 34 | return doc.GenMarkdownTree(root.New(nil), dir) 35 | }, 36 | } 37 | ) 38 | 39 | func init() { 40 | cmd.Flags().StringVarP(&dir, "dir", "d", ".", "Path to directory in which to generate docs") 41 | } 42 | 43 | func main() { 44 | if err := cmd.Execute(); err != nil { 45 | fmt.Println(err) 46 | os.Exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/committer-verification.md: -------------------------------------------------------------------------------- 1 | # Committer Verification 2 | 3 | Gitsign can be optionally configured to verify that the committer user identity 4 | matches the git user configuration (i.e. `user.name` and `user.email`) 5 | 6 | To enable committer verification, run `git config gitsign.matchCommitter true`. 7 | 8 | Committer verification is done by matching the certificate Subject Alternative 9 | Name (SAN) against the issued Fulcio certificate in the following order: 10 | 11 | 1. An `EmailAddresses` cert value matches the committer `user.email`. This 12 | should be used for most human committer verification. 13 | 2. A `URI` cert value matches the committer `user.name`. This should be used for 14 | most automated user committer verification. 15 | 16 | In the event that multiple SAN values are provided, verification will succeed if 17 | at least one value matches. 18 | 19 | ## Configuring Automated Users 20 | 21 | If running in an environment where the authenticated user does **not** have a 22 | user email to bind against (i.e. GitHub Actions, other workload identity 23 | workflows), users are expected to be identified by the SAN URI instead. 24 | 25 | See https://github.com/sigstore/fulcio/blob/main/docs/oidc.md for more details 26 | 27 | ### GitHub Actions 28 | 29 | ```sh 30 | # This configures the SAN URI for the expected identity in the Fulcio cert. 31 | $ git config user.name "https://myorg/myrepo/path/to/workflow" 32 | # This configures GitHub UI to recognize the commit as coming from a GitHub Action. 33 | $ git config user.email 1234567890+github-actions@users.noreply.github.com 34 | ``` 35 | -------------------------------------------------------------------------------- /e2e/sign_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build e2e 16 | // +build e2e 17 | 18 | package e2e 19 | 20 | import ( 21 | "context" 22 | "encoding/json" 23 | "os" 24 | "testing" 25 | "time" 26 | 27 | "github.com/go-git/go-billy/v5/memfs" 28 | "github.com/go-git/go-git/v5" 29 | "github.com/go-git/go-git/v5/plumbing/object" 30 | "github.com/go-git/go-git/v5/storage/memory" 31 | "github.com/sigstore/cosign/v2/pkg/providers" 32 | "github.com/sigstore/gitsign/internal/git/gittest" 33 | "github.com/sigstore/gitsign/pkg/fulcio" 34 | gsgit "github.com/sigstore/gitsign/pkg/git" 35 | "github.com/sigstore/gitsign/pkg/gitsign" 36 | "github.com/sigstore/gitsign/pkg/rekor" 37 | "github.com/sigstore/sigstore/pkg/oauth" 38 | "github.com/sigstore/sigstore/pkg/oauthflow" 39 | 40 | // Enable OIDC providers 41 | _ "github.com/sigstore/cosign/v2/pkg/providers/all" 42 | ) 43 | 44 | func TestSign(t *testing.T) { 45 | ctx := context.Background() 46 | 47 | var flow oauthflow.TokenGetter = &oauthflow.InteractiveIDTokenGetter{ 48 | HTMLPage: oauth.InteractiveSuccessHTML, 49 | } 50 | if providers.Enabled(ctx) { 51 | // If automatic token provisioning is enabled, use it. 52 | token, err := providers.Provide(ctx, "sigstore") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | flow = &oauthflow.StaticTokenGetter{ 57 | RawToken: token, 58 | } 59 | } 60 | fulcio, err := fulcio.NewClient("https://fulcio.sigstore.dev", fulcio.OIDCOptions{ 61 | ClientID: "sigstore", 62 | Issuer: "https://oauth2.sigstore.dev/auth", 63 | TokenGetter: flow, 64 | }) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | rekor, err := rekor.NewWithOptions(ctx, "https://rekor.sigstore.dev") 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | signer, err := gitsign.NewSigner(ctx, fulcio, rekor) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | // Make a commit + sign it 78 | storage := memory.NewStorage() 79 | repo, err := git.Init(storage, memfs.New()) 80 | if err != nil { 81 | panic(err) 82 | } 83 | w, err := repo.Worktree() 84 | if err != nil { 85 | panic(err) 86 | } 87 | sha, err := w.Commit("example commit", &git.CommitOptions{ 88 | Author: &object.Signature{ 89 | Name: "John Doe", 90 | Email: "john@example.com", 91 | When: time.UnixMicro(1234567890).UTC(), 92 | }, 93 | Signer: signer, 94 | AllowEmptyCommits: true, 95 | }) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | commit, err := repo.CommitObject(sha) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | body := gittest.MarshalCommitBody(t, commit) 104 | sig := []byte(commit.PGPSignature) 105 | 106 | // Verify the commit 107 | verifier, err := gsgit.NewDefaultVerifier(ctx) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | summary, err := gsgit.Verify(ctx, verifier, rekor, body, sig, true) 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | enc := json.NewEncoder(os.Stdout) 116 | enc.SetIndent("", " ") 117 | enc.Encode(summary.LogEntry) 118 | } 119 | -------------------------------------------------------------------------------- /hack/presubmit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 The Sigstore Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | 22 | PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 23 | 24 | pushd "${PROJECT_ROOT}" 25 | trap popd EXIT 26 | 27 | # Verify that generated Markdown docs are up-to-date. 28 | tmpdir=$(mktemp -d) 29 | go run docs/cli/main.go --dir "${tmpdir}" 30 | diff -Naur -I '###### Auto generated' -x "*.go" "${tmpdir}" docs/cli -------------------------------------------------------------------------------- /images/unverified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigstore/gitsign/a49bf9ed4e9718414c1556d2a625b1d86ee4ef77/images/unverified.png -------------------------------------------------------------------------------- /internal/attest/testdata/bar.txt: -------------------------------------------------------------------------------- 1 | bar -------------------------------------------------------------------------------- /internal/attest/testdata/foo.txt: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /internal/attest/testdata/test.json: -------------------------------------------------------------------------------- 1 | {"foo":"bar"} -------------------------------------------------------------------------------- /internal/cache/api/api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package api 16 | 17 | import "github.com/sigstore/gitsign/internal/config" 18 | 19 | type Credential struct { 20 | PrivateKey []byte 21 | Cert []byte 22 | Chain []byte 23 | } 24 | 25 | type StoreCredentialRequest struct { 26 | ID string 27 | Credential *Credential 28 | } 29 | 30 | type GetCredentialRequest struct { 31 | ID string 32 | Config *config.Config 33 | } 34 | -------------------------------------------------------------------------------- /internal/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cache_test 16 | 17 | import ( 18 | "context" 19 | "crypto/ecdsa" 20 | "crypto/elliptic" 21 | "crypto/rand" 22 | "fmt" 23 | "net" 24 | "net/rpc" 25 | "os" 26 | "path/filepath" 27 | "testing" 28 | 29 | "github.com/github/smimesign/fakeca" 30 | "github.com/google/go-cmp/cmp" 31 | "github.com/sigstore/gitsign/internal/cache" 32 | "github.com/sigstore/gitsign/internal/cache/api" 33 | "github.com/sigstore/gitsign/internal/cache/service" 34 | "github.com/sigstore/sigstore/pkg/cryptoutils" 35 | ) 36 | 37 | func TestCache(t *testing.T) { 38 | ctx := context.Background() 39 | 40 | path := filepath.Join(t.TempDir(), "cache.sock") 41 | l, err := net.Listen("unix", path) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | srv := rpc.NewServer() 46 | srv.Register(service.NewService()) 47 | go func() { 48 | for { 49 | srv.Accept(l) 50 | } 51 | }() 52 | 53 | rpcClient, _ := rpc.Dial("unix", path) 54 | defer rpcClient.Close() 55 | ca := fakeca.New() 56 | client := &cache.Client{ 57 | Client: rpcClient, 58 | Roots: ca.ChainPool(), 59 | } 60 | 61 | if _, _, _, err := client.GetCredentials(ctx, nil); err == nil { 62 | t.Fatal("GetSignerVerifier: expected err, got not") 63 | } 64 | 65 | priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 66 | certPEM, _ := cryptoutils.MarshalCertificateToPEM(ca.Certificate) 67 | 68 | if err := client.StoreCert(ctx, priv, certPEM, nil); err != nil { 69 | t.Fatalf("StoreCert: %v", err) 70 | } 71 | 72 | host, _ := os.Hostname() 73 | wd, _ := os.Getwd() 74 | id := fmt.Sprintf("%s@%s", host, wd) 75 | cred := new(api.Credential) 76 | if err := client.Client.Call("Service.GetCredential", &api.GetCredentialRequest{ID: id}, cred); err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | privPEM, _ := cryptoutils.MarshalPrivateKeyToPEM(priv) 81 | want := &api.Credential{ 82 | PrivateKey: privPEM, 83 | Cert: certPEM, 84 | } 85 | 86 | if diff := cmp.Diff(want, cred); diff != "" { 87 | t.Error(diff) 88 | } 89 | 90 | gotPriv, gotCert, _, err := client.GetCredentials(ctx, nil) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | if !priv.Equal(gotPriv) { 95 | t.Fatal("private key did not match") 96 | } 97 | if ok := cmp.Equal(certPEM, gotCert); !ok { 98 | t.Error("stored cert does not match") 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /internal/cache/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cache 16 | 17 | import ( 18 | "context" 19 | "crypto" 20 | "crypto/x509" 21 | "encoding/asn1" 22 | "fmt" 23 | "net/rpc" 24 | "os" 25 | "time" 26 | 27 | "github.com/sigstore/gitsign/internal/cache/api" 28 | "github.com/sigstore/gitsign/internal/config" 29 | "github.com/sigstore/sigstore/pkg/cryptoutils" 30 | ) 31 | 32 | type Client struct { 33 | Client *rpc.Client 34 | Roots *x509.CertPool 35 | Intermediates *x509.CertPool 36 | } 37 | 38 | func (c *Client) GetCredentials(_ context.Context, cfg *config.Config) (crypto.PrivateKey, []byte, []byte, error) { 39 | id, err := id() 40 | if err != nil { 41 | return nil, nil, nil, fmt.Errorf("error getting credential ID: %w", err) 42 | } 43 | resp := new(api.Credential) 44 | if err := c.Client.Call("Service.GetCredential", api.GetCredentialRequest{ 45 | ID: id, 46 | Config: cfg, 47 | }, resp); err != nil { 48 | return nil, nil, nil, err 49 | } 50 | 51 | privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey(resp.PrivateKey, cryptoutils.SkipPassword) 52 | if err != nil { 53 | return nil, nil, nil, fmt.Errorf("error unmarshalling private key: %w", err) 54 | } 55 | 56 | // Check that the cert is in fact still valid. 57 | certs, err := cryptoutils.UnmarshalCertificatesFromPEM(resp.Cert) 58 | if err != nil { 59 | return nil, nil, nil, fmt.Errorf("error unmarshalling cert: %w", err) 60 | } 61 | // There should really only be 1 cert, but check them all anyway. 62 | for _, cert := range certs { 63 | if len(cert.UnhandledCriticalExtensions) > 0 { 64 | var unhandledExts []asn1.ObjectIdentifier 65 | for _, oid := range cert.UnhandledCriticalExtensions { 66 | if !oid.Equal(cryptoutils.SANOID) { 67 | unhandledExts = append(unhandledExts, oid) 68 | } 69 | } 70 | 71 | cert.UnhandledCriticalExtensions = unhandledExts 72 | } 73 | 74 | if _, err := cert.Verify(x509.VerifyOptions{ 75 | Roots: c.Roots, 76 | Intermediates: c.Intermediates, 77 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, 78 | // We're going to be using this key immediately, so we don't need a long window. 79 | // Just make sure it's not about to expire. 80 | CurrentTime: time.Now().Add(30 * time.Second), 81 | }); err != nil { 82 | return nil, nil, nil, fmt.Errorf("stored cert no longer valid: %w", err) 83 | } 84 | } 85 | 86 | return privateKey, resp.Cert, resp.Chain, nil 87 | } 88 | 89 | func (c *Client) StoreCert(_ context.Context, priv crypto.PrivateKey, cert, chain []byte) error { 90 | id, err := id() 91 | if err != nil { 92 | return fmt.Errorf("error getting credential ID: %w", err) 93 | } 94 | privPEM, err := cryptoutils.MarshalPrivateKeyToPEM(priv) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | if err := c.Client.Call("Service.StoreCredential", api.StoreCredentialRequest{ 100 | ID: id, 101 | Credential: &api.Credential{ 102 | PrivateKey: privPEM, 103 | Cert: cert, 104 | Chain: chain, 105 | }, 106 | }, new(api.Credential)); err != nil { 107 | return err 108 | } 109 | 110 | return err 111 | } 112 | 113 | func id() (string, error) { 114 | // Prefix host name in case cache socket is being shared over a SSH session. 115 | host, err := os.Hostname() 116 | if err != nil { 117 | return "", fmt.Errorf("error getting hostname: %w", err) 118 | } 119 | wd, err := os.Getwd() 120 | if err != nil { 121 | return "", fmt.Errorf("error getting working directory: %w", err) 122 | } 123 | return fmt.Sprintf("%s@%s", host, wd), nil 124 | } 125 | -------------------------------------------------------------------------------- /internal/cache/service/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "time" 22 | 23 | "github.com/patrickmn/go-cache" 24 | "github.com/sigstore/gitsign/internal/cache/api" 25 | "github.com/sigstore/gitsign/internal/fulcio" 26 | "github.com/sigstore/sigstore/pkg/cryptoutils" 27 | ) 28 | 29 | type Service struct { 30 | store *cache.Cache 31 | } 32 | 33 | const ( 34 | defaultExpiration = 10 * time.Minute 35 | cleanupInterval = 1 * time.Minute 36 | ) 37 | 38 | func NewService() *Service { 39 | s := &Service{ 40 | store: cache.New(defaultExpiration, cleanupInterval), 41 | } 42 | return s 43 | } 44 | 45 | func (s *Service) StoreCredential(req api.StoreCredentialRequest, resp *api.Credential) error { 46 | fmt.Println("Store", req.ID) 47 | if err := s.store.Add(req.ID, req.Credential, 10*time.Minute); err != nil { 48 | return err 49 | } 50 | *resp = *req.Credential 51 | return nil 52 | } 53 | 54 | func (s *Service) GetCredential(req api.GetCredentialRequest, resp *api.Credential) error { 55 | ctx := context.Background() 56 | fmt.Println("Get", req.ID) 57 | i, ok := s.store.Get(req.ID) 58 | if ok { 59 | fmt.Println("gitsign-credential-cache: found credential!") 60 | cred, ok := i.(*api.Credential) 61 | if !ok { 62 | return fmt.Errorf("unknown credential type %T", i) 63 | } 64 | *resp = *cred 65 | return nil 66 | } 67 | 68 | if req.Config == nil { 69 | // No config set, nothing to do. 70 | return fmt.Errorf("%q not found", req.ID) 71 | } 72 | 73 | // If nothing is in the cache, fallback to interactive flow. 74 | fmt.Println("gitsign-credential-cache: no cached credential found, falling back to interactive flow...") 75 | idf := fulcio.NewIdentityFactory(os.Stdin, os.Stdout) 76 | id, err := idf.NewIdentity(ctx, req.Config) 77 | if err != nil { 78 | return fmt.Errorf("error getting new identity: %w", err) 79 | } 80 | privPEM, err := cryptoutils.MarshalPrivateKeyToPEM(id.PrivateKey) 81 | if err != nil { 82 | return err 83 | } 84 | cred := &api.Credential{ 85 | PrivateKey: privPEM, 86 | Cert: id.CertPEM, 87 | Chain: id.ChainPEM, 88 | } 89 | if err := s.store.Add(req.ID, cred, 10*time.Minute); err != nil { 90 | // We still generated the credential just fine, so only log the error. 91 | fmt.Printf("error storing credential: %v\n", err) 92 | } 93 | *resp = *cred 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/cert/verify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cert 16 | 17 | import ( 18 | "crypto/x509" 19 | 20 | "github.com/sigstore/cosign/v2/pkg/cosign" 21 | ) 22 | 23 | // Verifier verifies a given cert for a set of claims. 24 | type Verifier interface { 25 | Verify(cert *x509.Certificate) error 26 | } 27 | 28 | // CosignVerifier borrows its certificate verification logic from cosign. 29 | type CosignVerifier struct { 30 | opts *cosign.CheckOpts 31 | } 32 | 33 | func NewCosignVerifier(opts *cosign.CheckOpts) *CosignVerifier { 34 | return &CosignVerifier{opts: opts} 35 | } 36 | 37 | func (v *CosignVerifier) Verify(cert *x509.Certificate) error { 38 | _, err := cosign.ValidateAndUnpackCert(cert, v.opts) 39 | return err 40 | } 41 | -------------------------------------------------------------------------------- /internal/commands/attest/README.md: -------------------------------------------------------------------------------- 1 | # gitsign-attest 2 | 3 | NOTE: This is an **experimental demo**. This will be added as a subcommand to 4 | gitsign if/when we decide to support this. 5 | 6 | `gitsign-attest` will add attestations to the latest commit SHA in your Git 7 | working directory (if using a dirty workspace, the last commit is used). Data is 8 | stored as a commit under `refs/attestations/commits` or 9 | `refs/attestations/trees` (depending what you're attesting to), separate from 10 | the primary source tree. This means that the original commit is **unmodified**. 11 | Within this commit, there contains a folder for each commit SHA attested to. 12 | 13 | gitsign-attest will store the following: 14 | 15 | - the raw data given by the user 16 | - a signed DSSE message attesting to the file 17 | 18 | For now, only public sigstore is supported. 19 | 20 | ## Usage 21 | 22 | ### Commit attestations 23 | 24 | Commit attestations signs and attaches the given attestation file to the latest 25 | commit. Data is stored in `refs/attestations/commits` 26 | 27 | ```sh 28 | $ git log 29 | f44de7a (HEAD -> main) commit 30 | 2b0ff1e commit 1 31 | 760568f initial commit 32 | $ gitsign-attest -f test.json 33 | $ gitsign-attest -f spdx.sbom --type spdx 34 | $ git checkout refs/attestations/commits 35 | $ tree 36 | . 37 | └── f44de7aee552f119f94d70137b3bebb93f6bca5d 38 | ├── sbom.spdx 39 | ├── sbom.spdx.sig 40 | ├── test.json 41 | └── test.json.sig 42 | ``` 43 | 44 | ### Tree attestations 45 | 46 | Tree attestations signs and attaches the given attestation file to the latest 47 | commit. Data is stored in `refs/attestations/trees`. This can be used to sign 48 | directory content regardless of the commit they came from. This can be useful to 49 | preserve attestations for squash commits, or between sub-directories. 50 | 51 | ```sh 52 | $ git log --oneline --format="Commit: %h Tree: %t" -1 53 | Commit: edd19d9 Tree: 853a6ca 54 | $ gitsign-attest -f test.json --objtype tree 55 | $ git checkout refs/attestations/trees 56 | $ tree . 57 | . 58 | ├── 853a6ca8dd0e1fb84d67c397f6d8daac5926176c 59 | │   ├── test.json 60 | │   └── test.json.sig 61 | ``` 62 | -------------------------------------------------------------------------------- /internal/commands/attest/attest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package attest 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/go-git/go-git/v5" 22 | cosignopts "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" 23 | "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" 24 | "github.com/sigstore/cosign/v2/pkg/cosign" 25 | "github.com/sigstore/gitsign/internal/attest" 26 | "github.com/sigstore/gitsign/internal/config" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | const ( 31 | attCommitRef = "refs/attestations/commits" 32 | attTreeRef = "refs/attestations/trees" 33 | 34 | FlagObjectTypeCommit = "commit" 35 | FlagObjectTypeTree = "tree" 36 | ) 37 | 38 | type options struct { 39 | Config *config.Config 40 | 41 | FlagObjectType string 42 | FlagPath string 43 | FlagAttestationType string 44 | } 45 | 46 | func (o *options) AddFlags(cmd *cobra.Command) { 47 | cmd.Flags().StringVar(&o.FlagObjectType, "objtype", FlagObjectTypeCommit, "[commit | tree] - Git object type to attest") 48 | cmd.Flags().StringVarP(&o.FlagPath, "filepath", "f", "", "attestation filepath") 49 | cmd.Flags().StringVar(&o.FlagAttestationType, "type", "", `specify a predicate type URI`) 50 | } 51 | 52 | func (o *options) Run(ctx context.Context) error { 53 | repo, err := git.PlainOpen(".") 54 | if err != nil { 55 | return fmt.Errorf("error opening repo: %w", err) 56 | } 57 | 58 | head, err := repo.Head() 59 | if err != nil { 60 | return fmt.Errorf("error getting repository head: %w", err) 61 | } 62 | 63 | // If we're attaching the attestation to a tree, resolve the tree SHA. 64 | sha := head.Hash() 65 | refName := attCommitRef 66 | digestType := attest.DigestTypeCommit 67 | if o.FlagObjectType == FlagObjectTypeTree { 68 | commit, err := repo.CommitObject(head.Hash()) 69 | if err != nil { 70 | return fmt.Errorf("error getting tree: %w", err) 71 | } 72 | sha = commit.TreeHash 73 | 74 | refName = attTreeRef 75 | digestType = attest.DigestTypeTree 76 | } 77 | 78 | sv, err := sign.SignerFromKeyOpts(ctx, "", "", cosignopts.KeyOpts{ 79 | FulcioURL: o.Config.Fulcio, 80 | RekorURL: o.Config.Rekor, 81 | OIDCIssuer: o.Config.Issuer, 82 | OIDCClientID: o.Config.ClientID, 83 | }) 84 | if err != nil { 85 | return fmt.Errorf("getting signer: %w", err) 86 | } 87 | defer sv.Close() 88 | 89 | attestor := attest.NewAttestor(repo, sv, cosign.TLogUploadInTotoAttestation, o.Config, digestType) 90 | 91 | out, err := attestor.WriteFile(ctx, refName, sha, o.FlagPath, o.FlagAttestationType) 92 | if err != nil { 93 | return err 94 | } 95 | fmt.Println(out) 96 | 97 | return nil 98 | } 99 | 100 | func New(cfg *config.Config) *cobra.Command { 101 | o := &options{ 102 | Config: cfg, 103 | } 104 | cmd := &cobra.Command{ 105 | Use: "attest", 106 | Short: "add attestations to Git objects", 107 | Args: cobra.ArbitraryArgs, 108 | RunE: func(_ *cobra.Command, _ []string) error { 109 | ctx := context.Background() 110 | return o.Run(ctx) 111 | }, 112 | } 113 | o.AddFlags(cmd) 114 | 115 | return cmd 116 | } 117 | -------------------------------------------------------------------------------- /internal/commands/initialize/initialize.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | // Package initialize inits the TUF root for the tool. 17 | // This is intended to replicate the behavior of `gitsign initialize`. 18 | package initialize 19 | 20 | import ( 21 | "github.com/sigstore/cosign/v2/cmd/cosign/cli/initialize" 22 | "github.com/sigstore/sigstore/pkg/tuf" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | type options struct { 27 | Mirror string 28 | Root string 29 | } 30 | 31 | // AddFlags implements Interface 32 | func (o *options) AddFlags(cmd *cobra.Command) { 33 | cmd.Flags().StringVar(&o.Mirror, "mirror", tuf.DefaultRemoteRoot, 34 | "GCS bucket to a Sigstore TUF repository, or HTTP(S) base URL, or file:/// for local filestore remote (air-gap)") 35 | 36 | cmd.Flags().StringVar(&o.Root, "root", "", 37 | "path to trusted initial root. defaults to embedded root") 38 | _ = cmd.Flags().SetAnnotation("root", cobra.BashCompSubdirsInDir, []string{}) 39 | } 40 | 41 | func New() *cobra.Command { 42 | o := &options{} 43 | 44 | cmd := &cobra.Command{ 45 | Use: "initialize", 46 | Short: "Initializes Sigstore root to retrieve trusted certificate and key targets for verification.", 47 | Long: `Initializes Sigstore root to retrieve trusted certificate and key targets for verification. 48 | 49 | The following options are used by default: 50 | - The current trusted Sigstore TUF root is embedded inside gitsign at the time of release. 51 | - Sigstore remote TUF repository is pulled from the CDN mirror at tuf-repo-cdn.sigstore.dev. 52 | 53 | To provide an out-of-band trusted initial root.json, use the -root flag with a file or URL reference. 54 | This will enable you to point gitsign to a separate TUF root. 55 | 56 | Any updated TUF repository will be written to $HOME/.sigstore/root/. 57 | 58 | Trusted keys and certificate used in gitsign verification (e.g. verifying Fulcio issued certificates 59 | with Fulcio root CA) are pulled form the trusted metadata.`, 60 | Example: `gitsign initialize -mirror -out 61 | 62 | # initialize root with distributed root keys, default mirror, and default out path. 63 | gitsign initialize 64 | 65 | # initialize with an out-of-band root key file, using the default mirror. 66 | gitsign initialize -root 67 | 68 | # initialize with an out-of-band root key file and custom repository mirror. 69 | gitsign initialize -mirror -root `, 70 | RunE: func(cmd *cobra.Command, _ []string) error { 71 | return initialize.DoInitialize(cmd.Context(), o.Root, o.Mirror) 72 | }, 73 | } 74 | 75 | o.AddFlags(cmd) 76 | return cmd 77 | } 78 | -------------------------------------------------------------------------------- /internal/commands/root/root.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package root 17 | 18 | import ( 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/sigstore/gitsign/internal/commands/attest" 22 | "github.com/sigstore/gitsign/internal/commands/initialize" 23 | "github.com/sigstore/gitsign/internal/commands/show" 24 | "github.com/sigstore/gitsign/internal/commands/verify" 25 | verifytag "github.com/sigstore/gitsign/internal/commands/verify-tag" 26 | "github.com/sigstore/gitsign/internal/commands/version" 27 | "github.com/sigstore/gitsign/internal/config" 28 | "github.com/sigstore/gitsign/internal/io" 29 | ) 30 | 31 | type options struct { 32 | Config *config.Config 33 | 34 | FlagSign bool 35 | FlagVerify bool 36 | FlagVersion bool 37 | 38 | FlagLocalUser string 39 | FlagDetachedSignature bool 40 | FlagArmor bool 41 | FlagStatusFD int 42 | FlagIncludeCerts int 43 | } 44 | 45 | func (o *options) AddFlags(cmd *cobra.Command) { 46 | cmd.Flags().BoolVarP(&o.FlagSign, "sign", "s", false, "make a signature") 47 | cmd.Flags().BoolVarP(&o.FlagVerify, "verify", "v", false, "verify a signature") 48 | cmd.Flags().BoolVar(&o.FlagVersion, "version", false, "print Gitsign version") 49 | 50 | cmd.Flags().StringVarP(&o.FlagLocalUser, "local-user", "u", "", "use USER-ID to sign") 51 | cmd.Flags().BoolVarP(&o.FlagDetachedSignature, "detached-sign", "", false, "make a detached signature") 52 | cmd.Flags().BoolVarP(&o.FlagDetachedSignature, "detach-sign", "b", false, "make a detached signature") 53 | cmd.Flags().BoolVarP(&o.FlagArmor, "armor", "a", false, "create ascii armored output") 54 | cmd.Flags().IntVar(&o.FlagStatusFD, "status-fd", -1, "write special status strings to the file descriptor n.") 55 | cmd.Flags().IntVar(&o.FlagIncludeCerts, "include-certs", -2, "-3 is the same as -2, but omits issuer when cert has Authority Information Access extension. -2 includes all certs except root. -1 includes all certs. 0 includes no certs. 1 includes leaf cert. >1 includes n from the leaf. Default -2.") 56 | 57 | cmd.Flags().MarkDeprecated("detached-sign", "--detached-sign has been deprecated in favor of --detach-sign to match the interface of other signing tools") //nolint:errcheck 58 | } 59 | 60 | func New(cfg *config.Config) *cobra.Command { 61 | o := &options{Config: cfg} 62 | 63 | rootCmd := &cobra.Command{ 64 | Use: "gitsign", 65 | Short: "Keyless Git signing with Sigstore!", 66 | Args: cobra.ArbitraryArgs, 67 | DisableAutoGenTag: true, 68 | RunE: func(cmd *cobra.Command, args []string) error { 69 | s := io.New(o.Config.LogPath) 70 | defer s.Close() 71 | return s.Wrap(func() error { 72 | switch { 73 | case o.FlagVersion: 74 | // Alias root --version with version subcommand 75 | for _, item := range cmd.Commands() { 76 | if item.Name() == "version" { 77 | return item.RunE(item, cmd.Flags().Args()) 78 | } 79 | } 80 | case o.FlagSign, o.FlagDetachedSignature: 81 | return commandSign(o, s, args...) 82 | case o.FlagVerify: 83 | return commandVerify(o, s, args...) 84 | default: 85 | return cmd.Help() 86 | } 87 | return nil 88 | }) 89 | }, 90 | } 91 | 92 | rootCmd.AddCommand(version.New(cfg)) 93 | rootCmd.AddCommand(show.New(cfg)) 94 | rootCmd.AddCommand(attest.New(cfg)) 95 | rootCmd.AddCommand(verify.New(cfg)) 96 | rootCmd.AddCommand(verifytag.New(cfg)) 97 | rootCmd.AddCommand(initialize.New()) 98 | o.AddFlags(rootCmd) 99 | 100 | return rootCmd 101 | } 102 | -------------------------------------------------------------------------------- /internal/commands/root/sign.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package root 17 | 18 | import ( 19 | "bytes" 20 | "context" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "os" 25 | 26 | "github.com/sigstore/gitsign/internal/fulcio" 27 | "github.com/sigstore/gitsign/internal/git" 28 | "github.com/sigstore/gitsign/internal/gpg" 29 | gsio "github.com/sigstore/gitsign/internal/io" 30 | "github.com/sigstore/gitsign/internal/rekor" 31 | "github.com/sigstore/gitsign/internal/signature" 32 | ) 33 | 34 | // commandSign implements gitsign commit signing. 35 | // This is implemented as a root command so that user can specify the 36 | // gitsign binary directly in their gitconfigs. 37 | func commandSign(o *options, s *gsio.Streams, args ...string) error { 38 | ctx := context.Background() 39 | 40 | // Flag validation 41 | if o.FlagVerify { 42 | return errors.New("specify --help, --sign, or --verify") 43 | } 44 | 45 | userIdent, err := fulcio.NewIdentity(ctx, o.Config, s.TTYIn, s.TTYOut) 46 | if err != nil { 47 | return fmt.Errorf("failed to get identity: %w", err) 48 | } 49 | 50 | // Git is looking for "\n[GNUPG:] SIG_CREATED ", meaning we need to print a 51 | // line before SIG_CREATED. BEGIN_SIGNING seems appropriate. GPG emits this, 52 | // though GPGSM does not. 53 | gpgout := gpg.NewStatusWriterFromFD(uintptr(o.FlagStatusFD)) 54 | gpgout.Emit(gpg.StatusBeginSigning) 55 | 56 | var f io.Reader 57 | if len(args) == 1 { 58 | f2, err := os.Open(args[0]) 59 | if err != nil { 60 | return fmt.Errorf("failed to open message file (%s): %w", args[0], err) 61 | } 62 | defer f2.Close() 63 | f = f2 64 | } else { 65 | f = s.In 66 | } 67 | 68 | dataBuf := new(bytes.Buffer) 69 | if _, err = io.Copy(dataBuf, f); err != nil { 70 | return fmt.Errorf("failed to read message from stdin: %w", err) 71 | } 72 | 73 | rekor, err := rekor.NewClientContext(ctx, o.Config.Rekor) 74 | if err != nil { 75 | return fmt.Errorf("failed to create rekor client: %w", err) 76 | } 77 | 78 | opts := signature.SignOptions{ 79 | Detached: o.FlagDetachedSignature, 80 | TimestampAuthority: o.Config.TimestampURL, 81 | Armor: o.FlagArmor, 82 | IncludeCerts: o.FlagIncludeCerts, 83 | } 84 | if o.Config.MatchCommitter { 85 | opts.UserName = o.Config.CommitterName 86 | opts.UserEmail = o.Config.CommitterEmail 87 | } 88 | 89 | var fn git.SignFunc = git.LegacySHASign 90 | if o.Config.RekorMode == "offline" { 91 | fn = git.Sign 92 | } 93 | resp, err := fn(ctx, rekor, userIdent, dataBuf.Bytes(), opts) 94 | if err != nil { 95 | return fmt.Errorf("failed to sign message: %w", err) 96 | } 97 | 98 | if tlog := resp.LogEntry; tlog != nil && tlog.LogIndex != nil { 99 | fmt.Fprintf(s.TTYOut, "tlog entry created with index: %d\n", *tlog.LogIndex) 100 | } 101 | 102 | gpgout.EmitSigCreated(resp.Cert, o.FlagDetachedSignature) 103 | 104 | if _, err := s.Out.Write(resp.Signature); err != nil { 105 | return errors.New("failed to write signature") 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/commands/root/verify.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package root 17 | 18 | import ( 19 | "bytes" 20 | "context" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "os" 25 | 26 | "github.com/sigstore/gitsign/internal/commands/verify" 27 | "github.com/sigstore/gitsign/internal/gitsign" 28 | "github.com/sigstore/gitsign/internal/gpg" 29 | gsio "github.com/sigstore/gitsign/internal/io" 30 | ) 31 | 32 | // commandSign implements gitsign commit verification. 33 | // This is implemented as a root command so that user can specify the 34 | // gitsign binary directly in their gitconfigs. 35 | func commandVerify(o *options, s *gsio.Streams, args ...string) error { 36 | ctx := context.Background() 37 | 38 | // Flag validation 39 | if o.FlagSign { 40 | return errors.New("specify --help, --sign, or --verify") 41 | } 42 | if o.FlagDetachedSignature { 43 | return errors.New("detach-sign cannot be specified for verification") 44 | } 45 | if o.FlagArmor { 46 | return errors.New("armor cannot be specified for verification") 47 | } 48 | 49 | gpgout := gpg.NewStatusWriterFromFD(uintptr(o.FlagStatusFD)) 50 | gpgout.Emit(gpg.StatusNewSig) 51 | 52 | var ( 53 | data, sig []byte 54 | err error 55 | ) 56 | detached := len(args) >= 2 57 | if detached { 58 | data, sig, err = readDetached(s, args...) 59 | } else { 60 | sig, err = readAttached(s, args...) 61 | } 62 | if err != nil { 63 | return fmt.Errorf("failed to read signature data (detached: %T): %w", detached, err) 64 | } 65 | 66 | v, err := gitsign.NewVerifierWithCosignOpts(ctx, o.Config, nil) 67 | if err != nil { 68 | return err 69 | } 70 | summary, err := v.Verify(ctx, data, sig, true) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | if err != nil { 76 | if summary != nil && summary.Cert != nil { 77 | gpgout.EmitBadSig(summary.Cert) 78 | } else { 79 | // TODO: We're omitting a bunch of arguments here. 80 | gpgout.Emit(gpg.StatusErrSig) 81 | } 82 | return fmt.Errorf("failed to verify signature: %w", err) 83 | } 84 | 85 | verify.PrintSummary(s.Err, summary) 86 | fmt.Fprintln(s.Err, "WARNING: git verify-commit does not verify cert claims. Prefer using `gitsign verify` instead.") 87 | 88 | gpgout.EmitGoodSig(summary.Cert) 89 | gpgout.EmitTrustFully() 90 | 91 | return nil 92 | } 93 | 94 | func readAttached(s *gsio.Streams, args ...string) ([]byte, error) { 95 | var ( 96 | f io.Reader 97 | err error 98 | ) 99 | 100 | // Read in signature 101 | if len(args) == 1 { 102 | f2, err := os.Open(args[0]) 103 | if err != nil { 104 | return nil, fmt.Errorf("failed to open signature file (%s): %w", args[0], err) 105 | } 106 | defer f2.Close() 107 | f = f2 108 | } else { 109 | f = s.In 110 | } 111 | 112 | sig := new(bytes.Buffer) 113 | if _, err = io.Copy(sig, f); err != nil { 114 | return nil, fmt.Errorf("failed to read signature: %w", err) 115 | } 116 | 117 | return sig.Bytes(), nil 118 | } 119 | 120 | func readDetached(s *gsio.Streams, args ...string) ([]byte, []byte, error) { 121 | // Read in signature 122 | sigFile, err := os.Open(args[0]) 123 | if err != nil { 124 | return nil, nil, fmt.Errorf("failed to open signature file (%s): %w", args[0], err) 125 | } 126 | defer sigFile.Close() 127 | sig := new(bytes.Buffer) 128 | if _, err = io.Copy(sig, sigFile); err != nil { 129 | return nil, nil, fmt.Errorf("failed to read signature file: %w", err) 130 | } 131 | 132 | var dataFile io.Reader 133 | // Read in signed data 134 | if args[1] == "-" { 135 | dataFile = s.In 136 | } else { 137 | f2, err := os.Open(args[1]) 138 | if err != nil { 139 | return nil, nil, fmt.Errorf("failed to open message file (%s): %w", args[1], err) 140 | } 141 | defer f2.Close() 142 | dataFile = f2 143 | } 144 | buf := new(bytes.Buffer) 145 | if _, err = io.Copy(buf, dataFile); err != nil { 146 | return nil, nil, fmt.Errorf("failed to read message file: %w", err) 147 | } 148 | 149 | return buf.Bytes(), sig.Bytes(), nil 150 | } 151 | -------------------------------------------------------------------------------- /internal/commands/show/show_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package show 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "os" 21 | "testing" 22 | 23 | "github.com/go-git/go-git/v5" 24 | "github.com/go-git/go-git/v5/config" 25 | "github.com/go-git/go-git/v5/plumbing" 26 | "github.com/go-git/go-git/v5/storage/memory" 27 | "github.com/google/go-cmp/cmp" 28 | "github.com/in-toto/in-toto-golang/in_toto" 29 | "github.com/sigstore/gitsign/pkg/predicate" 30 | ) 31 | 32 | func TestShow(t *testing.T) { 33 | storage := memory.NewStorage() 34 | repo := &git.Repository{ 35 | Storer: storage, 36 | } 37 | if err := repo.SetConfig(&config.Config{ 38 | Remotes: map[string]*config.RemoteConfig{ 39 | "origin": { 40 | Name: "origin", 41 | URLs: []string{"git@github.com:wlynch/gitsign.git"}, 42 | }, 43 | }, 44 | }); err != nil { 45 | t.Fatalf("error setting git config: %v", err) 46 | } 47 | 48 | // Expect files in testdata directory: 49 | // foo.in.txt -> foo.out.json 50 | // IMPORTANT: When generating new test files, use a command like `git cat-file commit main > foo.in.txt`. 51 | // If you try and copy/paste the content, you may get burned by file encodings and missing \r characters. 52 | for _, tc := range []string{ 53 | "fulcio-cert", 54 | "gpg", 55 | } { 56 | t.Run(tc, func(t *testing.T) { 57 | raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.in.txt", tc)) 58 | if err != nil { 59 | t.Fatalf("error reading input: %v", err) 60 | } 61 | obj := storage.NewEncodedObject() 62 | obj.SetType(plumbing.CommitObject) 63 | w, err := obj.Writer() 64 | if err != nil { 65 | t.Fatalf("error getting git object writer: %v", err) 66 | } 67 | _, err = w.Write(raw) 68 | if err != nil { 69 | t.Fatalf("error writing git commit: %v", err) 70 | } 71 | h, err := storage.SetEncodedObject(obj) 72 | if err != nil { 73 | t.Fatalf("error storing git commit: %v", err) 74 | } 75 | 76 | got, err := statement(repo, "origin", h.String()) 77 | if err != nil { 78 | t.Fatalf("statement(): %v", err) 79 | } 80 | 81 | wantRaw, err := os.ReadFile(fmt.Sprintf("testdata/%s.out.json", tc)) 82 | if err != nil { 83 | t.Fatalf("error reading want json: %v", err) 84 | } 85 | want := &in_toto.Statement{ 86 | Predicate: &predicate.GitCommit{}, 87 | } 88 | if err := json.Unmarshal(wantRaw, want); err != nil { 89 | t.Fatalf("error decoding want json: %v", err) 90 | } 91 | 92 | if diff := cmp.Diff(want, got); diff != "" { 93 | t.Error(diff) 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/commands/show/testdata/fulcio-cert.in.txt: -------------------------------------------------------------------------------- 1 | tree 194fca354a2439028e347ce5e19e4db45bd708a6 2 | parent 2eaf8fc6d66505baa90640d018e1131cd8e99334 3 | author Billy Lynch 1668460399 -0500 4 | committer Billy Lynch 1668460399 -0500 5 | gpgsig -----BEGIN SIGNED MESSAGE----- 6 | MIIEAwYJKoZIhvcNAQcCoIID9DCCA/ACAQExDTALBglghkgBZQMEAgEwCwYJKoZI 7 | hvcNAQcBoIICpDCCAqAwggImoAMCAQICFFTzLmXKAlKX5xTUaYoUE5giCxZvMAoG 8 | CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln 9 | c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIyMTExNDIxMTMyM1oXDTIyMTExNDIxMjMy 10 | M1owADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH++UCDlF9MaQCSgDKQ0bWhD 11 | eOmTrk1sEHw9Oel1eCyrr3SFhDAghcO3VwO7baYmL16fUwRYwMhj5urowsLVrjKj 12 | ggFFMIIBQTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD 13 | VR0OBBYEFHMPDOs6IDY/iRnVqacIj/yvJbNpMB8GA1UdIwQYMBaAFN/T6c9WJBGW 14 | +ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2 15 | MCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYK 16 | KwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAv 17 | Ke6OAAABhHf9WZAAAAQDAEcwRQIgV8anMDEjbHI/WvGxpJmm44DgBTYf5bkfBJIP 18 | 6FJtqXYCIQD/noLzthDKgjrXoiep/BqqnygoTRM9HKim+DRMbwHteDAKBggqhkjO 19 | PQQDAwNoADBlAjEAvHvqOAKT34QQx9PSuOswQfquByALdzA1ES0nx4M5i47kqNeE 20 | Bl612/hYTD1ydpLIAjBTWiHDtdxM9rriTIyGGJubC0+vNcccsURDTJ+A3XnMAER3 21 | ikl/cJ2wG9c8ZN7AUS8xggElMIIBIQIBATBPMDcxFTATBgNVBAoTDHNpZ3N0b3Jl 22 | LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlAhRU8y5lygJSl+cU 23 | 1GmKFBOYIgsWbzALBglghkgBZQMEAgGgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN 24 | AQcBMBwGCSqGSIb3DQEJBTEPFw0yMjExMTQyMTEzMjNaMC8GCSqGSIb3DQEJBDEi 25 | BCC9Yk93XCRKy6FPCb8dAqjdWpjb1NIbFtTo9CP6yYOZQjAKBggqhkjOPQQDAgRH 26 | MEUCIQCq+2Zs0bBcAAciePeeRpzmfVJ2gEu7sGngy+TcYpS0ugIgL9Qix3V8taBV 27 | +Tb6rMZmt80sfGsYhUqE8KsIF1AEc+8= 28 | -----END SIGNED MESSAGE----- 29 | 30 | add sample 31 | -------------------------------------------------------------------------------- /internal/commands/show/testdata/fulcio-cert.out.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "https://in-toto.io/Statement/v0.1", 3 | "predicateType": "https://gitsign.sigstore.dev/predicate/git/v0.1", 4 | "subject": [ 5 | { 6 | "name": "git@github.com:wlynch/gitsign.git", 7 | "digest": { 8 | "sha1": "10a3086104c5331623be85a5e30d709f457b536b" 9 | } 10 | } 11 | ], 12 | "predicate": { 13 | "source": { 14 | "tree": "194fca354a2439028e347ce5e19e4db45bd708a6", 15 | "parents": [ 16 | "2eaf8fc6d66505baa90640d018e1131cd8e99334" 17 | ], 18 | "author": { 19 | "name": "Billy Lynch", 20 | "email": "billy@chainguard.dev", 21 | "date": "2022-11-14T16:13:19-05:00" 22 | }, 23 | "committer": { 24 | "name": "Billy Lynch", 25 | "email": "billy@chainguard.dev", 26 | "date": "2022-11-14T16:13:19-05:00" 27 | }, 28 | "message": "add sample\n" 29 | }, 30 | "signature": "-----BEGIN SIGNED MESSAGE-----\nMIIEAwYJKoZIhvcNAQcCoIID9DCCA/ACAQExDTALBglghkgBZQMEAgEwCwYJKoZI\nhvcNAQcBoIICpDCCAqAwggImoAMCAQICFFTzLmXKAlKX5xTUaYoUE5giCxZvMAoG\nCCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln\nc3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIyMTExNDIxMTMyM1oXDTIyMTExNDIxMjMy\nM1owADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH++UCDlF9MaQCSgDKQ0bWhD\neOmTrk1sEHw9Oel1eCyrr3SFhDAghcO3VwO7baYmL16fUwRYwMhj5urowsLVrjKj\nggFFMIIBQTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD\nVR0OBBYEFHMPDOs6IDY/iRnVqacIj/yvJbNpMB8GA1UdIwQYMBaAFN/T6c9WJBGW\n+ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2\nMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYK\nKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAv\nKe6OAAABhHf9WZAAAAQDAEcwRQIgV8anMDEjbHI/WvGxpJmm44DgBTYf5bkfBJIP\n6FJtqXYCIQD/noLzthDKgjrXoiep/BqqnygoTRM9HKim+DRMbwHteDAKBggqhkjO\nPQQDAwNoADBlAjEAvHvqOAKT34QQx9PSuOswQfquByALdzA1ES0nx4M5i47kqNeE\nBl612/hYTD1ydpLIAjBTWiHDtdxM9rriTIyGGJubC0+vNcccsURDTJ+A3XnMAER3\nikl/cJ2wG9c8ZN7AUS8xggElMIIBIQIBATBPMDcxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlAhRU8y5lygJSl+cU\n1GmKFBOYIgsWbzALBglghkgBZQMEAgGgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN\nAQcBMBwGCSqGSIb3DQEJBTEPFw0yMjExMTQyMTEzMjNaMC8GCSqGSIb3DQEJBDEi\nBCC9Yk93XCRKy6FPCb8dAqjdWpjb1NIbFtTo9CP6yYOZQjAKBggqhkjOPQQDAgRH\nMEUCIQCq+2Zs0bBcAAciePeeRpzmfVJ2gEu7sGngy+TcYpS0ugIgL9Qix3V8taBV\n+Tb6rMZmt80sfGsYhUqE8KsIF1AEc+8=\n-----END SIGNED MESSAGE-----\n", 31 | "signer_info": [ 32 | { 33 | "attributes": "MWkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjIxMTE0MjExMzIzWjAvBgkqhkiG9w0BCQQxIgQgvWJPd1wkSsuhTwm/HQKo3VqY29TSGxbU6PQj+smDmUI=", 34 | "certificate": "-----BEGIN CERTIFICATE-----\nMIICoDCCAiagAwIBAgIUVPMuZcoCUpfnFNRpihQTmCILFm8wCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjIxMTE0MjExMzIzWhcNMjIxMTE0MjEyMzIzWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEf75QIOUX0xpAJKAMpDRtaEN46ZOuTWwQfD05\n6XV4LKuvdIWEMCCFw7dXA7ttpiYvXp9TBFjAyGPm6ujCwtWuMqOCAUUwggFBMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUcw8M\n6zogNj+JGdWppwiP/K8ls2kwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wIgYDVR0RAQH/BBgwFoEUYmlsbHlAY2hhaW5ndWFyZC5kZXYwKQYKKwYBBAGD\nvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGKBgorBgEEAdZ5AgQC\nBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGEd/1Z\nkAAABAMARzBFAiBXxqcwMSNscj9a8bGkmabjgOAFNh/luR8Ekg/oUm2pdgIhAP+e\ngvO2EMqCOteiJ6n8GqqfKChNEz0cqKb4NExvAe14MAoGCCqGSM49BAMDA2gAMGUC\nMQC8e+o4ApPfhBDH09K46zBB+q4HIAt3MDURLSfHgzmLjuSo14QGXrXb+FhMPXJ2\nksgCMFNaIcO13Ez2uuJMjIYYm5sLT681xxyxRENMn4DdecwARHeKSX9wnbAb1zxk\n3sBRLw==\n-----END CERTIFICATE-----\n" 35 | } 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/commands/show/testdata/gpg.in.txt: -------------------------------------------------------------------------------- 1 | tree 6cd5cc5fbf2bac0aac027e411ec3072699e7028c 2 | parent 7bac7137354d99bd876d453e6dc98c6961975d93 3 | author Billy Lynch 1668000929 -0500 4 | committer GitHub 1668000929 -0500 5 | gpgsig -----BEGIN PGP SIGNATURE----- 6 | 7 | wsBcBAABCAAQBQJja6yhCRBK7hj4Ov3rIwAAYakIAEvzgwrtNu/GLTIgMXOPylQy 8 | Bo4FBGRnBshaA/nOVfgjinw8Cixrb9N9/YvkQU+ub/I24MEc+R6YsjbGDXqre/Ny 9 | YSakMBYLJeiPAP0y9GMBXoD8HEk3nl1Ae1BXpRMS9dzHzGOwESuv9BEo5D+RhpXw 10 | GeeoUFjK8/ISB7Qad5n61brQtinFYwP+3qu+14hwFMJPkQcIqtdHeXd0uFkO0/Th 11 | 1vellvL9yTSOspaWD9qG7s4x2ZRPwf9MjgRp1NTkGDfH9xpqcXWqJl5AorHZdSTk 12 | bzg98srLKvqcCTBj09WJbWDIu4e/pZc4lFEd3APCSU6kEIJigizXf2uEIfuPZpE= 13 | =/vdg 14 | -----END PGP SIGNATURE----- 15 | 16 | 17 | Refactor commands with Cobra. (#185) 18 | 19 | This PR rewrites the commands using Cobra so that it can be easier to 20 | add additional subcommands (i.e. gitsign cache, gitsign attest, etc.) 21 | 22 | This change doesn't add any new functionality, though it does refactor a 23 | good chunk of the config, flags, status printing, and other global state 24 | to make it more easily consumable by packages. 25 | 26 | Co-authored-by: Eddie Zaneski 27 | Signed-off-by: Billy Lynch 28 | 29 | Signed-off-by: Billy Lynch 30 | Co-authored-by: Eddie Zaneski -------------------------------------------------------------------------------- /internal/commands/show/testdata/gpg.out.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "https://in-toto.io/Statement/v0.1", 3 | "predicateType": "https://gitsign.sigstore.dev/predicate/git/v0.1", 4 | "subject": [ 5 | { 6 | "name": "git@github.com:wlynch/gitsign.git", 7 | "digest": { 8 | "sha1": "262c05491554c57ee641461f315bf4023d0e93c7" 9 | } 10 | } 11 | ], 12 | "predicate": { 13 | "source": { 14 | "tree": "6cd5cc5fbf2bac0aac027e411ec3072699e7028c", 15 | "parents": [ 16 | "7bac7137354d99bd876d453e6dc98c6961975d93" 17 | ], 18 | "author": { 19 | "name": "Billy Lynch", 20 | "email": "billy@chainguard.dev", 21 | "date": "2022-11-09T08:35:29-05:00" 22 | }, 23 | "committer": { 24 | "name": "GitHub", 25 | "email": "noreply@github.com", 26 | "date": "2022-11-09T08:35:29-05:00" 27 | }, 28 | "message": "Refactor commands with Cobra. (#185)\n\nThis PR rewrites the commands using Cobra so that it can be easier to\r\nadd additional subcommands (i.e. gitsign cache, gitsign attest, etc.)\r\n\r\nThis change doesn't add any new functionality, though it does refactor a\r\ngood chunk of the config, flags, status printing, and other global state\r\nto make it more easily consumable by packages.\r\n\r\nCo-authored-by: Eddie Zaneski \u003ceddiezane@chainguard.dev\u003e\r\nSigned-off-by: Billy Lynch \u003cbilly@chainguard.dev\u003e\r\n\r\nSigned-off-by: Billy Lynch \u003cbilly@chainguard.dev\u003e\r\nCo-authored-by: Eddie Zaneski \u003ceddiezane@chainguard.dev\u003e" 29 | }, 30 | "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJja6yhCRBK7hj4Ov3rIwAAYakIAEvzgwrtNu/GLTIgMXOPylQy\nBo4FBGRnBshaA/nOVfgjinw8Cixrb9N9/YvkQU+ub/I24MEc+R6YsjbGDXqre/Ny\nYSakMBYLJeiPAP0y9GMBXoD8HEk3nl1Ae1BXpRMS9dzHzGOwESuv9BEo5D+RhpXw\nGeeoUFjK8/ISB7Qad5n61brQtinFYwP+3qu+14hwFMJPkQcIqtdHeXd0uFkO0/Th\n1vellvL9yTSOspaWD9qG7s4x2ZRPwf9MjgRp1NTkGDfH9xpqcXWqJl5AorHZdSTk\nbzg98srLKvqcCTBj09WJbWDIu4e/pZc4lFEd3APCSU6kEIJigizXf2uEIfuPZpE=\n=/vdg\n-----END PGP SIGNATURE-----\n\n" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/commands/verify-tag/verify_tag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package verifytag 16 | 17 | import ( 18 | "context" 19 | "encoding/pem" 20 | "fmt" 21 | "io" 22 | "os" 23 | 24 | gogit "github.com/go-git/go-git/v5" 25 | "github.com/go-git/go-git/v5/plumbing" 26 | cosignopts "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" 27 | "github.com/sigstore/gitsign/internal/commands/verify" 28 | "github.com/sigstore/gitsign/internal/config" 29 | "github.com/sigstore/gitsign/internal/gitsign" 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | type options struct { 34 | Config *config.Config 35 | cosignopts.CertVerifyOptions 36 | } 37 | 38 | func (o *options) AddFlags(cmd *cobra.Command) { 39 | o.CertVerifyOptions.AddFlags(cmd) 40 | } 41 | 42 | func (o *options) Run(_ io.Writer, args []string) error { 43 | ctx := context.Background() 44 | repo, err := gogit.PlainOpenWithOptions(".", &gogit.PlainOpenOptions{ 45 | DetectDotGit: true, 46 | }) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if len(args) == 0 { 52 | return fmt.Errorf("tag reference is required") 53 | } 54 | tagRef := args[0] 55 | 56 | // Resolve the tag reference 57 | ref, err := repo.Reference(plumbing.ReferenceName(fmt.Sprintf("refs/tags/%s", tagRef)), true) 58 | if err != nil { 59 | return fmt.Errorf("error resolving tag reference: %w", err) 60 | } 61 | 62 | // Get the tag object 63 | tagObj, err := repo.TagObject(ref.Hash()) 64 | if err != nil { 65 | return fmt.Errorf("error reading tag object: %w", err) 66 | } 67 | 68 | // Extract the signature 69 | sig := []byte(tagObj.PGPSignature) 70 | p, _ := pem.Decode(sig) 71 | if p == nil || p.Type != "SIGNED MESSAGE" { 72 | return fmt.Errorf("unsupported signature type") 73 | } 74 | 75 | // Get the tag data without the signature 76 | tagData := new(plumbing.MemoryObject) 77 | if err := tagObj.EncodeWithoutSignature(tagData); err != nil { 78 | return err 79 | } 80 | r, err := tagData.Reader() 81 | if err != nil { 82 | return err 83 | } 84 | defer r.Close() 85 | data, err := io.ReadAll(r) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | // Verify the signature 91 | v, err := gitsign.NewVerifierWithCosignOpts(ctx, o.Config, &o.CertVerifyOptions) 92 | if err != nil { 93 | return err 94 | } 95 | summary, err := v.Verify(ctx, data, sig, true) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | // Import the internal package just for the PrintSummary function 101 | verify.PrintSummary(os.Stdout, summary) 102 | 103 | return nil 104 | } 105 | 106 | func New(cfg *config.Config) *cobra.Command { 107 | o := &options{Config: cfg} 108 | 109 | cmd := &cobra.Command{ 110 | Use: "verify-tag ", 111 | Args: cobra.ExactArgs(1), 112 | SilenceUsage: true, 113 | Short: "Verify a tag", 114 | Long: `Verify a tag. 115 | 116 | verify-tag verifies a tag against a set of certificate claims. 117 | This should generally be used over git verify-tag, since verify-tag will 118 | check the identity included in the signature's certificate.`, 119 | RunE: func(_ *cobra.Command, args []string) error { 120 | // Simulate unknown flag errors. 121 | if o.Cert != "" { 122 | return fmt.Errorf("unknown flag: --certificate") 123 | } 124 | if o.CertChain != "" { 125 | return fmt.Errorf("unknown flag: --certificate-chain") 126 | } 127 | 128 | return o.Run(os.Stdout, args) 129 | }, 130 | } 131 | o.AddFlags(cmd) 132 | 133 | // Hide flags we don't implement. 134 | // --certificate: The cert should always come from the tag. 135 | _ = cmd.Flags().MarkHidden("certificate") 136 | // --certificate-chain: We only support reading from a TUF root at the moment. 137 | // TODO: add support for this. 138 | _ = cmd.Flags().MarkHidden("certificate-chain") 139 | // --ca-intermediates and --ca-roots 140 | _ = cmd.Flags().MarkHidden("ca-intermediates") 141 | _ = cmd.Flags().MarkHidden("ca-roots") 142 | 143 | return cmd 144 | } 145 | -------------------------------------------------------------------------------- /internal/commands/verify/verify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package verify 16 | 17 | import ( 18 | "context" 19 | "encoding/pem" 20 | "fmt" 21 | "io" 22 | "os" 23 | 24 | gogit "github.com/go-git/go-git/v5" 25 | "github.com/go-git/go-git/v5/plumbing" 26 | cosignopts "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" 27 | "github.com/sigstore/cosign/v2/pkg/cosign" 28 | "github.com/sigstore/gitsign/internal" 29 | "github.com/sigstore/gitsign/internal/config" 30 | "github.com/sigstore/gitsign/internal/gitsign" 31 | "github.com/sigstore/gitsign/pkg/git" 32 | "github.com/sigstore/sigstore/pkg/cryptoutils" 33 | "github.com/spf13/cobra" 34 | ) 35 | 36 | type options struct { 37 | Config *config.Config 38 | cosignopts.CertVerifyOptions 39 | } 40 | 41 | func (o *options) AddFlags(cmd *cobra.Command) { 42 | o.CertVerifyOptions.AddFlags(cmd) 43 | } 44 | 45 | func (o *options) Run(_ io.Writer, args []string) error { 46 | ctx := context.Background() 47 | repo, err := gogit.PlainOpenWithOptions(".", &gogit.PlainOpenOptions{ 48 | DetectDotGit: true, 49 | }) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | revision := "HEAD" 55 | if len(args) > 0 { 56 | revision = args[0] 57 | } 58 | 59 | h, err := repo.ResolveRevision(plumbing.Revision(revision)) 60 | if err != nil { 61 | return fmt.Errorf("error resolving commit object: %w", err) 62 | } 63 | c, err := repo.CommitObject(*h) 64 | if err != nil { 65 | return fmt.Errorf("error reading commit object: %w", err) 66 | } 67 | 68 | sig := []byte(c.PGPSignature) 69 | p, _ := pem.Decode(sig) 70 | if p == nil || p.Type != "SIGNED MESSAGE" { 71 | return fmt.Errorf("unsupported signature type") 72 | } 73 | 74 | c2 := new(plumbing.MemoryObject) 75 | if err := c.EncodeWithoutSignature(c2); err != nil { 76 | return err 77 | } 78 | r, err := c2.Reader() 79 | if err != nil { 80 | return err 81 | } 82 | defer r.Close() 83 | data, err := io.ReadAll(r) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | v, err := gitsign.NewVerifierWithCosignOpts(ctx, o.Config, &o.CertVerifyOptions) 89 | if err != nil { 90 | return err 91 | } 92 | summary, err := v.Verify(ctx, data, sig, true) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | PrintSummary(os.Stdout, summary) 98 | 99 | return nil 100 | } 101 | 102 | func PrintSummary(w io.Writer, summary *git.VerificationSummary) { 103 | fpr := internal.CertHexFingerprint(summary.Cert) 104 | 105 | fmt.Fprintln(w, "tlog index:", *summary.LogEntry.LogIndex) 106 | fmt.Fprintf(w, "gitsign: Signature made using certificate ID 0x%s | %v\n", fpr, summary.Cert.Issuer) 107 | 108 | ce := cosign.CertExtensions{Cert: summary.Cert} 109 | fmt.Fprintf(w, "gitsign: Good signature from %v(%s)\n", cryptoutils.GetSubjectAlternateNames(summary.Cert), ce.GetIssuer()) 110 | 111 | for _, c := range summary.Claims { 112 | fmt.Fprintf(w, "%s: %t\n", string(c.Key), c.Value) 113 | } 114 | } 115 | 116 | func New(cfg *config.Config) *cobra.Command { 117 | o := &options{Config: cfg} 118 | 119 | cmd := &cobra.Command{ 120 | Use: "verify [commit]", 121 | Args: cobra.MaximumNArgs(1), 122 | SilenceUsage: true, 123 | Short: "Verify a commit", 124 | Long: `Verify a commit. 125 | 126 | verify verifies a commit against a set of certificate claims. 127 | This should generally be used over git verify-commit, since verify will 128 | check the identity included in the signature's certificate. 129 | 130 | If no revision is specified, HEAD is used.`, 131 | RunE: func(_ *cobra.Command, args []string) error { 132 | // Simulate unknown flag errors. 133 | if o.Cert != "" { 134 | return fmt.Errorf("unknown flag: --certificate") 135 | } 136 | if o.CertChain != "" { 137 | return fmt.Errorf("unknown flag: --certificate-chain") 138 | } 139 | 140 | return o.Run(os.Stdout, args) 141 | }, 142 | } 143 | o.AddFlags(cmd) 144 | 145 | // Hide flags we don't implement. 146 | // --certificate: The cert should always come from the commit. 147 | _ = cmd.Flags().MarkHidden("certificate") 148 | // --certificate-chain: We only support reading from a TUF root at the moment. 149 | // TODO: add support for this. 150 | _ = cmd.Flags().MarkHidden("certificate-chain") 151 | // --ca-intermediates and --ca-roots 152 | _ = cmd.Flags().MarkHidden("ca-intermediates") 153 | _ = cmd.Flags().MarkHidden("ca-roots") 154 | 155 | return cmd 156 | } 157 | -------------------------------------------------------------------------------- /internal/commands/version/version.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package version 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/sigstore/gitsign/internal/config" 24 | "github.com/sigstore/gitsign/pkg/version" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | func New(cfg *config.Config) *cobra.Command { 29 | cmd := &cobra.Command{ 30 | Use: "version", 31 | Short: "print Gitsign version", 32 | RunE: func(_ *cobra.Command, _ []string) error { 33 | v := version.GetVersionInfo() 34 | fmt.Println("gitsign version", v.GitVersion) 35 | if len(v.Env) > 0 { 36 | fmt.Println("env:") 37 | for _, e := range v.Env { 38 | fmt.Println("\t", e) 39 | } 40 | } 41 | fmt.Println("parsed config:") 42 | enc := json.NewEncoder(os.Stdout) 43 | enc.SetIndent("", " ") 44 | 45 | return enc.Encode(cfg) 46 | }, 47 | } 48 | return cmd 49 | } 50 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "io" 19 | "os" 20 | "testing" 21 | 22 | "github.com/go-git/go-billy/v5/memfs" 23 | "github.com/go-git/go-git/v5" 24 | "github.com/go-git/go-git/v5/config" 25 | format "github.com/go-git/go-git/v5/plumbing/format/config" 26 | "github.com/go-git/go-git/v5/storage/memory" 27 | "github.com/google/go-cmp/cmp" 28 | ) 29 | 30 | func TestGet(t *testing.T) { 31 | // Create in-memory repo for testing. 32 | repo, err := git.Init(memory.NewStorage(), memfs.New()) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | cfg := &format.Config{ 38 | Sections: format.Sections{ 39 | &format.Section{ 40 | Name: "gitsign", 41 | Options: format.Options{ 42 | // This will be ignored. 43 | &format.Option{ 44 | Key: "foo", 45 | Value: "bar", 46 | }, 47 | &format.Option{ 48 | Key: "fulcio", 49 | Value: "example.com", 50 | }, 51 | &format.Option{ 52 | Key: "rekor", 53 | Value: "example.com", 54 | }, 55 | }, 56 | }, 57 | }, 58 | } 59 | if err := repo.SetConfig(&config.Config{ 60 | Raw: cfg, 61 | }); err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | // This should take precedence over config value. 66 | t.Setenv("GITSIGN_REKOR_URL", "rekor.example.com") 67 | // This just overrides default value. 68 | t.Setenv("GITSIGN_OIDC_ISSUER", "tacocat") 69 | 70 | // Recognize SIGSTORE prefixes. 71 | t.Setenv("SIGSTORE_OIDC_REDIRECT_URL", "example.com") 72 | 73 | // GITSIGN prefix takes priority over SIGSTORE. 74 | t.Setenv("SIGSTORE_CONNECTOR_ID", "foo") 75 | t.Setenv("GITSIGN_CONNECTOR_ID", "bar") 76 | 77 | want := &Config{ 78 | // Default overridden by config 79 | Fulcio: "example.com", 80 | // Overridden by config, then by env var 81 | Rekor: "rekor.example.com", 82 | // Default value 83 | ClientID: "sigstore", 84 | // Overridden by env var 85 | Issuer: "tacocat", 86 | RedirectURL: "example.com", 87 | ConnectorID: "bar", 88 | RekorMode: "online", 89 | Autoclose: true, 90 | AutocloseTimeout: 6, 91 | } 92 | 93 | execFn = func() (io.Reader, error) { 94 | return os.Open("testdata/config.txt") 95 | } 96 | 97 | got, err := Get() 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | if diff := cmp.Diff(want, got, cmp.AllowUnexported(Config{})); diff != "" { 103 | t.Error(diff) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/config/testdata/config.txt: -------------------------------------------------------------------------------- 1 | foo bar 2 | gitsign.foo bar 3 | gitsign.foo.bar baz 4 | gitsign 5 | 6 | gitsign.connectorid https://accounts.google.com 7 | gitsign.FULCIO example.com 8 | gitsign.Rekor example.com -------------------------------------------------------------------------------- /internal/e2e/offline_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build e2e 16 | // +build e2e 17 | 18 | package e2e 19 | 20 | import ( 21 | "context" 22 | "crypto/x509" 23 | "testing" 24 | 25 | "github.com/sigstore/gitsign/internal/fulcio/fulcioroots" 26 | "github.com/sigstore/gitsign/internal/git/gittest" 27 | "github.com/sigstore/gitsign/pkg/git" 28 | "github.com/sigstore/gitsign/pkg/rekor" 29 | "github.com/sigstore/sigstore/pkg/tuf" 30 | ) 31 | 32 | func TestVerifyOffline(t *testing.T) { 33 | ctx := context.Background() 34 | 35 | // Initialize to prod root. 36 | tuf.Initialize(ctx, tuf.DefaultRemoteRoot, nil) 37 | root, intermediate, err := fulcioroots.New(x509.NewCertPool(), fulcioroots.FromTUF(ctx)) 38 | if err != nil { 39 | t.Fatalf("error getting certificate root: %v", err) 40 | } 41 | 42 | client, err := rekor.New("https://rekor.sigstore.dev") 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | commit := gittest.ParseCommit(t, "testdata/offline.commit") 48 | body := gittest.MarshalCommitBody(t, commit) 49 | sig := []byte(commit.PGPSignature) 50 | 51 | verifier, err := git.NewCertVerifier(git.WithRootPool(root), git.WithIntermediatePool(intermediate)) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | cert, err := verifier.Verify(ctx, body, sig, true) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | tlog, err := client.VerifyInclusion(ctx, sig, cert) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | t.Log(*tlog.LogIndex) 65 | } 66 | -------------------------------------------------------------------------------- /internal/e2e/testdata/offline.commit: -------------------------------------------------------------------------------- 1 | tree 045f683042529876c1fe28f97f416c1501ffa433 2 | parent 74f0d242677d95a477f953260a2686712846c619 3 | author Billy Lynch 1685981477 -0400 4 | committer Billy Lynch 1685981477 -0400 5 | gpgsig -----BEGIN SIGNED MESSAGE----- 6 | MIIHPQYJKoZIhvcNAQcCoIIHLjCCByoCAQExDTALBglghkgBZQMEAgEwCwYJKoZI 7 | hvcNAQcBoIIC0DCCAswwggJSoAMCAQICFAG6/a9VuUO5SlyJA02E3aCq+I7nMAoG 8 | CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln 9 | c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIzMDYwNTE2MTExOVoXDTIzMDYwNTE2MjEx 10 | OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKMghfIq5VOfOhMrxqHFiu8c 11 | 91EbkxLIDLL1kjdhbNS2EfXg7xH3Q789yMKGGWwh3PxMJ3+6tXYi62b2mWVYwOej 12 | ggFxMIIBbTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD 13 | VR0OBBYEFLNvhRMNCI6GVLzZjAKPQHfG+H62MB8GA1UdIwQYMBaAFN/T6c9WJBGW 14 | +ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2 15 | MCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgor 16 | BgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBiQYKKwYB 17 | BAHWeQIEAgR7BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6O 18 | AAABiIxTwqMAAAQDAEYwRAIgHi+WbGvSz/kRzolvZfTodQZWomPXXVzV617mdESS 19 | SzsCIG9lfwWn7Mh+m/EbdIIp7OxjA3av+ywcUDw1o/kSzegbMAoGCCqGSM49BAMD 20 | A2gAMGUCMQDVOD6f+899Tiy7l+wominmzOPvkB2AZBr0OQ0JeEFeNRPuyBHrKF1l 21 | UCupUEg9JsYCMCIR+qJqcKYkvDfrjQAnYXvuwc/lSZeLlQWCzNcZzACv14IcjklM 22 | GA7DgiYJ6aL9tTGCBDMwggQvAgEBME8wNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2 23 | MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUCFAG6/a9VuUO5SlyJA02E 24 | 3aCq+I7nMAsGCWCGSAFlAwQCAaBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw 25 | HAYJKoZIhvcNAQkFMQ8XDTIzMDYwNTE2MTExOVowLwYJKoZIhvcNAQkEMSIEIFVL 26 | lVMl0NAe/AWmTBlo1LsuuVEnTnqXvagu9a1+USZ5MAoGCCqGSM49BAMCBEcwRQIg 27 | MvJRYH8YjGI6aKjuj2BIvKtXauKmmfnTw6ftFHKHd7ICIQCtvl/bIvRutgXs2aIE 28 | nLMhBKbUH7L3yIxCXXioSlMbbaGCAwowggMGBgorBgEEAYO/MAMBMYIC9gSCAvII 29 | /9TuChIiCiDA0j1q1AaXP5VZ87otHKAfhBR9j/xbhEXCJPmLlZGAHRoVCgxoYXNo 30 | ZWRyZWtvcmQSBTAuMC4xIKiS+KMGKkgKRjBEAiA2urRGPSYHNU7xitI+aOApUJy1 31 | NoYOC+AhkrypnjFpEQIgf8+9b5Dyrut5SiCLZa9XdbyI3TjEIAFtOkpZtVObzrEy 32 | 3wQImMbwCBIgIwe35eP4Z02VSBqLyIBE1vGiNv+XuvMirTgfBSZKntsYmcbwCCIg 33 | b8pcUOF3RAEn3Ky+i8xIkB35UCyvB9HLrnXBAbeNXSYiIDBOHEdPTzANJeEaYkQs 34 | DHM9bIr0t1XLZY7buhOssc5HIiDIVqC3K3WpKXE5SzsayoO0znmjPcRJlswzcNNn 35 | AR4KACIglPzBfGEElradbmop8hj8wjnhiC8SQEjpcSKPyiuNbyciIIffDga3d73Q 36 | vvvgiTxtfGbEWjsVtYsZDrBsUtbvF7knIiA1tbjz90bJjXQVCOY8Q9SBJ09haNvU 37 | gD4uolKhxpw1/SIg4XF+XqpJWXICWkRlRyHWQSx1Fc7Dc7HG4tF9k9nb9EUiIP4p 38 | dv5jdzDOGD+/6oOodk2cCq4ukQkrI29bfF/p9UPFIiCtcSyYQk3g8ShNTxRLipW1 39 | 0iwYHUwKJGUY56miIL32Qyr+AQr7AXJla29yLnNpZ3N0b3JlLmRldiAtIDI2MDU3 40 | MzY2NzA5NzI3OTQ3NDYKMTg2MjEyMDkKSXdlMzVlUDRaMDJWU0JxTHlJQkUxdkdp 41 | TnYrWHV2TWlyVGdmQlNaS250cz0KVGltZXN0YW1wOiAxNjg1OTgxNDgwMTg3Mzc4 42 | MzQ5CgrigJQgcmVrb3Iuc2lnc3RvcmUuZGV2IHdOSTlhakJFQWlBTTE2enROTGtN 43 | azRtUzNqWW02TGVKLy9CdFhkV3VXZEpjd1JQMWtLeXYwUUlnSXB1Zm15MWVHNExF 44 | TGh5RjJXbVJmVHdXbldXaGJnMzlxeE5IWWpEY0x6dz0K 45 | -----END SIGNED MESSAGE----- 46 | 47 | Mon Jun 5 12:11:17 EDT 2023 48 | -------------------------------------------------------------------------------- /internal/fork/ietf-cms/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 GitHub, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /internal/fork/ietf-cms/README.md: -------------------------------------------------------------------------------- 1 | # CMS 2 | 3 | This package is forked from [github/smimesign](https://github.com/github/smimesign) with the following changes: 4 | 5 | - Adds inclusive checking for cert timestamps in timestamp authorities (https://github.com/github/smimesign/pull/121) 6 | - Fixes tests for MacOS due to regressions in return types in Go 1.18 crypto libraries (https://github.com/golang/go/issues/52010) 7 | - Adds support for separate cert pools for cert validation and TSA validation. 8 | 9 | [CMS (Cryptographic Message Syntax)](https://tools.ietf.org/html/rfc5652) is a syntax for signing, digesting, and encrypting arbitrary messages. It evolved from PKCS#7 and is the basis for higher level protocols such as S/MIME. This package implements the SignedData CMS content-type, allowing users to digitally sign data as well as verify data signed by others. 10 | 11 | ## Signing and Verifying Data 12 | 13 | High level APIs are provided for signing a message with a certificate and key: 14 | 15 | ```go 16 | msg := []byte("some data") 17 | cert, _ := x509.ParseCertificate(someCertificateData) 18 | key, _ := x509.ParseECPrivateKey(somePrivateKeyData) 19 | 20 | der, _ := cms.Sign(msg, []*x509.Certificate{cert}, key) 21 | 22 | //// 23 | /// At another time, in another place... 24 | // 25 | 26 | sd, _ := ParseSignedData(der) 27 | if err, _ := sd.Verify(x509.VerifyOptions{}); err != nil { 28 | panic(err) 29 | } 30 | ``` 31 | 32 | By default, CMS SignedData includes the original message. High level APIs are also available for creating and verifying detached signatures: 33 | 34 | ```go 35 | msg := []byte("some data") 36 | cert, _ := x509.ParseCertificate(someCertificateData) 37 | key, _ := x509.ParseECPrivateKey(somePrivateKeyData) 38 | 39 | der, _ := cms.SignDetached(msg, cert, key) 40 | 41 | //// 42 | /// At another time, in another place... 43 | // 44 | 45 | sd, _ := ParseSignedData(der) 46 | if err, _ := sd.VerifyDetached(msg, x509.VerifyOptions{}); err != nil { 47 | panic(err) 48 | } 49 | ``` 50 | 51 | ## Timestamping 52 | 53 | Because certificates expire and can be revoked, it is may be helpful to attach certified timestamps to signatures, proving that they existed at a given time. RFC3161 timestamps can be added to signatures like so: 54 | 55 | ```go 56 | signedData, _ := NewSignedData([]byte("Hello, world!")) 57 | signedData.Sign(identity.Chain(), identity.PrivateKey) 58 | signedData.AddTimestamps("http://timestamp.digicert.com") 59 | 60 | derEncoded, _ := signedData.ToDER() 61 | io.Copy(os.Stdout, bytes.NewReader(derEncoded)) 62 | ``` 63 | 64 | Verification functions implicitly verify timestamps as well. Without a timestamp, verification will fail if the certificate is no longer valid. 65 | -------------------------------------------------------------------------------- /internal/fork/ietf-cms/main_test.go: -------------------------------------------------------------------------------- 1 | package cms 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/x509" 9 | "encoding/asn1" 10 | "io" 11 | "io/ioutil" 12 | "math/big" 13 | "net/http" 14 | "time" 15 | 16 | "github.com/github/smimesign/fakeca" 17 | "github.com/github/smimesign/ietf-cms/oid" 18 | "github.com/github/smimesign/ietf-cms/protocol" 19 | "github.com/sigstore/gitsign/internal/fork/ietf-cms/timestamp" 20 | ) 21 | 22 | var ( 23 | // fake PKI setup 24 | root = fakeca.New(fakeca.IsCA) 25 | otherRoot = fakeca.New(fakeca.IsCA) 26 | 27 | intermediateKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 28 | intermediate = root.Issue(fakeca.IsCA, fakeca.PrivateKey(intermediateKey)) 29 | 30 | leaf = intermediate.Issue( 31 | fakeca.NotBefore(time.Now().Add(-time.Hour)), 32 | fakeca.NotAfter(time.Now().Add(time.Hour)), 33 | ) 34 | 35 | rootOpts = x509.VerifyOptions{Roots: root.ChainPool()} 36 | otherRootOpts = x509.VerifyOptions{Roots: otherRoot.ChainPool()} 37 | intermediateOpts = x509.VerifyOptions{Roots: intermediate.ChainPool()} 38 | 39 | // fake timestamp authority setup 40 | tsa = &testTSA{ident: intermediate.Issue()} 41 | thc = &testHTTPClient{tsa} 42 | ) 43 | 44 | func init() { 45 | timestamp.DefaultHTTPClient = thc 46 | } 47 | 48 | type testTSA struct { 49 | ident *fakeca.Identity 50 | sn int64 51 | hookInfo func(timestamp.Info) timestamp.Info 52 | hookToken func(*protocol.SignedData) *protocol.SignedData 53 | hookResponse func(timestamp.Response) timestamp.Response 54 | } 55 | 56 | func (tt *testTSA) Clear() { 57 | tt.hookInfo = nil 58 | tt.hookToken = nil 59 | tt.hookResponse = nil 60 | } 61 | 62 | func (tt *testTSA) HookInfo(hook func(timestamp.Info) timestamp.Info) { 63 | tt.Clear() 64 | tt.hookInfo = hook 65 | } 66 | 67 | func (tt *testTSA) HookToken(hook func(*protocol.SignedData) *protocol.SignedData) { 68 | tt.Clear() 69 | tt.hookToken = hook 70 | } 71 | 72 | func (tt *testTSA) HookResponse(hook func(timestamp.Response) timestamp.Response) { 73 | tt.Clear() 74 | tt.hookResponse = hook 75 | } 76 | 77 | func (tt *testTSA) nextSN() *big.Int { 78 | defer func() { tt.sn++ }() 79 | return big.NewInt(tt.sn) 80 | } 81 | 82 | func (tt *testTSA) Do(req timestamp.Request) (timestamp.Response, error) { 83 | info := timestamp.Info{ 84 | Version: 1, 85 | Policy: asn1.ObjectIdentifier{1, 2, 3}, 86 | SerialNumber: tt.nextSN(), 87 | GenTime: time.Now(), 88 | MessageImprint: req.MessageImprint, 89 | Nonce: req.Nonce, 90 | } 91 | 92 | if tt.hookInfo != nil { 93 | info = tt.hookInfo(info) 94 | } 95 | 96 | eciDER, err := asn1.Marshal(info) 97 | if err != nil { 98 | panic(err) 99 | } 100 | 101 | eci, err := protocol.NewEncapsulatedContentInfo(oid.ContentTypeTSTInfo, eciDER) 102 | if err != nil { 103 | panic(err) 104 | } 105 | 106 | tst, err := protocol.NewSignedData(eci) 107 | if err != nil { 108 | panic(err) 109 | } 110 | 111 | if err = tst.AddSignerInfo(tsa.ident.Chain(), tsa.ident.PrivateKey); err != nil { 112 | panic(err) 113 | } 114 | 115 | if tt.hookToken != nil { 116 | tt.hookToken(tst) 117 | } 118 | 119 | ci, err := tst.ContentInfo() 120 | if err != nil { 121 | panic(err) 122 | } 123 | 124 | resp := timestamp.Response{ 125 | Status: timestamp.PKIStatusInfo{Status: 0}, 126 | TimeStampToken: ci, 127 | } 128 | 129 | if tt.hookResponse != nil { 130 | resp = tt.hookResponse(resp) 131 | } 132 | 133 | return resp, nil 134 | } 135 | 136 | type testHTTPClient struct { 137 | tt *testTSA 138 | } 139 | 140 | func (thc *testHTTPClient) Do(httpReq *http.Request) (*http.Response, error) { 141 | buf := new(bytes.Buffer) 142 | if _, err := io.Copy(buf, httpReq.Body); err != nil { 143 | return nil, err 144 | } 145 | 146 | var tsReq timestamp.Request 147 | if _, err := asn1.Unmarshal(buf.Bytes(), &tsReq); err != nil { 148 | return nil, err 149 | } 150 | 151 | tsResp, err := thc.tt.Do(tsReq) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | respDER, err := asn1.Marshal(tsResp) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | return &http.Response{ 162 | StatusCode: 200, 163 | Header: http.Header{"Content-Type": {"application/timestamp-reply"}}, 164 | Body: ioutil.NopCloser(bytes.NewReader(respDER)), 165 | }, nil 166 | } 167 | -------------------------------------------------------------------------------- /internal/fork/ietf-cms/sign.go: -------------------------------------------------------------------------------- 1 | package cms 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | ) 7 | 8 | // Sign creates a CMS SignedData from the content and signs it with signer. At 9 | // minimum, chain must contain the leaf certificate associated with the signer. 10 | // Any additional intermediates will also be added to the SignedData. The DER 11 | // encoded CMS message is returned. 12 | func Sign(data []byte, chain []*x509.Certificate, signer crypto.Signer) ([]byte, error) { 13 | sd, err := NewSignedData(data) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | if err = sd.Sign(chain, signer); err != nil { 19 | return nil, err 20 | } 21 | 22 | return sd.ToDER() 23 | } 24 | 25 | // SignDetached creates a detached CMS SignedData from the content and signs it 26 | // with signer. At minimum, chain must contain the leaf certificate associated 27 | // with the signer. Any additional intermediates will also be added to the 28 | // SignedData. The DER encoded CMS message is returned. 29 | func SignDetached(data []byte, chain []*x509.Certificate, signer crypto.Signer) ([]byte, error) { 30 | sd, err := NewSignedData(data) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if err = sd.Sign(chain, signer); err != nil { 36 | return nil, err 37 | } 38 | 39 | sd.Detached() 40 | 41 | return sd.ToDER() 42 | } 43 | 44 | // Sign adds a signature to the SignedData.At minimum, chain must contain the 45 | // leaf certificate associated with the signer. Any additional intermediates 46 | // will also be added to the SignedData. 47 | func (sd *SignedData) Sign(chain []*x509.Certificate, signer crypto.Signer) error { 48 | return sd.psd.AddSignerInfo(chain, signer) 49 | } 50 | -------------------------------------------------------------------------------- /internal/fork/ietf-cms/signed_data.go: -------------------------------------------------------------------------------- 1 | package cms 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/asn1" 6 | 7 | "github.com/github/smimesign/ietf-cms/protocol" 8 | ) 9 | 10 | // SignedData represents a signed message or detached signature. 11 | type SignedData struct { 12 | psd *protocol.SignedData 13 | } 14 | 15 | // NewSignedData creates a new SignedData from the given data. 16 | func NewSignedData(data []byte) (*SignedData, error) { 17 | eci, err := protocol.NewDataEncapsulatedContentInfo(data) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | psd, err := protocol.NewSignedData(eci) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return &SignedData{psd}, nil 28 | } 29 | 30 | // ParseSignedData parses a SignedData from BER encoded data. 31 | func ParseSignedData(ber []byte) (*SignedData, error) { 32 | ci, err := protocol.ParseContentInfo(ber) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | psd, err := ci.SignedDataContent() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return &SignedData{psd}, nil 43 | } 44 | 45 | // GetData gets the encapsulated data from the SignedData. Nil will be returned 46 | // if this is a detached signature. A protocol.ErrWrongType will be returned if 47 | // the SignedData encapsulates something other than data (1.2.840.113549.1.7.1). 48 | func (sd *SignedData) GetData() ([]byte, error) { 49 | return sd.psd.EncapContentInfo.DataEContent() 50 | } 51 | 52 | // GetCertificates gets all the certificates stored in the SignedData. 53 | func (sd *SignedData) GetCertificates() ([]*x509.Certificate, error) { 54 | return sd.psd.X509Certificates() 55 | } 56 | 57 | // SetCertificates replaces the certificates stored in the SignedData with new 58 | // ones. 59 | func (sd *SignedData) SetCertificates(certs []*x509.Certificate) error { 60 | sd.psd.ClearCertificates() 61 | for _, cert := range certs { 62 | if err := sd.psd.AddCertificate(cert); err != nil { 63 | return err 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | // Detached removes the data content from this SignedData. No more signatures 70 | // can be added after this method has been called. 71 | func (sd *SignedData) Detached() { 72 | sd.psd.EncapContentInfo.EContent = asn1.RawValue{} 73 | } 74 | 75 | // IsDetached checks if this SignedData has data content. 76 | func (sd *SignedData) IsDetached() bool { 77 | return sd.psd.EncapContentInfo.EContent.Bytes == nil 78 | } 79 | 80 | // ToDER encodes this SignedData message using DER. 81 | func (sd *SignedData) ToDER() ([]byte, error) { 82 | return sd.psd.ContentInfoDER() 83 | } 84 | 85 | // Raw returns the underlying CMS SignedData struct. 86 | func (sd *SignedData) Raw() *protocol.SignedData { 87 | return sd.psd 88 | } 89 | -------------------------------------------------------------------------------- /internal/fork/ietf-cms/timestamp.go: -------------------------------------------------------------------------------- 1 | package cms 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "errors" 7 | 8 | "github.com/github/smimesign/ietf-cms/oid" 9 | "github.com/github/smimesign/ietf-cms/protocol" 10 | "github.com/sigstore/gitsign/internal/fork/ietf-cms/timestamp" 11 | ) 12 | 13 | // AddTimestamps adds a timestamp to the SignedData using the RFC3161 14 | // timestamping service at the given URL. This timestamp proves that the signed 15 | // message existed the time of generation, allowing verifiers to have more trust 16 | // in old messages signed with revoked keys. 17 | func (sd *SignedData) AddTimestamps(url string) error { 18 | var ( 19 | attrs = make([]protocol.Attribute, len(sd.psd.SignerInfos)) 20 | err error 21 | ) 22 | 23 | // Fetch all timestamp tokens before adding any to sd. This avoids a partial 24 | // failure. 25 | for i := range attrs { 26 | if attrs[i], err = fetchTS(url, sd.psd.SignerInfos[i]); err != nil { 27 | return err 28 | } 29 | } 30 | 31 | for i := range attrs { 32 | sd.psd.SignerInfos[i].UnsignedAttrs = append(sd.psd.SignerInfos[i].UnsignedAttrs, attrs[i]) 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func fetchTS(url string, si protocol.SignerInfo) (protocol.Attribute, error) { 39 | nilAttr := protocol.Attribute{} 40 | 41 | req, err := tsRequest(si) 42 | if err != nil { 43 | return nilAttr, err 44 | } 45 | 46 | resp, err := req.Do(url) 47 | if err != nil { 48 | return nilAttr, err 49 | } 50 | 51 | if tsti, err := resp.Info(); err != nil { 52 | return nilAttr, err 53 | } else if !req.Matches(tsti) { 54 | return nilAttr, errors.New("invalid message imprint") 55 | } 56 | 57 | return protocol.NewAttribute(oid.AttributeTimeStampToken, resp.TimeStampToken) 58 | } 59 | 60 | func tsRequest(si protocol.SignerInfo) (timestamp.Request, error) { 61 | hash, err := si.Hash() 62 | if err != nil { 63 | return timestamp.Request{}, err 64 | } 65 | 66 | mi, err := timestamp.NewMessageImprint(hash, bytes.NewReader(si.Signature)) 67 | if err != nil { 68 | return timestamp.Request{}, err 69 | } 70 | 71 | return timestamp.Request{ 72 | Version: 1, 73 | CertReq: true, 74 | Nonce: timestamp.GenerateNonce(), 75 | MessageImprint: mi, 76 | }, nil 77 | } 78 | 79 | // getTimestamp verifies and returns the timestamp.Info from the SignerInfo. 80 | func getTimestamp(si protocol.SignerInfo, opts x509.VerifyOptions) (timestamp.Info, error) { 81 | rawValue, err := si.UnsignedAttrs.GetOnlyAttributeValueBytes(oid.AttributeTimeStampToken) 82 | if err != nil { 83 | return timestamp.Info{}, err 84 | } 85 | 86 | tst, err := ParseSignedData(rawValue.FullBytes) 87 | if err != nil { 88 | return timestamp.Info{}, err 89 | } 90 | 91 | tsti, err := timestamp.ParseInfo(tst.psd.EncapContentInfo) 92 | if err != nil { 93 | return timestamp.Info{}, err 94 | } 95 | 96 | if tsti.Version != 1 { 97 | return timestamp.Info{}, protocol.ErrUnsupported 98 | } 99 | 100 | // verify timestamp signature and certificate chain.. 101 | if _, err = tst.Verify(opts, opts); err != nil { 102 | return timestamp.Info{}, err 103 | } 104 | 105 | // verify timestamp token matches SignerInfo. 106 | hash, err := tsti.MessageImprint.Hash() 107 | if err != nil { 108 | return timestamp.Info{}, err 109 | } 110 | mi, err := timestamp.NewMessageImprint(hash, bytes.NewReader(si.Signature)) 111 | if err != nil { 112 | return timestamp.Info{}, err 113 | } 114 | if !mi.Equal(tsti.MessageImprint) { 115 | return timestamp.Info{}, errors.New("invalid message imprint") 116 | } 117 | 118 | return tsti, nil 119 | } 120 | 121 | // hasTimestamp checks if si has a timestamp. 122 | func hasTimestamp(si protocol.SignerInfo) (bool, error) { 123 | vals, err := si.UnsignedAttrs.GetValues(oid.AttributeTimeStampToken) 124 | if err != nil { 125 | return false, err 126 | } 127 | 128 | return len(vals) > 0, nil 129 | } 130 | -------------------------------------------------------------------------------- /internal/fulcio/fulcioroots/fulcioroots.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package fulcioroots 17 | 18 | import ( 19 | "bytes" 20 | "context" 21 | "crypto/x509" 22 | "errors" 23 | "fmt" 24 | "os" 25 | 26 | "github.com/sigstore/gitsign/internal/config" 27 | "github.com/sigstore/sigstore/pkg/cryptoutils" 28 | "github.com/sigstore/sigstore/pkg/tuf" 29 | ) 30 | 31 | type CertificateSource func() ([]*x509.Certificate, error) 32 | 33 | // New returns new CertPool(s) with certificates populated by provided CertificateSources. 34 | func New(root *x509.CertPool, opts ...CertificateSource) (*x509.CertPool, *x509.CertPool, error) { 35 | var intermediate *x509.CertPool 36 | 37 | certs := []*x509.Certificate{} 38 | for _, fn := range opts { 39 | c, err := fn() 40 | if err != nil { 41 | return nil, nil, err 42 | } 43 | certs = append(certs, c...) 44 | } 45 | 46 | for _, cert := range certs { 47 | // root certificates are self-signed 48 | if bytes.Equal(cert.RawSubject, cert.RawIssuer) { 49 | root.AddCert(cert) 50 | } else { 51 | if intermediate == nil { 52 | intermediate = x509.NewCertPool() 53 | } 54 | intermediate.AddCert(cert) 55 | } 56 | } 57 | return root, intermediate, nil 58 | } 59 | 60 | func NewFromConfig(ctx context.Context, cfg *config.Config) (*x509.CertPool, *x509.CertPool, error) { 61 | src := []CertificateSource{FromTUF(ctx)} 62 | 63 | if cfg.FulcioRoot != "" { 64 | src = []CertificateSource{FromFile(cfg.FulcioRoot)} 65 | } 66 | 67 | return New(x509.NewCertPool(), src...) 68 | } 69 | 70 | const ( 71 | // This is the root in the fulcio project. 72 | fulcioTargetStr = "fulcio.crt.pem" 73 | 74 | // This is the v1 migrated root. 75 | fulcioV1TargetStr = "fulcio_v1.crt.pem" 76 | 77 | // This is the untrusted v1 intermediate CA certificate, used or chain building. 78 | fulcioV1IntermediateTargetStr = "fulcio_intermediate_v1.crt.pem" 79 | ) 80 | 81 | // FromTUF loads certs from the TUF client. 82 | func FromTUF(ctx context.Context) CertificateSource { 83 | return func() ([]*x509.Certificate, error) { 84 | tufClient, err := tuf.NewFromEnv(ctx) 85 | if err != nil { 86 | return nil, fmt.Errorf("initializing tuf: %w", err) 87 | } 88 | // Retrieve from the embedded or cached TUF root. If expired, a network 89 | // call is made to update the root. 90 | targets, err := tufClient.GetTargetsByMeta(tuf.Fulcio, []string{fulcioTargetStr, fulcioV1TargetStr, fulcioV1IntermediateTargetStr}) 91 | if err != nil { 92 | return nil, fmt.Errorf("error getting targets: %w", err) 93 | } 94 | if len(targets) == 0 { 95 | return nil, errors.New("none of the Fulcio roots have been found") 96 | } 97 | 98 | certs := []*x509.Certificate{} 99 | for _, t := range targets { 100 | c, err := cryptoutils.UnmarshalCertificatesFromPEM(t.Target) 101 | if err != nil { 102 | return nil, fmt.Errorf("error unmarshalling certificates: %w", err) 103 | } 104 | certs = append(certs, c...) 105 | } 106 | return certs, nil 107 | } 108 | } 109 | 110 | // FromFile loads certs from a PEM file. 111 | func FromFile(path string) CertificateSource { 112 | return func() ([]*x509.Certificate, error) { 113 | b, err := os.ReadFile(path) 114 | if err != nil { 115 | return nil, err 116 | } 117 | return cryptoutils.UnmarshalCertificatesFromPEM(b) 118 | } 119 | } 120 | 121 | // Static loads a static set of Certificates. 122 | func Static(certs ...*x509.Certificate) CertificateSource { 123 | return func() ([]*x509.Certificate, error) { 124 | return certs, nil 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /internal/fulcio/fulcioroots/fulcioroots_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package fulcioroots 17 | 18 | import ( 19 | "crypto/x509" 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | "github.com/github/smimesign/fakeca" 25 | "github.com/sigstore/sigstore/pkg/cryptoutils" 26 | ) 27 | 28 | func TestNew(t *testing.T) { 29 | ca := fakeca.New() 30 | certpath := filepath.Join(t.TempDir(), "cert.pem") 31 | b, err := cryptoutils.MarshalCertificateToPEM(ca.Certificate) 32 | if err != nil { 33 | t.Fatalf("error marshalling cert: %v", err) 34 | } 35 | if err := os.WriteFile(certpath, b, 0600); err != nil { 36 | t.Fatalf("error writing cert: %v", err) 37 | } 38 | 39 | for _, tc := range []struct { 40 | name string 41 | opts []CertificateSource 42 | root []*x509.Certificate 43 | }{ 44 | { 45 | name: "FromFile", 46 | opts: []CertificateSource{FromFile(certpath)}, 47 | root: []*x509.Certificate{ca.Certificate}, 48 | }, 49 | { 50 | name: "Static", 51 | opts: []CertificateSource{Static(ca.Certificate)}, 52 | root: []*x509.Certificate{ca.Certificate}, 53 | }, 54 | { 55 | name: "None", 56 | }, 57 | // TODO: Figure out how to test TUF locally. 58 | } { 59 | t.Run(tc.name, func(t *testing.T) { 60 | base := x509.NewCertPool() 61 | root, _, err := New(base, tc.opts...) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | if !root.Equal(certpool(tc.root...)) { 66 | t.Errorf("Root CertPool did not match, want: %+v", tc.root) 67 | } 68 | }) 69 | } 70 | } 71 | 72 | func certpool(certs ...*x509.Certificate) *x509.CertPool { 73 | pool := x509.NewCertPool() 74 | for _, c := range certs { 75 | pool.AddCert(c) 76 | } 77 | return pool 78 | } 79 | -------------------------------------------------------------------------------- /internal/git/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package git provides higher level funcs for signing and verifying Git 16 | // commits. Functions here generally tie together low level signature writing 17 | // and Sigstore components together into useful abstractions for working with 18 | // Git objects. 19 | package git 20 | -------------------------------------------------------------------------------- /internal/git/git.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package git 17 | 18 | import ( 19 | "bytes" 20 | "context" 21 | "fmt" 22 | 23 | "github.com/sigstore/gitsign/internal/fulcio" 24 | "github.com/sigstore/gitsign/internal/signature" 25 | "github.com/sigstore/gitsign/pkg/git" 26 | "github.com/sigstore/gitsign/pkg/rekor" 27 | ) 28 | 29 | type SignFunc func(ctx context.Context, rekor rekor.Writer, ident *fulcio.Identity, data []byte, opts signature.SignOptions) (*signature.SignResponse, error) 30 | 31 | // Sign signs the commit, uploading a HashedRekord of the commit content to Rekor 32 | // and embedding the Rekor log entry in the signature. 33 | // This is suitable for offline verification. 34 | func Sign(ctx context.Context, rekor rekor.Writer, ident *fulcio.Identity, data []byte, opts signature.SignOptions) (*signature.SignResponse, error) { 35 | opts.Rekor = rekor 36 | return signature.Sign(ctx, ident, data, opts) 37 | } 38 | 39 | // LegacySHASign is the old-style signing that signs the commit content, but uploads a signed SHA to Rekor. 40 | // Verification for this style of signing relies on the Rekor Search API to match the signed SHA + commit content certs, 41 | // and cannot be done offline. 42 | // This may be removed in the future. 43 | func LegacySHASign(ctx context.Context, rekor rekor.Writer, ident *fulcio.Identity, data []byte, opts signature.SignOptions) (*signature.SignResponse, error) { 44 | resp, err := signature.Sign(ctx, ident, data, opts) 45 | if err != nil { 46 | return nil, fmt.Errorf("failed to sign message: %w", err) 47 | } 48 | 49 | // This uploads the commit SHA + sig(commit SHA) to the tlog using the same 50 | // key used to sign the commit data itself. 51 | // Since the commit SHA ~= hash(commit data + sig(commit data)) and we're 52 | // using the same key, this is probably okay? e.g. even if you could cause a SHA1 collision, 53 | // you would still need the underlying commit to be valid and using the same key which seems hard. 54 | 55 | commit, err := git.ObjectHash(data, resp.Signature) 56 | if err != nil { 57 | return nil, fmt.Errorf("error generating commit hash: %w", err) 58 | } 59 | 60 | sv, err := ident.SignerVerifier() 61 | if err != nil { 62 | return nil, fmt.Errorf("error getting signer: %w", err) 63 | } 64 | commitSig, err := sv.SignMessage(bytes.NewBufferString(commit)) 65 | if err != nil { 66 | return nil, fmt.Errorf("error signing commit hash: %w", err) 67 | } 68 | resp.LogEntry, err = rekor.Write(ctx, commit, commitSig, resp.Cert) 69 | if err != nil { 70 | return nil, fmt.Errorf("error uploading tlog (commit): %w", err) 71 | } 72 | 73 | return resp, nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/git/gittest/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package gittest 15 | 16 | import ( 17 | "io" 18 | "os" 19 | "testing" 20 | 21 | "github.com/go-git/go-git/v5/plumbing" 22 | "github.com/go-git/go-git/v5/plumbing/object" 23 | "github.com/go-git/go-git/v5/storage/memory" 24 | ) 25 | 26 | func ParseCommit(t *testing.T, path string) *object.Commit { 27 | raw, err := os.ReadFile(path) 28 | if err != nil { 29 | t.Fatalf("error reading input: %v", err) 30 | } 31 | 32 | storage := memory.NewStorage() 33 | obj := storage.NewEncodedObject() 34 | obj.SetType(plumbing.CommitObject) 35 | w, err := obj.Writer() 36 | if err != nil { 37 | t.Fatalf("error getting git object writer: %v", err) 38 | } 39 | if _, err := w.Write(raw); err != nil { 40 | t.Fatalf("error writing git commit: %v", err) 41 | } 42 | 43 | c, err := object.DecodeCommit(storage, obj) 44 | if err != nil { 45 | t.Fatalf("error decoding commit: %v", err) 46 | } 47 | return c 48 | } 49 | 50 | func MarshalCommitBody(t *testing.T, commit *object.Commit) []byte { 51 | t.Helper() 52 | storage := memory.NewStorage() 53 | obj := storage.NewEncodedObject() 54 | if err := commit.EncodeWithoutSignature(obj); err != nil { 55 | t.Fatal(err) 56 | } 57 | r, err := obj.Reader() 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | body, err := io.ReadAll(r) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | return body 67 | } 68 | -------------------------------------------------------------------------------- /internal/gitsign/gitsign.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitsign 16 | 17 | import ( 18 | "context" 19 | "crypto/x509" 20 | "fmt" 21 | "os" 22 | 23 | cosignopts "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" 24 | "github.com/sigstore/cosign/v2/pkg/cosign" 25 | "github.com/sigstore/gitsign/internal/cert" 26 | "github.com/sigstore/gitsign/internal/config" 27 | "github.com/sigstore/gitsign/internal/fulcio/fulcioroots" 28 | rekorinternal "github.com/sigstore/gitsign/internal/rekor" 29 | "github.com/sigstore/gitsign/pkg/git" 30 | "github.com/sigstore/gitsign/pkg/rekor" 31 | "github.com/sigstore/sigstore/pkg/cryptoutils" 32 | ) 33 | 34 | type Verifier struct { 35 | git git.Verifier 36 | cert cert.Verifier 37 | rekor rekor.Verifier 38 | } 39 | 40 | // NewVerifierWithCosignOpts implements a Gitsign verifier using Cosign CertVerifyOptions. 41 | // Note: not all options are supported. 42 | // - cert: This is always taken from the commit. 43 | func NewVerifierWithCosignOpts(ctx context.Context, cfg *config.Config, opts *cosignopts.CertVerifyOptions) (*Verifier, error) { 44 | root, intermediate, err := fulcioroots.NewFromConfig(ctx, cfg) 45 | if err != nil { 46 | return nil, fmt.Errorf("error getting certificate root: %w", err) 47 | } 48 | 49 | tsa, err := x509.SystemCertPool() 50 | if err != nil { 51 | return nil, fmt.Errorf("error getting system root pool: %w", err) 52 | } 53 | if path := cfg.TimestampCert; path != "" { 54 | f, err := os.Open(path) 55 | if err != nil { 56 | return nil, err 57 | } 58 | defer f.Close() 59 | cert, err := cryptoutils.LoadCertificatesFromPEM(f) 60 | if err != nil { 61 | return nil, fmt.Errorf("error loading certs from %s: %w", path, err) 62 | } 63 | for _, c := range cert { 64 | tsa.AddCert(c) 65 | } 66 | } 67 | 68 | gitverifier, err := git.NewCertVerifier( 69 | git.WithRootPool(root), 70 | git.WithIntermediatePool(intermediate), 71 | git.WithTimestampCertPool(tsa), 72 | ) 73 | if err != nil { 74 | return nil, fmt.Errorf("error creating Git verifier: %w", err) 75 | } 76 | 77 | rekor, err := rekorinternal.NewClientContext(ctx, cfg.Rekor) 78 | if err != nil { 79 | return nil, fmt.Errorf("failed to create rekor client: %w", err) 80 | } 81 | 82 | // Optionally include cert.Verifier. 83 | // This needs to be optional because when verifying with 84 | // `git verify-commit` we don't have access to issuer / identity details. 85 | // In these cases, clients should look for the certificate validated claim 86 | // and warn if missing. 87 | var certverifier cert.Verifier 88 | if opts != nil { 89 | ctpub, err := cosign.GetCTLogPubs(ctx) 90 | if err != nil { 91 | return nil, fmt.Errorf("error getting CT log public key: %w", err) 92 | } 93 | identities, err := opts.Identities() 94 | if err != nil { 95 | return nil, fmt.Errorf("error parsing identities: %w", err) 96 | } 97 | certverifier = cert.NewCosignVerifier(&cosign.CheckOpts{ 98 | RekorClient: rekor.Rekor, 99 | RootCerts: root, 100 | IntermediateCerts: intermediate, 101 | CTLogPubKeys: ctpub, 102 | RekorPubKeys: rekor.PublicKeys(), 103 | CertGithubWorkflowTrigger: opts.CertGithubWorkflowTrigger, 104 | CertGithubWorkflowSha: opts.CertGithubWorkflowSha, 105 | CertGithubWorkflowName: opts.CertGithubWorkflowName, 106 | CertGithubWorkflowRepository: opts.CertGithubWorkflowRepository, 107 | CertGithubWorkflowRef: opts.CertGithubWorkflowRef, 108 | Identities: identities, 109 | IgnoreSCT: opts.IgnoreSCT, 110 | }) 111 | } 112 | 113 | return &Verifier{ 114 | git: gitverifier, 115 | cert: certverifier, 116 | rekor: rekor, 117 | }, nil 118 | } 119 | 120 | func (v *Verifier) Verify(ctx context.Context, data []byte, sig []byte, detached bool) (*git.VerificationSummary, error) { 121 | // TODO: we probably want to deprecate git.Verify in favor of this struct. 122 | summary, err := git.Verify(ctx, v.git, v.rekor, data, sig, detached) 123 | if err != nil { 124 | return summary, err 125 | } 126 | 127 | if v.cert != nil { 128 | if err := v.cert.Verify(summary.Cert); err != nil { 129 | summary.Claims = append(summary.Claims, git.NewClaim(git.ClaimValidatedCerificate, false)) 130 | return summary, err 131 | } 132 | summary.Claims = append(summary.Claims, git.NewClaim(git.ClaimValidatedCerificate, true)) 133 | } else { 134 | summary.Claims = append(summary.Claims, git.NewClaim(git.ClaimValidatedCerificate, false)) 135 | } 136 | 137 | return summary, nil 138 | } 139 | -------------------------------------------------------------------------------- /internal/gitsign/gitsign_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitsign 16 | 17 | import ( 18 | "context" 19 | "crypto" 20 | "crypto/ecdsa" 21 | "crypto/elliptic" 22 | "crypto/rand" 23 | "crypto/x509" 24 | "crypto/x509/pkix" 25 | "encoding/asn1" 26 | "io" 27 | "math/big" 28 | "testing" 29 | "time" 30 | 31 | "github.com/go-git/go-git/v5/plumbing/object" 32 | "github.com/go-git/go-git/v5/storage/memory" 33 | "github.com/sigstore/cosign/v2/pkg/cosign" 34 | certverifier "github.com/sigstore/gitsign/internal/cert" 35 | "github.com/sigstore/gitsign/internal/signature" 36 | "github.com/sigstore/gitsign/pkg/git" 37 | "github.com/sigstore/rekor/pkg/generated/models" 38 | ) 39 | 40 | func TestVerify(t *testing.T) { 41 | ctx := context.Background() 42 | 43 | // Generate cert 44 | cert, priv := generateCert(t, &x509.Certificate{ 45 | SerialNumber: big.NewInt(1), 46 | Subject: pkix.Name{ 47 | CommonName: "tacocat", 48 | }, 49 | EmailAddresses: []string{"tacocat@example.com"}, 50 | ExtraExtensions: []pkix.Extension{{ 51 | Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, 52 | Value: []byte("example.com"), 53 | }}, 54 | NotBefore: time.Now(), 55 | NotAfter: time.Now().Add(5 * time.Minute), 56 | }) 57 | 58 | // Git verifier 59 | roots := x509.NewCertPool() 60 | roots.AddCert(cert) 61 | gv, err := git.NewCertVerifier(git.WithRootPool(roots)) 62 | if err != nil { 63 | t.Fatalf("error creating git verifer: %v", err) 64 | } 65 | 66 | // Cert verifier 67 | cv := certverifier.NewCosignVerifier(&cosign.CheckOpts{ 68 | RootCerts: roots, 69 | Identities: []cosign.Identity{{ 70 | Issuer: "example.com", 71 | Subject: "tacocat@example.com", 72 | }}, 73 | IgnoreSCT: true, 74 | IgnoreTlog: true, 75 | }) 76 | 77 | // Rekor verifier - we don't have a good way to test this right now so mock it out. 78 | rekor := fakeRekor{} 79 | 80 | v := Verifier{ 81 | git: gv, 82 | cert: cv, 83 | rekor: rekor, 84 | } 85 | 86 | data, sig := generateData(t, cert, priv) 87 | if _, err := v.Verify(ctx, data, sig, true); err != nil { 88 | t.Fatal(err) 89 | } 90 | } 91 | 92 | func generateCert(t *testing.T, tmpl *x509.Certificate) (*x509.Certificate, *ecdsa.PrivateKey) { 93 | t.Helper() 94 | 95 | priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) 96 | if err != nil { 97 | t.Fatalf("error generating private key: %v", err) 98 | } 99 | pub := &priv.PublicKey 100 | raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, priv) 101 | if err != nil { 102 | t.Fatalf("error generating certificate: %v", err) 103 | } 104 | cert, err := x509.ParseCertificate(raw) 105 | if err != nil { 106 | t.Fatalf("ParseCertificate: %v", err) 107 | } 108 | return cert, priv 109 | } 110 | 111 | func generateData(t *testing.T, cert *x509.Certificate, priv crypto.Signer) ([]byte, []byte) { 112 | t.Helper() 113 | ctx := context.Background() 114 | 115 | // Generate commit data 116 | commit := object.Commit{ 117 | Message: "hello world!", 118 | } 119 | obj := memory.NewStorage().NewEncodedObject() 120 | if err := commit.Encode(obj); err != nil { 121 | t.Fatal(err) 122 | } 123 | reader, err := obj.Reader() 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | data, err := io.ReadAll(reader) 128 | if err != nil { 129 | t.Fatalf("error reading git data: %v", err) 130 | } 131 | 132 | id := &identity{ 133 | cert: cert, 134 | priv: priv, 135 | } 136 | resp, err := signature.Sign(ctx, id, data, signature.SignOptions{ 137 | Detached: true, 138 | Armor: true, 139 | // Fake CA outputs self-signed certs, so we need to use -1 to make sure 140 | // the self-signed cert itself is included in the chain, otherwise 141 | // Verify cannot find a cert to use for verification. 142 | IncludeCerts: 0, 143 | }) 144 | if err != nil { 145 | t.Fatalf("Sign() = %v", err) 146 | } 147 | 148 | return data, resp.Signature 149 | } 150 | 151 | type fakeRekor struct{} 152 | 153 | func (fakeRekor) Verify(_ context.Context, _ string, _ *x509.Certificate) (*models.LogEntryAnon, error) { 154 | return nil, nil 155 | } 156 | 157 | func (fakeRekor) VerifyInclusion(_ context.Context, _ []byte, _ *x509.Certificate) (*models.LogEntryAnon, error) { 158 | return nil, nil 159 | } 160 | 161 | type identity struct { 162 | signature.Identity 163 | cert *x509.Certificate 164 | priv crypto.Signer 165 | } 166 | 167 | func (i *identity) Certificate() (*x509.Certificate, error) { 168 | return i.cert, nil 169 | } 170 | 171 | func (i *identity) CertificateChain() ([]*x509.Certificate, error) { 172 | return []*x509.Certificate{i.cert}, nil 173 | } 174 | 175 | func (i *identity) Signer() (crypto.Signer, error) { 176 | return i.priv, nil 177 | } 178 | -------------------------------------------------------------------------------- /internal/io/streams.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package io 17 | 18 | import ( 19 | "fmt" 20 | "io" 21 | "os" 22 | "runtime/debug" 23 | 24 | "github.com/mattn/go-tty" 25 | ) 26 | 27 | type Streams struct { 28 | In io.Reader 29 | Out io.Writer 30 | Err io.Writer 31 | 32 | TTYIn io.Reader 33 | TTYOut io.Writer 34 | 35 | close []func() error 36 | } 37 | 38 | func New(logPath string) *Streams { 39 | s := &Streams{ 40 | In: os.Stdin, 41 | Out: os.Stdout, 42 | Err: os.Stderr, 43 | } 44 | 45 | if logPath != "" { 46 | // Since Git eats both stdout and stderr, we don't have a good way of 47 | // getting error information back from clients if things go wrong. 48 | // As a janky way to preserve error message, tee stderr to 49 | // a temp file. 50 | if f, err := os.Create(logPath); err == nil { 51 | s.close = append(s.close, f.Close) 52 | s.Err = io.MultiWriter(s.Err, f) 53 | } 54 | } 55 | 56 | // A TTY may not be available in all environments (e.g. in CI), so only 57 | // set the input/output if we can actually open it. 58 | tty, err := tty.Open() 59 | if err == nil { 60 | s.close = append(s.close, tty.Close) 61 | s.TTYIn = tty.Input() 62 | s.TTYOut = tty.Output() 63 | } else { 64 | // If we can't connect to a TTY, fall back to stderr for output (which 65 | // will also log to file if GITSIGN_LOG is set). 66 | s.TTYOut = s.Err 67 | } 68 | return s 69 | } 70 | 71 | func (s *Streams) Wrap(fn func() error) error { 72 | // Log any panics to ttyout, since otherwise they will be lost to os.Stderr. 73 | defer func() { 74 | if r := recover(); r != nil { 75 | fmt.Fprintln(s.TTYOut, r, string(debug.Stack())) 76 | } 77 | }() 78 | 79 | if err := fn(); err != nil { 80 | fmt.Fprintln(s.TTYOut, err) 81 | return err 82 | } 83 | return nil 84 | } 85 | 86 | func (s *Streams) Close() error { 87 | for _, fn := range s.close { 88 | if err := fn(); err != nil { 89 | return err 90 | } 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/rekor/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rekor 16 | 17 | import ( 18 | "context" 19 | 20 | gitrekor "github.com/sigstore/gitsign/pkg/rekor" 21 | rekor "github.com/sigstore/rekor/pkg/client" 22 | ) 23 | 24 | // NewClient returns a new Rekor client with common client options set. 25 | // Deprecated: Use NewClientContext instead. 26 | func NewClient(url string) (*gitrekor.Client, error) { 27 | return NewClientContext(context.TODO(), url) 28 | } 29 | 30 | // NewClientContext returns a new Rekor client with common client options set. 31 | func NewClientContext(ctx context.Context, url string) (*gitrekor.Client, error) { 32 | return gitrekor.NewWithOptions(ctx, url, gitrekor.WithClientOption(rekor.WithUserAgent("gitsign"))) 33 | } 34 | -------------------------------------------------------------------------------- /internal/rekor/oid/oid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oid 16 | 17 | import ( 18 | "context" 19 | "crypto/sha256" 20 | "crypto/x509" 21 | "encoding/asn1" 22 | "encoding/base64" 23 | "encoding/hex" 24 | "fmt" 25 | 26 | "github.com/github/smimesign/ietf-cms/protocol" 27 | "github.com/go-openapi/strfmt" 28 | "github.com/go-openapi/swag" 29 | rekorpb "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1" 30 | "github.com/sigstore/rekor/pkg/generated/models" 31 | "github.com/sigstore/rekor/pkg/types" 32 | "github.com/sigstore/rekor/pkg/types/hashedrekord" 33 | hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" 34 | "github.com/sigstore/sigstore/pkg/cryptoutils" 35 | "google.golang.org/protobuf/proto" 36 | ) 37 | 38 | var ( 39 | // OIDRekorTransparencyLogEntry is the OID for a serialized Rekor TransparencyLogEntry proto. 40 | // See https://github.com/sigstore/rekor/pull/1390 41 | OIDRekorTransparencyLogEntry = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 3, 1} 42 | ) 43 | 44 | // ToLogEntry reconstructs a Rekor HashedRekord from Git commit signature PKCS7 components. 45 | func ToLogEntry(ctx context.Context, message []byte, sig []byte, cert *x509.Certificate, attrs protocol.Attributes) (*models.LogEntryAnon, error) { 46 | var b []byte 47 | if err := unmarshalAttribute(attrs, OIDRekorTransparencyLogEntry, &b); err != nil { 48 | return nil, fmt.Errorf("error unmarshalling attribute: %w", err) 49 | } 50 | pb := new(rekorpb.TransparencyLogEntry) 51 | if err := proto.Unmarshal(b, pb); err != nil { 52 | return nil, fmt.Errorf("error unmarshalling TransparencyLogEntry attribute: %w", err) 53 | } 54 | out := logEntryAnonFromProto(pb) 55 | 56 | // Recompute HashedRekord body. 57 | hash := sha256.Sum256(message) 58 | certPEM, err := cryptoutils.MarshalCertificateToPEM(cert) 59 | if err != nil { 60 | return nil, fmt.Errorf("error marshalling cert: %w", err) 61 | } 62 | re := &hashedrekord_v001.V001Entry{ 63 | HashedRekordObj: models.HashedrekordV001Schema{ 64 | Data: &models.HashedrekordV001SchemaData{ 65 | Hash: &models.HashedrekordV001SchemaDataHash{ 66 | Algorithm: swag.String("sha256"), 67 | Value: swag.String(hex.EncodeToString(hash[:])), 68 | }, 69 | }, 70 | Signature: &models.HashedrekordV001SchemaSignature{ 71 | Content: strfmt.Base64(sig), 72 | PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ 73 | Content: strfmt.Base64(certPEM), 74 | }, 75 | }, 76 | }, 77 | } 78 | body, err := types.CanonicalizeEntry(ctx, re) 79 | if err != nil { 80 | return nil, fmt.Errorf("error canonicalizing entry: %w", err) 81 | } 82 | out.Body = base64.StdEncoding.EncodeToString(body) 83 | 84 | return out, nil 85 | } 86 | 87 | func unmarshalAttribute(attrs protocol.Attributes, oid asn1.ObjectIdentifier, target any) error { 88 | rv, err := attrs.GetOnlyAttributeValueBytes(oid) 89 | if err != nil { 90 | return fmt.Errorf("get oid: %w", err) 91 | } 92 | 93 | if _, err := asn1.Unmarshal(rv.FullBytes, target); err != nil { 94 | return fmt.Errorf("asn1.unmarshal(%v): %w", oid, err) 95 | } 96 | return nil 97 | } 98 | 99 | // ToAttributes takes a Rekor log entry and extracts fields into Attributes suitable to be included in the signature's 100 | // unauthenticated attributes. 101 | func ToAttributes(tlog *models.LogEntryAnon) (protocol.Attributes, error) { 102 | pb, err := logEntryAnonToProto(tlog, &rekorpb.KindVersion{ 103 | Kind: hashedrekord.KIND, 104 | Version: hashedrekord_v001.APIVERSION, 105 | }) 106 | if err != nil { 107 | return nil, err 108 | } 109 | // Clear out body - we store this data elsewhere so including is in the serialized log entry is redundant. 110 | pb.CanonicalizedBody = nil 111 | out, err := proto.Marshal(pb) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | attrs := protocol.Attributes{} 117 | attr, err := protocol.NewAttribute(OIDRekorTransparencyLogEntry, out) 118 | if err != nil { 119 | return nil, err 120 | } 121 | attrs = append(attrs, attr) 122 | return attrs, nil 123 | } 124 | -------------------------------------------------------------------------------- /internal/rekor/oid/oid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oid 16 | 17 | import ( 18 | "context" 19 | "crypto/x509" 20 | "encoding/json" 21 | "encoding/pem" 22 | "os" 23 | "testing" 24 | 25 | "github.com/go-git/go-git/v5/plumbing" 26 | "github.com/go-git/go-git/v5/plumbing/object" 27 | "github.com/go-git/go-git/v5/storage/memory" 28 | "github.com/google/go-cmp/cmp" 29 | cms "github.com/sigstore/gitsign/internal/fork/ietf-cms" 30 | "github.com/sigstore/rekor/pkg/generated/models" 31 | ) 32 | 33 | func TestOID(t *testing.T) { 34 | tlog := new(models.LogEntryAnon) 35 | if err := json.Unmarshal(readfile(t, "testdata/tlog.json"), tlog); err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | attr, err := ToAttributes(tlog) 40 | if err != nil { 41 | t.Fatalf("ToAttributes: %v", err) 42 | } 43 | 44 | commit := parseCommit(t, "testdata/commit.txt") 45 | message, sig, cert := parseSignature(t, commit) 46 | 47 | ctx := context.Background() 48 | got, err := ToLogEntry(ctx, message, sig, cert, attr) 49 | if err != nil { 50 | t.Fatalf("ToLogEntry: %v", err) 51 | } 52 | 53 | if diff := cmp.Diff(tlog, got); diff != "" { 54 | t.Error(diff) 55 | } 56 | } 57 | 58 | func readfile(t *testing.T, path string) []byte { 59 | t.Helper() 60 | b, err := os.ReadFile(path) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | return b 65 | } 66 | 67 | func parseCommit(t *testing.T, path string) *object.Commit { 68 | raw, err := os.ReadFile(path) 69 | if err != nil { 70 | t.Fatalf("error reading input: %v", err) 71 | } 72 | 73 | storage := memory.NewStorage() 74 | obj := storage.NewEncodedObject() 75 | obj.SetType(plumbing.CommitObject) 76 | w, err := obj.Writer() 77 | if err != nil { 78 | t.Fatalf("error getting git object writer: %v", err) 79 | } 80 | if _, err := w.Write(raw); err != nil { 81 | t.Fatalf("error writing git commit: %v", err) 82 | } 83 | 84 | c, err := object.DecodeCommit(storage, obj) 85 | if err != nil { 86 | t.Fatalf("error decoding commit: %v", err) 87 | } 88 | return c 89 | } 90 | 91 | // Returns: body, sig, cert 92 | func parseSignature(t *testing.T, c *object.Commit) ([]byte, []byte, *x509.Certificate) { 93 | // Parse signature 94 | blk, _ := pem.Decode([]byte(c.PGPSignature)) 95 | sd, err := cms.ParseSignedData(blk.Bytes) 96 | if err != nil { 97 | t.Fatalf("failed to parse signature: %v", err) 98 | } 99 | si := sd.Raw().SignerInfos[0] 100 | 101 | body, err := si.SignedAttrs.MarshaledForVerification() 102 | if err != nil { 103 | t.Fatalf("error marshalling commit body for verification: %v", err) 104 | } 105 | 106 | certs, err := sd.GetCertificates() 107 | if err != nil { 108 | t.Fatalf("error getting signature certs: %v", err) 109 | } 110 | cert := certs[0] 111 | 112 | return body, si.Signature, cert 113 | } 114 | -------------------------------------------------------------------------------- /internal/rekor/oid/pbcompat.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oid 16 | 17 | import ( 18 | "encoding/hex" 19 | "fmt" 20 | 21 | "github.com/go-openapi/swag" 22 | v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" 23 | rekorpb "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1" 24 | "github.com/sigstore/rekor/pkg/generated/models" 25 | ) 26 | 27 | // This file contains helper functions from going to/from Rekor types <-> protobuf-specs. 28 | // This may be pulled out into a more general library in the future. 29 | 30 | func logEntryAnonToProto(le *models.LogEntryAnon, kind *rekorpb.KindVersion) (*rekorpb.TransparencyLogEntry, error) { 31 | if le == nil { 32 | return nil, nil 33 | } 34 | 35 | logID, err := hex.DecodeString(*le.LogID) 36 | if err != nil { 37 | return nil, fmt.Errorf("error decoding LogID: %w", err) 38 | } 39 | 40 | hashes := make([][]byte, 0, len(le.Verification.InclusionProof.Hashes)) 41 | for i, h := range le.Verification.InclusionProof.Hashes { 42 | b, err := hex.DecodeString(h) 43 | if err != nil { 44 | return nil, fmt.Errorf("error decoding Verification.InclusionProof.Hashes[%d]: %w", i, err) 45 | } 46 | hashes = append(hashes, b) 47 | } 48 | 49 | rootHash, err := hex.DecodeString(*le.Verification.InclusionProof.RootHash) 50 | if err != nil { 51 | return nil, fmt.Errorf("error decoding Verification.InclusionProof.RootHash: %w", err) 52 | } 53 | 54 | out := &rekorpb.TransparencyLogEntry{ 55 | LogIndex: *le.LogIndex, 56 | LogId: &v1.LogId{ 57 | KeyId: logID, 58 | }, 59 | IntegratedTime: *le.IntegratedTime, 60 | InclusionPromise: &rekorpb.InclusionPromise{ 61 | SignedEntryTimestamp: le.Verification.SignedEntryTimestamp, 62 | }, 63 | InclusionProof: &rekorpb.InclusionProof{ 64 | LogIndex: *le.Verification.InclusionProof.LogIndex, 65 | RootHash: rootHash, 66 | TreeSize: *le.Verification.InclusionProof.TreeSize, 67 | Hashes: hashes, 68 | Checkpoint: &rekorpb.Checkpoint{ 69 | Envelope: *le.Verification.InclusionProof.Checkpoint, 70 | }, 71 | }, 72 | KindVersion: kind, 73 | } 74 | 75 | switch b := le.Body.(type) { 76 | case string: 77 | out.CanonicalizedBody = []byte(b) 78 | default: 79 | return nil, fmt.Errorf("unknown body type %T", le.Body) 80 | } 81 | return out, nil 82 | } 83 | 84 | func logEntryAnonFromProto(in *rekorpb.TransparencyLogEntry) *models.LogEntryAnon { 85 | out := &models.LogEntryAnon{ 86 | LogID: swag.String(hex.EncodeToString(in.GetLogId().GetKeyId())), 87 | LogIndex: swag.Int64(in.GetLogIndex()), 88 | IntegratedTime: swag.Int64(in.GetIntegratedTime()), 89 | Verification: &models.LogEntryAnonVerification{ 90 | SignedEntryTimestamp: in.GetInclusionPromise().GetSignedEntryTimestamp(), 91 | InclusionProof: &models.InclusionProof{ 92 | LogIndex: swag.Int64(in.GetInclusionProof().GetLogIndex()), 93 | Checkpoint: swag.String(in.GetInclusionProof().GetCheckpoint().GetEnvelope()), 94 | TreeSize: swag.Int64(in.GetInclusionProof().GetTreeSize()), 95 | RootHash: swag.String(hex.EncodeToString(in.GetInclusionProof().GetRootHash())), 96 | Hashes: make([]string, 0, len(in.GetInclusionProof().GetHashes())), 97 | }, 98 | }, 99 | Body: string(in.GetCanonicalizedBody()), 100 | } 101 | for _, h := range in.GetInclusionProof().GetHashes() { 102 | out.Verification.InclusionProof.Hashes = append(out.Verification.InclusionProof.Hashes, hex.EncodeToString(h)) 103 | } 104 | return out 105 | } 106 | -------------------------------------------------------------------------------- /internal/rekor/oid/pbcompat_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oid 16 | 17 | import ( 18 | "encoding/json" 19 | "testing" 20 | 21 | "github.com/google/go-cmp/cmp" 22 | "github.com/sigstore/rekor/pkg/generated/models" 23 | ) 24 | 25 | // Simple test to make sure we can go to/from rekor types to proto types. 26 | func TestConvert(t *testing.T) { 27 | in := new(models.LogEntryAnon) 28 | json.Unmarshal(readfile(t, "testdata/tlog.json"), in) 29 | 30 | // Kind is useful debug information, but isn't really used by us since we assume input/output types. 31 | pb, err := logEntryAnonToProto(in, nil) 32 | if err != nil { 33 | t.Fatalf("logEntryAnonToProto(): %v", err) 34 | } 35 | 36 | out := logEntryAnonFromProto(pb) 37 | 38 | if diff := cmp.Diff(in, out); diff != "" { 39 | t.Error(diff) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/rekor/oid/testdata/commit.txt: -------------------------------------------------------------------------------- 1 | tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 2 | parent 8f71f8da0720689322457d69be6cbcf6701ce978 3 | author Billy Lynch 1681496842 -0400 4 | committer Billy Lynch 1681496842 -0400 5 | gpgsig -----BEGIN SIGNED MESSAGE----- 6 | MIIH5wYJKoZIhvcNAQcCoIIH2DCCB9QCAQExDTALBglghkgBZQMEAgEwCwYJKoZI 7 | hvcNAQcBoIIC0DCCAswwggJToAMCAQICFD/6FqofcAbhhJj/WM1S79/2A7tWMAoG 8 | CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln 9 | c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDE4MjcyM1oXDTIzMDQxNDE4Mzcy 10 | M1owADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFZiKeh8JJtHGmy2ibIAPeKK 11 | zHqz0qYC88YJl0+h33H8qPCJ1Fn8VMRahJPigTirbWVMucGIUTXcfumQIhlqnHej 12 | ggFyMIIBbjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD 13 | VR0OBBYEFC3DWs+Yowo88LrHSaIGZ9oLnPFDMB8GA1UdIwQYMBaAFN/T6c9WJBGW 14 | +ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2 15 | MCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgor 16 | BgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYKKwYB 17 | BAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6O 18 | AAABh4EFpsEAAAQDAEcwRQIgeLZJCeysHbCMGk9NH0uG3hj1YrmWx9cPFVZwjF1/ 19 | 7EQCIQC6RvPxocQJFlImT4iP0GJaeFVV1L75Y/QJPsHFYULiXjAKBggqhkjOPQQD 20 | AwNnADBkAjB4ERsW18xtsT0cu7A3N4atNo4Ol5301gZYcfRmxXXEYU5INrhBGtBw 21 | bhizmji7rtACMDoq1brbKYBvaVZp1U5X2o0wsYPXYe9PwvkYQwat2c+j43wWn4ye 22 | V4Ot/OUigERBBDGCBN0wggTZAgEBME8wNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2 23 | MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUCFD/6FqofcAbhhJj/WM1S 24 | 79/2A7tWMAsGCWCGSAFlAwQCAaBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw 25 | HAYJKoZIhvcNAQkFMQ8XDTIzMDQxNDE4MjcyNFowLwYJKoZIhvcNAQkEMSIEIMoV 26 | PQYpb6Zf9fHMVI7fYfkEnA+3gLZ92vyf1p+05Jn4MAoGCCqGSM49BAMCBEcwRQIg 27 | Rn+DgE+ulf2+N37i+4CrTQv3ve+VjwkZ+6TTD/AQmjQCIQDp2QYzJl1FaWXpkbmR 28 | 0UE/J+NhRXn6kkyp7nUGs1Y/gqGCA7QwggOwBgorBgEEAYO/MAMBMYIDoASCA5wI 29 | jIzJCBIiCiDA0j1q1AaXP5VZ87otHKAfhBR9j/xbhEXCJPmLlZGAHRoVCgxoYXNo 30 | ZWRyZWtvcmQSBTAuMC4xIIy25qEGKkgKRjBEAiAsfOdZz8kcJtekpPZWadC36IOv 31 | 3h9FJMMsIBe+7m7aowIgfioerM8yC8GgBa7uOHuFvXT04aMUx1gFCk7TLdAHMM8y 32 | iQYIpf3KBhIgXqLVvFcpaU5EMF/fPebBSGsN2Vo9pWMKt0URqlhoFpkYpv3KBiIg 33 | pthkFudFq2tR6i4TQTdEpjH44kmjaMGefgMCx2CX/MYiIFOjPPtkdT17sgh4eHnB 34 | egpyo1IlSIQBoBWNP6kUHtmsIiBOsaQm4W2Pm319XZLqTpyTb2FIH67VnEdl14Oo 35 | mcgaBSIge76TDfaxRCpFCNikrSj5Mk43TwEKxd+Sga2kHpgPLHsiIPjmzhbgn6DL 36 | EeL3zjoqCudUmpTe6CglvUXshb90Ve62IiBDOYUeZ4icPH2zQeAQfbXWsTYnDqg+ 37 | WDnVzANzWfkNAyIgHV2F/1Ak+sOuRXQI6sgyi0C8fdS6LqSI5TwFlGkl5c0iIMIG 38 | El+osngj/HSKFFsOJxXqhb1OXQUiQZoom3HRwcz/IiCX6AMB5FLqAGg6rd9qlckN 39 | Q9yyyTzYJ46BPYM9uIv1UCIgfToSm87uhT0NgbdUMtkyLPJhweIRzEzcJb1HmQGM 40 | spUiIPX72A2FM9pdqkHSckAElySSZMLi/4sBg4KRBP9QapDxIiDsTGUVVjpnakEe 41 | RK0Gst8t/9osA3eH7roAyVvDtTRZVSIg1jCSwid4Bdy0yzYb6m4JrH7Z6ekZJyS4 42 | 9R5X5UvfNTEiIJ4EAGbf5fAgBGWDhqxmzwu2/+hX7XHLM3x/VUXs9FWLKv4BCvsB 43 | cmVrb3Iuc2lnc3RvcmUuZGV2IC0gMjYwNTczNjY3MDk3Mjc5NDc0NgoxMzgxMTM2 44 | NgpYcUxWdkZjcGFVNUVNRi9mUGViQlNHc04yVm85cFdNS3QwVVJxbGhvRnBrPQpU 45 | aW1lc3RhbXA6IDE2ODE0OTY4NDQ0ODY0MzI0MTYKCuKAlCByZWtvci5zaWdzdG9y 46 | ZS5kZXYgd05JOWFqQkZBaUVBempKTGE0Wk1NOXNmaDlIVks0ZGowMDBzRG5pU0Z2 47 | S3hxYmdtR3VUdThwMENJRSswMi90Wlh5Nnd4Vm5pWkhsbExRcEx1RFhLUnF1YjQ5 48 | bW03VDVNQVlpTgo= 49 | -----END SIGNED MESSAGE----- 50 | 51 | asdf 52 | -------------------------------------------------------------------------------- /internal/rekor/oid/testdata/tlog.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmZTI4YTkwOWY4OTVkY2JmNDU3OTVjMjhjNzJiYmZhNDg2MGM5YTdlMGFjOTJjNTBjZDFjN2M1MWEyNjNjM2I1In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRVovZzRCUHJwWDl2amQrNHZ1QXEwMEw5NzN2bFk4SkdmdWswdy93RUpvMEFpRUE2ZGtHTXlaZFJXbGw2Wkc1a2RGQlB5ZmpZVVY1K3BKTXFlNTFCck5XUDRJPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZSRU5EUVd4UFowRjNTVUpCWjBsVlVDOXZWM0ZvT1hkQ2RVZEZiVkE1V1hwV1RIWXpMMWxFZFRGWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcE5kMDVFUlRCTlZHZDVUbnBKZWxkb1kwNU5hazEzVGtSRk1FMVVaM3BPZWtsNlYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZXYlVsd05raDNhMjB3WTJGaVRHRktjMmRCT1RSdmNrMWxjbEJUY0dkTWVuaG5iVmdLVkRaSVptTm1lVzg0U1c1VlYyWjRWWGhHY1VWckswdENUMHQwZEZwVmVUVjNXV2hTVG1SNEt6WmFRV2xIVjNGalpEWlBRMEZZU1hkblowWjFUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZNWTA1aENubzFhV3BEYW5wM2RYTmtTbTluV200eVozVmpPRlZOZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsbldVUldVakJTUVZGSUwwSkNaM2RHYjBWVldXMXNjMkpJYkVGWk1taG9ZVmMxYm1SWFJubGFRelZyV2xoWmQwdFJXVXRMZDFsQ1FrRkhSQXAyZWtGQ1FWRlJZbUZJVWpCalNFMDJUSGs1YUZreVRuWmtWelV3WTNrMWJtSXlPVzVpUjFWMVdUSTVkRTFEYzBkRGFYTkhRVkZSUW1jM09IZEJVV2RGQ2toUmQySmhTRkl3WTBoTk5reDVPV2haTWs1MlpGYzFNR041Tlc1aU1qbHVZa2RWZFZreU9YUk5TVWRMUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpJZDBVS1pXZENORUZJV1VFelZEQjNZWE5pU0VWVVNtcEhValJqYlZkak0wRnhTa3RZY21wbFVFc3pMMmcwY0hsblF6aHdOMjgwUVVGQlIwaG5VVmR0ZDFGQlFRcENRVTFCVW5wQ1JrRnBRalIwYTJ0S04wdDNaSE5KZDJGVU1EQm1VelJpWlVkUVZtbDFXbUpJTVhjNFZsWnVRMDFZV0M5elVrRkphRUZNY0VjNEwwZG9DbmhCYTFkVmFWcFFhVWt2VVZsc2NEUldWbGhWZG5ac2FqbEJheXQzWTFab1VYVktaVTFCYjBkRFEzRkhVMDAwT1VKQlRVUkJNbU5CVFVkUlEwMUlaMUlLUjNoaVdIcEhNbmhRVW5rM2MwUmpNMmh4TURKcVp6WllibVpVVjBKc2FIZzVSMkpHWkdOU2FGUnJaekoxUlVWaE1FaENkVWRNVDJGUFRIVjFNRUZKZHdwUGFYSldkWFJ6Y0dkSE9YQldiVzVXVkd4bVlXcFVRM2huT1dSb056QXZReXRTYUVSQ2NUTmFlalpRYW1aQ1lXWnFTalZZWnpZek9EVlRTMEZTUlVWRkNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19", 3 | "integratedTime": 1681496844, 4 | "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", 5 | "logIndex": 17974796, 6 | "verification": { 7 | "inclusionProof": { 8 | "checkpoint": "rekor.sigstore.dev - 2605736670972794746\n14023763\nObGwMlDNPGiBMjt9BOFqk2H07lHLWQppUQuRE7wAg9o=\nTimestamp: 1681741564475733616\n\n— rekor.sigstore.dev wNI9ajBGAiEAll1SlTQdBQLIo0PHPTvfkQOQ+P5qzZuMaqslBqbdD4gCIQCj59tzyX12Z3iDVy8PRRZDJ1AF8gnp6bt6g0awPMwZbw==\n", 9 | "hashes": [ 10 | "a6d86416e745ab6b51ea2e13413744a631f8e249a368c19e7e0302c76097fcc6", 11 | "6c1caf296a95ba1a443187abb3b2673b774a0aab647b7f63f2b3bb2508b4eadd", 12 | "53a33cfb64753d7bb208787879c17a0a72a35225488401a0158d3fa9141ed9ac", 13 | "564af2b2877e2a41950c04108471c2bd0267544efa0cae23b4a6882c722732f8", 14 | "0fa696e667612372a23495fc520a15bd5f7e0ff23829a56dd6ca0c77eebf8ac4", 15 | "4eb1a426e16d8f9b7d7d5d92ea4e9c936f61481faed59c4765d783a899c81a05", 16 | "ad3a1e4cc6112361b7eeb4cdd466357e6074920b96d9d28f0d95d312b8431d8f", 17 | "7bbe930df6b1442a4508d8a4ad28f9324e374f010ac5df9281ada41e980f2c7b", 18 | "ac4cfd341e5e6cd320c163f186303f236585b4317dd0a4a7789a0d6cc1360686", 19 | "f8e6ce16e09fa0cb11e2f7ce3a2a0ae7549a94dee82825bd45ec85bf7455eeb6", 20 | "4339851e67889c3c7db341e0107db5d6b136270ea83e5839d5cc037359f90d03", 21 | "1d5d85ff5024fac3ae457408eac8328b40bc7dd4ba2ea488e53c05946925e5cd", 22 | "c206125fa8b27823fc748a145b0e2715ea85bd4e5d0522419a289b71d1c1ccff", 23 | "97e80301e452ea00683aaddf6a95c90d43dcb2c93cd8278e813d833db88bf550", 24 | "646807ef641abbad2c6dac2de56d1119625a05ed80ae8532028061e345fd7e4e", 25 | "7d3a129bceee853d0d81b75432d9322cf261c1e211cc4cdc25bd4799018cb295", 26 | "1bc65fbc780ba3bbb0a4d55c3690ac2dc65bc4a59ee0dd58ed4c497da53c9dab", 27 | "f5fbd80d8533da5daa41d272400497249264c2e2ff8b0183829104ff506a90f1", 28 | "d1d0bf047266e39d6f450c219384a1d327a72344e43b07105d9f73363100ed28", 29 | "ec4c6515563a676a411e44ad06b2df2dffda2c037787eeba00c95bc3b5345955", 30 | "d63092c2277805dcb4cb361bea6e09ac7ed9e9e9192724b8f51e57e54bdf3531", 31 | "9e040066dfe5f02004658386ac66cf0bb6ffe857ed71cb337c7f5545ecf4558b" 32 | ], 33 | "logIndex": 13811365, 34 | "rootHash": "39b1b03250cd3c6881323b7d04e16a9361f4ee51cb590a69510b9113bc0083da", 35 | "treeSize": 14023763 36 | }, 37 | "signedEntryTimestamp": "MEQCIB1DYO4lIhrzVqP2lahPhW+IKiaMmrw2MwoEmsME2by5AiBTFR5YZvLNpMxKulwXST8QN9qjBqD52wwTn/fgW9OR0g==" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/signature/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package signature handles the nitty-gritty of formatting and writing out 16 | // signatures. Functions here should not require any network/Sigstore access. 17 | package signature 18 | -------------------------------------------------------------------------------- /internal/signature/sign_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signature 16 | 17 | import ( 18 | "crypto/x509" 19 | "net/url" 20 | "testing" 21 | ) 22 | 23 | func TestMatchSAN(t *testing.T) { 24 | for _, tc := range []struct { 25 | testname string 26 | cert *x509.Certificate 27 | name string 28 | email string 29 | want bool 30 | }{ 31 | { 32 | testname: "email match", 33 | cert: &x509.Certificate{ 34 | EmailAddresses: []string{"foo@example.com"}, 35 | }, 36 | name: "Foo Bar", 37 | email: "foo@example.com", 38 | want: true, 39 | }, 40 | { 41 | testname: "uri match", 42 | cert: &x509.Certificate{ 43 | URIs: []*url.URL{parseURL("https://github.com/foo/bar")}, 44 | }, 45 | name: "https://github.com/foo/bar", 46 | email: "foo@example.com", 47 | want: true, 48 | }, 49 | { 50 | testname: "no match", 51 | cert: &x509.Certificate{}, 52 | name: "https://github.com/foo/bar", 53 | email: "foo@example.com", 54 | want: false, 55 | }, 56 | } { 57 | t.Run(tc.testname, func(t *testing.T) { 58 | got := matchSAN(tc.cert, tc.name, tc.email) 59 | if got != tc.want { 60 | t.Fatalf("got %t, want %t", got, tc.want) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | func parseURL(raw string) *url.URL { 67 | u, err := url.Parse(raw) 68 | if err != nil { 69 | panic(err) 70 | } 71 | return u 72 | } 73 | -------------------------------------------------------------------------------- /internal/signerverifier/cert.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signerverifier 16 | 17 | import "github.com/sigstore/sigstore/pkg/signature" 18 | 19 | // CertSignerVerifier wraps a SignerVerifier with a Certificate. 20 | type CertSignerVerifier struct { 21 | signature.SignerVerifier 22 | 23 | Cert []byte 24 | Chain []byte 25 | } 26 | -------------------------------------------------------------------------------- /internal/utils.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package internal 17 | 18 | import ( 19 | "crypto/sha1" // #nosec G505 20 | "crypto/x509" 21 | "encoding/hex" 22 | "net/url" 23 | ) 24 | 25 | // certHexFingerprint calculated the hex SHA1 fingerprint of a certificate. 26 | func CertHexFingerprint(cert *x509.Certificate) string { 27 | return hex.EncodeToString(certFingerprint(cert)) 28 | } 29 | 30 | // certFingerprint calculated the SHA1 fingerprint of a certificate. 31 | func certFingerprint(cert *x509.Certificate) []byte { 32 | if len(cert.Raw) == 0 { 33 | return nil 34 | } 35 | 36 | fpr := sha1.Sum(cert.Raw) // nolint:gosec 37 | return fpr[:] 38 | } 39 | 40 | // StripURL returns the baseHost with the basePath given a full endpoint 41 | func StripURL(endpoint string) (string, string) { 42 | u, err := url.Parse(endpoint) 43 | if err != nil { 44 | return "", "" 45 | } 46 | return u.Host, u.Path 47 | } 48 | -------------------------------------------------------------------------------- /internal/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package internal 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestStripUrl(t *testing.T) { 22 | endpoint := "https://private.rekor.com/rekor" 23 | host, basePath := StripURL(endpoint) 24 | if host != "private.rekor.com" || basePath != "/rekor" { 25 | t.Fatalf("Host and/or BasePath are not correct") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | // Enable OIDC providers 23 | _ "github.com/sigstore/cosign/v2/pkg/providers/all" 24 | "github.com/sigstore/gitsign/internal/commands/root" 25 | "github.com/sigstore/gitsign/internal/config" 26 | ) 27 | 28 | func main() { 29 | cfg, err := config.Get() 30 | if err != nil { 31 | fmt.Fprintln(os.Stderr, err) 32 | os.Exit(2) 33 | } 34 | 35 | rootCmd := root.New(cfg) 36 | 37 | if err := rootCmd.Execute(); err != nil { 38 | os.Exit(1) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/fulcio/fulcio.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fulcio 16 | 17 | import ( 18 | "crypto" 19 | "crypto/rand" 20 | "crypto/sha256" 21 | "crypto/x509" 22 | "net/url" 23 | "reflect" 24 | "strings" 25 | 26 | "github.com/sigstore/fulcio/pkg/api" 27 | "github.com/sigstore/sigstore/pkg/oauthflow" 28 | ) 29 | 30 | type Client interface { 31 | GetCert(crypto.Signer) (*api.CertificateResponse, error) 32 | } 33 | 34 | // Client provides a fulcio client with helpful options for configuring OIDC 35 | // flows. 36 | type ClientImpl struct { 37 | api.LegacyClient 38 | oidc OIDCOptions 39 | } 40 | 41 | // OIDCOptions contains settings for OIDC operations. 42 | type OIDCOptions struct { 43 | Issuer string 44 | ClientID string 45 | ClientSecret string 46 | RedirectURL string 47 | TokenGetter oauthflow.TokenGetter 48 | } 49 | 50 | func NewClient(fulcioURL string, opts OIDCOptions) (*ClientImpl, error) { 51 | u, err := url.Parse(fulcioURL) 52 | if err != nil { 53 | return nil, err 54 | } 55 | client := api.NewClient(u, api.WithUserAgent("gitsign")) 56 | return &ClientImpl{ 57 | LegacyClient: client, 58 | oidc: opts, 59 | }, nil 60 | } 61 | 62 | // GetCert exchanges the given private key for a Fulcio certificate. 63 | func (c *ClientImpl) GetCert(priv crypto.Signer) (*api.CertificateResponse, error) { 64 | pubBytes, err := x509.MarshalPKIXPublicKey(priv.Public()) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | tok, err := oauthflow.OIDConnect(c.oidc.Issuer, c.oidc.ClientID, c.oidc.ClientSecret, c.oidc.RedirectURL, c.oidc.TokenGetter) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | // Sign the email address as part of the request 75 | h := sha256.Sum256([]byte(tok.Subject)) 76 | proof, err := priv.Sign(rand.Reader, h[:], nil) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | cr := api.CertificateRequest{ 82 | PublicKey: api.Key{ 83 | Algorithm: keyAlgorithm(priv), 84 | Content: pubBytes, 85 | }, 86 | SignedEmailAddress: proof, 87 | } 88 | 89 | return c.SigningCert(cr, tok.RawString) 90 | } 91 | 92 | // keyAlgorithm returns a string representation of the type of signer. 93 | // Currently this is dervived from the package name - 94 | // e.g. crypto/ecdsa.PrivateKey -> ecdsa. 95 | // if Signer is nil, "" is returned. 96 | func keyAlgorithm(signer crypto.Signer) string { 97 | // This is a bit of a hack, but let's us use the package name as an approximation for 98 | // algorithm type. 99 | // e.g. *ecdsa.PrivateKey -> ecdsa 100 | t := reflect.TypeOf(signer) 101 | if t == nil { 102 | return "" 103 | } 104 | s := strings.Split(strings.TrimPrefix(t.String(), "*"), ".") 105 | return s[0] 106 | } 107 | -------------------------------------------------------------------------------- /pkg/fulcio/fulcio_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fulcio 16 | 17 | import ( 18 | "crypto" 19 | "crypto/ecdsa" 20 | "crypto/elliptic" 21 | "crypto/rand" 22 | "crypto/sha256" 23 | "crypto/x509" 24 | "encoding/json" 25 | "errors" 26 | "fmt" 27 | "net/http" 28 | "net/http/httptest" 29 | "testing" 30 | 31 | "github.com/coreos/go-oidc/v3/oidc" 32 | "github.com/google/go-cmp/cmp" 33 | "github.com/sigstore/fulcio/pkg/api" 34 | "github.com/sigstore/sigstore/pkg/oauthflow" 35 | "golang.org/x/oauth2" 36 | ) 37 | 38 | type fakeSigner struct { 39 | crypto.Signer 40 | } 41 | 42 | func TestKeyAlgorithm(t *testing.T) { 43 | key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 44 | for _, tc := range []struct { 45 | signer crypto.Signer 46 | want string 47 | }{ 48 | { 49 | signer: key, 50 | want: "ecdsa", 51 | }, 52 | { 53 | signer: fakeSigner{}, 54 | want: "fulcio", 55 | }, 56 | { 57 | signer: nil, 58 | want: "", 59 | }, 60 | } { 61 | t.Run(tc.want, func(t *testing.T) { 62 | got := keyAlgorithm(tc.signer) 63 | if got != tc.want { 64 | t.Errorf("want %s, got %s", tc.want, got) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | type fakeFulcio struct { 71 | api.LegacyClient 72 | signer *ecdsa.PrivateKey 73 | email string 74 | } 75 | 76 | func (f *fakeFulcio) SigningCert(cr api.CertificateRequest, _ string) (*api.CertificateResponse, error) { 77 | if want := keyAlgorithm(f.signer); want != cr.PublicKey.Algorithm { 78 | return nil, fmt.Errorf("want algorithm %s, got %s", want, cr.PublicKey.Algorithm) 79 | } 80 | pem, err := x509.MarshalPKIXPublicKey(f.signer.Public()) 81 | if err != nil { 82 | return nil, err 83 | } 84 | want := api.Key{ 85 | Algorithm: keyAlgorithm(f.signer), 86 | Content: pem, 87 | } 88 | if diff := cmp.Diff(want, cr.PublicKey); diff != "" { 89 | return nil, errors.New(diff) 90 | } 91 | 92 | // Verify checksum separately since this is non-deterministic. 93 | h := sha256.Sum256([]byte(f.email)) 94 | if !ecdsa.VerifyASN1(&f.signer.PublicKey, h[:], cr.SignedEmailAddress) { 95 | return nil, errors.New("signed email did not match") 96 | } 97 | 98 | return &api.CertificateResponse{}, nil 99 | } 100 | 101 | type fakeTokenGetter struct { 102 | email string 103 | } 104 | 105 | func (f *fakeTokenGetter) GetIDToken(*oidc.Provider, oauth2.Config) (*oauthflow.OIDCIDToken, error) { 106 | return &oauthflow.OIDCIDToken{ 107 | Subject: f.email, 108 | }, nil 109 | } 110 | 111 | func TestGetCert(t *testing.T) { 112 | // Implements a fake OIDC discovery. 113 | oidc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 114 | json.NewEncoder(w).Encode(map[string]interface{}{ 115 | "issuer": fmt.Sprintf("http://%s", r.Host), 116 | }) 117 | })) 118 | defer oidc.Close() 119 | 120 | key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 121 | email := "foo@example.com" 122 | 123 | client := &ClientImpl{ 124 | // fakeFulcio is what will be doing the validation. 125 | LegacyClient: &fakeFulcio{ 126 | signer: key, 127 | email: email, 128 | }, 129 | oidc: OIDCOptions{ 130 | Issuer: oidc.URL, 131 | TokenGetter: &fakeTokenGetter{ 132 | email: email, 133 | }, 134 | }, 135 | } 136 | 137 | // fakeFulcio is returning a bogus response, so only check if we returned 138 | // error. 139 | if _, err := client.GetCert(key); err != nil { 140 | t.Fatalf("GetCert: %v", err) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /pkg/git/git_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package git 16 | 17 | import "testing" 18 | 19 | const ( 20 | // These are real commit values generated in a test repo that were manually verified. 21 | 22 | // Rekor index: 2802961 23 | tagBody = `object 040b9af339e69d18848b7bbe05cb27ee42bb0161 24 | type commit 25 | tag signed-tag2 26 | tagger Billy Lynch 1656531453 -0400 27 | 28 | asdf 29 | ` 30 | tagSig = `-----BEGIN SIGNED MESSAGE----- 31 | MIIEBQYJKoZIhvcNAQcCoIID9jCCA/ICAQExDTALBglghkgBZQMEAgEwCwYJKoZI 32 | hvcNAQcBoIICpjCCAqIwggIooAMCAQICFGc8V7+B2VlJeFLpglonkbyb2kVeMAoG 33 | CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln 34 | c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIyMDYyOTE5MzczOVoXDTIyMDYyOTE5NDcz 35 | OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABP8JBFjhGqQsQCBmZqyuSHcG 36 | KZpDDRdpq7cl8Bhwuvu9A2bDz0gcuA/Nv18fKtikguBw6YBmEPi8S/YMYgMctVyj 37 | ggFHMIIBQzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD 38 | VR0OBBYEFMhi60DZPBYkwhDEuiltjyvxYYTDMB8GA1UdIwQYMBaAFN/T6c9WJBGW 39 | +ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2 40 | MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCB 41 | iQYKKwYBBAHWeQIEAgR7BHkAdwB1AAhgkvAoUv9oRdHRayeEnEVnGKwWPcM40m3m 42 | vCIGNm9yAAABgbD4HlAAAAQDAEYwRAIgON4g6BzdFgOIcCFk+8EXKpEw1XD0/DZ2 43 | 7gcb9Q/Jeg0CIGozxLGJS71uA2OU3JD6pGWCdnpYVsiG44/Em5w34SHmMAoGCCqG 44 | SM49BAMDA2gAMGUCMQDjLNl6Zaj5HbfLqqUvWNgz/R1VoQ3QG88kzu3GY0PodO8K 45 | QDcgt8bcGXzEdKkSFg4CMHIkGGLrG3bOYsjyIqZxiO6ess1jJxsFnM+GzvjwNRJk 46 | eWF9g96u/pNN8KA5VhveljGCASUwggEhAgEBME8wNzEVMBMGA1UEChMMc2lnc3Rv 47 | cmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUCFGc8V7+B2VlJ 48 | eFLpglonkbyb2kVeMAsGCWCGSAFlAwQCAaBpMBgGCSqGSIb3DQEJAzELBgkqhkiG 49 | 9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIyMDYyOTE5MzczOVowLwYJKoZIhvcNAQkE 50 | MSIEINZzCK5apWIVIKK26tVflr6zNoFkJm8SXQC5T65qwF1BMAoGCCqGSM49BAMC 51 | BEcwRQIgfAl7Elc0DB8UEMOXo3ZxKmN7zTrMO/tvhu1Himgc9IYCIQCxf06wWHVw 52 | YKHxU2tY8MNGomLVk0LyA/QaHQnoo34t8A== 53 | -----END SIGNED MESSAGE----- 54 | ` 55 | tagSHA = "ed092bb8688d6e37185bcdb58900940703c1a292" 56 | 57 | // Rekor index: 2801760 58 | commitBody = `tree b333504b8cf3d9c314fed2cc242c5c38e89534a5 59 | parent 2dc0ab59d7f0a7a62423bd181d9e2ab3adb7b56d 60 | author Billy Lynch 1656524971 -0400 61 | committer Billy Lynch 1656524971 -0400 62 | 63 | foo 64 | ` 65 | commitSig = `-----BEGIN SIGNED MESSAGE----- 66 | MIIEBwYJKoZIhvcNAQcCoIID+DCCA/QCAQExDTALBglghkgBZQMEAgEwCwYJKoZI 67 | hvcNAQcBoIICqDCCAqQwggIqoAMCAQICFHtMvZZL50P5bLkgDxwMf2MN4jdAMAoG 68 | CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln 69 | c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIyMDYyOTE3NDkzNFoXDTIyMDYyOTE3NTkz 70 | NFowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNf9io+JonCZhwe/dSkSoJ/Y 71 | eRun8C7xhPVF3FhoPnPVWdywaAEIkniA2WSHXLHt5aQN/08bV65haMZA/Luhmhaj 72 | ggFJMIIBRTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD 73 | VR0OBBYEFGzhjCzFUI0caspJJfD4bToYxfDhMB8GA1UdIwQYMBaAFN/T6c9WJBGW 74 | +ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2 75 | MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCB 76 | iwYKKwYBBAHWeQIEAgR9BHsAeQB3AAhgkvAoUv9oRdHRayeEnEVnGKwWPcM40m3m 77 | vCIGNm9yAAABgbCVKBkAAAQDAEgwRgIhAJHJalxdErw5icNqfgWtyrv75XGXxAZz 78 | F/J4b7B8ikQAAiEAj8g8ZiSIGmePmES19Y/yFeGj6Fz0NGE2Rk5uJdKyAGEwCgYI 79 | KoZIzj0EAwMDaAAwZQIxAKpQFL9D5s1YVEmNWBoEQ1oo6gBESGhd5L1Kcdq52Ltt 80 | KWXKKB7tpVRwC0lfof2ILgIwU1LTaKeKWb0vToMY9InoS2+hAVljbEh3oxKm/JoX 81 | hiRx2GiDe2OyLCs76/kbH6C/MYIBJTCCASECAQEwTzA3MRUwEwYDVQQKEwxzaWdz 82 | dG9yZS5kZXYxHjAcBgNVBAMTFXNpZ3N0b3JlLWludGVybWVkaWF0ZQIUe0y9lkvn 83 | Q/lsuSAPHAx/Yw3iN0AwCwYJYIZIAWUDBAIBoGkwGAYJKoZIhvcNAQkDMQsGCSqG 84 | SIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjIwNjI5MTc0OTM0WjAvBgkqhkiG9w0B 85 | CQQxIgQgSbThfvXoc6INDxPzRtlUu0TTBjFLm4XmwuxXAzfsZmkwCgYIKoZIzj0E 86 | AwIERzBFAiBeNZewVOFI5aa7bPUXa05HDgz5yevQ9aPclDX6U+koTAIhAMbyysil 87 | 7I/UWLzhwM+9iusn3JXy71akUTcrqi2MNPaO 88 | -----END SIGNED MESSAGE----- 89 | ` 90 | commitSHA = "040b9af339e69d18848b7bbe05cb27ee42bb0161" 91 | ) 92 | 93 | func TestObjectHash(t *testing.T) { 94 | for _, tc := range []struct { 95 | name string 96 | body string 97 | sig string 98 | sha string 99 | }{ 100 | { 101 | name: "tag", 102 | body: tagBody, 103 | sig: tagSig, 104 | sha: tagSHA, 105 | }, 106 | { 107 | name: "commit", 108 | body: commitBody, 109 | sig: commitSig, 110 | sha: commitSHA, 111 | }, 112 | } { 113 | t.Run(tc.name, func(t *testing.T) { 114 | got, err := ObjectHash([]byte(tc.body), []byte(tc.sig)) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | if got != tc.sha { 119 | t.Errorf("want %s, got %s", tc.sha, got) 120 | } 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pkg/git/signature_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package git 17 | 18 | import ( 19 | "context" 20 | "crypto" 21 | "crypto/x509" 22 | "fmt" 23 | "os" 24 | "path/filepath" 25 | "testing" 26 | 27 | "github.com/github/smimesign/fakeca" 28 | "github.com/sigstore/gitsign/internal/signature" 29 | "github.com/sigstore/sigstore/pkg/cryptoutils" 30 | ) 31 | 32 | type identity struct { 33 | signature.Identity 34 | base *fakeca.Identity 35 | } 36 | 37 | func (i *identity) Certificate() (*x509.Certificate, error) { 38 | return i.base.Certificate, nil 39 | } 40 | 41 | func (i *identity) CertificateChain() ([]*x509.Certificate, error) { 42 | return i.base.Chain(), nil 43 | } 44 | 45 | func (i *identity) Signer() (crypto.Signer, error) { 46 | return i.base.PrivateKey, nil 47 | } 48 | 49 | // TestSignVerify is a basic test to ensure that the Sign/Verify funcs can be 50 | // used with each other. We're assuming that the actual signature format has 51 | // been more thoroghly vetted in other packages (i.e. ietf-cms). 52 | func TestSignVerify(t *testing.T) { 53 | ctx := context.Background() 54 | ca := fakeca.New() 55 | id := &identity{ 56 | base: ca, 57 | } 58 | roots := x509.NewCertPool() 59 | roots.AddCert(ca.Certificate) 60 | data := []byte("tacocat") 61 | 62 | certpath := filepath.Join(t.TempDir(), "cert.pem") 63 | b, err := cryptoutils.MarshalCertificateToPEM(ca.Certificate) 64 | if err != nil { 65 | t.Fatalf("error marshalling cert: %v", err) 66 | } 67 | if err := os.WriteFile(certpath, b, 0600); err != nil { 68 | t.Fatalf("error writing cert: %v", err) 69 | } 70 | 71 | for _, detached := range []bool{true, false} { 72 | t.Run(fmt.Sprintf("detached(%t)", detached), func(t *testing.T) { 73 | resp, err := signature.Sign(ctx, id, data, signature.SignOptions{ 74 | Detached: detached, 75 | Armor: true, 76 | // Fake CA outputs self-signed certs, so we need to use -1 to make sure 77 | // the self-signed cert itself is included in the chain, otherwise 78 | // Verify cannot find a cert to use for verification. 79 | IncludeCerts: 0, 80 | }) 81 | if err != nil { 82 | t.Fatalf("Sign() = %v", err) 83 | } 84 | 85 | // Deprecated, included for completeness 86 | t.Run("VerifySignature", func(t *testing.T) { 87 | if _, err := VerifySignature(data, resp.Signature, detached, roots, ca.ChainPool()); err != nil { 88 | t.Fatalf("Verify() = %v", err) 89 | } 90 | }) 91 | 92 | t.Run("CertVerifier.Verify", func(t *testing.T) { 93 | cv, err := NewCertVerifier(WithRootPool(roots)) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | if _, err := cv.Verify(ctx, data, resp.Signature, detached); err != nil { 98 | t.Fatalf("Verify() = %v", err) 99 | } 100 | }) 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pkg/git/verifier.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package git 17 | 18 | import ( 19 | "context" 20 | "crypto/x509" 21 | "encoding/pem" 22 | "fmt" 23 | "time" 24 | 25 | cms "github.com/sigstore/gitsign/internal/fork/ietf-cms" 26 | "github.com/sigstore/gitsign/internal/fulcio/fulcioroots" 27 | "github.com/sigstore/sigstore/pkg/tuf" 28 | ) 29 | 30 | // Verifier verifies git commit signature data. 31 | type Verifier interface { 32 | Verify(ctx context.Context, data, sig []byte, detached bool) (*x509.Certificate, error) 33 | } 34 | 35 | // CertVerifier is the default implementation of Verifier. 36 | // It verifies git commits against a given CertPool. By default, the system 37 | // CertPool + Fulcio roots are used for validation. 38 | type CertVerifier struct { 39 | roots *x509.CertPool 40 | intermediates *x509.CertPool 41 | tsa *x509.CertPool 42 | } 43 | 44 | type CertVerifierOption func(*CertVerifier) error 45 | 46 | func NewCertVerifier(opts ...CertVerifierOption) (*CertVerifier, error) { 47 | v := &CertVerifier{} 48 | 49 | for _, o := range opts { 50 | if err := o(v); err != nil { 51 | return nil, err 52 | } 53 | } 54 | 55 | // Use empty pool if not set - this makes it so that we don't fallback 56 | // to the system pool. 57 | if v.roots == nil { 58 | v.roots = x509.NewCertPool() 59 | } 60 | 61 | return v, nil 62 | } 63 | 64 | // WithRootPool sets the base CertPool for the verifier. 65 | func WithRootPool(pool *x509.CertPool) CertVerifierOption { 66 | return func(v *CertVerifier) error { 67 | v.roots = pool 68 | return nil 69 | } 70 | } 71 | 72 | // WithIntermediatePool sets the base intermediate CertPool for the verifier. 73 | func WithIntermediatePool(pool *x509.CertPool) CertVerifierOption { 74 | return func(v *CertVerifier) error { 75 | v.intermediates = pool 76 | return nil 77 | } 78 | } 79 | 80 | // WithIntermediatePool sets the base intermediate CertPool for the verifier. 81 | func WithTimestampCertPool(pool *x509.CertPool) CertVerifierOption { 82 | return func(v *CertVerifier) error { 83 | v.tsa = pool 84 | return nil 85 | } 86 | } 87 | 88 | // Verify verifies for a given Git data + signature pair. 89 | // 90 | // Data should be the Git data that was signed (i.e. everything in the commit 91 | // besides the signature). Note: passing in the commit object itself will not 92 | // work. 93 | // 94 | // Signatures should be CMS/PKCS7 formatted. 95 | func (v *CertVerifier) Verify(_ context.Context, data, sig []byte, detached bool) (*x509.Certificate, error) { 96 | // Try decoding as PEM 97 | var der []byte 98 | if blk, _ := pem.Decode(sig); blk != nil { 99 | der = blk.Bytes 100 | } else { 101 | der = sig 102 | } 103 | // Parse signature 104 | sd, err := cms.ParseSignedData(der) 105 | if err != nil { 106 | return nil, fmt.Errorf("failed to parse signature: %w", err) 107 | } 108 | 109 | // Generate verification options. 110 | certs, err := sd.GetCertificates() 111 | if err != nil { 112 | return nil, fmt.Errorf("error getting signature certs: %w", err) 113 | } 114 | cert := certs[0] 115 | 116 | opts := x509.VerifyOptions{ 117 | Roots: v.roots, 118 | Intermediates: v.intermediates, 119 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, 120 | // cosign hack: ignore the current time for now - we'll use the tlog to 121 | // verify whether the commit was signed at a valid time. 122 | CurrentTime: cert.NotBefore.Add(1 * time.Minute), 123 | } 124 | 125 | tsaOpts := x509.VerifyOptions{ 126 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}, 127 | } 128 | if v.tsa != nil { 129 | tsaOpts.Roots = v.tsa 130 | } 131 | 132 | if detached { 133 | if _, err := sd.VerifyDetached(data, opts, tsaOpts); err != nil { 134 | return nil, fmt.Errorf("failed to verify detached signature: %w", err) 135 | } 136 | } else { 137 | if _, err := sd.Verify(opts, tsaOpts); err != nil { 138 | return nil, fmt.Errorf("failed to verify attached signature: %w", err) 139 | } 140 | } 141 | 142 | return cert, nil 143 | } 144 | 145 | // NewDefaultVerifier returns a new CertVerifier with the default Fulcio roots loaded from the local TUF client. 146 | // See https://docs.sigstore.dev/system_config/custom_components/ for how to customize this behavior. 147 | func NewDefaultVerifier(ctx context.Context) (*CertVerifier, error) { 148 | if err := tuf.Initialize(ctx, tuf.DefaultRemoteRoot, nil); err != nil { 149 | return nil, err 150 | } 151 | root, intermediate, err := fulcioroots.New(x509.NewCertPool(), fulcioroots.FromTUF(ctx)) 152 | if err != nil { 153 | return nil, err 154 | } 155 | return NewCertVerifier(WithRootPool(root), WithIntermediatePool(intermediate)) 156 | } 157 | -------------------------------------------------------------------------------- /pkg/gitsign/signer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package gitsign provides a signer for signing git commits and tags via Gitsign keyless flow. 16 | package gitsign 17 | 18 | import ( 19 | "context" 20 | "crypto" 21 | "crypto/ecdsa" 22 | "crypto/elliptic" 23 | "crypto/rand" 24 | "fmt" 25 | "io" 26 | 27 | "github.com/go-git/go-git/v5" 28 | fulciointernal "github.com/sigstore/gitsign/internal/fulcio" 29 | "github.com/sigstore/gitsign/internal/signature" 30 | "github.com/sigstore/gitsign/pkg/fulcio" 31 | "github.com/sigstore/gitsign/pkg/rekor" 32 | ) 33 | 34 | type PrivateKeySigner interface { 35 | crypto.PrivateKey 36 | crypto.Signer 37 | } 38 | 39 | var ( 40 | _ git.Signer = &Signer{} 41 | ) 42 | 43 | type Signer struct { 44 | ctx context.Context 45 | key PrivateKeySigner 46 | fulcio fulcio.Client 47 | rekor rekor.Writer 48 | } 49 | 50 | func NewSigner(ctx context.Context, fulcio fulcio.Client, rekor rekor.Writer) (*Signer, error) { 51 | priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 52 | if err != nil { 53 | return nil, fmt.Errorf("generating private key: %w", err) 54 | } 55 | return &Signer{ 56 | ctx: ctx, 57 | key: priv, 58 | fulcio: fulcio, 59 | rekor: rekor, 60 | }, nil 61 | } 62 | 63 | func (f *Signer) Sign(message io.Reader) ([]byte, error) { 64 | cert, err := f.fulcio.GetCert(f.key) 65 | if err != nil { 66 | return nil, fmt.Errorf("error getting fulcio cert: %w", err) 67 | } 68 | 69 | id := &fulciointernal.Identity{ 70 | PrivateKey: f.key, 71 | CertPEM: cert.CertPEM, 72 | ChainPEM: cert.ChainPEM, 73 | } 74 | 75 | body, err := io.ReadAll(message) 76 | if err != nil { 77 | return nil, fmt.Errorf("error reading message: %w", err) 78 | } 79 | 80 | resp, err := signature.Sign(f.ctx, id, body, signature.SignOptions{ 81 | Rekor: f.rekor, 82 | 83 | // TODO: make SignOptions configurable? 84 | Armor: true, 85 | Detached: true, 86 | IncludeCerts: -2, 87 | }) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return resp.Signature, nil 92 | } 93 | -------------------------------------------------------------------------------- /pkg/predicate/commit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Sigstore Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package predicate 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | type GitCommit struct { 22 | Commit *Commit `json:"source,omitempty"` 23 | Signature string `json:"signature,omitempty"` 24 | // SignerInfo contains select fields from the PKCS7 SignerInfo. 25 | // This is intended as a convenience for consumers to access relevant 26 | // fields like certificate instead of needing to parse the signature. 27 | // See https://datatracker.ietf.org/doc/html/rfc5652#section-5.3 for details. 28 | SignerInfo []*SignerInfo `json:"signer_info,omitempty"` 29 | } 30 | 31 | type Commit struct { 32 | Tree string `json:"tree,omitempty"` 33 | Parents []string `json:"parents,omitempty"` 34 | Author *Author `json:"author,omitempty"` 35 | Committer *Author `json:"committer,omitempty"` 36 | Message string `json:"message,omitempty"` 37 | } 38 | 39 | type Author struct { 40 | Name string `json:"name,omitempty"` 41 | Email string `json:"email,omitempty"` 42 | Date time.Time `json:"date,omitempty"` 43 | } 44 | 45 | type SignerInfo struct { 46 | // Attributes contains a base64 encoded ASN.1 marshalled signed attributes. 47 | // See https://datatracker.ietf.org/doc/html/rfc5652#section-5.6 for more details. 48 | Attributes string `json:"attributes,omitempty"` 49 | Certificate string `json:"certificate,omitempty"` 50 | } 51 | -------------------------------------------------------------------------------- /pkg/rekor/option.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package rekor 17 | 18 | import ( 19 | "context" 20 | 21 | "github.com/sigstore/cosign/v2/pkg/cosign" 22 | "github.com/sigstore/rekor/pkg/client" 23 | ) 24 | 25 | type Option func(*options) 26 | 27 | type options struct { 28 | rekorPublicKeys CosignRekorKeyProvider 29 | clientOpts []client.Option 30 | } 31 | 32 | // CosignRekorKeyProvider is a function that returns the Rekor public keys in cosign's specialized format. 33 | type CosignRekorKeyProvider func(ctx context.Context) (*cosign.TrustedTransparencyLogPubKeys, error) 34 | 35 | func WithCosignRekorKeyProvider(f CosignRekorKeyProvider) Option { 36 | return func(o *options) { 37 | o.rekorPublicKeys = f 38 | } 39 | } 40 | 41 | func WithClientOption(opts ...client.Option) Option { 42 | return func(o *options) { 43 | o.clientOpts = opts 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package version 17 | 18 | import ( 19 | "os" 20 | "runtime/debug" 21 | "strings" 22 | ) 23 | 24 | // Base version information. 25 | // 26 | // This is the fallback data used when version information from git is not 27 | // provided via go ldflags. 28 | var ( 29 | // Output of "git describe". The prerequisite is that the 30 | // branch should be tagged using the correct versioning strategy. 31 | gitVersion = "devel" 32 | 33 | envVarPrefixes = []string{ 34 | "GITSIGN_", 35 | // Can modify Sigstore/TUF client behavior - https://github.com/sigstore/sigstore/blob/35d6a82c15183f7fe7a07eca45e17e378aa32126/pkg/tuf/client.go#L52 36 | "SIGSTORE_", 37 | "TUF_", 38 | } 39 | ) 40 | 41 | type Info struct { 42 | GitVersion string `json:"gitVersion"` 43 | Env []string `json:"env"` 44 | } 45 | 46 | func getBuildInfo() *debug.BuildInfo { 47 | bi, ok := debug.ReadBuildInfo() 48 | if !ok { 49 | return nil 50 | } 51 | return bi 52 | } 53 | 54 | func getGitVersion(bi *debug.BuildInfo) string { 55 | if bi == nil { 56 | return "unknown" 57 | } 58 | 59 | // https://github.com/golang/go/issues/29228 60 | if bi.Main.Version == "(devel)" || bi.Main.Version == "" { 61 | return gitVersion 62 | } 63 | 64 | return bi.Main.Version 65 | } 66 | 67 | func getGitsignEnv() []string { 68 | out := []string{} 69 | for _, e := range os.Environ() { 70 | // Prefixes to look for. err on the side of showing too much rather 71 | // than too little. We'll only output things that have values set. 72 | for _, prefix := range envVarPrefixes { 73 | if strings.HasPrefix(e, prefix) { 74 | eComponents := strings.Split(strings.TrimSpace(e), "=") 75 | if len(eComponents) == 1 || len(eComponents[1]) == 0 { 76 | // The variable is set to nothing 77 | // eg: SIGSTORE_ROOT_FILE= 78 | continue 79 | } 80 | out = append(out, e) 81 | } 82 | } 83 | } 84 | return out 85 | } 86 | 87 | // GetVersionInfo represents known information on how this binary was built. 88 | func GetVersionInfo() Info { 89 | buildInfo := getBuildInfo() 90 | gitVersion = getGitVersion(buildInfo) 91 | return Info{ 92 | GitVersion: gitVersion, 93 | Env: getGitsignEnv(), 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pkg/version/version_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Sigstore Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package version 17 | 18 | import ( 19 | "os" 20 | "strings" 21 | "testing" 22 | 23 | "github.com/google/go-cmp/cmp" 24 | ) 25 | 26 | func TestVersionText(t *testing.T) { 27 | sut := GetVersionInfo() 28 | if sut.GitVersion != gitVersion { 29 | t.Errorf("GetVersionInfo: got %q, want %q", sut, gitVersion) 30 | } 31 | } 32 | 33 | func TestEnv(t *testing.T) { 34 | for _, envVar := range os.Environ() { 35 | for _, prefix := range envVarPrefixes { 36 | if strings.HasPrefix(envVar, prefix) { 37 | t.Setenv(strings.Split(envVar, "=")[0], "") // t.Setenv restores value during cleanup 38 | break 39 | } 40 | } 41 | } 42 | 43 | os.Setenv("GITSIGN_CONNECTOR_ID", "foobar") 44 | os.Setenv("GITSIGN_TEST", "foo") 45 | os.Setenv("TUF_ROOT", "bar") 46 | got := GetVersionInfo() 47 | want := []string{ 48 | "GITSIGN_CONNECTOR_ID=foobar", 49 | "GITSIGN_TEST=foo", 50 | "TUF_ROOT=bar", 51 | } 52 | 53 | if diff := cmp.Diff(got.Env, want); diff != "" { 54 | t.Error(diff) 55 | } 56 | 57 | // want doesn't change because the variable is set to nothing and must be 58 | // ignored 59 | os.Setenv("SIGSTORE_ROOT_FILE", "") 60 | got = GetVersionInfo() 61 | if diff := cmp.Diff(got.Env, want); diff != "" { 62 | t.Error(diff) 63 | } 64 | } 65 | --------------------------------------------------------------------------------