Skip to content

Commit

Permalink
Merge pull request #147 from puerco/att-verify
Browse files Browse the repository at this point in the history
Fix keyless verification of openvex attesatations
  • Loading branch information
cpanato authored Dec 6, 2023
2 parents badceec + d10bee4 commit f168a82
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 47 deletions.
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

0 comments on commit f168a82

Please sign in to comment.