Complete cloudcas using CAS v1beta1.

This commit is contained in:
Mariano Cano 2020-09-10 16:19:18 -07:00
parent 1b1f73dec6
commit c8d9cb0a1d
8 changed files with 318 additions and 56 deletions

57
cas/apiv1/extension.go Normal file
View file

@ -0,0 +1,57 @@
package apiv1
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"github.com/pkg/errors"
)
// CertificateAuthorityExtension is type used to encode the certificate
// authority extension.
type CertificateAuthorityExtension struct {
Type string
CertificateID string `asn1:"optional,omitempty"`
KeyValuePairs []string `asn1:"optional,omitempty"`
}
// CreateCertificateAuthorityExtension returns a X.509 extension that shows the
// CAS type, id and a list of optional key value pairs.
func CreateCertificateAuthorityExtension(typ Type, certificateID string, keyValuePairs ...string) (pkix.Extension, error) {
b, err := asn1.Marshal(CertificateAuthorityExtension{
Type: typ.String(),
CertificateID: certificateID,
KeyValuePairs: keyValuePairs,
})
if err != nil {
return pkix.Extension{}, errors.Wrapf(err, "error marshaling certificate id extension")
}
return pkix.Extension{
Id: oidStepCertificateAuthority,
Critical: false,
Value: b,
}, nil
}
// FindCertificateAuthorityExtension returns the certificate authority extension
// from a signed certificate.
func FindCertificateAuthorityExtension(cert *x509.Certificate) (pkix.Extension, bool) {
for _, ext := range cert.Extensions {
if ext.Id.Equal(oidStepCertificateAuthority) {
return ext, true
}
}
return pkix.Extension{}, false
}
// RemoveCertificateAuthorityExtension removes the certificate authority
// extension from a certificate template.
func RemoveCertificateAuthorityExtension(cert *x509.Certificate) {
for i, ext := range cert.ExtraExtensions {
if ext.Id.Equal(oidStepCertificateAuthority) {
cert.ExtraExtensions = append(cert.ExtraExtensions[:i], cert.ExtraExtensions[i+1:]...)
return
}
}
}

View file

@ -30,3 +30,11 @@ func (o *Options) Validate() error {
return nil
}
// HasType returns if the options have the given type.
func (o *Options) HasType(t Type) bool {
if o == nil {
return SoftCAS == t.String()
}
return Type(o.Type).String() == t.String()
}

View file

