Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

git: Add support for adding tag info to commit #597

Merged
merged 5 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 77 additions & 17 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ type Commit struct {
Encoded []byte
// Message is the commit message, containing arbitrary text.
Message string
// ReferencingTag is the tag that points to this commit.
ReferencingTag *Tag
}

// String returns a string representation of the Commit, composed
Expand All @@ -113,24 +115,15 @@ func (c *Commit) AbsoluteReference() string {

// Verify the Signature of the commit with the given key rings.
// It returns the fingerprint of the key the signature was verified
// with, or an error.
func (c *Commit) Verify(keyRing ...string) (string, error) {
if c.Signature == "" {
return "", fmt.Errorf("commit does not have a PGP signature")
}

for _, r := range keyRing {
reader := strings.NewReader(r)
keyring, err := openpgp.ReadArmoredKeyRing(reader)
if err != nil {
return "", fmt.Errorf("unable to read armored key ring: %w", err)
}
signer, err := openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewBuffer(c.Encoded), bytes.NewBufferString(c.Signature), nil)
if err == nil {
return signer.PrimaryKey.KeyIdString(), nil
}
// with, or an error. It does not verify the signature of the referencing
// tag (if present). Users are expected to explicitly verify the referencing
// tag's signature using `c.ReferencingTag.Verify()`
func (c *Commit) Verify(keyRings ...string) (string, error) {
fingerprint, err := verifySignature(c.Signature, c.Encoded, keyRings...)
if err != nil {
return "", fmt.Errorf("unable to verify Git commit: %w", err)
}
return "", fmt.Errorf("unable to verify commit with any of the given key rings")
return fingerprint, nil
}

// ShortMessage returns the first 50 characters of a commit subject.
Expand All @@ -143,6 +136,44 @@ func (c *Commit) ShortMessage() string {
return subject
}

// Tag represents a Git tag.
type Tag struct {
// Hash is the hash of the tag.
Hash Hash
// Name is the name of the tag.
Name string
// Author is the original author of the tag.
Author Signature
// Signature is the PGP signature of the tag.
Signature string
// Encoded is the encoded tag, without any signature.
Encoded []byte
// Message is the tag message, containing arbitrary text.
Message string
}

// Verify the Signature of the tag with the given key rings.
// It returns the fingerprint of the key the signature was verified
// with, or an error.
func (t *Tag) Verify(keyRings ...string) (string, error) {
fingerprint, err := verifySignature(t.Signature, t.Encoded, keyRings...)
if err != nil {
return "", fmt.Errorf("unable to verify Git tag: %w", err)
}
return fingerprint, nil
}

// String returns a short string representation of the tag in the format
// of <name@hash>, for eg: "1.0.0@a0c14dc8580a23f79bc654faa79c4f62b46c2c22"
// If the tag is lightweight, it won't have a hash, so it'll simply return
// the tag name, i.e. "1.0.0".
func (t *Tag) String() string {
if len(t.Hash) == 0 {
return t.Name
}
return fmt.Sprintf("%s@%s", t.Name, t.Hash.String())
}

// ErrRepositoryNotFound indicates that the repository (or the ref in
// question) does not exist at the given URL.
type ErrRepositoryNotFound struct {
Expand All @@ -168,3 +199,32 @@ func IsConcreteCommit(c Commit) bool {
}
return false
}

// IsAnnotatedTag returns true if the provided tag is annotated.
func IsAnnotatedTag(t Tag) bool {
return len(t.Encoded) > 0
}

// IsSignedTag returns true if the provided tag has a signature.
func IsSignedTag(t Tag) bool {
return t.Signature != ""
}

func verifySignature(sig string, payload []byte, keyRings ...string) (string, error) {
if sig == "" {
return "", fmt.Errorf("unable to verify payload as the provided signature is empty")
}

for _, r := range keyRings {
reader := strings.NewReader(r)
keyring, err := openpgp.ReadArmoredKeyRing(reader)
if err != nil {
return "", fmt.Errorf("unable to read armored key ring: %w", err)
}
signer, err := openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewBuffer(payload), bytes.NewBufferString(sig), nil)
if err == nil {
return signer.PrimaryKey.KeyIdString(), nil
}
}
return "", fmt.Errorf("unable to verify payload with any of the given key rings")
}
117 changes: 55 additions & 62 deletions git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,61 @@ func TestHash_Algorithm(t *testing.T) {
}
}

func Test_verifySignature(t *testing.T) {
tests := []struct {
name string
payload []byte
sig string
keyRings []string
want string
wantErr string
}{
{
name: "Valid commit signature",
payload: []byte(encodedCommitFixture),
sig: signatureCommitFixture,
keyRings: []string{armoredKeyRingFixture},
want: keyRingFingerprintFixture,
},
{
name: "Malformed encoded commit",
payload: []byte(malformedEncodedCommitFixture),
sig: signatureCommitFixture,
keyRings: []string{armoredKeyRingFixture},
wantErr: "unable to verify payload with any of the given key rings",
},
{
name: "Malformed key ring",
payload: []byte(encodedCommitFixture),
sig: signatureCommitFixture,
keyRings: []string{malformedKeyRingFixture},
wantErr: "unable to read armored key ring: unexpected EOF",
},
{
name: "Missing signature",
payload: []byte(encodedCommitFixture),
keyRings: []string{armoredKeyRingFixture},
wantErr: "unable to verify payload as the provided signature is empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

got, err := verifySignature(tt.sig, tt.payload, tt.keyRings...)
if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
g.Expect(got).To(BeEmpty())
return
}

g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(Equal(tt.want))
})
}
}

func TestHash_Digest(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -266,68 +321,6 @@ func TestCommit_AbsoluteReference(t *testing.T) {
}
}

func TestCommit_Verify(t *testing.T) {
tests := []struct {
name string
commit *Commit
keyRings []string
want string
wantErr string
}{
{
name: "Valid commit signature",
commit: &Commit{
Encoded: []byte(encodedCommitFixture),
Signature: signatureCommitFixture,
},
keyRings: []string{armoredKeyRingFixture},
want: keyRingFingerprintFixture,
},
{
name: "Malformed encoded commit",
commit: &Commit{
Encoded: []byte(malformedEncodedCommitFixture),
Signature: signatureCommitFixture,
},
keyRings: []string{armoredKeyRingFixture},
wantErr: "unable to verify commit with any of the given key rings",
},
{
name: "Malformed key ring",
commit: &Commit{
Encoded: []byte(encodedCommitFixture),
Signature: signatureCommitFixture,
},
keyRings: []string{malformedKeyRingFixture},
wantErr: "unable to read armored key ring: unexpected EOF",
},
{
name: "Missing signature",
commit: &Commit{
Encoded: []byte(encodedCommitFixture),
},
keyRings: []string{armoredKeyRingFixture},
wantErr: "commit does not have a PGP signature",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

got, err := tt.commit.Verify(tt.keyRings...)
if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
g.Expect(got).To(BeEmpty())
return
}

g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(Equal(tt.want))
})
}
}

func TestCommit_ShortMessage(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading