diff --git a/cmd/app/root.go b/cmd/app/root.go index 7f74b76b8..080e23f8a 100644 --- a/cmd/app/root.go +++ b/cmd/app/root.go @@ -49,6 +49,7 @@ func init() { rootCmd.PersistentFlags().String("ca", "", "googleca | pkcs11ca | ephemeralca (for testing)") rootCmd.PersistentFlags().String("aws-hsm-root-ca-path", "", "Path to root CA on disk (only used with AWS HSM)") rootCmd.PersistentFlags().String("gcp_private_ca_parent", "", "private ca parent: /projects//locations// (only used with --ca googleca)") + rootCmd.PersistentFlags().String("gcp_private_ca_version", "v1", "private ca version: [v1|v1beta1] (only used with --ca googleca)") rootCmd.PersistentFlags().String("hsm-caroot-id", "", "HSM ID for Root CA (only used with --ca pkcs11ca)") rootCmd.PersistentFlags().String("ct-log-url", "http://localhost:6962/test", "host and path (with log prefix at the end) to the ct log") rootCmd.PersistentFlags().String("config-path", "/etc/fulcio-config/config.json", "path to fulcio config json") diff --git a/config/deployment.yaml b/config/deployment.yaml index 104291902..7705c0e83 100644 --- a/config/deployment.yaml +++ b/config/deployment.yaml @@ -42,7 +42,9 @@ spec: - containerPort: 2112 # metrics args: [ "serve", - "--host=0.0.0.0", "--port=5555", "--ca=googleca", "--gcp_private_ca_parent=$(CA_PARENT)", "--ct-log-url=http://ct-log/test", "--log_type=prod", + "--host=0.0.0.0", "--port=5555", + "--ca=googleca", "--gcp_private_ca_parent=$(CA_PARENT)", "--gcp_private_ca_version=v1beta1", + "--ct-log-url=http://ct-log/test", "--log_type=prod", ] env: - name: CA_PARENT diff --git a/go.mod b/go.mod index 3bea590eb..04dad2610 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.19.1 golang.org/x/net v0.0.0-20210614182718-04defd469f4e - google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2 + google.golang.org/genproto v0.0.0-20211027162914-98a5263abeca google.golang.org/protobuf v1.27.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum index 1b0c59e26..a418830cf 100644 --- a/go.sum +++ b/go.sum @@ -1530,8 +1530,9 @@ google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2 h1:CUp93KYgL06Y/PdI8aRJaFiAHevPIGWQmijSqaUhue8= google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211027162914-98a5263abeca h1:+e+aQDO4/c9KaG8PXWHTc6/+Du6kz+BKcXCSnV4SSTE= +google.golang.org/genproto v0.0.0-20211027162914-98a5263abeca/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= diff --git a/pkg/api/ca.go b/pkg/api/ca.go index bf5c3b076..30b9fccfa 100644 --- a/pkg/api/ca.go +++ b/pkg/api/ca.go @@ -26,19 +26,19 @@ import ( "strings" "sync" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/go-openapi/runtime/middleware" certauth "github.com/sigstore/fulcio/pkg/ca" "github.com/sigstore/fulcio/pkg/ca/ephemeralca" - "github.com/sigstore/fulcio/pkg/ca/googlecabeta" + googlecav1 "github.com/sigstore/fulcio/pkg/ca/googleca/v1" + googlecav1beta1 "github.com/sigstore/fulcio/pkg/ca/googleca/v1beta1" "github.com/sigstore/fulcio/pkg/ca/x509ca" "github.com/sigstore/fulcio/pkg/challenges" - "github.com/sigstore/sigstore/pkg/cryptoutils" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/go-openapi/runtime/middleware" "github.com/sigstore/fulcio/pkg/config" "github.com/sigstore/fulcio/pkg/ctl" "github.com/sigstore/fulcio/pkg/generated/restapi/operations" "github.com/sigstore/fulcio/pkg/log" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/spf13/viper" ) @@ -53,7 +53,15 @@ func CA() certauth.CertificateAuthority { var err error switch viper.GetString("ca") { case "googleca": - ca, err = googlecabeta.NewCertAuthorityService() + version := viper.GetString("gcp_private_ca_version") + switch version { + case "v1": + ca, err = googlecav1beta1.NewCertAuthorityService() + case "v1beta1": + ca, err = googlecav1.NewCertAuthorityService() + default: + err = fmt.Errorf("invalid value for gcp_private_ca_version: %v", version) + } case "pkcs11ca": ca, err = x509ca.NewX509CA() case "ephemeralca": diff --git a/pkg/api/error.go b/pkg/api/error.go index 6f54f6727..ff6d475e3 100644 --- a/pkg/api/error.go +++ b/pkg/api/error.go @@ -34,8 +34,9 @@ const ( failedToEnterCertInCTL = "Error entering certificate in CTL @ '%v'" failedToMarshalSCT = "Error marshaling signed certificate timestamp" failedToMarshalCert = "Error marshaling code signing certificate" - invalidCredentials = "There was an error processing the credentials for this request" - genericCAError = "error communicating with CA backend" + //nolint + invalidCredentials = "There was an error processing the credentials for this request" + genericCAError = "error communicating with CA backend" ) func errorMsg(message string, code int) *models.Error { diff --git a/pkg/ca/googleca/v1/googleca.go b/pkg/ca/googleca/v1/googleca.go new file mode 100644 index 000000000..10559ac18 --- /dev/null +++ b/pkg/ca/googleca/v1/googleca.go @@ -0,0 +1,219 @@ +// 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 v1 + +import ( + "context" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "sync" + + privateca "cloud.google.com/go/security/privateca/apiv1" + "github.com/sigstore/fulcio/pkg/ca" + "github.com/sigstore/fulcio/pkg/challenges" + "github.com/sigstore/fulcio/pkg/log" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/spf13/viper" + privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1" + "google.golang.org/protobuf/types/known/durationpb" +) + +var ( + once sync.Once + c *privateca.CertificateAuthorityClient + cErr error +) + +type CertAuthorityService struct { + parent string + client *privateca.CertificateAuthorityClient +} + +func NewCertAuthorityService() (*CertAuthorityService, error) { + cas := &CertAuthorityService{ + parent: viper.GetString("gcp_private_ca_parent"), + } + var err error + cas.client, err = casClient() + if err != nil { + return nil, err + } + return cas, nil +} + +func casClient() (*privateca.CertificateAuthorityClient, error) { + // Use a once block to avoid creating a new client every time. + once.Do(func() { + c, cErr = privateca.NewCertificateAuthorityClient(context.Background()) + }) + + return c, cErr +} + +// getPubKeyFormat Returns the PublicKey KeyFormat required by gcp privateca. +// https://pkg.go.dev/google.golang.org/genproto/googleapis/cloud/security/privateca/v1#PublicKey_KeyType +func getPubKeyFormat(pemBytes []byte) (privatecapb.PublicKey_KeyFormat, error) { + block, _ := pem.Decode(pemBytes) + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return 0, fmt.Errorf("failed to parse public key: " + err.Error()) + } + switch pub := pub.(type) { + case *rsa.PublicKey, *ecdsa.PublicKey: + return privatecapb.PublicKey_PEM, nil + default: + return 0, fmt.Errorf("unknown public key type: %v", pub) + } +} + +func Req(parent string, subject *privatecapb.CertificateConfig_SubjectConfig, pemBytes []byte, extensions []*privatecapb.X509Extension) (*privatecapb.CreateCertificateRequest, error) { + // TODO, use the right fields :) + pubkeyFormat, err := getPubKeyFormat(pemBytes) + if err != nil { + return nil, err + } + return &privatecapb.CreateCertificateRequest{ + Parent: parent, + Certificate: &privatecapb.Certificate{ + Lifetime: &durationpb.Duration{Seconds: 20 * 60}, + CertificateConfig: &privatecapb.Certificate_Config{ + Config: &privatecapb.CertificateConfig{ + PublicKey: &privatecapb.PublicKey{ + Format: pubkeyFormat, + Key: pemBytes, + }, + X509Config: &privatecapb.X509Parameters{ + KeyUsage: &privatecapb.KeyUsage{ + BaseKeyUsage: &privatecapb.KeyUsage_KeyUsageOptions{ + DigitalSignature: true, + }, + ExtendedKeyUsage: &privatecapb.KeyUsage_ExtendedKeyUsageOptions{ + CodeSigning: true, + }, + }, + AdditionalExtensions: extensions, + }, + SubjectConfig: subject, + }, + }, + }, + }, nil +} + +func emailSubject(email string) *privatecapb.CertificateConfig_SubjectConfig { + return &privatecapb.CertificateConfig_SubjectConfig{ + SubjectAltName: &privatecapb.SubjectAltNames{ + EmailAddresses: []string{email}, + }} +} + +// SPIFFE IDs go as "Uris" according to the spec: https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md +func spiffeSubject(id string) *privatecapb.CertificateConfig_SubjectConfig { + return &privatecapb.CertificateConfig_SubjectConfig{ + SubjectAltName: &privatecapb.SubjectAltNames{ + Uris: []string{id}, + }, + } +} + +func githubWorkflowSubject(id string) *privatecapb.CertificateConfig_SubjectConfig { + return &privatecapb.CertificateConfig_SubjectConfig{ + SubjectAltName: &privatecapb.SubjectAltNames{ + Uris: []string{id}, + }, + } +} + +func AdditionalExtensions(subject *challenges.ChallengeResult) []*privatecapb.X509Extension { + res := []*privatecapb.X509Extension{} + if subject.TypeVal == challenges.GithubWorkflowValue { + if trigger, ok := subject.AdditionalInfo[challenges.GithubWorkflowTrigger]; ok { + res = append(res, &privatecapb.X509Extension{ + ObjectId: &privatecapb.ObjectId{ + ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 57264, 1, 3}, + }, + Value: []byte(trigger), + }) + } + + if sha, ok := subject.AdditionalInfo[challenges.GithubWorkflowSha]; ok { + res = append(res, &privatecapb.X509Extension{ + ObjectId: &privatecapb.ObjectId{ + ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 57264, 1, 2}, + }, + Value: []byte(sha), + }) + } + } + return res +} + +func KubernetesSubject(id string) *privatecapb.CertificateConfig_SubjectConfig { + return &privatecapb.CertificateConfig_SubjectConfig{ + SubjectAltName: &privatecapb.SubjectAltNames{ + Uris: []string{id}, + }, + } +} + +func IssuerExtension(issuer string) []*privatecapb.X509Extension { + if issuer == "" { + return nil + } + + return []*privatecapb.X509Extension{{ + ObjectId: &privatecapb.ObjectId{ + ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 57264, 1, 1}, + }, + Value: []byte(issuer), + }} +} + +func (c *CertAuthorityService) CreateCertificate(ctx context.Context, subj *challenges.ChallengeResult) (*ca.CodeSigningCertificate, error) { + logger := log.ContextLogger(ctx) + var privca *privatecapb.CertificateConfig_SubjectConfig + switch subj.TypeVal { + case challenges.EmailValue: + privca = emailSubject(subj.Value) + case challenges.SpiffeValue: + privca = spiffeSubject(subj.Value) + case challenges.GithubWorkflowValue: + privca = githubWorkflowSubject(subj.Value) + } + + pubKeyBytes, err := cryptoutils.MarshalPublicKeyToPEM(subj.PublicKey) + if err != nil { + return nil, ca.ValidationError(err) + } + + extensions := append(IssuerExtension(subj.Issuer), AdditionalExtensions(subj)...) + + req, err := Req(c.parent, privca, pubKeyBytes, extensions) + if err != nil { + return nil, ca.ValidationError(err) + } + logger.Infof("requesting cert from %s for %v", c.parent, subj.Value) + + resp, err := c.client.CreateCertificate(ctx, req) + if err != nil { + return nil, err + } + + return ca.CreateCSCFromPEM(subj, resp.PemCertificate, resp.PemCertificateChain) +} diff --git a/pkg/ca/googlecabeta/googleca_test.go b/pkg/ca/googleca/v1/googleca_test.go similarity index 99% rename from pkg/ca/googlecabeta/googleca_test.go rename to pkg/ca/googleca/v1/googleca_test.go index 85b892393..081221b37 100644 --- a/pkg/ca/googlecabeta/googleca_test.go +++ b/pkg/ca/googleca/v1/googleca_test.go @@ -13,7 +13,7 @@ // limitations under the License. // -package googlecabeta +package v1 import ( "crypto" diff --git a/pkg/ca/googlecabeta/googleca.go b/pkg/ca/googleca/v1beta1/googleca.go similarity index 96% rename from pkg/ca/googlecabeta/googleca.go rename to pkg/ca/googleca/v1beta1/googleca.go index 88b19839e..9864c052a 100644 --- a/pkg/ca/googlecabeta/googleca.go +++ b/pkg/ca/googleca/v1beta1/googleca.go @@ -13,7 +13,7 @@ // limitations under the License. // -package googlecabeta +package v1beta1 import ( "context" @@ -37,6 +37,7 @@ import ( var ( once sync.Once c *privateca.CertificateAuthorityClient + cErr error ) type CertAuthorityService struct { @@ -58,15 +59,14 @@ func NewCertAuthorityService() (*CertAuthorityService, error) { func casClient() (*privateca.CertificateAuthorityClient, error) { // Use a once block to avoid creating a new client every time. - var err error once.Do(func() { - c, err = privateca.NewCertificateAuthorityClient(context.Background()) + c, cErr = privateca.NewCertificateAuthorityClient(context.Background()) }) - return c, err + return c, cErr } -// Returns the PublicKey type required by gcp privateca (to handle both PEM_RSA_KEY / PEM_EC_KEY) +// getPubKeyType Returns the PublicKey type required by gcp privateca (to handle both PEM_RSA_KEY / PEM_EC_KEY) // https://pkg.go.dev/google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1#PublicKey_KeyType func getPubKeyType(pemBytes []byte) (interface{}, error) { block, _ := pem.Decode(pemBytes) diff --git a/pkg/ca/googleca/v1beta1/googleca_test.go b/pkg/ca/googleca/v1beta1/googleca_test.go new file mode 100644 index 000000000..326b38146 --- /dev/null +++ b/pkg/ca/googleca/v1beta1/googleca_test.go @@ -0,0 +1,81 @@ +// 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 v1beta1 + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "testing" + + "github.com/sigstore/fulcio/pkg/challenges" +) + +func failErr(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} + +func TestCheckSignatureECDSA(t *testing.T) { + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + failErr(t, err) + + email := "test@gmail.com" + if err := challenges.CheckSignature(&priv.PublicKey, []byte("foo"), email); err == nil { + t.Fatal("check should have failed") + } + + h := sha256.Sum256([]byte(email)) + signature, err := priv.Sign(rand.Reader, h[:], crypto.SHA256) + failErr(t, err) + + if err := challenges.CheckSignature(&priv.PublicKey, signature, email); err != nil { + t.Fatal(err) + } + + // Try a bad email but "good" signature + if err := challenges.CheckSignature(&priv.PublicKey, signature, "bad@email.com"); err == nil { + t.Fatal("check should have failed") + } +} + +func TestCheckSignatureRSA(t *testing.T) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + failErr(t, err) + + email := "test@gmail.com" + if err := challenges.CheckSignature(&priv.PublicKey, []byte("foo"), email); err == nil { + t.Fatal("check should have failed") + } + + h := sha256.Sum256([]byte(email)) + signature, err := priv.Sign(rand.Reader, h[:], crypto.SHA256) + failErr(t, err) + + if err := challenges.CheckSignature(&priv.PublicKey, signature, email); err != nil { + t.Fatal(err) + } + + // Try a bad email but "good" signature + if err := challenges.CheckSignature(&priv.PublicKey, signature, "bad@email.com"); err == nil { + t.Fatal("check should have failed") + } +}