blob: 5dd2927ba03717f6ae1b9407755cfb18e6318695 [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package bcid
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"os/exec"
spb "github.com/in-toto/attestation/go/v1"
"go.chromium.org/luci/common/logging"
"google.golang.org/protobuf/encoding/protojson"
structpb "google.golang.org/protobuf/types/known/structpb"
)
const (
ProdKeyID = "projects/fuchsia-bcid/locations/global/keyRings/bcid/cryptoKeys/attestation_signer"
StmtType = "https://in-toto.io/Statement/v1"
PredicateType = "https://slsa.dev/provenance/v1"
)
type attestCommander struct{}
func (a *attestCommander) CombinedOutput(ctx context.Context, stdin []byte, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "attestation_tool", args...)
if stdin != nil {
pipe, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("unable to pipe to stdin: %v", err)
}
pipe.Write(stdin)
// This close must occur before CombinedOutput() is called otherwise the command will wait forever.
pipe.Close()
}
logging.Infof(ctx, "Executing %+v\n", cmd)
return cmd.CombinedOutput()
}
func Attest(ctx context.Context, stmt *spb.Statement, keyID string) ([]byte, error) {
return attestWithCommander(ctx, &attestCommander{}, stmt, keyID)
}
type CombinedOutputer interface {
CombinedOutput(context.Context, []byte, ...string) ([]byte, error)
}
func attestWithCommander(ctx context.Context, cmd CombinedOutputer, stmt *spb.Statement, keyID string) ([]byte, error) {
stmtJSON, err := protojson.Marshal(stmt)
if err != nil {
return nil, err
}
return cmd.CombinedOutput(ctx, stmtJSON, "sign_intoto_statement",
"--input", "-",
"--key_id", keyID,
"--output", "-")
}
type Signature struct {
KeyID string `json:"keyid"`
Sig string `json:"sig"`
}
// InToToBundle represents the attestation bundle.
// TODO(https://github.com/in-toto/attestation/issues/280): This struct should ideally be provided upstream.
type InTotoBundle struct {
ContentType string `json:"payloadType"`
Payload string `json:"payload"`
Signatures []Signature `json:"signatures"`
}
func UnmarshalBundle(data io.Reader) (*InTotoBundle, error) {
all, err := io.ReadAll(data)
if err != nil {
return nil, fmt.Errorf("can't read input data: %v", err)
}
var itt InTotoBundle
if err := json.Unmarshal(all, &itt); err != nil {
return nil, fmt.Errorf("unable to pass in-toto payload: %v", err)
}
return &itt, nil
}
type subject struct {
name string
data io.Reader
}
func NewStmt(subject *subject) (*spb.Statement, error) {
h := sha256.New()
if _, err := io.Copy(h, subject.data); err != nil {
return nil, err
}
// The statement proto uses this struct proto which is dynamically typed.
// It then uses reflection to try and figure out what it all is.
// The only real way to work through this is by using raw types, not
// real Go structs.
// It's possible to just dump raw JSON here, but that feels even less
// idomatic than this does.
pred, err := structpb.NewStruct(map[string]any{
// TODO(cflewis): This definition will probably require more fields.
// Obvious candidates: `resolvedDependencies`, `internalParameters`.
"buildDefinition": map[string]any{
"externalParameters": map[string]any{
"buildConfigSource": map[string]any{
"repository": "git+https://fuchsia.googlesource.com/infra/recipes",
"path": "recipes.py",
},
},
},
"runDetails": map[string]any{
"builder": map[string]any{
"id": "//bcid.corp.google.com/builders/luci/fuchsia/l1",
// TODO(cflewis): Add the version here.
},
},
})
if err != nil {
return nil, err
}
return &spb.Statement{
Type: StmtType,
Subject: []*spb.ResourceDescriptor{
{
Name: subject.name,
Digest: map[string]string{"sha256": fmt.Sprintf("%x", h.Sum(nil))},
},
},
PredicateType: PredicateType,
Predicate: pred,
}, nil
}