Skip to content

Commit

Permalink
go/registry: Avoid storing full TLS certificates
Browse files Browse the repository at this point in the history
Previously the node registry descriptor contained full TLS certificates for
talking with nodes via gRPC. This changes it so that only TLS public keys are
used when verifying peer certificates for TLS authentication.

This makes the registry descriptors smaller and also makes it easier to pass
around TLS identities (as public keys are much shorter).

Obviously, this change BREAKS the consensus protocol and all previously
signed node descriptors.
  • Loading branch information
kostko committed May 19, 2020
1 parent 8a842aa commit b2ee5dc
Show file tree
Hide file tree
Showing 43 changed files with 861 additions and 737 deletions.
11 changes: 11 additions & 0 deletions .changelog/2556.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
go/registry: Avoid storing full TLS certificates

Previously the node registry descriptor contained full TLS certificates for
talking with nodes via gRPC. This changes it so that only TLS public keys are
used when verifying peer certificates for TLS authentication.

This makes the registry descriptors smaller and also makes it easier to pass
around TLS identities (as public keys are much shorter).

Obviously, this change BREAKS the consensus protocol and all previously
signed node descriptors.
24 changes: 17 additions & 7 deletions go/common/accessctl/accessctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
package accessctl

import (
"crypto/ed25519"
"crypto/x509"
"fmt"

"github.com/oasislabs/oasis-core/go/common/crypto/hash"
"github.com/oasislabs/oasis-core/go/common/crypto/signature"
)

// Subject is an access control subject.
Expand All @@ -14,14 +15,23 @@ type Subject string
// SubjectFromX509Certificate returns a Subject from the given X.509
// certificate.
func SubjectFromX509Certificate(cert *x509.Certificate) Subject {
return SubjectFromDER(cert.Raw)
pk, ok := cert.PublicKey.(ed25519.PublicKey)
if !ok {
// This should never happen if certificates are properly verified.
return ""
}
var spk signature.PublicKey
if err := spk.UnmarshalBinary(pk[:]); err != nil {
// This should NEVER happen.
return ""
}

return SubjectFromPublicKey(spk)
}

// SubjectFromDER returns a Subject from the given certificate's ASN.1 DER
// representation. To do so, it computes the hash of the DER representation.
func SubjectFromDER(der []byte) Subject {
h := hash.NewFromBytes(der)
return Subject(h.String())
// SubjectFromPublicKey returns a Subject from the given public key.
func SubjectFromPublicKey(pubKey signature.PublicKey) Subject {
return Subject(pubKey.String())
}

// Action is an access control action.
Expand Down
121 changes: 121 additions & 0 deletions go/common/crypto/tls/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package tls

import (
"crypto/ed25519"
"crypto/x509"
"fmt"
"time"

"github.com/oasislabs/oasis-core/go/common/crypto/signature"
)

// VerifyOptions are the certificate verification options.
type VerifyOptions struct {
// CommonName is the expected certificate common name.
CommonName string

// Keys is the set of public keys that are allowed to sign the certificate.
Keys map[signature.PublicKey]bool

// AllowUnknownKeys specifies whether any key will be allowed iff Keys is nil.
AllowUnknownKeys bool

// AllowNoCertificate specifies whether connections presenting no certificates will be allowed.
AllowNoCertificate bool
}

// VerifyCertificate verifies a TLS certificate as required by Oasis Core. Instead of using CAs,
// public key pinning is used and certificates must follow the template.
func VerifyCertificate(rawCerts [][]byte, opts VerifyOptions) error {
// Allowing no certificate is useful in case access control is performed by a higher layer.
if len(rawCerts) == 0 && opts.AllowNoCertificate {
return nil
}

// Make sure there is only a single certificate.
if len(rawCerts) != 1 {
return fmt.Errorf("tls: expecting a single certificate (got: %d)", len(rawCerts))
}

cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("tls: bad X509 certificate: %w", err)
}

// Public key should match the pinned key.
if cert.PublicKeyAlgorithm != x509.Ed25519 || cert.SignatureAlgorithm != x509.PureEd25519 {
return fmt.Errorf("tls: bad public key algorithm (expected: Ed25519)")
}
pk, ok := cert.PublicKey.(ed25519.PublicKey)
if !ok {
// This should never happen due to the above check.
return fmt.Errorf("tls: bad public key type (expected: Ed2551 got: %T)", cert.PublicKey)
}
if !opts.AllowUnknownKeys || opts.Keys != nil {
var spk signature.PublicKey
if err = spk.UnmarshalBinary(pk[:]); err != nil {
// This should NEVER happen.
return fmt.Errorf("tls: bad public key: %w", err)
}
if !opts.Keys[spk] {
return fmt.Errorf("tls: bad public key (%s)", spk)
}
}

// Common name should match.
if cert.Subject.CommonName != opts.CommonName {
return fmt.Errorf("tls: bad common name (expected: %s got: %s)",
opts.CommonName,
cert.Subject.CommonName,
)
}

// Certificate serial number should match the template.
if cert.SerialNumber.Cmp(certTemplate.SerialNumber) != 0 {
return fmt.Errorf("tls: bad serial number (expected: %s got: %s)",
certTemplate.SerialNumber,
cert.SerialNumber,
)
}

// Certificate key usage should match the template.
if cert.KeyUsage != certTemplate.KeyUsage {
return fmt.Errorf("tls: bad key usage (expected: %d got: %d)",
certTemplate.KeyUsage,
cert.KeyUsage,
)
}

// Certificate extended key usage should match the template.
if len(cert.ExtKeyUsage) != len(certTemplate.ExtKeyUsage) || len(cert.UnknownExtKeyUsage) != 0 {
return fmt.Errorf("tls: bad extended key usage")
}
for i, eku := range certTemplate.ExtKeyUsage {
if eku != cert.ExtKeyUsage[i] {
return fmt.Errorf("tls: bad extended key usage (expected: %d got: %d)",
eku,
cert.ExtKeyUsage[i],
)
}
}

// There should be no extra extensions.
if len(cert.ExtraExtensions) != 0 || len(cert.UnhandledCriticalExtensions) != 0 {
return fmt.Errorf("tls: bad extensions")
}

// Certificate should not be expired.
now := time.Now()
if now.Before(cert.NotBefore) {
return fmt.Errorf("tls: current time %s is before %s", now.Format(time.RFC3339), cert.NotBefore.Format(time.RFC3339))
} else if now.After(cert.NotAfter) {
return fmt.Errorf("tls: current time %s is after %s", now.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339))
}

