. "$basedir/workspace-status.local"
fi
-ROOT_DOMAIN=${ROOT_DOMAIN:-"fuhry.dev"}
-DEFAULT_REGION=${DEFAULT_REGION:-"hq"}
-DEFAULT_HOST_DOMAIN=${DEFAULT_HOST_DOMAIN:-"${DEFAULT_REGION}.${ROOT_DOMAIN}"}
-SD_DOMAIN=${SD_DOMAIN:-"v.${ROOT_DOMAIN}"}
-WEB_SERVICES_DOMAIN=${WEB_SERVICES_DOMAIN:-"${ROOT_DOMAIN}"}
-MACHINES_HOST=${MACHINES_HOST:-"machines.${WEB_SERVICES_DOMAIN}"}
-MACHINES_MQTT_TOPIC=${MACHINES_MQTT_TOPIC:-"machines/events"}
-DBUS_PREFIX=${DBUS_PREFIX:-"dev.fuhry.runtime"}
-DBUS_PATH=${DBUS_PATH:-"/${DBUS_PREFIX//\./\/}"}
-ORG_NAME=${ORG_NAME:-"FooCorp"}
-ORG_SLUG=${ORG_SLUG:-"runtime"}
-SYSTEM_CONF_DIR=${SYSTEM_CONF_DIR:-"/etc/${ORG_SLUG}"}
-ROOT_CA_NAME=${ROOT_CA_NAME:-"${ORG_NAME} Root"}
-INT_CA_NAME=${INT_CA_NAME:-"${ORG_NAME} Intermediate mTLS"}
-DEVICE_TRUST_TOKEN_NAME=${DEVICE_TRUST_TOKEN_NAME:-"${ORG_NAME} Device Trust"}
+ROOT_DOMAIN="${ROOT_DOMAIN:-"fuhry.dev"}"
+DEFAULT_REGION="${DEFAULT_REGION:-"hq"}"
+DEFAULT_HOST_DOMAIN="${DEFAULT_HOST_DOMAIN:-"${DEFAULT_REGION}.${ROOT_DOMAIN}"}"
+SD_DOMAIN="${SD_DOMAIN:-"v.${ROOT_DOMAIN}"}"
+WEB_SERVICES_DOMAIN="${WEB_SERVICES_DOMAIN:-"${ROOT_DOMAIN}"}"
+MACHINES_HOST="${MACHINES_HOST:-"machines.${WEB_SERVICES_DOMAIN}"}"
+MACHINES_MQTT_TOPIC="${MACHINES_MQTT_TOPIC:-"machines/events"}"
+DBUS_PREFIX="${DBUS_PREFIX:-"dev.fuhry.runtime"}"
+DBUS_PATH="${DBUS_PATH:-"/${DBUS_PREFIX//\./\/}"}"
+ORG_NAME="${ORG_NAME:-"FooCorp"}"
+ORG_SLUG="${ORG_SLUG:-"runtime"}"
+SYSTEM_CONF_DIR="${SYSTEM_CONF_DIR:-"/etc/${ORG_SLUG}"}"
+ROOT_CA_NAME="${ROOT_CA_NAME:-"${ORG_NAME} Root"}"
+INT_CA_NAME="${INT_CA_NAME:-"${ORG_NAME} Intermediate mTLS"}"
+DEVICE_TRUST_TOKEN_NAME="${DEVICE_TRUST_TOKEN_NAME:-"${ORG_NAME} Device Trust"}"
+DEVICE_TRUST_PRINCIPAL="${DEVICE_TRUST_PRINCIPAL:-"devicetrust"}"
TAG="$(cd "$basedir/.."; git describe HEAD 2>/dev/null)"
VERSION="${TAG:-0.0.0+unset}"
ROOT_CA_NAME
INT_CA_NAME
DEVICE_TRUST_TOKEN_NAME
+ DEVICE_TRUST_PRINCIPAL
VERSION
)
load("@rules_go//go:def.bzl", "go_library")
# Ignore this package in gazelle so constants_in.go is picked up by IDEs but not builds.
-# gazelle:ignore
+# gazelle:exclude constants_in.go
genrule(
name = "constants_go",
RootCAName = "$ROOT_CA_NAME"
IntCAName = "$INT_CA_NAME"
DeviceTrustTokenName = "$DEVICE_TRUST_TOKEN_NAME"
+ DeviceTrustPrincipal = "$DEVICE_TRUST_PRINCIPAL"
Version = "$VERSION"
)
name = "mtls",
srcs = [
"identity.go",
+ "lazy_identity.go",
"pkcs11.go",
"provider_anonymous.go",
"provider_file.go",
"net/url"
"os"
"strings"
+ "time"
)
var oidSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
hasher.Write(cert.Raw)
return hasher.Sum(dest)
}
+
+func ValidNow(cert *x509.Certificate) error {
+ now := time.Now()
+
+ if now.Before(cert.NotBefore) {
+ return fmt.Errorf("certificate is not valid until %s (now = %s)",
+ cert.NotBefore.UTC().Format(time.RFC3339),
+ now.UTC().Format(time.RFC3339))
+ }
+
+ if now.After(cert.NotAfter) {
+ return fmt.Errorf("certificate expired at %s (now = %s)",
+ cert.NotAfter.UTC().Format(time.RFC3339),
+ now.UTC().Format(time.RFC3339))
+ }
+
+ return nil
+}
import (
"crypto/tls"
+ "errors"
"flag"
"fmt"
"os/user"
"strings"
- "time"
"go.fuhry.dev/runtime/mtls/certutil"
"go.fuhry.dev/runtime/utils/log"
CertificateProvider
}
-type identityLoaderFunc func(name string) (CertificateProvider, error)
+var _ Identity = &substantiatedIdentity{}
+
+type stubIdentity struct {
+ *inaccessibleCertificate
+
+ name string
+ cls PrincipalClass
+}
+
+var _ Identity = &stubIdentity{}
+
+type identityLoaderFunc func(cls PrincipalClass, name string) (CertificateProvider, error)
type identityDriver struct {
name string
var identityDrivers []*identityDriver
+var ErrUnsupportedClass = errors.New("this driver does not support loading this identity class")
+
func RegisterIdentityDriver(name string, load identityLoaderFunc) {
driver := &identityDriver{
name: name,
return false
}
- if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) {
+ if err := certutil.ValidNow(cert); err != nil {
return false
}
return a.Name() == b.Name() && a.Class() == b.Class()
}
-type stubIdentity struct {
- *inaccessibleCertificate
-
- name string
- cls PrincipalClass
-}
-
func (id *stubIdentity) Name() string {
return id.name
}
return false
}
-func ParseIdentity(identity string) Identity {
+func ParseIdentity(identity string) LazyIdentity {
const (
anonymousIdentityStr = "anonymous"
userPrefix = "user."
)
if identity == anonymousIdentityStr {
- logger.V(3).Debugf("ParseIdentity(%q) -> Anonymous()", identity)
- return Anonymous()
+ logger.V(3).Debugf("ParseIdentity(%q) -> Anonymous", identity)
+ return NewLazyIdentity(AnonymousPrincipal, anonymousIdentityStr)
} else if strings.HasPrefix(identity, userPrefix) {
- logger.V(3).Debugf("ParseIdentity(%q) -> NewUserIdentity(%q)", identity, strings.TrimPrefix(identity, userPrefix))
- return NewUserIdentity(strings.TrimPrefix(identity, userPrefix))
+ logger.V(3).Debugf("ParseIdentity(%q) -> user:%s", identity, strings.TrimPrefix(identity, userPrefix))
+ return NewLazyIdentity(UserPrincipal, strings.TrimPrefix(identity, userPrefix))
} else if strings.HasPrefix(identity, sslPrefix) {
- logger.V(3).Debugf("ParseIdentity(%q) -> NewSSLCertificate(%q)", identity, strings.TrimPrefix(identity, sslPrefix))
- return NewSSLCertificate(strings.TrimPrefix(identity, sslPrefix))
+ logger.V(3).Debugf("ParseIdentity(%q) -> ssl:%s", identity, strings.TrimPrefix(identity, sslPrefix))
+ return NewLazyIdentity(SSLCertificatePrincipal, strings.TrimPrefix(identity, sslPrefix))
}
- logger.V(3).Debugf("ParseIdentity(%q) -> NewServiceIdentity(%q)", identity, identity)
- return NewServiceIdentity(identity)
+ logger.V(3).Debugf("ParseIdentity(%q) -> service:%s", identity, identity)
+ return NewLazyIdentity(ServicePrincipal, identity)
}
-func NewServiceIdentity(service string) Identity {
- for _, driver := range identityDrivers {
- logger.V(1).Infof("trying driver %s to load service identity %s", driver.name, service)
- identity, err := driver.load(service)
-
- if err == nil {
- subst := &substantiatedIdentity{
- CertificateProvider: identity,
- }
- logger.V(2).Infof("driver %s reports it loaded identity %s:%s", driver.name, subst.Class().String(), subst.Name())
-
- if subst.Name() == service && subst.Class() == ServicePrincipal {
- logger.V(1).Noticef("successfully loaded %s(%s) with driver %s", subst.Class().String(), subst.Name(), driver.name)
- return subst
- } else {
- logger.V(2).Warnf(
- "driver %s successfully loaded a certificate, but it doesn't match what "+
- "we expected: class %s (expected)/%s (got); name %s (expected)/%s (got)",
- driver.name, ServicePrincipal.String(), subst.Class().String(),
- service, subst.Name())
- }
- } else {
- logger.V(2).Warnf("driver %s failed to load service identity %s: %+v", driver.name, service, err)
- }
- }
+func NewRemoteServiceIdentity(service string) Identity {
+ return NewStubIdentity(ServicePrincipal, service)
+}
+func NewStubIdentity(cls PrincipalClass, principal string) Identity {
return &stubIdentity{
inaccessibleCertificate: &inaccessibleCertificate{},
- name: service,
- cls: ServicePrincipal,
+ name: principal,
+ cls: cls,
}
}
-func NewUserIdentity(username string) Identity {
- for _, driver := range identityDrivers {
- logger.V(1).Infof("trying driver %s to load user identity %s", driver.name, username)
- identity, err := driver.load(username)
- if err == nil {
- subst := &substantiatedIdentity{
- CertificateProvider: identity,
- }
- logger.V(2).Infof("driver %s reports it loaded identity %s:%s", driver.name, subst.Class().String(), subst.Name())
-
- if subst.Name() == username && subst.Class() == UserPrincipal {
- logger.V(1).Noticef("successfully loaded %s(%s) with driver %s", subst.Class().String(), subst.Name(), driver.name)
- return subst
- } else {
- logger.V(2).Warnf(
- "driver %s successfully loaded a certificate, but it doesn't match what "+
- "we expected: class %s (expected)/%s (got); name %s (expected)/%s (got)",
- driver.name, ServicePrincipal.String(), subst.Class().String(),
- username, subst.Name())
- }
- } else {
- logger.V(2).Warnf("driver %s failed to load service identity %s: %+v", driver.name, username, err)
- }
- }
-
- return &stubIdentity{
- inaccessibleCertificate: &inaccessibleCertificate{},
+func NewServiceIdentity(service string) Identity {
+ id, _ := NewLazyIdentity(ServicePrincipal, service).Substantiate()
+ return id
+}
- name: username,
- cls: UserPrincipal,
- }
+func NewUserIdentity(username string) Identity {
+ id, _ := NewLazyIdentity(UserPrincipal, username).Substantiate()
+ return id
}
func NewDefaultUserIdentity() (Identity, error) {
return nil, err
}
- return NewUserIdentity(user.Username), nil
+ id := NewLazyIdentity(UserPrincipal, user.Username)
+ if subst, ok := id.Substantiate(); ok {
+ return subst, nil
+ } else {
+ return subst, ErrCertificateInaccessible
+ }
}
type substantiatedSslCertificate struct {
logger = log.Default().WithPrefix("mtls")
}
+// SetDefaultIdentity sets the mtls id that is used when -mtls.id is not specified
+// on the command line.
func SetDefaultIdentity(ident string) {
+ if flag.Parsed() {
+ panic("must call SetDefaultIdentity before flags are parsed")
+ }
defaultMtlsIdentity = ident
}
log.Default().V(2).Debugf("couldn't load a user identity: err: %+v", err)
}
- return NewServiceIdentity(defaultDefaultIdentity)
+ return NewLazyIdentity(ServicePrincipal, defaultDefaultIdentity)
}
return ParseIdentity(defaultMtlsIdentity)
--- /dev/null
+package mtls
+
+import (
+ "context"
+ "crypto"
+ "crypto/tls"
+ "crypto/x509"
+ "sync"
+)
+
+// LazyIdentity represents an identity that is validated on first use, rather than at creation time.
+type LazyIdentity interface {
+ Identity
+
+ // Substantiate attempts to load the certificate and private key corresponding to the Identity.
+ //
+ // It always returns a non-nil Identity; if ok is true, the credentials are available for use.
+ // If ok is false, the returned Identity is invalid. It can be compared to other Identities, but
+ // calls to CertificateProvider functions always return `ErrCertificateInaccessible`.
+ Substantiate() (ident Identity, ok bool)
+}
+
+type lazyIdentity struct {
+ cp CertificateProvider
+
+ mu sync.Mutex
+ name string
+ cls PrincipalClass
+}
+
+var _ Identity = &lazyIdentity{}
+
+func NewLazyIdentity(cls PrincipalClass, name string) LazyIdentity {
+ return &lazyIdentity{name: name, cls: cls}
+}
+
+func (id *lazyIdentity) Name() string {
+ return id.name
+}
+
+func (id *lazyIdentity) Class() PrincipalClass {
+ return id.cls
+}
+
+func (id *lazyIdentity) Equals(other Identity) bool {
+ return identityEquals(id, other)
+}
+
+func (id *lazyIdentity) IsValid() bool {
+ id.tryLoad()
+
+ if id.cp != nil {
+ return identityIsValid(id.cp)
+ }
+
+ return false
+}
+
+func (id *lazyIdentity) tryLoad() {
+ id.mu.Lock()
+ defer id.mu.Unlock()
+
+ if id.cp != nil {
+ return
+ }
+
+ if id.cls == AnonymousPrincipal {
+ id.cp = Anonymous()
+ return
+ }
+ if id.cls == SSLCertificatePrincipal {
+ if cert := NewSSLCertificate(id.name); cert.IsValid() {
+ id.cp = cert
+ }
+ return
+ }
+ for _, driver := range identityDrivers {
+ logger.V(1).Infof("trying driver %s to load %s identity %s", driver.name, id.cls, id.name)
+ if cert, err := driver.load(id.cls, id.name); err == nil {
+ subst := &substantiatedIdentity{cert}
+ if subst.Name() == id.name && subst.Class() == id.cls {
+ logger.V(2).Infof("driver %s reports it loaded identity %s:%s", driver.name, id.cls.String(), id.name)
+ id.cp = cert
+ return
+ } else {
+ logger.V(2).Warnf(
+ "driver %s successfully loaded a certificate, but it doesn't match what "+
+ "we expected: class %s (expected)/%s (got); name %s (expected)/%s (got)",
+ driver.name, id.cls.String(), subst.Class().String(),
+ id.name, subst.Name())
+ }
+ } else {
+ logger.V(2).Warnf("driver %s failed to load %s identity %s: %+v", driver.name, id.cls.String(), id.name, err)
+ }
+ }
+}
+
+func (id *lazyIdentity) LeafCertificate() (*x509.Certificate, error) {
+ id.tryLoad()
+ if id.cp == nil {
+ return nil, ErrCertificateInaccessible
+ }
+ return id.cp.LeafCertificate()
+}
+
+func (id *lazyIdentity) IntermediateCertificates() ([]*x509.Certificate, error) {
+ id.tryLoad()
+ if id.cp == nil {
+ return nil, ErrCertificateInaccessible
+ }
+ return id.cp.IntermediateCertificates()
+}
+
+func (id *lazyIdentity) RootCertificate() (*x509.Certificate, error) {
+ id.tryLoad()
+ if id.cp == nil {
+ return nil, ErrCertificateInaccessible
+ }
+ return id.cp.RootCertificate()
+}
+
+func (id *lazyIdentity) PrivateKey() (crypto.PrivateKey, error) {
+ id.tryLoad()
+ if id.cp == nil {
+ return nil, ErrCertificateInaccessible
+ }
+ return id.cp.PrivateKey()
+}
+
+func (id *lazyIdentity) NewTlsCertificate() (*tls.Certificate, error) {
+ id.tryLoad()
+ if id.cp == nil {
+ return nil, ErrCertificateInaccessible
+ }
+ return id.cp.NewTlsCertificate()
+}
+
+func (id *lazyIdentity) NewDialContextFunc() DialContextFunc {
+ id.tryLoad()
+ if id.cp == nil {
+ return (&inaccessibleCertificate{}).NewDialContextFunc()
+ }
+ return id.cp.NewDialContextFunc()
+}
+
+func (id *lazyIdentity) TlsConfig(ctx context.Context) (*tls.Config, error) {
+ id.tryLoad()
+ if id.cp == nil {
+ return nil, ErrCertificateInaccessible
+ }
+ return id.cp.TlsConfig(ctx)
+}
+
+func (id *lazyIdentity) Substantiate() (Identity, bool) {
+ id.tryLoad()
+
+ if id.cp != nil {
+ return &substantiatedIdentity{
+ CertificateProvider: id.cp,
+ }, true
+ }
+
+ return &stubIdentity{
+ inaccessibleCertificate: &inaccessibleCertificate{},
+ name: id.name,
+ cls: id.cls,
+ }, false
+}
}, nil
}
-func LoadUserIdentityFromFilesystem() (*FileBackedCertificate, error) {
+func LoadUserIdentityFromFilesystem(username string) (*FileBackedCertificate, error) {
fullChainPath, ok := os.LookupEnv("STEP_PERSONAL_CERTIFICATE")
if !ok {
return nil, fmt.Errorf("failed to get user certificate path from env STEP_PERSONAL_CERTIFICATE")
}
}
- return &FileBackedCertificate{
+ cert := &FileBackedCertificate{
LeafPath: fullChainPath,
IntermediatesPath: fullChainPath,
PrivateKeyPath: keyPath,
RootPath: rootPath,
- }, nil
+ }
+
+ leaf, err := cert.LeafCertificate()
+ if err != nil {
+ return nil, fmt.Errorf("error reading leaf certificate: %v", err)
+ }
+
+ spiffe := certutil.SpiffeUrlFromCertificate(leaf)
+ if spiffe == nil {
+ return nil, fmt.Errorf("error getting spiffe URL from loaded leaf certificate %s", fullChainPath)
+ }
+
+ id, err := ParseRemoteIdentity(spiffe.String())
+ if err != nil {
+ return nil, fmt.Errorf("failure parsing spiffe URL %q from loaded leaf certificate %s: %v",
+ spiffe, fullChainPath, err)
+ }
+
+ if id.Class != User || id.Principal != username {
+ return nil, fmt.Errorf("successfully loaded certificate at %s, but certificate contains the wrong credential: %s",
+ id.ToSpiffe())
+ }
+
+ return cert, nil
}
func LoadSSLCertificateFromFilesystem(certName string) (*FileBackedCertificate, error) {
flag.StringVar(&sslCertsBaseDir, "tls.certs-dir", sslCertsBaseDir, "directory to look under for public-site SSL certificates (NOT mTLS certs)")
flag.Func("mtls.certs-dir", "additional directory to search for mTLS certificates", appendMtlsCertificateDir)
- RegisterIdentityDriver("file_service_global", func(serviceName string) (CertificateProvider, error) {
+ RegisterIdentityDriver("file_service_global", func(cls PrincipalClass, serviceName string) (CertificateProvider, error) {
+ if cls != ServicePrincipal {
+ return nil, ErrUnsupportedClass
+ }
return LoadServiceIdentityFromFilesystem(serviceName)
})
- RegisterIdentityDriver("file_service_csi_spiffe", func(serviceName string) (CertificateProvider, error) {
+ RegisterIdentityDriver("file_service_csi_spiffe", func(cls PrincipalClass, serviceName string) (CertificateProvider, error) {
+ if cls != ServicePrincipal {
+ return nil, ErrUnsupportedClass
+ }
return LoadServiceIdentityFromKubernetesCSIDriverSPIFFE(serviceName)
})
- RegisterIdentityDriver("file_user_home", func(_ string) (CertificateProvider, error) {
- return LoadUserIdentityFromFilesystem()
+ RegisterIdentityDriver("file_user_home", func(cls PrincipalClass, username string) (CertificateProvider, error) {
+ if cls != UserPrincipal {
+ return nil, ErrUnsupportedClass
+ }
+ return LoadUserIdentityFromFilesystem(username)
})
RegisterRootDriver("file_etc_mtls", func() (RootsPrimitive, error) {
return defaultFileBackedRoots, nil
var kcLogger log.Logger
-func NewCertificateFromMacKeychain(principal string) (CertificateProvider, error) {
+func NewCertificateFromMacKeychain(cls PrincipalClass, principal string) (CertificateProvider, error) {
root, err := getMtlsRootFromMacKeychain()
if err != nil {
return nil, err
kcLogger.V(2).Debugf("loaded intermediate cert from keychain: %s", c.Subject.String())
}
- leaves, err := getLeafCertificatesFromKeychainMatchingPrincipal(ServicePrincipal, principal)
+ leaves, err := getLeafCertificatesFromKeychainMatchingPrincipal(cls, principal)
if err != nil {
return nil, err
}
"fmt"
"path"
+ "go.fuhry.dev/runtime/constants"
"go.fuhry.dev/runtime/mtls/certutil"
"go.fuhry.dev/runtime/utils/log"
)
}
func init() {
- RegisterIdentityDriver("tpm2-pkcs11", func(_ string) (CertificateProvider, error) {
+ RegisterIdentityDriver("tpm2-pkcs11", func(cls PrincipalClass, serviceName string) (CertificateProvider, error) {
+ if cls != ServicePrincipal || serviceName != constants.DeviceTrustPrincipal {
+ return nil, ErrUnsupportedClass
+ }
return NewTPMBackedCertificate()
})
}
"crypto/tls"
"crypto/x509"
"fmt"
+ "net/url"
"regexp"
"strings"
return nil, fmt.Errorf("cannot understand CN/SAN %q as an mTLS identity", name)
}
+
+func (i *RemoteIdentity) ToSpiffe() *url.URL {
+ return &url.URL{
+ Scheme: "spiffe",
+ Host: i.Domain,
+ Path: fmt.Sprintf("%s/%s", i.Class.String(), i.Principal),
+ }
+}
+
+func (i *RemoteIdentity) ToDnsName() string {
+ switch i.Class {
+ case User:
+ return fmt.Sprintf("%s.%s.%s",
+ i.Principal,
+ i.Class.String(),
+ i.Domain)
+ default:
+ return fmt.Sprintf("%s.%s.%s.mtls.internal",
+ i.Principal,
+ i.Class.String(),
+ i.Domain)
+ }
+}