Skip to content

Commit

Permalink
Adding a flag to select the google private ca api version at runtime
Browse files Browse the repository at this point in the history
Signed-off-by: Scott Nichols <n3wscott@chainguard.dev>
  • Loading branch information
n3wscott committed Oct 27, 2021
1 parent 00d621a commit c0bc54f
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 22 deletions.
1 change: 1 addition & 0 deletions cmd/app/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/<project>/locations/<location>/<name> (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")
Expand Down
4 changes: 3 additions & 1 deletion config/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 9 additions & 21 deletions pkg/api/googleca_signing_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,26 @@ 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"
)

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))
}
54 changes: 54 additions & 0 deletions pkg/api/googleca_v1_signing_cert.go
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 54 additions & 0 deletions pkg/api/googleca_v1beta1_signing_cert.go
Original file line number Diff line number Diff line change
@@ -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
}
File renamed without changes.
File renamed without changes.
138 changes: 138 additions & 0 deletions pkg/ca/googleca/v1beta1/googleca.go
Original file line number Diff line number Diff line change
@@ -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),
}}
}
81 changes: 81 additions & 0 deletions pkg/ca/googleca/v1beta1/googleca_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}

0 comments on commit c0bc54f

Please sign in to comment.