• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gittuf / gittuf / 29552153989

17 Jul 2026 03:20AM UTC coverage: 78.033% (+0.2%) from 77.791%
29552153989

push

github

web-flow
Merge pull request #1472 from pjbgf/sha256

feat(sha256): support SHA-256 object format

179 of 220 new or added lines in 11 files covered. (81.36%)

2 existing lines in 2 files now uncovered.

12721 of 16302 relevant lines covered (78.03%)

36.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.85
/pkg/gitinterface/commit.go
1
// Copyright The gittuf Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package gitinterface
5

6
import (
7
        "context"
8
        "errors"
9
        "fmt"
10
        "io"
11
        "strings"
12
        "testing"
13
        "time"
14

15
        "github.com/gittuf/gittuf/internal/signerverifier/gpg"
16
        "github.com/gittuf/gittuf/internal/signerverifier/sigstore"
17
        "github.com/gittuf/gittuf/internal/signerverifier/ssh"
18
        "github.com/go-git/go-git/v6/plumbing"
19
        "github.com/go-git/go-git/v6/plumbing/object"
20
        "github.com/go-git/go-git/v6/storage/memory"
21
        "github.com/secure-systems-lab/go-securesystemslib/signerverifier"
22
)
23

24
// Commit creates a new commit in the repo and sets targetRef's to the commit.
25
// This function is meant only for gittuf references, and therefore it does not
26
// mutate repository worktrees.
27
func (r *Repository) Commit(treeID Hash, targetRef, message string, sign bool) (Hash, error) {
190✔
28
        currentGitID, err := r.GetReference(targetRef)
190✔
29
        if err != nil {
307✔
30
                if !errors.Is(err, ErrReferenceNotFound) {
117✔
31
                        return ZeroHash, err
×
32
                }
×
33
        }
34

35
        args := []string{"commit-tree", "-m", message}
190✔
36

190✔
37
        if !currentGitID.IsZero() {
263✔
38
                args = append(args, "-p", currentGitID.String())
73✔
39
        }
73✔
40

41
        if sign {
196✔
42
                args = append(args, "-S")
6✔
43
        }
6✔
44

45
        args = append(args, treeID.String())
190✔
46

190✔
47
        now := r.clock.Now().Format(time.RFC3339)
190✔
48
        env := []string{fmt.Sprintf("%s=%s", committerTimeKey, now), fmt.Sprintf("%s=%s", authorTimeKey, now)}
190✔
49

190✔
50
        stdOut, err := r.executor(args...).withEnv(env...).executeString()
190✔
51
        if err != nil {
190✔
52
                return ZeroHash, fmt.Errorf("unable to create commit: %w", err)
×
53
        }
×
54
        commitID, err := NewHash(stdOut)
190✔
55
        if err != nil {
190✔
56
                return ZeroHash, fmt.Errorf("received invalid commit ID: %w", err)
×
57
        }
×
58

59
        return commitID, r.CheckAndSetReference(targetRef, commitID, currentGitID)
190✔
60
}
61

62
// CommitUsingSpecificKey creates a new commit in the repository for the
63
// specified parameters. The commit is signed using the PEM encoded SSH or GPG
64
// private key. This function is expected for use in tests and gittuf's
65
// developer mode. In standard workflows, Commit() must be used instead which
66
// infers the signing key from the user's Git config.
67
func (r *Repository) CommitUsingSpecificKey(treeID Hash, targetRef, message string, signingKeyPEMBytes []byte) (Hash, error) {
6✔
68
        gitConfig, err := r.GetGitConfig()
6✔
69
        if err != nil {
6✔
70
                return ZeroHash, err
×
71
        }
×
72

73
        commitMetadata := object.Signature{
6✔
74
                Name:  gitConfig["user.name"],
6✔
75
                Email: gitConfig["user.email"],
6✔
76
                When:  r.clock.Now(),
6✔
77
        }
6✔
78

6✔
79
        commit := &object.Commit{
6✔
80
                Author:    commitMetadata,
6✔
81
                Committer: commitMetadata,
6✔
82
                TreeHash:  plumbing.NewHash(treeID.String()),
6✔
83
                Message:   message,
6✔
84
        }
6✔
85

6✔
86
        refTip, err := r.GetReference(targetRef)
6✔
87
        if err != nil {
10✔
88
                if !errors.Is(err, ErrReferenceNotFound) {
4✔
89
                        return ZeroHash, err
×
90
                }
×
91
        }
92

93
        if !refTip.IsZero() {
8✔
94
                commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(refTip.String())}
2✔
95
        }
