Skip to content

Commit

Permalink
Upgrade fulcios to use of the google privateca api at v1 (#218)
Browse files Browse the repository at this point in the history
* Upgrade fulcios usage of the goole privateca api to v1

Signed-off-by: Scott Nichols <n3wscott@chainguard.dev>

* Adding a flag to select the google private ca api version at runtime

Signed-off-by: Scott Nichols <n3wscott@chainguard.dev>

* lint fix.

Signed-off-by: Scott Nichols <n3wscott@chainguard.dev>
  • Loading branch information
n3wscott authored Nov 29, 2021
1 parent 48b5188 commit 229be8b
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 17 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 | 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/<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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
20 changes: 14 additions & 6 deletions pkg/api/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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":
Expand Down
5 changes: 3 additions & 2 deletions pkg/api/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
219 changes: 219 additions & 0 deletions pkg/ca/googleca/v1/googleca.go
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.
//

package googlecabeta
package v1

import (
"crypto"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.
//

package googlecabeta
package v1beta1

import (
"context"
Expand All @@ -37,6 +37,7 @@ import (
var (
once sync.Once
c *privateca.CertificateAuthorityClient
cErr error
)

type CertAuthorityService struct {
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 229be8b

Please sign in to comment.