Skip to content

Commit

Permalink
git: add support for linking a parent tag to a commit
Browse files Browse the repository at this point in the history
Add a new struct `AnnotatedTag` that represents a Git annotated tag and
add it as a new field `Commit.ReferencingTag`, allowing for a commit to
contain information about its referencing tag. Add support for verifying
referecning tag as well.

Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
  • Loading branch information
aryan9600 committed Aug 21, 2023
1 parent 540f61e commit fba7100
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 79 deletions.
79 changes: 62 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 parent tag, that points to this commit.
ReferencingTag *AnnotatedTag
}

// 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")
// with, or an error. It does not verify the signature of the parent
// tag (if present). Users are expected to explicitly verify the parent
// 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)
}

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
}
}
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,39 @@ func (c *Commit) ShortMessage() string {
return subject
}

// AnnotatedTag represents an annotated Git tag.
type AnnotatedTag 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 *AnnotatedTag) 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>
func (t *AnnotatedTag) String() string {
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 +194,22 @@ func IsConcreteCommit(c Commit) bool {
}
return false
}

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

0 comments on commit fba7100

Please sign in to comment.