2✔
96

97
        commitContents, err := getCommitBytesWithoutSignature(commit)
6✔
98
        if err != nil {
6✔
99
                return ZeroHash, err
×
100
        }
×
101
        signature, err := signGitObjectUsingKey(commitContents, signingKeyPEMBytes)
6✔
102
        if err != nil {
7✔
103
                return ZeroHash, err
1✔
104
        }
1✔
105
        // SHA-256 repositories store the signature under the `gpgsig-sha256`
106
        // header (go-git's SignatureSHA256), matching Git's own behavior, so it
107
        // can be read back during verification.
108
        if r.GetObjectFormat() == ObjectFormatSHA256 {
6✔
109
                commit.SignatureSHA256 = signature
1✔
110
        } else {
5✔
111
                commit.Signature = signature
4✔
112
        }
4✔
113

114
        goGitRepo, err := r.GetGoGitRepository()
5✔
115
        if err != nil {
5✔
116
                return ZeroHash, err
×
117
        }
×
118

119
        obj := goGitRepo.Storer.NewEncodedObject()
5✔
120
        if err := commit.Encode(obj); err != nil {
5✔
121
                return ZeroHash, err
×
122
        }
×
123
        commitID, err := goGitRepo.Storer.SetEncodedObject(obj)
5✔
124
        if err != nil {
5✔
125
                return ZeroHash, err
×
126
        }
×
127

128
        commitIDHash, err := NewHash(commitID.String())
5✔
129
        if err != nil {
5✔
130
                return ZeroHash, err
×
131
        }
×
132

133
        return commitIDHash, r.CheckAndSetReference(targetRef, commitIDHash, refTip)
5✔
134
}
135

136
// commitWithParents creates a new commit in the repo but does not update any
137
// references. It is only meant to be used for tests, and therefore accepts
138
// specific parent commit IDs.
139
func (r *Repository) commitWithParents(t *testing.T, treeID Hash, parentIDs []Hash, message string, sign bool) Hash { //nolint:unparam
14✔
140
        args := []string{"commit-tree", "-m", message}
14✔
141

14✔
142
        for _, commitID := range parentIDs {
33✔
143
                args = append(args, "-p", commitID.String())
19✔
144
        }
19✔
145

146
        if sign {
14✔
147
                args = append(args, "-S")
×
148
        }
×
149

150
        args = append(args, treeID.String())
14✔
151

14✔
152
        now := r.clock.Now().Format(time.RFC3339)
14✔
153
        env := []string{fmt.Sprintf("%s=%s", committerTimeKey, now), fmt.Sprintf("%s=%s", authorTimeKey, now)}
14✔
154

14✔
155
        stdOut, err := r.executor(args...).withEnv(env...).executeString()
14✔
156
        if err != nil {
14✔
157
                t.Fatal(fmt.Errorf("unable to create commit: %w", err))
×
158
        }
×
159
        commitID, err := NewHash(stdOut)
14✔
160
        if err != nil {
14✔
161
                t.Fatal(fmt.Errorf("received invalid commit ID: %w", err))
×
162
        }
×
163

164
        return commitID
14✔
165
}
166