// Signature should be valid.
if err = cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil {
return fmt.Errorf("tls: bad signature: %w", err)
}

return nil
}
58 changes: 58 additions & 0 deletions go/common/crypto/tls/verify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package tls

import (
"crypto/ed25519"
"testing"

"github.com/stretchr/testify/require"

"github.com/oasislabs/oasis-core/go/common/crypto/signature"
"github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/memory"
)

func TestVerifyCertificate(t *testing.T) {
require := require.New(t)

cert, err := Generate("my-common-name")
require.NoError(err, "Generate")

signer := memory.NewFromRuntime(cert.PrivateKey.(ed25519.PrivateKey))
signer2 := memory.NewTestSigner("common/crypto/tls: test signer")

rawCerts := cert.Certificate
err = VerifyCertificate(rawCerts, VerifyOptions{
CommonName: "my-common-name",
Keys: map[signature.PublicKey]bool{
signer.Public(): true,
},
})
require.NoError(err, "VerifyCertificate")

err = VerifyCertificate(rawCerts, VerifyOptions{
CommonName: "my-common-name",
AllowUnknownKeys: true,
})
require.NoError(err, "VerifyCertificate")

err = VerifyCertificate(nil, VerifyOptions{
CommonName: "my-common-name",
AllowNoCertificate: true,
})
require.NoError(err, "VerifyCertificate")

err = VerifyCertificate(rawCerts, VerifyOptions{
CommonName: "other-common-name",
Keys: map[signature.PublicKey]bool{
signer.Public(): true,
},
})
require.Error(err, "VerifyCertificate should fail with mismatched common name")

err = VerifyCertificate(rawCerts, VerifyOptions{
CommonName: "my-common-name",
Keys: map[signature.PublicKey]bool{
signer2.Public(): true,
},
})
require.Error(err, "VerifyCertificate should fail with mismatched public key")
}
73 changes: 56 additions & 17 deletions go/common/grpc/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,62 @@ package grpc

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"

"google.golang.org/grpc/credentials"
"google.golang.org/grpc/security/advancedtls"

"github.com/oasislabs/oasis-core/go/common/crypto/signature"
cmnTLS "github.com/oasislabs/oasis-core/go/common/crypto/tls"
)

