GitHub Security Advisory: GHSA-7c37-gx6w-8vc5
Package: github.com/sigstore/gitsign (GO)
Severity: MODERATE (CVSS 5.4)
CWE: CWE-129: Improper Validation of Array Index, CWE-390: Detection of Error Condition Without Action
Vulnerable versions: >= 0.4.0, < 0.15.0
Patched in: 0.15.0

Summary: gitsign --verify panics on empty-certificate PKCS7 and exits 0, bypassing exit-code callers

Details:
## Summary

`CertVerifier.Verify()` in `pkg/git/verifier.go` unconditionally dereferences `certs[0]` after `sd.GetCertificates()` without checking the slice length. A CMS/PKCS7 signed message with an empty certificate set is a structurally valid DER payload; `GetCertificates()` returns an empty slice with no error, causing an immediate index-out-of-range panic. On the `gitsign --verify` code path (the GPG-compatible mode invoked by `git verify-commit`), the panic is silently recovered by `internal/io/streams.go`'s `Wrap()` function, which returns `nil` instead of an error. `main.go` then exits with code 0, causing exit-code-only verification callers to interpret the failed verification as success.

## Severity

**Medium** (CVSS 3.1: 5.8)

`CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L`

- **Attack Vector:** Network — attacker pushes a commit carrying a crafted signature to any accessible repository, or delivers the signature file out-of-band
- **Attack Complexity:** Low — stripping certificates from a PKCS7 object requires only standard ASN.1 tooling
- **Privileges Required:** None — writing to an accessible repo (or creating a repo a victim clones) is sufficient
- **User Interaction:** Required — victim must run `git verify-commit`, `gitsign --verify`, or an equivalent verification step
- **Scope:** Unchanged
- **Confidentiality Impact:** None
- **Integrity Impact:** Low — exit-code-only callers (scripts, some CI pipelines) treat the panicked verification as success; git's own status-fd path checks for `GOODSIG` and is therefore partially protected
- **Availability Impact:** Low — the verification process aborts via panic on every invocation with such a signature

## Affected Component

- `pkg/git/verifier.go` — `(*CertVerifier).Verify` (line 114)
- `internal/io/streams.go` — `(*Streams).Wrap` (lines 71–84, the recovery that returns nil on panic)

## CWE

- **CWE-129**: Improper Validation of Array Index
- **CWE-390**: Detection of Error Condition Without Action Taken (panic swallowed, nil returned)

## Description

### Unconditional index dereference after GetCertificates

`CertVerifier.Verify()` parses the incoming signature as CMS/PKCS7 and calls `GetCertificates()` to extract the signer's certificate before any signature math takes place:

```go
// pkg/git/verifier.go:109–114
certs, err := sd.GetCertificates()
if err != nil {
    return nil, fmt.Errorf("error getting signature certs: %w", err)
}
cert := certs[0]   // panic: index out of range if certs is empty
```

`GetCertificates()` delegates to `sd.psd.X509Certificates()` (the upstream `smimesign/ietf-cms` library). RFC 5652 §5.1 marks the `certificates` field in `SignedData` as `OPTIONAL`, and an empty or absent set is a structurally valid CMS message. The library returns `(nil, nil)` or `([]*, nil)` for such a message — an empty slice with no error — so the length check on `err` is irrelevant:

```go
// internal/fork/ietf-cms/signed_data.go:53–55
func (sd *SignedData) GetCertificates() (