436 lines
14 KiB
Go
436 lines
14 KiB
Go
// Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
|
|
// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
|
|
// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// 1. Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
//
|
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
// this list of conditions and the following disclaimer in the documentation
|
|
// and/or other materials provided with the distribution.
|
|
//
|
|
// 3. Neither the name of the copyright holder nor the names of its contributors
|
|
// may be used to endorse or promote products derived from this software without
|
|
// specific prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
package keykeeperv1
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/google/uuid"
|
|
"go.temporal.io/sdk/activity"
|
|
"go.temporal.io/sdk/client"
|
|
"go.temporal.io/sdk/workflow"
|
|
"google.golang.org/genproto/googleapis/rpc/errdetails"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
peridotworkflow "peridot.resf.org/peridot/builder/v1/workflow"
|
|
"peridot.resf.org/peridot/db/models"
|
|
keykeeperpb "peridot.resf.org/peridot/keykeeper/pb"
|
|
peridotpb "peridot.resf.org/peridot/pb"
|
|
"peridot.resf.org/utils"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ErrUnsupportedExtension = errors.New("unsupported extension")
|
|
)
|
|
|
|
func (s *Server) SignArtifactsWorkflow(ctx workflow.Context, artifacts models.TaskArtifacts, buildId string, task *models.Task, keyName string) (*keykeeperpb.SignArtifactsTask, error) {
|
|
taskResponse := &keykeeperpb.SignArtifactsTask{
|
|
SignedArtifacts: []*keykeeperpb.SignedArtifact{},
|
|
}
|
|
|
|
err := s.db.SetTaskStatus(task.ID.String(), peridotpb.TaskStatus_TASK_STATUS_RUNNING)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
task.Status = peridotpb.TaskStatus_TASK_STATUS_FAILED
|
|
|
|
defer func() {
|
|
if taskResponse != nil {
|
|
taskResponseAny, err := anypb.New(taskResponse)
|
|
if err != nil {
|
|
s.log.Errorf("could not create anypb for task: %v", err)
|
|
task.Status = peridotpb.TaskStatus_TASK_STATUS_FAILED
|
|
} else {
|
|
err = s.db.SetTaskResponse(task.ID.String(), taskResponseAny)
|
|
if err != nil {
|
|
s.log.Errorf("could not set task info: %v", err)
|
|
task.Status = peridotpb.TaskStatus_TASK_STATUS_FAILED
|
|
}
|
|
}
|
|
}
|
|
|
|
err := s.db.SetTaskStatus(task.ID.String(), task.Status)
|
|
if err != nil {
|
|
s.log.Errorf("could not set task status: %v", err)
|
|
}
|
|
}()
|
|
|
|
if artifacts == nil {
|
|
var err error
|
|
artifacts, err = s.db.GetArtifactsForBuild(buildId)
|
|
if err != nil {
|
|
s.log.Errorf("failed to get artifacts for build %s: %v", buildId, err)
|
|
return nil, status.Error(codes.Internal, "failed to get artifacts")
|
|
}
|
|
if len(artifacts) == 0 {
|
|
return taskResponse, nil
|
|
}
|
|
}
|
|
|
|
var futures []peridotworkflow.FutureContext
|
|
for _, artifact := range artifacts {
|
|
signArtifactCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
|
ScheduleToStartTimeout: 10 * time.Hour,
|
|
StartToCloseTimeout: 24 * time.Hour,
|
|
HeartbeatTimeout: 10 * time.Minute,
|
|
TaskQueue: TaskQueue,
|
|
})
|
|
futures = append(futures, peridotworkflow.FutureContext{
|
|
Ctx: signArtifactCtx,
|
|
Future: workflow.ExecuteActivity(signArtifactCtx, s.SignArtifactActivity, artifact.ID.String(), keyName),
|
|
TaskQueue: TaskQueue,
|
|
})
|
|
}
|
|
|
|
for _, future := range futures {
|
|
signedArtifact := &keykeeperpb.SignedArtifact{}
|
|
err := future.Future.Get(future.Ctx, signedArtifact)
|
|
if err != nil && !strings.Contains(err.Error(), "unsupported extension") {
|
|
s.log.Errorf("could not get sign artifact: %v", err)
|
|
return nil, err
|
|
}
|
|
if signedArtifact != nil {
|
|
taskResponse.SignedArtifacts = append(taskResponse.SignedArtifacts, signedArtifact)
|
|
}
|
|
}
|
|
|
|
task.Status = peridotpb.TaskStatus_TASK_STATUS_SUCCEEDED
|
|
|
|
return taskResponse, nil
|
|
}
|
|
|
|
func (s *Server) SignArtifactActivity(ctx context.Context, artifactId string, keyName string) (*keykeeperpb.SignedArtifact, error) {
|
|
go func() {
|
|
for {
|
|
activity.RecordHeartbeat(ctx)
|
|
time.Sleep(4 * time.Second)
|
|
}
|
|
}()
|
|
|
|
artifact, err := s.db.GetTaskArtifactById(artifactId)
|
|
if err != nil {
|
|
s.log.Errorf("could not get artifact: %v", err)
|
|
return nil, status.Errorf(codes.Internal, "could not get artifact")
|
|
}
|
|
|
|
key, err := s.EnsureGPGKey(keyName)
|
|
if err != nil {
|
|
s.log.Errorf("failed to load key %s: %v", keyName, err)
|
|
return nil, status.Error(codes.Internal, "failed to load key")
|
|
}
|
|
|
|
newObjectKey := fmt.Sprintf("%s/%s/%s", filepath.Dir(artifact.Name), key.gpgId, filepath.Base(artifact.Name))
|
|
existingHash, err := s.db.GetTaskArtifactSignatureHash(artifact.Name, key.keyUuid.String())
|
|
if err != nil && err != sql.ErrNoRows {
|
|
s.log.Errorf("failed to get existing hash: %v", err)
|
|
return nil, status.Error(codes.Internal, "failed to get existing hash")
|
|
}
|
|
if err == nil && existingHash != "" {
|
|
return &keykeeperpb.SignedArtifact{
|
|
Path: newObjectKey,
|
|
HashSha256: existingHash,
|
|
}, nil
|
|
}
|
|
|
|
ranUuid := uuid.New()
|
|
localPath := fmt.Sprintf("/keykeeper/artifacts/%s-%s", ranUuid.String(), filepath.Base(artifact.Name))
|
|
err = s.storage.DownloadObject(artifact.Name, localPath)
|
|
if err != nil {
|
|
s.log.Errorf("failed to download artifact %s: %v", artifact.Name, err)
|
|
return nil, fmt.Errorf("failed to download artifact %s: %v", artifact.Name, err)
|
|
}
|
|
defer func() {
|
|
_ = os.Remove(localPath)
|
|
}()
|
|
ext := filepath.Ext(artifact.Name)
|
|
|
|
switch ext {
|
|
case ".rpm":
|
|
beginTx, err := s.db.Begin()
|
|
if err != nil {
|
|
s.log.Errorf("failed to begin transaction: %v", err)
|
|
return nil, status.Error(codes.Internal, "failed to begin transaction")
|
|
}
|
|
tx := s.db.UseTransaction(beginTx)
|
|
|
|
rpmSign := func() (*keykeeperpb.SignedArtifact, error) {
|
|
var outBuf bytes.Buffer
|
|
opts := []string{
|
|
"--define", "_gpg_name " + keyName,
|
|
"--define", "_peridot_keykeeper_key " + key.keyUuid.String(),
|
|
"--addsign", localPath,
|
|
}
|
|
cmd := gpgCmdEnv(exec.Command("rpm", opts...))
|
|
cmd.Stdout = &outBuf
|
|
cmd.Stderr = &outBuf
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
s.log.Errorf("failed to sign artifact %s: %v", artifact.Name, err)
|
|
statusErr := status.New(codes.Internal, "failed to sign artifact")
|
|
statusErr, err2 := statusErr.WithDetails(&errdetails.ErrorInfo{
|
|
Reason: "rpmsign-failed",
|
|
Domain: "keykeeper.peridot.resf.org",
|
|
Metadata: map[string]string{
|
|
"logs": outBuf.String(),
|
|
"err": err.Error(),
|
|
},
|
|
})
|
|
if err2 != nil {
|
|
s.log.Errorf("failed to add error details to status: %v", err2)
|
|
}
|
|
return nil, statusErr.Err()
|
|
}
|
|
_, err = s.storage.PutObject(newObjectKey, localPath)
|
|
if err != nil {
|
|
s.log.Errorf("failed to upload artifact %s: %v", newObjectKey, err)
|
|
return nil, fmt.Errorf("failed to upload artifact %s: %v", newObjectKey, err)
|
|
}
|
|
|
|
f, err := os.Open(localPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hasher := sha256.New()
|
|
_, err = io.Copy(hasher, f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hash := hex.EncodeToString(hasher.Sum(nil))
|
|
|
|
err = tx.CreateTaskArtifactSignature(artifact.ID.String(), key.keyUuid.String(), hash)
|
|
if err != nil {
|
|
s.log.Errorf("failed to create task artifact signature: %v", err)
|
|
return nil, fmt.Errorf("failed to create task artifact signature: %v", err)
|
|
}
|
|
|
|
return &keykeeperpb.SignedArtifact{
|
|
Path: newObjectKey,
|
|
HashSha256: hash,
|
|
}, nil
|
|
}
|
|
verifySig := func() error {
|
|
var outBuf bytes.Buffer
|
|
opts := []string{
|
|
"--define", "_gpg_name " + keyName,
|
|
"--define", "_peridot_keykeeper_key " + key.keyUuid.String(),
|
|
"--checksig", localPath,
|
|
}
|
|
cmd := gpgCmdEnv(exec.Command("rpm", opts...))
|
|
cmd.Stdout = &outBuf
|
|
cmd.Stderr = &outBuf
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
s.log.Errorf("failed to verify artifact %s: %v", artifact.Name, err)
|
|
s.log.Errorf("buf: %s", outBuf.String())
|
|
return fmt.Errorf("failed to verify artifact %s: %v", artifact.Name, err)
|
|
}
|
|
return nil
|
|
}
|
|
res, err := rpmSign()
|
|
if err != nil {
|
|
_ = beginTx.Rollback()
|
|
return nil, err
|
|
}
|
|
err = verifySig()
|
|
if err != nil {
|
|
_ = beginTx.Rollback()
|
|
return nil, err
|
|
}
|
|
err = beginTx.Commit()
|
|
if err != nil {
|
|
s.log.Errorf("failed to commit transaction: %v", err)
|
|
return nil, status.Error(codes.Internal, "failed to commit transaction")
|
|
}
|
|
|
|
return res, nil
|
|
default:
|
|
s.log.Infof("skipping artifact %s, extension %s not supported", artifact.Name, ext)
|
|
return nil, ErrUnsupportedExtension
|
|
}
|
|
}
|
|
|
|
// SignArtifacts signs artifacts belonging to a build with the given key.
|
|
// The artifacts are loaded from the shared Peridot bucket and is
|
|
// then uploaded back.
|
|
// Since an artifact can have multiple signers, the resulting
|
|
// artifacts are uploaded to `{taskId}/{keyId}/{artifactName}`.
|
|
// Essentially we only add the keyId to the artifact key.
|
|
// Each artifact should in theory only be signed once,
|
|
// but signing it multiple times does not cause any harm.
|
|
// We currently fetch the artifact from the shared bucket,
|
|
// then sign it within the current container, then upload it back.
|
|
// todo(mustafa): Look into a way to avoid fetching the artifact from the shared bucket.
|
|
// todo(mustafa): We should still probably avoid signing the same
|
|
// todo(mustafa): artifact (with a key it's already signed with)
|
|
func (s *Server) SignArtifacts(_ context.Context, req *keykeeperpb.SignArtifactsRequest) (*peridotpb.AsyncTask, error) {
|
|
build, err := s.db.GetTaskByBuildId(req.BuildId)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, status.Error(codes.NotFound, "build not found")
|
|
}
|
|
s.log.Errorf("failed to get build %s: %v", req.BuildId, err)
|
|
return nil, status.Error(codes.Internal, "failed to get build")
|
|
}
|
|
artifacts, err := s.db.GetArtifactsForBuild(req.BuildId)
|
|
if err != nil {
|
|
s.log.Errorf("failed to get artifacts for build %s: %v", req.BuildId, err)
|
|
return nil, status.Error(codes.Internal, "failed to get artifacts")
|
|
}
|
|
if len(artifacts) == 0 {
|
|
return nil, status.Error(codes.InvalidArgument, "no artifacts to sign")
|
|
}
|
|
|
|
rollback := true
|
|
beginTx, err := s.db.Begin()
|
|
if err != nil {
|
|
s.log.Error(err)
|
|
return nil, utils.InternalError
|
|
}
|
|
defer func() {
|
|
if rollback {
|
|
_ = beginTx.Rollback()
|
|
}
|
|
}()
|
|
tx := s.db.UseTransaction(beginTx)
|
|
buildTaskId := build.ID.String()
|
|
|
|
task, err := tx.CreateTask(nil, "noarch", peridotpb.TaskType_TASK_TYPE_KEYKEEPER_SIGN_ARTIFACT, &build.ProjectId.String, &buildTaskId)
|
|
if err != nil {
|
|
s.log.Errorf("could not create build task in SubmitBuild: %v", err)
|
|
return nil, status.Error(codes.InvalidArgument, "could not create import task")
|
|
}
|
|
|
|
taskProto, err := task.ToProto(false)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "could not marshal task: %v", err)
|
|
}
|
|
|
|
rollback = false
|
|
err = beginTx.Commit()
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "could not save, try again")
|
|
}
|
|
|
|
_, err = s.temporal.ExecuteWorkflow(
|
|
context.Background(),
|
|
client.StartWorkflowOptions{
|
|
ID: task.ID.String(),
|
|
TaskQueue: TaskQueue,
|
|
},
|
|
s.SignArtifactsWorkflow,
|
|
nil,
|
|
req.BuildId,
|
|
task,
|
|
req.KeyName,
|
|
)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "could not start workflow: %v", err)
|
|
}
|
|
|
|
return &peridotpb.AsyncTask{
|
|
TaskId: task.ID.String(),
|
|
Subtasks: []*peridotpb.Subtask{taskProto},
|
|
Done: false,
|
|
}, nil
|
|
}
|
|
|
|
// SignText signs given text with the given key.
|
|
// This method only returns the signature part of the gpg clearsign
|
|
func (s *Server) SignText(_ context.Context, req *keykeeperpb.SignTextRequest) (*keykeeperpb.SignTextResponse, error) {
|
|
key, err := s.EnsureGPGKey(req.KeyName)
|
|
if err != nil {
|
|
s.log.Errorf("failed to load key %s: %v", req.KeyName, err)
|
|
return nil, status.Error(codes.Internal, "failed to load key")
|
|
}
|
|
|
|
tmpFile, err := os.CreateTemp("", "")
|
|
if err != nil {
|
|
s.log.Errorf("failed to create temp file: %v", err)
|
|
return nil, status.Error(codes.Internal, "failed to create temp file")
|
|
}
|
|
defer os.Remove(tmpFile.Name())
|
|
defer tmpFile.Close()
|
|
err = ioutil.WriteFile(tmpFile.Name(), []byte(req.Text), 0644)
|
|
if err != nil {
|
|
s.log.Errorf("failed to write to temp file: %v", err)
|
|
return nil, status.Error(codes.Internal, "failed to write to temp file")
|
|
}
|
|
|
|
cmdArgs := []string{
|
|
"--batch",
|
|
"--no-verbose",
|
|
"--armor",
|
|
"--pinentry-mode=loopback",
|
|
"--no-secmem-warning",
|
|
"--detach-sign",
|
|
"--passphrase",
|
|
key.keyUuid.String(),
|
|
"-u",
|
|
req.KeyName,
|
|
"-o",
|
|
tmpFile.Name() + ".asc",
|
|
tmpFile.Name(),
|
|
}
|
|
cmd := gpgCmdEnv(exec.Command("gpg", cmdArgs...))
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
s.log.Errorf("failed to sign text: %v", err)
|
|
return nil, status.Error(codes.Internal, "failed to sign text")
|
|
}
|
|
defer os.Remove(tmpFile.Name() + ".asc")
|
|
signedText, err := ioutil.ReadFile(tmpFile.Name() + ".asc")
|
|
if err != nil {
|
|
s.log.Errorf("failed to read signed text: %v", err)
|
|
return nil, status.Error(codes.Internal, "failed to read signed text")
|
|
}
|
|
|
|
return &keykeeperpb.SignTextResponse{
|
|
Signature: string(signedText),
|
|
}, nil
|
|
}
|