krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

main: internal/sig/commit.go · raw

  1// Package sig verifies OpenPGP and SSHSIG signatures on git commits and
  2// tags, and maps them to the forge's trust states.
  3package sig
  4
  5import (
  6	"bytes"
  7	"fmt"
  8	"strings"
  9)
 10
 11// Commit is a parsed raw commit object.
 12type Commit struct {
 13	Raw            []byte
 14	Payload        []byte // Raw with the gpgsig header removed, byte-exact
 15	Signature      []byte // armored signature block, nil if unsigned
 16	AuthorName     string
 17	AuthorEmail    string
 18	CommitterEmail string
 19	Subject        string
 20	AuthorUnix     int64
 21}
 22
 23// ParseCommit splits a raw commit object (as printed by `git cat-file
 24// commit`) into its signed payload and signature. The payload must be
 25// byte-exact: it is the original object minus the gpgsig header line and its
 26// continuation lines, nothing else.
 27func ParseCommit(raw []byte) (*Commit, error) {
 28	c := &Commit{Raw: raw}
 29
 30	headerEnd := bytes.Index(raw, []byte("\n\n"))
 31	if headerEnd < 0 {
 32		return nil, fmt.Errorf("malformed commit: no header/body separator")
 33	}
 34	headers := raw[:headerEnd+1] // include trailing newline of last header
 35	body := raw[headerEnd+2:]
 36
 37	var payload bytes.Buffer
 38	lines := bytes.SplitAfter(headers, []byte("\n"))
 39	for i := 0; i < len(lines); i++ {
 40		line := lines[i]
 41		if sigBody, ok := bytes.CutPrefix(line, []byte("gpgsig ")); ok {
 42			// The signature value continues on lines starting with a space.
 43			var sig bytes.Buffer
 44			sig.Write(sigBody)
 45			for i+1 < len(lines) && bytes.HasPrefix(lines[i+1], []byte(" ")) {
 46				sig.Write(lines[i+1][1:])
 47				i++
 48			}
 49			c.Signature = bytes.TrimSuffix(sig.Bytes(), []byte("\n"))
 50			continue
 51		}
 52		payload.Write(line)
 53
 54		switch {
 55		case bytes.HasPrefix(line, []byte("author ")):
 56			c.AuthorName, c.AuthorEmail, c.AuthorUnix = parseIdent(string(line[len("author "):]))
 57		case bytes.HasPrefix(line, []byte("committer ")):
 58			_, c.CommitterEmail, _ = parseIdent(string(line[len("committer "):]))
 59		}
 60	}
 61	payload.WriteByte('\n')
 62	payload.Write(body)
 63	c.Payload = payload.Bytes()
 64
 65	if i := bytes.IndexByte(body, '\n'); i >= 0 {
 66		c.Subject = string(body[:i])
 67	} else {
 68		c.Subject = strings.TrimRight(string(body), "\n")
 69	}
 70	return c, nil
 71}
 72
 73// parseIdent parses "Name <email> unix tz".
 74func parseIdent(s string) (name, email string, unix int64) {
 75	s = strings.TrimSuffix(s, "\n")
 76	lt := strings.IndexByte(s, '<')
 77	gt := strings.IndexByte(s, '>')
 78	if lt < 0 || gt < lt {
 79		return s, "", 0
 80	}
 81	name = strings.TrimSpace(s[:lt])
 82	email = s[lt+1 : gt]
 83	rest := strings.Fields(s[gt+1:])
 84	if len(rest) >= 1 {
 85		fmt.Sscanf(rest[0], "%d", &unix)
 86	}
 87	return name, email, unix
 88}
 89
 90// SigKind reports which signature format a gpgsig block holds.
 91type SigKind int
 92
 93const (
 94	SigNone SigKind = iota
 95	SigOpenPGP
 96	SigSSH
 97	SigUnknown
 98)
 99
100func KindOf(sig []byte) SigKind {
101	switch {
102	case sig == nil:
103		return SigNone
104	case bytes.Contains(sig, []byte("BEGIN PGP SIGNATURE")):
105		return SigOpenPGP
106	case bytes.Contains(sig, []byte("BEGIN SSH SIGNATURE")):
107		return SigSSH
108	default:
109		return SigUnknown
110	}
111}