| // 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 |
| } |