From 304c2b256bffb4bf35e3a5f4e51903ba828265bf Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Tue, 30 Nov 2021 09:34:09 -0800 Subject: [PATCH] continued sign refacc (#1098) * decompose `StaticSigner` into individual signing stages * move rekor upload logic to `RekorSignerWrapper` * select signing flows based on input variables * add comments to the `Signer` implementations * only perform signing actions in `cosign.Signer` * flip comments on Signer implementations * staticly assert that impls are impls * split Rekor and Fulcio signers into their own packages Signed-off-by: Jake Sanders --- cmd/cosign/cli/attest/attest.go | 48 +++++++- cmd/cosign/cli/sign/sign.go | 101 ++++------------ internal/pkg/cosign/fulcio/fulcio.go | 89 ++++++++++++++ internal/pkg/cosign/payload/payload.go | 77 ++++++++++++ internal/pkg/cosign/rekor/rekor.go | 157 +++++++++++++++++++++++++ internal/pkg/cosign/sign.go | 27 +++++ 6 files changed, 421 insertions(+), 78 deletions(-) create mode 100644 internal/pkg/cosign/fulcio/fulcio.go create mode 100644 internal/pkg/cosign/payload/payload.go create mode 100644 internal/pkg/cosign/rekor/rekor.go create mode 100644 internal/pkg/cosign/sign.go diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index fc936396fc1..a0daf322631 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -33,16 +33,62 @@ import ( "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/attestation" cremote "github.com/sigstore/cosign/pkg/cosign/remote" + "github.com/sigstore/cosign/pkg/oci" "github.com/sigstore/cosign/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/pkg/oci/remote" "github.com/sigstore/cosign/pkg/oci/static" + sigs "github.com/sigstore/cosign/pkg/signature" "github.com/sigstore/cosign/pkg/types" + rekPkgClient "github.com/sigstore/rekor/pkg/client" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore/pkg/signature/dsse" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) +// TODO(dekkagaijin): remove this in favor of a function in pkg which handles both signatures and attestations +func bundle(entry *models.LogEntryAnon) *oci.Bundle { + if entry.Verification == nil { + return nil + } + return &oci.Bundle{ + SignedEntryTimestamp: entry.Verification.SignedEntryTimestamp, + Payload: oci.BundlePayload{ + Body: entry.Body, + IntegratedTime: *entry.IntegratedTime, + LogIndex: *entry.LogIndex, + LogID: *entry.LogID, + }, + } +} + +type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error) + +func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*oci.Bundle, error) { + var rekorBytes []byte + // Upload the cert or the public key, depending on what we have + if sv.Cert != nil { + rekorBytes = sv.Cert + } else { + pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx)) + if err != nil { + return nil, err + } + rekorBytes = pemBytes + } + + rekorClient, err := rekPkgClient.GetRekorClient(rekorURL) + if err != nil { + return nil, err + } + entry, err := upload(rekorClient, rekorBytes) + if err != nil { + return nil, err + } + fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) + return bundle(entry), nil +} + //nolint func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string, noUpload bool, predicatePath string, force bool, predicateType string, replace bool, timeout time.Duration) error { @@ -133,7 +179,7 @@ func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpt // Check whether we should be uploading to the transparency log if sign.ShouldUploadToTlog(ctx, digest, force, ko.RekorURL) { - bundle, err := sign.UploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { + bundle, err := uploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) }) if err != nil { diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 390acc34f69..7b531e2f50c 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -21,7 +21,6 @@ import ( "crypto/ecdsa" "crypto/rsa" "crypto/x509" - "encoding/base64" "encoding/pem" "fmt" "net/url" @@ -37,6 +36,10 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier" "github.com/sigstore/cosign/cmd/cosign/cli/options" + icos "github.com/sigstore/cosign/internal/pkg/cosign" + ifulcio "github.com/sigstore/cosign/internal/pkg/cosign/fulcio" + ipayload "github.com/sigstore/cosign/internal/pkg/cosign/payload" + irekor "github.com/sigstore/cosign/internal/pkg/cosign/rekor" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/pivkey" "github.com/sigstore/cosign/pkg/cosign/pkcs11key" @@ -45,17 +48,12 @@ import ( ociempty "github.com/sigstore/cosign/pkg/oci/empty" "github.com/sigstore/cosign/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/pkg/oci/remote" - "github.com/sigstore/cosign/pkg/oci/static" "github.com/sigstore/cosign/pkg/oci/walk" providers "github.com/sigstore/cosign/pkg/providers/all" sigs "github.com/sigstore/cosign/pkg/signature" fulcPkgClient "github.com/sigstore/fulcio/pkg/client" - rekPkgClient "github.com/sigstore/rekor/pkg/client" - rekGenClient "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" sigPayload "github.com/sigstore/sigstore/pkg/signature/payload" ) @@ -85,32 +83,6 @@ func ShouldUploadToTlog(ctx context.Context, ref name.Reference, force bool, url return true } -type Uploader func(*rekGenClient.Rekor, []byte) (*models.LogEntryAnon, error) - -func UploadToTlog(ctx context.Context, sv *SignerVerifier, rekorURL string, upload Uploader) (*oci.Bundle, error) { - var rekorBytes []byte - // Upload the cert or the public key, depending on what we have - if sv.Cert != nil { - rekorBytes = sv.Cert - } else { - pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx)) - if err != nil { - return nil, err - } - rekorBytes = pemBytes - } - rekorClient, err := rekPkgClient.GetRekorClient(rekorURL) - if err != nil { - return nil, err - } - entry, err := upload(rekorClient, rekorBytes) - if err != nil { - return nil, err - } - fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) - return Bundle(entry), nil -} - func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremote.Option) (name.Reference, error) { if attachment == "" { return ref, nil @@ -230,12 +202,6 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyO } } - signature, err := sv.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return errors.Wrap(err, "signing") - } - b64sig := base64.StdEncoding.EncodeToString(signature) - out := os.Stdout if output != "" { out, err = os.Create(output) @@ -244,36 +210,33 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyO } defer out.Close() } - if _, err := out.Write([]byte(b64sig)); err != nil { - return errors.Wrap(err, "write signature to file") - } - if !upload { - return nil + var s icos.Signer + s = ipayload.NewSigner(sv, nil, nil) + s = ifulcio.NewSigner(s, sv.Cert, sv.Chain) + if ShouldUploadToTlog(ctx, digest, force, ko.RekorURL) { + s = irekor.NewSigner(s, ko.RekorURL) } - opts := []static.Option{} - if sv.Cert != nil { - opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) - } - if ShouldUploadToTlog(ctx, digest, force, ko.RekorURL) { - bundle, err := UploadToTlog(ctx, sv, ko.RekorURL, func(r *rekGenClient.Rekor, b []byte) (*models.LogEntryAnon, error) { - return cosign.TLogUpload(ctx, r, signature, payload, b) - }) - if err != nil { - return err - } - opts = append(opts, static.WithBundle(bundle)) + ociSig, _, err := s.Sign(ctx, bytes.NewReader(payload)) + if err != nil { + return err } - // Create the new signature for this entity. - sig, err := static.NewSignature(payload, b64sig, opts...) + b64sig, err := ociSig.Base64Signature() if err != nil { return err } + if _, err := out.Write([]byte(b64sig)); err != nil { + return errors.Wrap(err, "write signature to file") + } + + if !upload { + return nil + } // Attach the signature to the entity. - newSE, err := mutate.AttachSignatureToEntity(se, sig, mutate.WithDupeDetector(dd)) + newSE, err := mutate.AttachSignatureToEntity(se, ociSig, mutate.WithDupeDetector(dd)) if err != nil { return err } @@ -288,22 +251,8 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyO if err := ociremote.WriteSignatures(digest.Repository, newSE, walkOpts...); err != nil { return err } - return nil -} -func Bundle(entry *models.LogEntryAnon) *oci.Bundle { - if entry.Verification == nil { - return nil - } - return &oci.Bundle{ - SignedEntryTimestamp: entry.Verification.SignedEntryTimestamp, - Payload: oci.BundlePayload{ - Body: entry.Body, - IntegratedTime: *entry.IntegratedTime, - LogIndex: *entry.LogIndex, - LogID: *entry.LogID, - }, - } + return nil } func signerFromSecurityKey(keySlot string) (*SignerVerifier, error) { @@ -430,13 +379,11 @@ func keylessSigner(ctx context.Context, ko KeyOpts) (*SignerVerifier, error) { var k *fulcio.Signer if ko.InsecureSkipFulcioVerify { - k, err = fulcio.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, fClient) - if err != nil { + if k, err = fulcio.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, fClient); err != nil { return nil, errors.Wrap(err, "getting key from Fulcio") } } else { - k, err = fulcioverifier.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, fClient) - if err != nil { + if k, err = fulcioverifier.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, fClient); err != nil { return nil, errors.Wrap(err, "getting key from Fulcio") } } diff --git a/internal/pkg/cosign/fulcio/fulcio.go b/internal/pkg/cosign/fulcio/fulcio.go new file mode 100644 index 00000000000..aa655eec3dc --- /dev/null +++ b/internal/pkg/cosign/fulcio/fulcio.go @@ -0,0 +1,89 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fulcio + +import ( + "context" + "crypto" + "io" + + "github.com/sigstore/cosign/internal/pkg/cosign" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/static" +) + +// signerWrapper still needs to actually upload keys to Fulcio and receive +// the resulting `Cert` and `Chain`, which are added to the returned `oci.Signature` +type signerWrapper struct { + inner cosign.Signer + + cert, chain []byte +} + +var _ cosign.Signer = (*signerWrapper)(nil) + +// Sign implements `cosign.Signer` +func (fs *signerWrapper) Sign(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) { + sig, pub, err := fs.inner.Sign(ctx, payload) + if err != nil { + return nil, nil, err + } + + payloadBytes, err := sig.Payload() + if err != nil { + return nil, nil, err + } + b64Sig, err := sig.Base64Signature() + if err != nil { + return nil, nil, err + } + + // TODO(dekkagaijin): move the fulcio SignerVerififer logic here + + opts := []static.Option{static.WithCertChain(fs.cert, fs.chain)} + + // Copy over the other attributes: + if annotations, err := sig.Annotations(); err != nil { + return nil, nil, err + } else if len(annotations) > 0 { + opts = append(opts, static.WithAnnotations(annotations)) + } + if bundle, err := sig.Bundle(); err != nil { + return nil, nil, err + } else if bundle != nil { + opts = append(opts, static.WithBundle(bundle)) + } + if mt, err := sig.MediaType(); err != nil { + return nil, nil, err + } else if mt != "" { + opts = append(opts, static.WithLayerMediaType(mt)) + } + + newSig, err := static.NewSignature(payloadBytes, b64Sig, opts...) + if err != nil { + return nil, nil, err + } + + return newSig, pub, nil +} + +// NewSigner returns a `cosign.Signer` which leverages Fulcio to create a Cert and Chain for the signature +func NewSigner(inner cosign.Signer, cert, chain []byte) cosign.Signer { + return &signerWrapper{ + inner: inner, + cert: cert, + chain: chain, + } +} diff --git a/internal/pkg/cosign/payload/payload.go b/internal/pkg/cosign/payload/payload.go new file mode 100644 index 00000000000..3ca86fe7b90 --- /dev/null +++ b/internal/pkg/cosign/payload/payload.go @@ -0,0 +1,77 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package payload + +import ( + "bytes" + "context" + "crypto" + "encoding/base64" + "io" + + "github.com/sigstore/cosign/internal/pkg/cosign" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/static" + "github.com/sigstore/sigstore/pkg/signature" + signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" +) + +type payloadSigner struct { + payloadSigner signature.Signer + payloadSignerOpts []signature.SignOption + publicKeyProviderOpts []signature.PublicKeyOption +} + +var _ cosign.Signer = (*payloadSigner)(nil) + +// Sign implements `Signer` +func (ps *payloadSigner) Sign(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) { + payloadBytes, err := io.ReadAll(payload) + if err != nil { + return nil, nil, err + } + sOpts := []signature.SignOption{signatureoptions.WithContext(ctx)} + sOpts = append(sOpts, ps.payloadSignerOpts...) + sig, err := ps.payloadSigner.SignMessage(bytes.NewReader(payloadBytes), sOpts...) + if err != nil { + return nil, nil, err + } + + pkOpts := []signature.PublicKeyOption{signatureoptions.WithContext(ctx)} + pkOpts = append(pkOpts, ps.publicKeyProviderOpts...) + pk, err := ps.payloadSigner.PublicKey(pkOpts...) + if err != nil { + return nil, nil, err + } + + b64sig := base64.StdEncoding.EncodeToString(sig) + ociSig, err := static.NewSignature(payloadBytes, b64sig) + if err != nil { + return nil, nil, err + } + + return ociSig, pk, nil +} + +// NewSigner returns a `cosign.Signer` uses the given `signature.Signer` to sign the requested payload, then returns the signature, the public key associated with it, the signed payload +func NewSigner(s signature.Signer, + sOpts []signature.SignOption, + pkOpts []signature.PublicKeyOption) cosign.Signer { + return &payloadSigner{ + payloadSigner: s, + payloadSignerOpts: sOpts, + publicKeyProviderOpts: pkOpts, + } +} diff --git a/internal/pkg/cosign/rekor/rekor.go b/internal/pkg/cosign/rekor/rekor.go new file mode 100644 index 00000000000..30dc4de1046 --- /dev/null +++ b/internal/pkg/cosign/rekor/rekor.go @@ -0,0 +1,157 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rekor + +import ( + "context" + "crypto" + "encoding/base64" + "fmt" + "io" + "os" + + "github.com/sigstore/cosign/internal/pkg/cosign" + cosignv1 "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/static" + rekPkgClient "github.com/sigstore/rekor/pkg/client" + rekGenClient "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +func bundle(entry *models.LogEntryAnon) *oci.Bundle { + if entry.Verification == nil { + return nil + } + return &oci.Bundle{ + SignedEntryTimestamp: entry.Verification.SignedEntryTimestamp, + Payload: oci.BundlePayload{ + Body: entry.Body, + IntegratedTime: *entry.IntegratedTime, + LogIndex: *entry.LogIndex, + LogID: *entry.LogID, + }, + } +} + +type tlogUploadFn func(*rekGenClient.Rekor, []byte) (*models.LogEntryAnon, error) + +func uploadToTlog(rekorBytes []byte, rekorURL string, upload tlogUploadFn) (*oci.Bundle, error) { + rekorClient, err := rekPkgClient.GetRekorClient(rekorURL) + if err != nil { + return nil, err + } + entry, err := upload(rekorClient, rekorBytes) + if err != nil { + return nil, err + } + fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) + return bundle(entry), nil +} + +// signerWrapper calls a wrapped, inner signer then uploads either the Cert or Pub(licKey) of the results to Rekor, then adds the resulting `Bundle` +type signerWrapper struct { + inner cosign.Signer + + rekorURL string +} + +var _ cosign.Signer = (*signerWrapper)(nil) + +// Sign implements `cosign.Signer` +func (rs *signerWrapper) Sign(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) { + sig, pub, err := rs.inner.Sign(ctx, payload) + if err != nil { + return nil, nil, err + } + + payloadBytes, err := sig.Payload() + if err != nil { + return nil, nil, err + } + b64Sig, err := sig.Base64Signature() + if err != nil { + return nil, nil, err + } + sigBytes, err := base64.StdEncoding.DecodeString(b64Sig) + if err != nil { + return nil, nil, err + } + + // Upload the cert or the public key, depending on what we have + cert, err := sig.Cert() + if err != nil { + return nil, nil, err + } + + var rekorBytes []byte + if cert != nil { + rekorBytes, err = cryptoutils.MarshalCertificateToPEM(cert) + } else { + rekorBytes, err = cryptoutils.MarshalPublicKeyToPEM(pub) + } + if err != nil { + return nil, nil, err + } + + bundle, err := uploadToTlog(rekorBytes, rs.rekorURL, func(r *rekGenClient.Rekor, b []byte) (*models.LogEntryAnon, error) { + return cosignv1.TLogUpload(ctx, r, sigBytes, payloadBytes, b) + }) + if err != nil { + return nil, nil, err + } + + opts := []static.Option{static.WithBundle(bundle)} + + // Copy over the other attributes: + + if cert != nil { + chain, err := sig.Chain() + if err != nil { + return nil, nil, err + } + chainBytes, err := cryptoutils.MarshalCertificatesToPEM(chain) + if err != nil { + return nil, nil, err + } + opts = append(opts, static.WithCertChain(rekorBytes, chainBytes)) + } + if annotations, err := sig.Annotations(); err != nil { + return nil, nil, err + } else if len(annotations) > 0 { + opts = append(opts, static.WithAnnotations(annotations)) + } + if mt, err := sig.MediaType(); err != nil { + return nil, nil, err + } else if mt != "" { + opts = append(opts, static.WithLayerMediaType(mt)) + } + + newSig, err := static.NewSignature(payloadBytes, b64Sig, opts...) + if err != nil { + return nil, nil, err + } + + return newSig, pub, nil +} + +// NewSigner returns a `cosign.Signer` which uploads the signature to Rekor +func NewSigner(inner cosign.Signer, rekorURL string) cosign.Signer { + return &signerWrapper{ + inner: inner, + rekorURL: rekorURL, + } +} diff --git a/internal/pkg/cosign/sign.go b/internal/pkg/cosign/sign.go new file mode 100644 index 00000000000..4ea256d0999 --- /dev/null +++ b/internal/pkg/cosign/sign.go @@ -0,0 +1,27 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cosign + +import ( + "context" + "crypto" + "io" + + "github.com/sigstore/cosign/pkg/oci" +) + +type Signer interface { + Sign(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) +}