167
// verifyCommitSignature verifies a signature for the specified commit using
168
// the provided public key.
169
func (r *Repository) verifyCommitSignature(ctx context.Context, commitID Hash, key *signerverifier.SSLibKey) error {
14✔
170
        goGitRepo, err := r.GetGoGitRepository()
14✔
171
        if err != nil {
14✔
172
                return fmt.Errorf("error opening repository: %w", err)
×
173
        }
×
174

175
        commit, err := goGitRepo.CommitObject(plumbing.NewHash(commitID.String()))
14✔
176
        if err != nil {
14✔
177
                return fmt.Errorf("unable to load commit object: %w", err)
×
178
        }
×
179

180
        commitContents, err := getCommitBytesWithoutSignature(commit)
14✔
181
        if err != nil {
14✔
NEW
182
                return fmt.Errorf("unable to encode commit contents for verification: %w", err)
×
NEW
183
        }
×
184

185
        commitSignature := signatureForObjectID(commitID, commit.Signature, commit.SignatureSHA256)
14✔
186

14✔
187
        if signatureBlockCount(commitSignature) > 1 {
16✔
188
                return errors.Join(ErrIncorrectVerificationKey, ErrMultipleSignatures)
2✔
189
        }
2✔
190

191
        switch key.KeyType {
12✔
192
        case gpg.KeyType:
3✔
193
                verifier, err := gpg.NewVerifierFromKey(key)
3✔
194
                if err != nil {
3✔
NEW
195
                        return errors.Join(ErrIncorrectVerificationKey, err)
×
NEW
196
                }
×
197
                if err := verifier.Verify(ctx, commitContents, []byte(commitSignature)); err != nil {
5✔
198
                        return ErrIncorrectVerificationKey
2✔
199
                }
2✔
200

201
                return nil
1✔
202
        case ssh.KeyType:
7✔
203
                if err := verifySSHKeySignature(ctx, key, commitContents, []byte(commitSignature)); err != nil {
9✔
204
                        return errors.Join(ErrIncorrectVerificationKey, err)
2✔
205
                }
2✔
206

207
                return nil
5✔
208
        case sigstore.KeyType:
×
NEW
209
                if err := verifyGitsignSignature(ctx, r, key, commitContents, []byte(commitSignature)); err != nil {
×
210
                        return errors.Join(ErrIncorrectVerificationKey, err)
×
211
                }
×
212

213
                return nil
×
214
        }
215

216
        return ErrUnknownSigningMethod
2✔
217
}
218

219
// GetCommitMessage returns the commit's message.
220
func (r *Repository) GetCommitMessage(commitID Hash) (string, error) {
2✔
221
        if err := r.ensureIsCommit(commitID); err != nil {
3✔
222
                return "", err
1✔
223
        }
1✔
224

225
        commitMessage, err := r.executor("show", "-s", "--format=%B", commitID.String()).executeString()
1✔
226
        if err != nil {
1✔
227
                return "", fmt.Errorf("unable to identify message for commit '%s': %w", commitID.String(), err)
×
228
        }
×
229

230
        return commitMessage, nil
1✔
231
}
232

233
// GetCommitTreeID returns the commit's Git tree ID.
234
func (r *Repository) GetCommitTreeID(commitID Hash) (Hash, error) {
103✔
235
        if err := r.ensureIsCommit(commitID); err != nil {
104✔
236
                return ZeroHash, err
1✔
237
        }
1✔
238

239
        stdOut, err := r.executor("rev-parse", fmt.Sprintf("%s^{tree}", commitID.String())).executeString()
102✔
240
        if err != nil {
102✔
241
                return ZeroHash, fmt.Errorf("unable to identify tree for commit '%s': %w", commitID.String(), err)
×
242
        }
×
243

244
        hash, err := NewHash(stdOut)
102✔
245
        if err != nil {
102✔
246
                return ZeroHash, fmt.Errorf("invalid tree for commit ID '%s': %w", commitID, err)
×
247
        }
×
248
        return hash, nil
102✔
249
}
250

