diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 82861b533a9..8bcfcf30d85 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -204,7 +204,7 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyO } var s icos.Signer - s = ipayload.NewSigner(sv, nil, nil) + s = ipayload.NewSigner(sv) s = ifulcio.NewSigner(s, sv.Cert, sv.Chain) if ShouldUploadToTlog(ctx, digest, force, ko.RekorURL) { rClient, err := rekor.NewClient(ko.RekorURL) diff --git a/internal/pkg/cosign/dsse.go b/internal/pkg/cosign/dsse.go new file mode 100644 index 00000000000..f76f65469f1 --- /dev/null +++ b/internal/pkg/cosign/dsse.go @@ -0,0 +1,30 @@ +// 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" +) + +// DSSEAttestor creates attestations in the form of `oci.Signature`s +type DSSEAttestor interface { + // Attest creates an attestation, in the form of an `oci.Signature`, from the given payload. + // The signature and payload are stored as a DSSE envelope in `osi.Signature.Payload()` + DSSEAttest(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) +} diff --git a/internal/pkg/cosign/fulcio/fulcio.go b/internal/pkg/cosign/fulcio/signer.go similarity index 100% rename from internal/pkg/cosign/fulcio/fulcio.go rename to internal/pkg/cosign/fulcio/signer.go diff --git a/internal/pkg/cosign/payload/attestor.go b/internal/pkg/cosign/payload/attestor.go new file mode 100644 index 00000000000..b2ce7613561 --- /dev/null +++ b/internal/pkg/cosign/payload/attestor.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 payload + +import ( + "context" + "crypto" + "encoding/base64" + "encoding/json" + "io" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/internal/pkg/cosign" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/static" + "github.com/sigstore/cosign/pkg/types" + "github.com/sigstore/sigstore/pkg/signature" +) + +type payloadAttestor struct { + signer payloadSigner + payloadType string +} + +var _ cosign.DSSEAttestor = (*payloadAttestor)(nil) + +// Attest implements `cosign.DSSEAttestor` +func (pa *payloadAttestor) DSSEAttest(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) { + p, err := io.ReadAll(payload) + if err != nil { + return nil, nil, err + } + + pb := dsse.PAE(pa.payloadType, p) + + sig, err := pa.signer.signPayload(ctx, pb) + if err != nil { + return nil, nil, err + } + pk, err := pa.signer.publicKey(ctx) + if err != nil { + return nil, nil, err + } + + envelope := dsse.Envelope{ + PayloadType: pa.payloadType, + Payload: base64.StdEncoding.EncodeToString(pb), + Signatures: []dsse.Signature{{ + Sig: base64.StdEncoding.EncodeToString(sig), + }}, + } + + envelopeJSON, err := json.Marshal(envelope) + if err != nil { + return nil, nil, err + } + + opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} + + att, err := static.NewAttestation(envelopeJSON, opts...) + if err != nil { + return nil, nil, err + } + + return att, pk, nil +} + +// NewDSSEAttestor returns a `cosign.DSSEAttestor` which uses the given `signature.Signer` to sign and create a DSSE attestation of given payloads. +// Option types other than `signature.SignOption` and `signature.PublicKeyOption` cause a runtime panic. +func NewDSSEAttestor(payloadType string, + s signature.Signer, + signAndPublicKeyOptions ...interface{}) cosign.DSSEAttestor { + return &payloadAttestor{ + signer: newSigner(s, signAndPublicKeyOptions...), + payloadType: payloadType, + } +} diff --git a/internal/pkg/cosign/payload/attestor_test.go b/internal/pkg/cosign/payload/attestor_test.go new file mode 100644 index 00000000000..97f6d3d19e5 --- /dev/null +++ b/internal/pkg/cosign/payload/attestor_test.go @@ -0,0 +1,86 @@ +// 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" + "encoding/json" + "strings" + "testing" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/pkg/types" + "github.com/sigstore/sigstore/pkg/signature" +) + +func TestDSSEAttestor(t *testing.T) { + testPayloadType := "atTESTation type" + testSigner := NewDSSEAttestor(testPayloadType, mustGetNewSigner(t)) + + testPayload := "test payload" + + ociSig, pub, err := testSigner.DSSEAttest(context.Background(), strings.NewReader(testPayload)) + if err != nil { + t.Fatalf("DSSEAttest() returned error: %v", err) + } + + gotMT, err := ociSig.MediaType() + if err != nil { + t.Fatalf("ociSig.MediaType() failed: %v", err) + } + if gotMT != types.DssePayloadType { + t.Errorf("got MediaType() %q, wanted %q", gotMT, types.DssePayloadType) + } + + verifier, err := signature.LoadVerifier(pub, crypto.SHA256) + if err != nil { + t.Fatalf("signature.LoadVerifier(pub) returned error: %v", err) + } + + gotOCISigPayload, err := ociSig.Payload() + if err != nil { + t.Fatalf("ociSig.Payload() returned error: %v", err) + } + + envelope := dsse.Envelope{} + if err := json.Unmarshal(gotOCISigPayload, &envelope); err != nil { + t.Fatalf("json.Unmarshal() failed: %v", err) + } + + if envelope.PayloadType != testPayloadType { + t.Errorf("got PayloadType %q, wanted %q", envelope.PayloadType, testPayloadType) + } + + if len(envelope.Signatures) != 1 { + t.Errorf("expected a single signature in the envelope, got: %v", envelope.Signatures) + } + + gotPayload, err := base64.StdEncoding.DecodeString(envelope.Payload) + if err != nil { + t.Fatalf("base64.StdEncoding.DecodeString(envelope.Payload) failed: %v", err) + } + + gotSig, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig) + if err != nil { + t.Fatalf("base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig) failed: %v", err) + } + + if err = verifier.VerifySignature(bytes.NewReader(gotSig), bytes.NewReader(gotPayload)); err != nil { + t.Errorf("VerifySignature() returned error: %v", err) + } +} diff --git a/internal/pkg/cosign/payload/payload.go b/internal/pkg/cosign/payload/signer.go similarity index 59% rename from internal/pkg/cosign/payload/payload.go rename to internal/pkg/cosign/payload/signer.go index 3ca86fe7b90..e36abaa83c5 100644 --- a/internal/pkg/cosign/payload/payload.go +++ b/internal/pkg/cosign/payload/signer.go @@ -19,6 +19,7 @@ import ( "context" "crypto" "encoding/base64" + "fmt" "io" "github.com/sigstore/cosign/internal/pkg/cosign" @@ -42,16 +43,11 @@ func (ps *payloadSigner) Sign(ctx context.Context, payload io.Reader) (oci.Signa 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...) + sig, err := ps.signPayload(ctx, payloadBytes) if err != nil { return nil, nil, err } - - pkOpts := []signature.PublicKeyOption{signatureoptions.WithContext(ctx)} - pkOpts = append(pkOpts, ps.publicKeyProviderOpts...) - pk, err := ps.payloadSigner.PublicKey(pkOpts...) + pk, err := ps.publicKey(ctx) if err != nil { return nil, nil, err } @@ -65,13 +61,54 @@ func (ps *payloadSigner) Sign(ctx context.Context, payload io.Reader) (oci.Signa 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{ +func (ps *payloadSigner) publicKey(ctx context.Context) (pk crypto.PublicKey, err error) { + pkOpts := []signature.PublicKeyOption{signatureoptions.WithContext(ctx)} + pkOpts = append(pkOpts, ps.publicKeyProviderOpts...) + pk, err = ps.payloadSigner.PublicKey(pkOpts...) + if err != nil { + return nil, err + } + return pk, nil +} + +func (ps *payloadSigner) signPayload(ctx context.Context, payloadBytes []byte) (sig []byte, err error) { + 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, err + } + + return sig, nil +} + +func newSigner(s signature.Signer, + signAndPublicKeyOptions ...interface{}) payloadSigner { + var sOpts []signature.SignOption + var pkOpts []signature.PublicKeyOption + + for _, opt := range signAndPublicKeyOptions { + switch o := opt.(type) { + case signature.SignOption: + sOpts = append(sOpts, o) + case signature.PublicKeyOption: + pkOpts = append(pkOpts, o) + default: + panic(fmt.Sprintf("options must be of type `signature.SignOption` or `signature.PublicKeyOption`. Got a %T: %v", o, o)) + } + } + + return payloadSigner{ payloadSigner: s, payloadSignerOpts: sOpts, publicKeyProviderOpts: pkOpts, } } + +// NewSigner returns a `cosign.Signer` which uses the given `signature.Signer` to sign requested payloads. +// Option types other than `signature.SignOption` and `signature.PublicKeyOption` cause a runtime panic. +func NewSigner(s signature.Signer, + signAndPublicKeyOptions ...interface{}) cosign.Signer { + signer := newSigner(s, signAndPublicKeyOptions...) + return &signer +} diff --git a/internal/pkg/cosign/payload/signer_test.go b/internal/pkg/cosign/payload/signer_test.go new file mode 100644 index 00000000000..72ed3407d3e --- /dev/null +++ b/internal/pkg/cosign/payload/signer_test.go @@ -0,0 +1,79 @@ +// 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" + "strings" + "testing" + + "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/sigstore/pkg/signature" +) + +func mustGetNewSigner(t *testing.T) signature.Signer { + t.Helper() + priv, err := cosign.GeneratePrivateKey() + if err != nil { + t.Fatalf("cosign.GeneratePrivateKey() failed: %v", err) + } + s, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256) + if err != nil { + t.Fatalf("signature.LoadECDSASignerVerifier(key, crypto.SHA256) failed: %v", err) + } + return s +} + +func TestSigner(t *testing.T) { + testSigner := NewSigner(mustGetNewSigner(t)) + + testPayload := "test payload" + + ociSig, pub, err := testSigner.Sign(context.Background(), strings.NewReader(testPayload)) + if err != nil { + t.Fatalf("Sign() returned error: %v", err) + } + + verifier, err := signature.LoadVerifier(pub, crypto.SHA256) + if err != nil { + t.Fatalf("signature.LoadVerifier(pub) returned error: %v", err) + } + + b64Sig, err := ociSig.Base64Signature() + if err != nil { + t.Fatalf("ociSig.Base64Signature() returned error: %v", err) + } + + sig, err := base64.StdEncoding.DecodeString(b64Sig) + if err != nil { + t.Fatalf("base64.StdEncoding.DecodeString(b64Sig) returned error: %v", err) + } + + gotPayload, err := ociSig.Payload() + if err != nil { + t.Fatalf("ociSig.Payload() returned error: %v", err) + } + + if string(gotPayload) != testPayload { + t.Errorf("ociSig.Payload() returned %q, wanted %q", string(gotPayload), testPayload) + } + + if err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(gotPayload)); err != nil { + t.Errorf("VerifySignature() returned error: %v", err) + } +} diff --git a/internal/pkg/cosign/rekor/rekor.go b/internal/pkg/cosign/rekor/signer.go similarity index 100% rename from internal/pkg/cosign/rekor/rekor.go rename to internal/pkg/cosign/rekor/signer.go