package replica import ( "bytes" "crypto/sha512" "encoding/base64" "errors" "fmt" "io" "slices" "strings" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/storer" "golang.org/x/crypto/ssh" ) var ( ErrUnsigned = errors.New("replica: commit is not signed") ErrNotSSHSignature = errors.New("replica: signature is not an SSH signature") ErrMalformed = errors.New("replica: malformed SSH signature") ErrNamespace = errors.New("replica: signature namespace is not git") ErrWrongKey = errors.New("replica: commit signed by another key") ErrBadSignature = errors.New("replica: signature does not match the commit") ) const ( sigHeader = "gpgsig " sigHeaderSHA256 = "gpgsig-sha256 " ) func VerifyCommit(objects storer.EncodedObjectStorer, id plumbing.Hash, key ssh.PublicKey) (*object.Commit, error) { if key == nil || key.Type() != ssh.KeyAlgoED25519 { return nil, ErrUnsupportedKey } return VerifySSHCommit(objects, id, key, ssh.KeyAlgoED25519) } func VerifySSHCommit(objects storer.EncodedObjectStorer, id plumbing.Hash, key ssh.PublicKey, formats ...string) (*object.Commit, error) { if key == nil || len(formats) == 0 { return nil, ErrUnsupportedKey } obj, err := objects.EncodedObject(plumbing.CommitObject, id) if err != nil { return nil, err } rd, err := obj.Reader() if err != nil { return nil, err } raw, err := io.ReadAll(rd) rd.Close() if err != nil { return nil, err } payload, armored, err := splitSignature(raw) if err != nil { return nil, fmt.Errorf("%w: %s", err, id) } blob, err := dearmor(armored) if err != nil { return nil, fmt.Errorf("%w: %s", err, id) } if err := verifySSHBlob(blob, payload, key, formats); err != nil { return nil, fmt.Errorf("%w: %s", err, id) } return object.DecodeCommit(objects, obj) } func splitSignature(raw []byte) ([]byte, []byte, error) { end := bytes.Index(raw, []byte("\n\n")) if end < 0 { return nil, nil, ErrUnsigned } headers, rest := raw[:end+1], raw[end+1:] var payload, sig bytes.Buffer found, inSig := 0, false for len(headers) > 0 { i := bytes.IndexByte(headers, '\n') line := headers[:i+1] headers = headers[i+1:] if inSig && line[0] == ' ' { sig.Write(line[1:]) continue } inSig = false switch { case bytes.HasPrefix(line, []byte(sigHeader)): found++ inSig = true sig.Write(line[len(sigHeader):]) case bytes.HasPrefix(line, []byte(sigHeaderSHA256)): return nil, nil, ErrMalformed default: payload.Write(line) } } switch { case found == 0: return nil, nil, ErrUnsigned case found > 1: return nil, nil, ErrMalformed } payload.Write(rest) return payload.Bytes(), sig.Bytes(), nil } func dearmor(armored []byte) ([]byte, error) { text := string(armored) if !strings.HasPrefix(text, armorBegin+"\n") { return nil, ErrNotSSHSignature } body := strings.TrimSuffix(strings.TrimPrefix(text, armorBegin+"\n"), "\n") if !strings.HasSuffix(body, "\n"+armorEnd) { return nil, ErrMalformed } body = strings.TrimSuffix(body, "\n"+armorEnd) blob, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(body, "\n", "")) if err != nil { return nil, ErrMalformed } return blob, nil } func verifyBlob(blob, payload []byte, key ssh.PublicKey) error { return verifySSHBlob(blob, payload, key, []string{ssh.KeyAlgoED25519}) } func verifySSHBlob(blob, payload []byte, key ssh.PublicKey, formats []string) error { if !bytes.HasPrefix(blob, []byte(sigMagic)) { return ErrMalformed } var w sigBlob if err := ssh.Unmarshal(blob[len(sigMagic):], &w); err != nil { return ErrMalformed } if w.Version != sigVersion || w.Reserved != "" || w.HashAlg != sigHashAlg { return ErrMalformed } if w.Namespace != Namespace { return ErrNamespace } signer, err := ssh.ParsePublicKey(w.PublicKey) if err != nil { return ErrMalformed } if !bytes.Equal(signer.Marshal(), key.Marshal()) { return ErrWrongKey } var sig ssh.Signature if err := ssh.Unmarshal(w.Signature, &sig); err != nil || len(sig.Rest) != 0 || !slices.Contains(formats, sig.Format) { return ErrMalformed } sum := sha512.Sum512(payload) signed := append([]byte(sigMagic), ssh.Marshal(signedData{ Namespace: w.Namespace, HashAlg: w.HashAlg, Hash: sum[:], })...) if err := key.Verify(signed, &sig); err != nil { return ErrBadSignature } return nil }