251
// GetCommitParentIDs returns the commit's parent commit IDs.
252
func (r *Repository) GetCommitParentIDs(commitID Hash) ([]Hash, error) {
14✔
253
        if err := r.ensureIsCommit(commitID); err != nil {
15✔
254
                return nil, err
1✔
255
        }
1✔
256

257
        stdOut, err := r.executor("rev-parse", fmt.Sprintf("%s^@", commitID.String())).executeString()
13✔
258
        if err != nil {
13✔
259
                return nil, fmt.Errorf("unable to identify parents for commit '%s': %w", commitID.String(), err)
×
260
        }
×
261

262
        commitIDSplit := strings.Split(stdOut, "\n")
13✔
263
        if len(commitIDSplit) == 0 {
13✔
264
                return nil, nil
×
265
        }
×
266

267
        commitIDs := []Hash{}
13✔
268
        for _, commitID := range commitIDSplit {
30✔
269
                if commitID == "" {
19✔
270
                        continue
2✔
271
                }
272

273
                hash, err := NewHash(commitID)
15✔
274
                if err != nil {
15✔
275
                        return nil, fmt.Errorf("invalid parent commit ID '%s': %w", commitID, err)
×
276
                }
×
277

278
                commitIDs = append(commitIDs, hash)
15✔
279
        }
280

281
        if len(commitIDs) == 0 {
15✔
282
                return nil, nil
2✔
283
        }
2✔
284

285
        return commitIDs, nil
11✔
286
}
287

288
// KnowsCommit returns true if the `testCommit` is a descendent of the
289
// `ancestorCommit`. That is, the testCommit _knows_ the ancestorCommit as it
290
// has a path in the commit graph to the ancestorCommit.
291
func (r *Repository) KnowsCommit(testCommitID, ancestorCommitID Hash) (bool, error) {
7✔
292
        if err := r.ensureIsCommit(testCommitID); err != nil {
8✔
293
                return false, err
1✔
294
        }
1✔
295
        if err := r.ensureIsCommit(ancestorCommitID); err != nil {
7✔
296
                return false, err
1✔
297
        }
1✔
298

299
        _, err := r.executor("merge-base", "--is-ancestor", ancestorCommitID.String(), testCommitID.String()).executeString()
5✔
300
        return err == nil, nil
5✔
301
}
302

303
// GetCommonAncestor finds the common ancestor commit for the two supplied
304
// commits.
305
func (r *Repository) GetCommonAncestor(commitAID, commitBID Hash) (Hash, error) {
4✔
306
        if err := r.ensureIsCommit(commitAID); err != nil {
5✔
307
                return nil, err
1✔
308
        }
1✔
309
        if err := r.ensureIsCommit(commitBID); err != nil {
4✔
310
                return nil, err
1✔
311
        }
1✔
312

313
        mergeBase, err := r.executor("merge-base", commitAID.String(), commitBID.String()).executeString()
2✔
314
        if err != nil {
3✔
315
                return nil, err
1✔
316
        }
1✔
317

318
        mergeBaseID, err := NewHash(mergeBase)
1✔
319
        if err != nil {
1✔
320
                return nil, fmt.Errorf("received invalid commit ID: %w", err)
×
321
        }
×
322
        return mergeBaseID, nil
1✔
323
}
324

325
// ensureIsCommit is a helper to check that the ID represents a Git commit
326
// object.
327
func (r *Repository) ensureIsCommit(commitID Hash) error {
162✔
328
        objType, err := r.executor("cat-file", "-t", commitID.String()).executeString()
162✔
329
        if err != nil {
163✔
330
                return fmt.Errorf("unable to inspect if object is commit: %w", err)
1✔
331
        } else if objType != "commit" {
174✔
332
                return fmt.Errorf("requested Git ID '%s' is not a commit object", commitID.String())
12✔
333
        }
12✔
334

335
        return nil
149✔
336
}
337

338
func getCommitBytesWithoutSignature(commit *object.Commit) ([]byte, error) {
20✔
339
        commitEncoded := memory.NewStorage().NewEncodedObject()
20✔
340
        if err := commit.EncodeWithoutSignature(commitEncoded); err != nil {
20✔
341
                return nil, err
×
342
        }
×
343
        r, err := commitEncoded.Reader()
20✔
344
        if err != nil {
20✔
345
                return nil, err
×
346
        }
×
347

348
        return io.ReadAll(r)
20✔
349
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc