Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix keyless verification of openvex attesatations #147

Merged
merged 3 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/owenrumney/go-sarif v1.1.1
github.com/secure-systems-lab/go-securesystemslib v0.7.0
github.com/sigstore/cosign/v2 v2.2.2
github.com/sigstore/rekor v1.3.4
github.com/sigstore/sigstore v1.7.6
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
Expand Down Expand Up @@ -145,7 +146,6 @@ require (
github.com/sassoftware/relic v7.2.1+incompatible // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/sigstore/fulcio v1.4.3 // indirect
github.com/sigstore/rekor v1.3.4 // indirect
github.com/sigstore/timestamp-authority v1.2.0 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
Expand Down
166 changes: 122 additions & 44 deletions pkg/attestation/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,45 @@ import (
"errors"
"fmt"
"io"
"os"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/crane"
intoto "github.com/in-toto/in-toto-golang/in_toto"
ovattest "github.com/openvex/go-vex/pkg/attestation"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/v2/pkg/cosign"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/sigstore/pkg/signature/dsse"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
)

type Attestation struct {
signedData []byte `json:"-"`
ovattest.Attestation

// Sign is boolean that signals if the attestation has been signed
Signed bool `json:"-"`

// signatureData embeds the signed attestaion, the certificate used to sign
// it and the transparency log inclusion proof
SignatureData *SignatureData `json:"-"`
}

type SignatureData struct {
// CertData of the cert used to sign the attestation encodeded in PEM
CertData []byte `json:"-"`

// Chain contains the intermediate certificate chain of the attestation's cert
Chain []byte `json:"-"`

// Entry contains the proof of inclusion to the transparency log
Entry *models.LogEntryAnon `json:"-"`

// signedPayload contains the resulting blob after the attestation was
// signed.
signedPayload []byte
}

func New() *Attestation {
Expand All @@ -38,51 +61,18 @@ func New() *Attestation {

// Sign the attestation
func (att *Attestation) Sign() error {
ctx := context.Background()
var timeout time.Duration /// TODO move to options
var certPath, certChainPath string
ko := options.KeyOpts{
// KeyRef: s.options.PrivateKeyPath,
// IDToken: identityToken,
FulcioURL: options.DefaultFulcioURL,
RekorURL: options.DefaultRekorURL,
OIDCIssuer: options.DefaultOIDCIssuerURL,
OIDCClientID: "sigstore",

InsecureSkipFulcioVerify: false,
SkipConfirmation: true,
// FulcioAuthFlow: "",
}

if timeout != 0 {
var cancelFn context.CancelFunc
ctx, cancelFn = context.WithTimeout(ctx, timeout)
defer cancelFn()
}

sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko)
if err != nil {
return fmt.Errorf("getting signer: %w", err)
}
defer sv.Close()
ctx, ko := initSigning()

// Wrap the attestation in the DSSE envelope
wrapped := dsse.WrapSigner(sv, "application/vnd.in-toto+json")

var b bytes.Buffer
if err := att.ToJSON(&b); err != nil {
return fmt.Errorf("serializing attestation to json: %w", err)
// Sign the attestaion.
if err := signAttestation(ctx, &ko, att); err != nil {
return fmt.Errorf("signing attestation: %w", err)
}

signedPayload, err := wrapped.SignMessage(
bytes.NewReader(b.Bytes()), signatureoptions.WithContext(ctx),
)
if err != nil {
return fmt.Errorf("signing attestation: %w", err)
// Register the signature in rekor
if err := appendSignatureDataToTLog(ctx, &ko, att); err != nil {
return fmt.Errorf("recording signature data to transparency log: %w", err)
}

att.Signed = true
att.signedData = signedPayload
return nil
}

Expand Down Expand Up @@ -114,12 +104,100 @@ func (att *Attestation) ToJSON(w io.Writer) error {
if !att.Signed {
return att.Attestation.ToJSON(w)
}
if len(att.signedData) == 0 {
if att.SignatureData == nil || len(att.SignatureData.signedPayload) == 0 {
return errors.New("consistency error: attestation is signed but data is empty")
}

if _, err := w.Write(att.signedData); err != nil {
if _, err := w.Write(att.SignatureData.signedPayload); err != nil {
return fmt.Errorf("writing signed attestation: %w", err)
}
return nil
}

// initSigning initializes the options and context needed to sign. Right now
// it only sets up some default options and a backgrous context but we
// should wire the options set from the CLI to this function
func initSigning() (context.Context, options.KeyOpts) {
ko := options.KeyOpts{
FulcioURL: options.DefaultFulcioURL,
RekorURL: options.DefaultRekorURL,
OIDCIssuer: options.DefaultOIDCIssuerURL,
OIDCClientID: "sigstore",
InsecureSkipFulcioVerify: false,
SkipConfirmation: true,
}

ctx := context.Background()
// TODO(puerco): Support context.WithTimeout(ctx, timeout)

return ctx, ko
}

// signAttestation creates a signer and signs the attestation. The attestation's
// SignatureData field will be populated with the certificate, chain and the
// attestaion data wrapped in its DSSE envelope.
func signAttestation(ctx context.Context, ko *options.KeyOpts, att *Attestation) error {
// TODO(puerco): Investigate supporting certificates preloaded in the
// attestation. We would need to dump them to disk and load them into
// the args here and if we're reusing the bundle, set it in ko.BundlePath
// Note that in this call we hardocde the pats empty, but we should get them
// from somewhere.
sv, err := sign.SignerFromKeyOpts(ctx, "", "", *ko)
if err != nil {
return fmt.Errorf("getting signer: %w", err)
}
defer sv.Close()

// Wrap the attestation in the DSSE envelope
wrapped := dsse.WrapSigner(sv, "application/vnd.in-toto+json")

var b bytes.Buffer
if err := att.ToJSON(&b); err != nil {
return fmt.Errorf("serializing attestation to json: %w", err)
}

// SIGN!
signedPayload, err := wrapped.SignMessage(
bytes.NewReader(b.Bytes()), signatureoptions.WithContext(ctx),
)
if err != nil {
return fmt.Errorf("signing attestation: %w", err)
}

// Assign the new data to the attestation
att.SignatureData = &SignatureData{
CertData: sv.Cert,
Chain: sv.Chain,
signedPayload: signedPayload,
}
att.Signed = true

return nil
}

// appendSignatureDataToTLog records the signature data to the transparency log
// (rekor). The proof of inclusion will be added to the attestation's SignatureData
// struct.
// If uploading fails, the signature data will be destroyed to guarantee an atomic
// operation of attesation.Sign()
func appendSignatureDataToTLog(ctx context.Context, ko *options.KeyOpts, att *Attestation) error {
tlogClient, err := rekor.NewClient(ko.RekorURL)
if err != nil {
att.SignatureData = nil
return fmt.Errorf("creating rekor client: %w", err)
}

// ...and upload the signature data
entry, err := cosign.TLogUploadDSSEEnvelope(
ctx, tlogClient, att.SignatureData.signedPayload, att.SignatureData.CertData,
)
if err != nil {
att.SignatureData = nil
return fmt.Errorf("uploading to transparency log: %w", err)
}

att.SignatureData.Entry = entry
fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex)

return nil
}
21 changes: 19 additions & 2 deletions pkg/ctl/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/pkg/cosign"
cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle"
"github.com/sigstore/cosign/v2/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
"github.com/sigstore/cosign/v2/pkg/oci/static"
Expand Down Expand Up @@ -185,7 +186,7 @@ func (impl *defaultVexCtlImplementation) Attach(ctx context.Context, att *attest
}

for _, ref := range refs {
if err := attachAttestation(ctx, payload, ref); err != nil {
if err := attachAttestation(ctx, att, payload, ref); err != nil {
return fmt.Errorf("attaching attestation to %s: %w", ref, err)
}
}
Expand All @@ -196,7 +197,7 @@ func (impl *defaultVexCtlImplementation) Attach(ctx context.Context, att *attest

// attachAttestation is a utility function to do the actual attachment of
// the signed attestation
func attachAttestation(ctx context.Context, payload []byte, imageRef string) error {
func attachAttestation(ctx context.Context, original *attestation.Attestation, payload []byte, imageRef string) error {
regOpts := options.RegistryOptions{}
remoteOpts, err := regOpts.ClientOpts(ctx)
if err != nil {
Expand All @@ -216,6 +217,22 @@ func attachAttestation(ctx context.Context, payload []byte, imageRef string) err
ref = digest //nolint:ineffassign

opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)}

// Add the attestation certificate:
opts = append(opts, static.WithCertChain(original.SignatureData.CertData, original.SignatureData.Chain))

// Add the tlog entry to the annotations
if original.SignatureData.Entry != nil {
opts = append(opts, static.WithBundle(
cbundle.EntryToBundle(original.SignatureData.Entry),
))
}

// Add predicateType as manifest annotation
opts = append(opts, static.WithAnnotations(map[string]string{
"predicateType": vex.Context,
}))

att, err := static.NewAttestation(payload, opts...)
if err != nil {
return err
Expand Down
Loading