// NewClientTLSConfigFromFile is a variant of the
// "google.golang.org/grpc/credentials".NewClientTLSFromFile function that
// returns a plain "crypto/tls".Config struct instead of wrapping it in the
// "google.golang.org/grpc/credentials".TransportCredentials object.
func NewClientTLSConfigFromFile(certFile, serverNameOverride string) (*tls.Config, error) {
b, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, err
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
return nil, fmt.Errorf("credentials: failed to append certificates")
}
return &tls.Config{ServerName: serverNameOverride, RootCAs: cp}, nil
// ClientOptions contains all the fields needed to configure a TLS client.
type ClientOptions struct {
// CommonName is the expected certificate common name.
CommonName string

// ServerPubKeys is the set of public keys that are allowed to sign the server's certificate. If
// this field is set GetServerPubKeys will be ignored.
ServerPubKeys map[signature.PublicKey]bool

// If GetServerPubKeys is set and ServerPubKeys is nil, GetServerPubKeys will be invoked every
// time when verifying the server certificates.
GetServerPubKeys func() (map[signature.PublicKey]bool, error)

// If field Certificates is set, field GetClientCertificate will be ignored. The server will use
// Certificates every time when asked for a certificate, without performing certificate
// reloading.
Certificates []tls.Certificate

// If GetClientCertificate is set and Certificates is nil, the server will invoke this function
// every time asked to present certificates to the client when a new connection is established.
// This is known as peer certificate reloading.
GetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
}

// NewClientCreds creates new client TLS transport credentials.
func NewClientCreds(opts *ClientOptions) (credentials.TransportCredentials, error) {
return advancedtls.NewClientCreds(&advancedtls.ClientOptions{
Certificates: opts.Certificates,
GetClientCertificate: opts.GetClientCertificate,
VType: advancedtls.SkipVerification,
VerifyPeer: func(params *advancedtls.VerificationFuncParams) (*advancedtls.VerificationResults, error) {
var err error
keys := opts.ServerPubKeys
if keys == nil && opts.GetServerPubKeys != nil {
if keys, err = opts.GetServerPubKeys(); err != nil {
return nil, err
}
}

err = cmnTLS.VerifyCertificate(params.RawCerts, cmnTLS.VerifyOptions{
CommonName: opts.CommonName,
Keys: keys,
})
if err != nil {
return nil, err
}

return &advancedtls.VerificationResults{}, nil
},
})
}
16 changes: 16 additions & 0 deletions go/common/grpc/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package grpc
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"os"
Expand All @@ -22,6 +23,7 @@ import (
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/keepalive"

cmnTLS "github.com/oasislabs/oasis-core/go/common/crypto/tls"
"github.com/oasislabs/oasis-core/go/common/grpc/auth"
"github.com/oasislabs/oasis-core/go/common/identity"
"github.com/oasislabs/oasis-core/go/common/logging"
Expand Down Expand Up @@ -306,6 +308,9 @@ type ServerConfig struct { // nolint: maligned
InstallWrapper bool
// AuthFunc is the authentication function for access control.
AuthFunc auth.AuthenticationFunction
// ClientCommonName is the expected common name on client TLS certificates. If not specified,
// the default identity.CommonName will be used.
ClientCommonName string
// CustomOptions is an array of extra options for the grpc server.
CustomOptions []grpc.ServerOption
}
Expand Down Expand Up @@ -450,6 +455,10 @@ func NewServer(config *ServerConfig) (*Server, error) {
// Default to NoAuth.
config.AuthFunc = auth.NoAuth
}
if config.ClientCommonName == "" {
// Default to identity.CommonName.
config.ClientCommonName = identity.CommonName
}
var wrapper *grpcWrapper
unaryInterceptors := []grpc.UnaryServerInterceptor{
logAdapter.unaryLogger,
Expand Down Expand Up @@ -479,6 +488,13 @@ func NewServer(config *ServerConfig) (*Server, error) {
if config.Identity != nil && config.Identity.GetTLSCertificate() != nil {
tlsConfig := &tls.Config{
ClientAuth: clientAuthType,
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
return cmnTLS.VerifyCertificate(rawCerts, cmnTLS.VerifyOptions{
CommonName: config.ClientCommonName,
AllowUnknownKeys: true,
AllowNoCertificate: true,
})
},
GetCertificate: func(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
return config.Identity.GetTLSCertificate(), nil
},
Expand Down
Loading

0 comments on commit b2ee5dc

Please sign in to comment.