diff --git a/cmd/app/root.go b/cmd/app/root.go index 70b445ef9..e00ad416c 100644 --- a/cmd/app/root.go +++ b/cmd/app/root.go @@ -49,6 +49,7 @@ func init() { rootCmd.PersistentFlags().String("ca", "", "googleca | pkcs11ca") 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 2defb223d..e38dbfc78 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/pkg/api/googleca_signing_cert.go b/pkg/api/googleca_signing_cert.go index c64ec8898..821ff66ee 100644 --- a/pkg/api/googleca_signing_cert.go +++ b/pkg/api/googleca_signing_cert.go @@ -17,11 +17,10 @@ package api import ( "context" + "fmt" "github.com/spf13/viper" - privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1" - "github.com/sigstore/fulcio/pkg/ca/googleca" "github.com/sigstore/fulcio/pkg/challenges" "github.com/sigstore/fulcio/pkg/log" ) @@ -29,26 +28,15 @@ import ( func GoogleCASigningCertHandler(ctx context.Context, subj *challenges.ChallengeResult, publicKey []byte) (string, []string, error) { logger := log.ContextLogger(ctx) - parent := viper.GetString("gcp_private_ca_parent") - - // call a new function here to set the type, we may need to pass back the issuer? - var privca *privatecapb.CertificateConfig_SubjectConfig - switch subj.TypeVal { - case challenges.EmailValue: - privca = googleca.EmailSubject(subj.Value) - case challenges.SpiffeValue: - privca = googleca.SpiffeSubject(subj.Value) - case challenges.GithubWorkflowValue: - privca = googleca.GithubWorkflowSubject(subj.Value) - } + version := viper.GetString("gcp_private_ca_version") - extensions := googleca.IssuerExtension(subj.Issuer) - req := googleca.Req(parent, privca, publicKey, extensions) - logger.Infof("requesting cert from %s for %v", parent, Subject) + logger.Infof("using privateca api version %v", version) - resp, err := googleca.Client().CreateCertificate(ctx, req) - if err != nil { - return "", nil, err + switch version { + case "v1": + return GoogleCASigningCertHandlerV1(ctx, subj, publicKey) + case "v1beta1": + return GoogleCASigningCertHandlerV1Beta1(ctx, subj, publicKey) } - return resp.PemCertificate, resp.PemCertificateChain, nil + panic(fmt.Errorf("unknown gcp private ca version: %v", version)) } diff --git a/pkg/api/googleca_v1_signing_cert.go b/pkg/api/googleca_v1_signing_cert.go new file mode 100644 index 000000000..fe93f7bc6 --- /dev/null +++ b/pkg/api/googleca_v1_signing_cert.go @@ -0,0 +1,54 @@ +// 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 api + +import ( + "context" + + "github.com/spf13/viper" + privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1" + + googleca "github.com/sigstore/fulcio/pkg/ca/googleca/v1" + "github.com/sigstore/fulcio/pkg/challenges" + "github.com/sigstore/fulcio/pkg/log" +) + +func GoogleCASigningCertHandlerV1(ctx context.Context, subj *challenges.ChallengeResult, publicKey []byte) (string, []string, error) { + logger := log.ContextLogger(ctx) + + parent := viper.GetString("gcp_private_ca_parent") + + // call a new function here to set the type, we may need to pass back the issuer? + var privca *privatecapb.CertificateConfig_SubjectConfig + switch subj.TypeVal { + case challenges.EmailValue: + privca = googleca.EmailSubject(subj.Value) + case challenges.SpiffeValue: + privca = googleca.SpiffeSubject(subj.Value) + case challenges.GithubWorkflowValue: + privca = googleca.GithubWorkflowSubject(subj.Value) + } + + extensions := googleca.IssuerExtension(subj.Issuer) + req := googleca.Req(parent, privca, publicKey, extensions) + logger.Infof("requesting cert from %s for %v", parent, Subject) + + resp, err := googleca.Client().CreateCertificate(ctx, req) + if err != nil { + return "", nil, err + } + return resp.PemCertificate, resp.PemCertificateChain, nil +} diff --git a/pkg/api/googleca_v1beta1_signing_cert.go b/pkg/api/googleca_v1beta1_signing_cert.go new file mode 100644 index 000000000..9c55bded3 --- /dev/null +++ b/pkg/api/googleca_v1beta1_signing_cert.go @@ -0,0 +1,54 @@ +// 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 api + +import ( + "context" + + "github.com/spf13/viper" + privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" + + googleca "github.com/sigstore/fulcio/pkg/ca/googleca/v1beta1" + "github.com/sigstore/fulcio/pkg/challenges" + "github.com/sigstore/fulcio/pkg/log" +) + +func GoogleCASigningCertHandlerV1Beta1(ctx context.Context, subj *challenges.ChallengeResult, publicKey []byte) (string, []string, error) { + logger := log.ContextLogger(ctx) + + parent := viper.GetString("gcp_private_ca_parent") + + // call a new function here to set the type, we may need to pass back the issuer? + var privca *privatecapb.CertificateConfig_SubjectConfig + switch subj.TypeVal { + case challenges.EmailValue: + privca = googleca.EmailSubject(subj.Value) + case challenges.SpiffeValue: + privca = googleca.SpiffeSubject(subj.Value) + case challenges.GithubWorkflowValue: + privca = googleca.GithubWorkflowSubject(subj.Value) + } + + extensions := googleca.IssuerExtension(subj.Issuer) + req := googleca.Req(parent, privca, publicKey, extensions) + logger.Infof("requesting cert from %s for %v", parent, Subject) + + resp, err := googleca.Client().CreateCertificate(ctx, req) + if err != nil { + return "", nil, err + } + return resp.PemCertificate, resp.PemCertificateChain, nil +} diff --git a/pkg/ca/googleca/googleca.go b/pkg/ca/googleca/v1/googleca.go similarity index 100% rename from pkg/ca/googleca/googleca.go rename to pkg/ca/googleca/v1/googleca.go diff --git a/pkg/ca/googleca/googleca_test.go b/pkg/ca/googleca/v1/googleca_test.go similarity index 100% rename from pkg/ca/googleca/googleca_test.go rename to pkg/ca/googleca/v1/googleca_test.go diff --git a/pkg/ca/googleca/v1beta1/googleca.go b/pkg/ca/googleca/v1beta1/googleca.go new file mode 100644 index 000000000..3fc21b2a8 --- /dev/null +++ b/pkg/ca/googleca/v1beta1/googleca.go @@ -0,0 +1,138 @@ +// 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 googleca + +import ( + "context" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "sync" + + privateca "cloud.google.com/go/security/privateca/apiv1beta1" + privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" + "google.golang.org/protobuf/types/known/durationpb" +) + +var ( + once sync.Once + c *privateca.CertificateAuthorityClient +) + +func Client() *privateca.CertificateAuthorityClient { + // Use a once block to avoid creating a new client every time. + once.Do(func() { + var err error + c, err = privateca.NewCertificateAuthorityClient(context.Background()) + if err != nil { + panic(err) + } + }) + + return c +} + +// 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{} { + block, _ := pem.Decode(pemBytes) + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + panic("failed to parse public key: " + err.Error()) + } + switch pub := pub.(type) { + case *rsa.PublicKey: + return privatecapb.PublicKey_KeyType(1) + case *ecdsa.PublicKey: + return privatecapb.PublicKey_KeyType(2) + default: + panic(fmt.Errorf("unknown public key type: %v", pub)) + } +} + +func Req(parent string, subject *privatecapb.CertificateConfig_SubjectConfig, pemBytes []byte, extensions []*privatecapb.X509Extension) *privatecapb.CreateCertificateRequest { + // TODO, use the right fields :) + pubkeyType := getPubKeyType(pemBytes) + return &privatecapb.CreateCertificateRequest{ + Parent: parent, + Certificate: &privatecapb.Certificate{ + Lifetime: &durationpb.Duration{Seconds: 20 * 60}, + CertificateConfig: &privatecapb.Certificate_Config{ + Config: &privatecapb.CertificateConfig{ + PublicKey: &privatecapb.PublicKey{ + Type: pubkeyType.(privatecapb.PublicKey_KeyType), + Key: pemBytes, + }, + ReusableConfig: &privatecapb.ReusableConfigWrapper{ + ConfigValues: &privatecapb.ReusableConfigWrapper_ReusableConfigValues{ + ReusableConfigValues: &privatecapb.ReusableConfigValues{ + KeyUsage: &privatecapb.KeyUsage{ + BaseKeyUsage: &privatecapb.KeyUsage_KeyUsageOptions{ + DigitalSignature: true, + }, + ExtendedKeyUsage: &privatecapb.KeyUsage_ExtendedKeyUsageOptions{ + CodeSigning: true, + }, + }, + AdditionalExtensions: extensions, + }, + }, + }, + SubjectConfig: subject, + }, + }, + }, + } +} + +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 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), + }} +} diff --git a/pkg/ca/googleca/v1beta1/googleca_test.go b/pkg/ca/googleca/v1beta1/googleca_test.go new file mode 100644 index 000000000..0d18c12c9 --- /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 googleca + +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") + } +}