@ -7,11 +7,11 @@ import (
)
type CreateCertificateRequest struct {
Template *x509.Certificate
Issuer *x509.Certificate
Signer crypto.Signer
Lifetime time.Duration
Template *x509.Certificate
Issuer *x509.Certificate
Signer crypto.Signer
Lifetime time.Duration
Backdate time.Duration
RequestID string
}
type CreateCertificateResponse struct {
@ -19,8 +19,24 @@ type CreateCertificateResponse struct {
CertificateChain []*x509.Certificate
}
type RenewCertificateRequest struct{}
type RenewCertificateResponse struct{}
type RenewCertificateRequest struct {
Template *x509.Certificate
Issuer *x509.Certificate
Signer crypto.Signer
Lifetime time.Duration
Backdate time.Duration
RequestID string
}
type RenewCertificateResponse struct {
Certificate *x509.Certificate
CertificateChain []*x509.Certificate
}
type RevokeCertificateRequest struct{}
type RevokeCertificateResponse struct{}
// RevokeCertificateRequest is the request used to revoke a certificate.
type RevokeCertificateRequest struct {
Certificate *x509.Certificate
}
type RevokeCertificateResponse struct {
Certificate *x509.Certificate
CertificateChain []*x509.Certificate
}

View file

@ -1,5 +1,15 @@
package apiv1
import (
"encoding/asn1"
"strings"
)
var (
oidStepRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}
oidStepCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(oidStepRoot, 2)...)
)
// CertificateAuthorityService is the interface implemented to support external
// certificate authorities.
type CertificateAuthorityService interface {
@ -11,12 +21,24 @@ type CertificateAuthorityService interface {
// Type represents the KMS type used.
type Type string
//
const (
// DefaultCAS is a CertificateAuthorityService using software.
DefaultCAS = ""
// SoftCAS is a CertificateAuthorityService using software.
SoftCAS = "softcas"
SoftCAS = "SoftCAS"
// CloudCAS is a CertificateAuthorityService using Google Cloud CAS.
CloudCAS = "cloudcas"
CloudCAS = "CloudCAS"
)
// String returns the given type as a string. All the letters will be lowercase.
func (t Type) String() string {
if t == "" {
return SoftCAS
}
for _, s := range []string{SoftCAS, CloudCAS} {
if strings.EqualFold(s, string(t)) {
return s
}
}
return string(t)
}

View file

@ -63,7 +63,6 @@ func createCertificateConfig(tpl *x509.Certificate) (*pb.Certificate_Config, err
}
func createPublicKey(key crypto.PublicKey) (*pb.PublicKey, error) {
pk := new(pb.PublicKey)
switch key := key.(type) {
case *ecdsa.PublicKey:
asn1Bytes, err := x509.MarshalPKIXPublicKey(key)
@ -88,8 +87,6 @@ func createPublicKey(key crypto.PublicKey) (*pb.PublicKey, error) {
default:
return nil, errors.Errorf("unsupported public key type: %T", key)
}
return pk, nil
}
func createSubject(cert *x509.Certificate) *pb.Subject {
@ -138,12 +135,43 @@ func createSubjectAlternativeNames(cert *x509.Certificate) *pb.SubjectAltNames {
// Add extra SANs coming from the extensions
if ext, ok := findExtraExtension(cert, oidExtensionSubjectAltName); ok {
ret.CustomSans = []*pb.X509Extension{{
ObjectId: createObjectID(ext.Id),
Critical: ext.Critical,
Value: ext.Value,
}}
var rawValues []asn1.RawValue
if _, err := asn1.Unmarshal(ext.Value, &rawValues); err == nil {
var newValues []asn1.RawValue
for _, v := range rawValues {
switch v.Tag {
case nameTypeDNS:
if len(ret.DnsNames) == 0 {
newValues = append(newValues, v)
}
case nameTypeEmail:
if len(ret.EmailAddresses) == 0 {
newValues = append(newValues, v)
}
case nameTypeIP:
if len(ret.IpAddresses) == 0 {
newValues = append(newValues, v)
}
case nameTypeURI:
if len(ret.Uris) == 0 {
newValues = append(newValues, v)
}
default:
newValues = append(newValues, v)
}
}
if len(newValues) > 0 {
if b, err := asn1.Marshal(newValues); err == nil {
ret.CustomSans = []*pb.X509Extension{{
ObjectId: createObjectID(ext.Id),
Critical: ext.Critical,
Value: b,
}}
}
}
}
}
return ret
}
@ -210,12 +238,14 @@ func createReusableConfig(cert *x509.Certificate) *pb.ReusableConfigWrapper {
}
}
extraExtensions := make([]*pb.X509Extension, len(cert.ExtraExtensions))
for i, ext := range cert.ExtraExtensions {
extraExtensions[i] = &pb.X509Extension{
ObjectId: createObjectID(ext.Id),
Critical: ext.Critical,
Value: ext.Value,
var extraExtensions []*pb.X509Extension
for _, ext := range cert.ExtraExtensions {
if !ext.Id.Equal(oidExtensionSubjectAltName) {
extraExtensions = append(extraExtensions, &pb.X509Extension{
ObjectId: createObjectID(ext.Id),
Critical: ext.Critical,
Value: ext.Value,
})
}
}

View file

@ -3,14 +3,18 @@ package cloudcas
import (
"context"
"crypto/x509"
"encoding/asn1"
"encoding/json"
"encoding/pem"
"fmt"
"time"
privateca "cloud.google.com/go/security/privateca/apiv1beta1"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/smallstep/certificates/cas/apiv1"
privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1"
"google.golang.org/api/option"
pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1"
durationpb "google.golang.org/protobuf/types/known/durationpb"
)
@ -20,6 +24,16 @@ func init() {
})
}
func debug(v interface{}) {
b, _ := json.MarshalIndent(v, "", " ")
fmt.Println(string(b))
}
var (
stepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}
stepOIDCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 2)...)
)
// CloudCAS implements a Certificate Authority Service using Google Cloud CAS.
type CloudCAS struct {
client *privateca.CertificateAuthorityClient
@ -31,14 +45,19 @@ type caClient interface{}
// New creates a new CertificateAuthorityService implementation using Google
// Cloud CAS.
func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) {
client, err := privateca.NewCertificateAuthorityClient(ctx)
var cloudOpts []option.ClientOption
if opts.CredentialsFile != "" {
cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile))
}
client, err := privateca.NewCertificateAuthorityClient(ctx, cloudOpts...)
if err != nil {
return nil, errors.Wrap(err, "error creating client")
}
return &CloudCAS{
client: client,
certificateAuthority: "",
certificateAuthority: "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/Smallstep-Test-Intermediate-CA",
}, nil
}
@ -51,52 +70,131 @@ func (c *CloudCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
}
certConfig, err := createCertificateConfig(req.Template)
if err != nil {
return nil, err
}
ctx, cancel := defaultContext()
defer cancel()
certpb, err := c.client.CreateCertificate(ctx, &privatecapb.CreateCertificateRequest{
Parent: c.certificateAuthority,
CertificateId: "",
Certificate: &privatecapb.Certificate{
CertificateConfig: certConfig,
Lifetime: durationpb.New(req.Lifetime),
Labels: map[string]string{},
},
RequestId: req.RequestID,
})
if err != nil {
return nil, errors.Wrap(err, "cloudCAS CreateCertificate failed")
}
cert, err := parseCertificate(certpb.PemCertificate)
cert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID)
if err != nil {
return nil, err
}
return &apiv1.CreateCertificateResponse{
Certificate: cert,
Certificate: cert,
CertificateChain: chain,
}, nil
}
// RenewCertificate renews the given certificate using Google Cloud CAS.
// Google's CAS does not support the renew operation, so this method uses
// CreateCertificate.
func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {
return nil, fmt.Errorf("not implemented")
switch {
case req.Template == nil:
return nil, errors.New("renewCertificate `template` cannot be nil")
case req.Lifetime == 0:
return nil, errors.New("renewCertificate `lifetime` cannot be 0")
}
cert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID)
if err != nil {
return nil, err
}
return &apiv1.RenewCertificateResponse{
Certificate: cert,
CertificateChain: chain,
}, nil
}
// RevokeCertificate a certificate using Google Cloud CAS.
func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
if req.Certificate == nil {
return nil, errors.New("revokeCertificate `certificate` cannot be nil")
}
return nil, fmt.Errorf("not implemented")
ext, ok := apiv1.FindCertificateAuthorityExtension(req.Certificate)
if !ok {
return nil, errors.New("error revoking certificate: certificate authority extension was not found")
}
var cae apiv1.CertificateAuthorityExtension
if _, err := asn1.Unmarshal(ext.Value, &ext); err != nil {
return nil, errors.Wrap(err, "error unmarshaling certificate authority extension")
}
ctx, cancel := defaultContext()
defer cancel()
certpb, err := c.client.RevokeCertificate(ctx, &pb.RevokeCertificateRequest{
Name: c.certificateAuthority + "/certificates/" + cae.CertificateID,
Reason: pb.RevocationReason_REVOCATION_REASON_UNSPECIFIED,
})
if err != nil {
return nil, errors.Wrap(err, "cloudCAS RevokeCertificate failed")
}
cert, chain, err := getCertificateAndChain(certpb)
if err != nil {
return nil, err
}
return &apiv1.RevokeCertificateResponse{
Certificate: cert,
CertificateChain: chain,
}, nil
}
func (c *CloudCAS) createCertificate(tpl *x509.Certificate, lifetime time.Duration, requestID string) (*x509.Certificate, []*x509.Certificate, error) {
// Removes the CAS extension if it exists.
apiv1.RemoveCertificateAuthorityExtension(tpl)
// Create new CAS extension with the certificate id.
id, err := createCertificateID()
if err != nil {
return nil, nil, err
}
casExtension, err := apiv1.CreateCertificateAuthorityExtension(apiv1.CloudCAS, id)
if err != nil {
return nil, nil, err
}
tpl.ExtraExtensions = append(tpl.ExtraExtensions, casExtension)
// Create and submit certificate
certConfig, err := createCertificateConfig(tpl)
if err != nil {
return nil, nil, err
}
ctx, cancel := defaultContext()
defer cancel()
cert, err := c.client.CreateCertificate(ctx, &pb.CreateCertificateRequest{
Parent: c.certificateAuthority,
CertificateId: id,
Certificate: &pb.Certificate{
CertificateConfig: certConfig,
Lifetime: durationpb.New(lifetime),
Labels: map[string]string{},
},
RequestId: requestID,
})
if err != nil {
return nil, nil, errors.Wrap(err, "cloudCAS CreateCertificate failed")
}
// Return certificate and certificate chain
return getCertificateAndChain(cert)
}
func defaultContext() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 15*time.Second)
}
func createCertificateID() (string, error) {
id, err := uuid.NewRandom()
if err != nil {
return "", errors.Wrap(err, "error creating certificate id")
}
return id.String(), nil
}
func parseCertificate(pemCert string) (*x509.Certificate, error) {
block, _ := pem.Decode([]byte(pemCert))
if block == nil {
@ -108,3 +206,22 @@ func parseCertificate(pemCert string) (*x509.Certificate, error) {
}
return cert, nil
}
func getCertificateAndChain(certpb *pb.Certificate) (*x509.Certificate, []*x509.Certificate, error) {
cert, err := parseCertificate(certpb.PemCertificate)
if err != nil {
return nil, nil, err
}
pemChain := certpb.PemCertificateChain[:len(certpb.PemCertificateChain)-1]
chain := make([]*x509.Certificate, len(pemChain))
for i := range pemChain {
chain[i], err = parseCertificate(pemChain[i])
if err != nil {
return nil, nil, err
}
}
return cert, chain, nil
}

9
go.mod
View file

@ -4,10 +4,12 @@ go 1.14
require (
cloud.google.com/go v0.65.1-0.20200904011802-3c2db50b5678
github.com/Masterminds/sprig/v3 v3.1.0
github.com/aws/aws-sdk-go v1.30.29
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-piv/piv-go v1.5.0
github.com/google/uuid v1.1.2
github.com/googleapis/gax-go/v2 v2.0.5
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
@ -24,11 +26,16 @@ require (
golang.org/x/net v0.0.0-20200822124328-c89045814202
google.golang.org/api v0.31.0
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d
google.golang.org/grpc v1.31.1
google.golang.org/grpc v1.32.0
google.golang.org/protobuf v1.25.0
gopkg.in/square/go-jose.v2 v2.5.1
// cloud.google.com/go/security/privateca/apiv1alpha1 v0.0.0
// google.golang.org/genproto/googleapis/cloud/security/privateca/v1alpha1 v0.0.0
)
// replace github.com/smallstep/cli => ../cli
// replace github.com/smallstep/nosql => ../nosql
// replace go.step.sm/crypto => ../crypto
// replace cloud.google.com/go/security/privateca/apiv1alpha1 => ./pkg/cloud.google.com/go/security/privateca/apiv1alpha1
// replace google.golang.org/genproto/googleapis/cloud/security/privateca/v1alpha1 => ./pkg/google.golang.org/genproto/googleapis/cloud/security/privateca/v1alpha1

5
go.sum
View file

@ -298,6 +298,8 @@ github.com/google/trillian-examples v0.0.0-20190603134952-4e75ba15216c/go.mod h1
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@ -911,6 +913,7 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200910191746-8ad3c7ee2cd1 h1:Oi/dETbxPPblvoi4hgkzJun62A4dwuBsTM0UcZYpN3U=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -929,6 +932,8 @@ google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=