Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

236 lines
6.1 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gpg
import (
"bytes"
"context"
"encoding/pem"
"fmt"
"io"
"os/exec"
"strings"
"golang.org/x/sync/errgroup"
)
func execGpgAndReadOutput(ctx context.Context, in []byte, args []string) (*bytes.Buffer, *bytes.Buffer, error) {
cmdStr := fmt.Sprintf("gpg %s", strings.Join(args, " "))
cmd := exec.CommandContext(ctx, "gpg", args...)
stdOut, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, fmt.Errorf("failed to get stdout for command '%s': %w", cmdStr, err)
}
stdErr, err := cmd.StderrPipe()
if err != nil {
return nil, nil, fmt.Errorf("failed to get stderr for command '%s': %w", cmdStr, err)
}
stdIn, err := cmd.StdinPipe()
if err != nil {
return nil, nil, fmt.Errorf("failed to get stdin for command '%s': %w", cmdStr, err)
}
err = cmd.Start()
if err != nil {
return nil, nil, fmt.Errorf("failed to start command '%s': %w", cmdStr, err)
}
eg, egCtx := errgroup.WithContext(ctx)
outBuf := listenToOut(egCtx, eg, stdOut)
errBuf := listenToOut(egCtx, eg, stdErr)
n, err := io.Copy(stdIn, strings.NewReader(string(in)))
if err != nil {
return nil, nil, fmt.Errorf("failed to write stdin for command '%s': %w", cmdStr, err)
} else if n < int64(len(in)) {
return nil, nil, fmt.Errorf("failed to write stdin for command '%s': EOF before all bytes written", cmd)
}
err = stdIn.Close()
if err != nil {
return nil, nil, fmt.Errorf("failed to close stdin for command '%s': %w", cmdStr, err)
}
var exitCode int
for {
state, err := cmd.Process.Wait()
if err != nil {
return nil, nil, fmt.Errorf("failed to wait for command '%s': %w", cmdStr, err)
}
if !state.Exited() {
continue
}
exitCode = state.ExitCode()
break
}
err = eg.Wait()
if err != nil {
return nil, nil, fmt.Errorf("failed to read output for command '%s': %w", cmdStr, err)
}
if exitCode != 0 {
return nil, nil, fmt.Errorf("command '%s' exited with code %d. stdout: '%s', stderr: '%s'", cmdStr, exitCode, outBuf.String(), errBuf.String())
}
return outBuf, errBuf, nil
}
// ImportKey imports a key from a file using gpg
func ImportKey(ctx context.Context, keyFile string) error {
args := []string{"--import", keyFile}
_, ioErr, err := execGpgAndReadOutput(ctx, nil, args)
if err != nil && ioErr != nil && len(ioErr.String()) > 0 {
err = fmt.Errorf("`gpg --import %s` error gpg_output: %s - %w", keyFile, ioErr.String(), err)
}
return err
}
// ListKey returns the output of `gpg --list-keys`
func ListKeys(ctx context.Context) ([]byte, error) {
args := []string{"--list-keys"}
outBuf, _, err := execGpgAndReadOutput(ctx, nil, args)
if err != nil {
return nil, err
}
return outBuf.Bytes(), nil
}
// HasKey returns true if the key with the given keyId is present when `gpg --list-keys` is run
func HasKey(ctx context.Context, keyId string) (bool, error) {
args := []string{"--list-keys", keyId}
outBuf, _, err := execGpgAndReadOutput(ctx, nil, args)
if err != nil {
return false, err
}
return strings.Contains(outBuf.String(), keyId), nil
}
// Signer signs commit payloads using a GPG key. KeyId identifies which key from the
// local GPG keyring to use.
type Signer struct{ KeyId string }
// Sign produces a detached GPG signature of |payload| using the key identified by [Signer.KeyId].
func (s *Signer) Sign(ctx context.Context, payload []byte) ([]byte, error) {
outBuf, _, err := execGpgAndReadOutput(ctx, payload, []string{"--clear-sign", "-u", s.KeyId})
if err != nil {
return nil, err
}
return outBuf.Bytes(), nil
}
// Verify verifies a signature and returns the GPG output (signer info, etc).
func Verify(ctx context.Context, signature []byte) ([]byte, error) {
args := []string{"--verify"}
_, errBuf, err := execGpgAndReadOutput(ctx, signature, args)
if err != nil {
return nil, err
}
return errBuf.Bytes(), nil
}
func listenToOut(ctx context.Context, eg *errgroup.Group, r io.Reader) *bytes.Buffer {
buf := bytes.NewBuffer(nil)
eg.Go(func() error {
_, err := io.Copy(buf, r)
return err
})
return buf
}
// DecodeAllPEMBlocks decodes all PEM blocks from a byte slice.
func DecodeAllPEMBlocks(bs []byte) ([]*pem.Block, error) {
const beginHeaderPrefix = "BEGIN "
const pemSeperator = "-----"
sections := strings.Split(string(bs), pemSeperator)
filtered := make([]string, 0, len(sections))
for i, section := range sections {
section := strings.TrimSpace(section)
if i == 0 || i == len(sections)-1 {
if section == "" {
continue
}
}
filtered = append(filtered, section)
}
pemBlocks := make([]*pem.Block, 0, len(filtered))
for i := 0; i < len(filtered); {
headerName := filtered[i]
i++
if strings.HasPrefix(headerName, beginHeaderPrefix) {
headerName = headerName[len(beginHeaderPrefix):]
body := filtered[i]
i++
headers := make(map[string]string)
lines := strings.Split(body, "\n")
for j, line := range lines {
trimmed := strings.TrimSpace(line)
tokens := strings.Split(trimmed, ":")
if len(tokens) == 2 {
headers[strings.TrimSpace(tokens[0])] = strings.TrimSpace(tokens[1])
} else {
if j > 0 {
if lines[j] == "" {
j++
}
lines = lines[j:]
}
break
}
}
body = strings.Join(lines, "\n")
pemBlocks = append(pemBlocks, &pem.Block{
Type: headerName,
Headers: headers,
Bytes: []byte(body),
})
}
}
return pemBlocks, nil
}
// GetBlocksOfType returns all PEM blocks of a given type.
func GetBlocksOfType(blocks []*pem.Block, blTypeStr string) []*pem.Block {
var ret []*pem.Block
for _, block := range blocks {
if block.Type == blTypeStr {
ret = append(ret, block)
}
}
return ret
}