AB#2386: TrustedLaunch support for azure attestation

* There are now two attestation packages on azure.
The issuer on the server side is created base on successfully
querying the idkeydigest from the TPM. Fallback on err: Trusted Launch.
* The bootstrapper's issuer choice is validated by the CLI's validator,
which is created based on the local config.
* Add "azureCVM" field to new "internal-config" cm.
This field is populated by the bootstrapper.
* Group attestation OIDs by CSP (#42)
* Bootstrapper now uses IssuerWrapper type to pass
the issuer (and some context info) to the initserver.
* Introduce VMType package akin to cloudprovider. Used by
IssuerWrapper.
* Extend unittests.
* Remove CSP specific attestation integration tests

Co-authored-by: <dw@edgeless.systems>
Signed-off-by: Otto Bittner <cobittner@posteo.net>
This commit is contained in:
Otto Bittner 2022-08-31 20:10:49 +02:00
parent 4bfb98d35a
commit 405db3286e
33 changed files with 749 additions and 431 deletions

View file

@ -14,21 +14,24 @@ import (
"net" "net"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/edgelesssys/constellation/bootstrapper/internal/initserver"
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes" "github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes"
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi" "github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi"
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/kubectl" "github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/kubectl"
"github.com/edgelesssys/constellation/bootstrapper/internal/logging" "github.com/edgelesssys/constellation/bootstrapper/internal/logging"
"github.com/edgelesssys/constellation/internal/atls" "github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/attestation/azure" "github.com/edgelesssys/constellation/internal/attestation/azure/snp"
"github.com/edgelesssys/constellation/internal/attestation/azure/trustedlaunch"
"github.com/edgelesssys/constellation/internal/attestation/gcp" "github.com/edgelesssys/constellation/internal/attestation/gcp"
"github.com/edgelesssys/constellation/internal/attestation/qemu" "github.com/edgelesssys/constellation/internal/attestation/qemu"
"github.com/edgelesssys/constellation/internal/attestation/simulator" "github.com/edgelesssys/constellation/internal/attestation/simulator"
"github.com/edgelesssys/constellation/internal/attestation/vtpm" "github.com/edgelesssys/constellation/internal/attestation/vtpm"
azurecloud "github.com/edgelesssys/constellation/internal/cloud/azure" azurecloud "github.com/edgelesssys/constellation/internal/cloud/azure"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
gcpcloud "github.com/edgelesssys/constellation/internal/cloud/gcp" gcpcloud "github.com/edgelesssys/constellation/internal/cloud/gcp"
qemucloud "github.com/edgelesssys/constellation/internal/cloud/qemu" qemucloud "github.com/edgelesssys/constellation/internal/cloud/qemu"
"github.com/edgelesssys/constellation/internal/cloud/vmtype"
"github.com/edgelesssys/constellation/internal/constants" "github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/file" "github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/iproute" "github.com/edgelesssys/constellation/internal/iproute"
@ -65,20 +68,20 @@ func main() {
var clusterInitJoiner clusterInitJoiner var clusterInitJoiner clusterInitJoiner
var metadataAPI metadataAPI var metadataAPI metadataAPI
var cloudLogger logging.CloudLogger var cloudLogger logging.CloudLogger
var issuer atls.Issuer var issuer initserver.IssuerWrapper
var openTPM vtpm.TPMOpenFunc var openTPM vtpm.TPMOpenFunc
var fs afero.Fs var fs afero.Fs
switch strings.ToLower(os.Getenv(constellationCSP)) { switch cloudprovider.FromString(os.Getenv(constellationCSP)) {
case "aws": case cloudprovider.AWS:
panic("AWS cloud provider currently unsupported") panic("AWS cloud provider currently unsupported")
case "gcp": case cloudprovider.GCP:
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.GCPPCRSelection) pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.GCPPCRSelection)
if err != nil { if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs") log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs")
} }
issuer = gcp.NewIssuer() issuer = initserver.NewIssuerWrapper(gcp.NewIssuer(), vmtype.Unknown, nil)
gcpClient, err := gcpcloud.NewClient(ctx) gcpClient, err := gcpcloud.NewClient(ctx)
if err != nil { if err != nil {
@ -100,7 +103,7 @@ func main() {
} }
clusterInitJoiner = kubernetes.New( clusterInitJoiner = kubernetes.New(
"gcp", k8sapi.NewKubernetesUtil(), &k8sapi.CoreOSConfiguration{}, kubectl.New(), &gcpcloud.CloudControllerManager{}, "gcp", k8sapi.NewKubernetesUtil(), &k8sapi.CoreOSConfiguration{}, kubectl.New(), &gcpcloud.CloudControllerManager{},
&gcpcloud.CloudNodeManager{}, &gcpcloud.Autoscaler{}, metadata, pcrsJSON, nil, &gcpcloud.CloudNodeManager{}, &gcpcloud.Autoscaler{}, metadata, pcrsJSON,
) )
openTPM = vtpm.OpenVTPM openTPM = vtpm.OpenVTPM
fs = afero.NewOsFs() fs = afero.NewOsFs()
@ -108,19 +111,19 @@ func main() {
log.With(zap.Error(err)).Fatalf("Failed to set loadbalancer route") log.With(zap.Error(err)).Fatalf("Failed to set loadbalancer route")
} }
log.Infof("Added load balancer IP to routing table") log.Infof("Added load balancer IP to routing table")
case "azure": case cloudprovider.Azure:
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.AzurePCRSelection) pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.AzurePCRSelection)
if err != nil { if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs") log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs")
} }
idKeyDigest, err := azure.GetIdKeyDigest(vtpm.OpenVTPM) if idkeydigest, err := snp.GetIdKeyDigest(vtpm.OpenVTPM); err == nil {
if err != nil { issuer = initserver.NewIssuerWrapper(snp.NewIssuer(), vmtype.AzureCVM, idkeydigest)
log.With(zap.Error(err)).Fatalf("Failed to get idkeydigest") } else {
// assume we are running in a trusted-launch VM
issuer = initserver.NewIssuerWrapper(trustedlaunch.NewIssuer(), vmtype.AzureTrustedLaunch, idkeydigest)
} }
issuer = azure.NewIssuer()
metadata, err := azurecloud.NewMetadata(ctx) metadata, err := azurecloud.NewMetadata(ctx)
if err != nil { if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to create Azure metadata client") log.With(zap.Error(err)).Fatalf("Failed to create Azure metadata client")
@ -136,18 +139,18 @@ func main() {
} }
clusterInitJoiner = kubernetes.New( clusterInitJoiner = kubernetes.New(
"azure", k8sapi.NewKubernetesUtil(), &k8sapi.CoreOSConfiguration{}, kubectl.New(), azurecloud.NewCloudControllerManager(metadata), "azure", k8sapi.NewKubernetesUtil(), &k8sapi.CoreOSConfiguration{}, kubectl.New(), azurecloud.NewCloudControllerManager(metadata),
&azurecloud.CloudNodeManager{}, &azurecloud.Autoscaler{}, metadata, pcrsJSON, idKeyDigest, &azurecloud.CloudNodeManager{}, &azurecloud.Autoscaler{}, metadata, pcrsJSON,
) )
openTPM = vtpm.OpenVTPM openTPM = vtpm.OpenVTPM
fs = afero.NewOsFs() fs = afero.NewOsFs()
case "qemu": case cloudprovider.QEMU:
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.QEMUPCRSelection) pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.QEMUPCRSelection)
if err != nil { if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs") log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs")
} }
issuer = qemu.NewIssuer() issuer = initserver.NewIssuerWrapper(qemu.NewIssuer(), vmtype.Unknown, nil)
cloudLogger = qemucloud.NewLogger() cloudLogger = qemucloud.NewLogger()
metadata := &qemucloud.Metadata{} metadata := &qemucloud.Metadata{}
@ -157,14 +160,14 @@ func main() {
} }
clusterInitJoiner = kubernetes.New( clusterInitJoiner = kubernetes.New(
"qemu", k8sapi.NewKubernetesUtil(), &k8sapi.CoreOSConfiguration{}, kubectl.New(), &qemucloud.CloudControllerManager{}, "qemu", k8sapi.NewKubernetesUtil(), &k8sapi.CoreOSConfiguration{}, kubectl.New(), &qemucloud.CloudControllerManager{},
&qemucloud.CloudNodeManager{}, &qemucloud.Autoscaler{}, metadata, pcrsJSON, nil, &qemucloud.CloudNodeManager{}, &qemucloud.Autoscaler{}, metadata, pcrsJSON,
) )
metadataAPI = metadata metadataAPI = metadata
openTPM = vtpm.OpenVTPM openTPM = vtpm.OpenVTPM
fs = afero.NewOsFs() fs = afero.NewOsFs()
default: default:
issuer = atls.NewFakeIssuer(oid.Dummy{}) issuer = initserver.NewIssuerWrapper(atls.NewFakeIssuer(oid.Dummy{}), vmtype.Unknown, nil)
clusterInitJoiner = &clusterFake{} clusterInitJoiner = &clusterFake{}
metadataAPI = &providerMetadataFake{} metadataAPI = &providerMetadataFake{}
cloudLogger = &logging.NopLogger{} cloudLogger = &logging.NopLogger{}

View file

@ -20,13 +20,12 @@ import (
"github.com/edgelesssys/constellation/internal/file" "github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/grpc/dialer" "github.com/edgelesssys/constellation/internal/grpc/dialer"
"github.com/edgelesssys/constellation/internal/logger" "github.com/edgelesssys/constellation/internal/logger"
"github.com/edgelesssys/constellation/internal/oid"
"go.uber.org/zap" "go.uber.org/zap"
) )
var version = "0.0.0" var version = "0.0.0"
func run(issuer quoteIssuer, tpm vtpm.TPMOpenFunc, fileHandler file.Handler, func run(issuerWrapper initserver.IssuerWrapper, tpm vtpm.TPMOpenFunc, fileHandler file.Handler,
kube clusterInitJoiner, metadata metadataAPI, kube clusterInitJoiner, metadata metadataAPI,
bindIP, bindPort string, log *logger.Logger, bindIP, bindPort string, log *logger.Logger,
cloudLogger logging.CloudLogger, cloudLogger logging.CloudLogger,
@ -58,9 +57,9 @@ func run(issuer quoteIssuer, tpm vtpm.TPMOpenFunc, fileHandler file.Handler,
} }
nodeLock := nodelock.New(tpm) nodeLock := nodelock.New(tpm)
initServer := initserver.New(nodeLock, kube, issuer, fileHandler, log) initServer := initserver.New(nodeLock, kube, issuerWrapper, fileHandler, log)
dialer := dialer.New(issuer, nil, &net.Dialer{}) dialer := dialer.New(issuerWrapper, nil, &net.Dialer{})
joinClient := joinclient.New(nodeLock, dialer, kube, metadata, log) joinClient := joinclient.New(nodeLock, dialer, kube, metadata, log)
cleaner := clean.New().With(initServer).With(joinClient) cleaner := clean.New().With(initServer).With(joinClient)
@ -92,12 +91,6 @@ type clusterInitJoiner interface {
StartKubelet() error StartKubelet() error
} }
type quoteIssuer interface {
oid.Getter
// Issue issues a quote for remote attestation for a given message
Issue(userData []byte, nonce []byte) (quote []byte, err error)
}
type metadataAPI interface { type metadataAPI interface {
joinclient.MetadataAPI joinclient.MetadataAPI
GetLoadBalancerEndpoint(ctx context.Context) (string, error) GetLoadBalancerEndpoint(ctx context.Context) (string, error)

View file

@ -21,7 +21,7 @@ type clusterFake struct{}
// InitCluster fakes bootstrapping a new cluster with the current node being the master, returning the arguments required to join the cluster. // InitCluster fakes bootstrapping a new cluster with the current node being the master, returning the arguments required to join the cluster.
func (c *clusterFake) InitCluster( func (c *clusterFake) InitCluster(
context.Context, []string, string, string, []byte, []uint32, bool, context.Context, []string, string, string, []byte, []uint32, bool, []byte, bool,
resources.KMSConfig, map[string]string, []byte, *logger.Logger, resources.KMSConfig, map[string]string, []byte, *logger.Logger,
) ([]byte, error) { ) ([]byte, error) {
return []byte{}, nil return []byte{}, nil

View file

@ -18,6 +18,7 @@ import (
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources" "github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
"github.com/edgelesssys/constellation/internal/atls" "github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/attestation" "github.com/edgelesssys/constellation/internal/attestation"
"github.com/edgelesssys/constellation/internal/cloud/vmtype"
"github.com/edgelesssys/constellation/internal/crypto" "github.com/edgelesssys/constellation/internal/crypto"
"github.com/edgelesssys/constellation/internal/file" "github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/grpc/atlscredentials" "github.com/edgelesssys/constellation/internal/grpc/atlscredentials"
@ -42,6 +43,7 @@ type Server struct {
fileHandler file.Handler fileHandler file.Handler
grpcServer serveStopper grpcServer serveStopper
cleaner cleaner cleaner cleaner
issuerWrapper IssuerWrapper
log *logger.Logger log *logger.Logger
@ -49,18 +51,20 @@ type Server struct {
} }
// New creates a new initialization server. // New creates a new initialization server.
func New(lock locker, kube ClusterInitializer, issuer atls.Issuer, fh file.Handler, log *logger.Logger) *Server { func New(lock locker, kube ClusterInitializer, issuerWrapper IssuerWrapper, fh file.Handler, log *logger.Logger) *Server {
log = log.Named("initServer") log = log.Named("initServer")
server := &Server{ server := &Server{
nodeLock: lock, nodeLock: lock,
disk: diskencryption.New(), disk: diskencryption.New(),
initializer: kube, initializer: kube,
fileHandler: fh, fileHandler: fh,
issuerWrapper: issuerWrapper,
log: log, log: log,
} }
grpcServer := grpc.NewServer( grpcServer := grpc.NewServer(
grpc.Creds(atlscredentials.New(issuer, nil)), grpc.Creds(atlscredentials.New(issuerWrapper, nil)),
grpc.KeepaliveParams(keepalive.ServerParameters{Time: 15 * time.Second}), grpc.KeepaliveParams(keepalive.ServerParameters{Time: 15 * time.Second}),
log.Named("gRPC").GetServerUnaryInterceptor(), log.Named("gRPC").GetServerUnaryInterceptor(),
) )
@ -127,6 +131,8 @@ func (s *Server) Init(ctx context.Context, req *initproto.InitRequest) (*initpro
measurementSalt, measurementSalt,
req.EnforcedPcrs, req.EnforcedPcrs,
req.EnforceIdkeydigest, req.EnforceIdkeydigest,
s.issuerWrapper.IdKeyDigest(),
s.issuerWrapper.VMType() == vmtype.AzureCVM,
resources.KMSConfig{ resources.KMSConfig{
MasterSecret: req.MasterSecret, MasterSecret: req.MasterSecret,
Salt: req.Salt, Salt: req.Salt,
@ -175,6 +181,28 @@ func (s *Server) setupDisk(masterSecret, salt []byte) error {
return s.disk.UpdatePassphrase(string(diskKey)) return s.disk.UpdatePassphrase(string(diskKey))
} }
type IssuerWrapper struct {
atls.Issuer
vmType vmtype.VMType
idkeydigest []byte
}
func NewIssuerWrapper(issuer atls.Issuer, vmType vmtype.VMType, idkeydigest []byte) IssuerWrapper {
return IssuerWrapper{
Issuer: issuer,
vmType: vmType,
idkeydigest: idkeydigest,
}
}
func (i *IssuerWrapper) VMType() vmtype.VMType {
return i.vmType
}
func (i *IssuerWrapper) IdKeyDigest() []byte {
return i.idkeydigest
}
func sshProtoKeysToMap(keys []*initproto.SSHUserKey) map[string]string { func sshProtoKeysToMap(keys []*initproto.SSHUserKey) map[string]string {
keyMap := make(map[string]string) keyMap := make(map[string]string)
for _, key := range keys { for _, key := range keys {
@ -211,6 +239,8 @@ type ClusterInitializer interface {
measurementSalt []byte, measurementSalt []byte,
enforcedPcrs []uint32, enforcedPcrs []uint32,
enforceIdKeyDigest bool, enforceIdKeyDigest bool,
idKeyDigest []byte,
azureCVM bool,
kmsConfig resources.KMSConfig, kmsConfig resources.KMSConfig,
sshUserKeys map[string]string, sshUserKeys map[string]string,
helmDeployments []byte, helmDeployments []byte,

View file

@ -34,7 +34,7 @@ func TestNew(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
fh := file.NewHandler(afero.NewMemMapFs()) fh := file.NewHandler(afero.NewMemMapFs())
server := New(newFakeLock(), &stubClusterInitializer{}, nil, fh, logger.NewTest(t)) server := New(newFakeLock(), &stubClusterInitializer{}, IssuerWrapper{}, fh, logger.NewTest(t))
assert.NotNil(server) assert.NotNil(server)
assert.NotNil(server.log) assert.NotNil(server.log)
assert.NotNil(server.nodeLock) assert.NotNil(server.nodeLock)
@ -289,7 +289,7 @@ type stubClusterInitializer struct {
} }
func (i *stubClusterInitializer) InitCluster( func (i *stubClusterInitializer) InitCluster(
context.Context, []string, string, string, []byte, []uint32, bool, context.Context, []string, string, string, []byte, []uint32, bool, []byte, bool,
resources.KMSConfig, map[string]string, []byte, *logger.Logger, resources.KMSConfig, map[string]string, []byte, *logger.Logger,
) ([]byte, error) { ) ([]byte, error) {
return i.initClusterKubeconfig, i.initClusterErr return i.initClusterKubeconfig, i.initClusterErr

View file

@ -195,6 +195,13 @@ func NewJoinServiceDaemonset(csp, measurementsJSON, enforcedPCRsJSON, initialIdK
}, },
}, },
}, },
{
ConfigMap: &k8s.ConfigMapProjection{
LocalObjectReference: k8s.LocalObjectReference{
Name: constants.InternalConfigMap,
},
},
},
}, },
}, },
}, },

View file

@ -53,13 +53,12 @@ type KubeWrapper struct {
clusterAutoscaler ClusterAutoscaler clusterAutoscaler ClusterAutoscaler
providerMetadata ProviderMetadata providerMetadata ProviderMetadata
initialMeasurementsJSON []byte initialMeasurementsJSON []byte
initialIdKeyDigest []byte
getIPAddr func() (string, error) getIPAddr func() (string, error)
} }
// New creates a new KubeWrapper with real values. // New creates a new KubeWrapper with real values.
func New(cloudProvider string, clusterUtil clusterUtil, configProvider configurationProvider, client k8sapi.Client, cloudControllerManager CloudControllerManager, func New(cloudProvider string, clusterUtil clusterUtil, configProvider configurationProvider, client k8sapi.Client, cloudControllerManager CloudControllerManager,
cloudNodeManager CloudNodeManager, clusterAutoscaler ClusterAutoscaler, providerMetadata ProviderMetadata, initialMeasurementsJSON, initialIdKeyDigest []byte, cloudNodeManager CloudNodeManager, clusterAutoscaler ClusterAutoscaler, providerMetadata ProviderMetadata, initialMeasurementsJSON []byte,
) *KubeWrapper { ) *KubeWrapper {
return &KubeWrapper{ return &KubeWrapper{
cloudProvider: cloudProvider, cloudProvider: cloudProvider,
@ -72,7 +71,6 @@ func New(cloudProvider string, clusterUtil clusterUtil, configProvider configura
clusterAutoscaler: clusterAutoscaler, clusterAutoscaler: clusterAutoscaler,
providerMetadata: providerMetadata, providerMetadata: providerMetadata,
initialMeasurementsJSON: initialMeasurementsJSON, initialMeasurementsJSON: initialMeasurementsJSON,
initialIdKeyDigest: initialIdKeyDigest,
getIPAddr: getIPAddr, getIPAddr: getIPAddr,
} }
} }
@ -80,7 +78,7 @@ func New(cloudProvider string, clusterUtil clusterUtil, configProvider configura
// InitCluster initializes a new Kubernetes cluster and applies pod network provider. // InitCluster initializes a new Kubernetes cluster and applies pod network provider.
func (k *KubeWrapper) InitCluster( func (k *KubeWrapper) InitCluster(
ctx context.Context, autoscalingNodeGroups []string, cloudServiceAccountURI, versionString string, measurementSalt []byte, ctx context.Context, autoscalingNodeGroups []string, cloudServiceAccountURI, versionString string, measurementSalt []byte,
enforcedPCRs []uint32, enforceIdKeyDigest bool, kmsConfig resources.KMSConfig, sshUsers map[string]string, helmDeployments []byte, log *logger.Logger, enforcedPCRs []uint32, enforceIdKeyDigest bool, idKeyDigest []byte, azureCVM bool, kmsConfig resources.KMSConfig, sshUsers map[string]string, helmDeployments []byte, log *logger.Logger,
) ([]byte, error) { ) ([]byte, error) {
k8sVersion, err := versions.NewValidK8sVersion(versionString) k8sVersion, err := versions.NewValidK8sVersion(versionString)
if err != nil { if err != nil {
@ -185,7 +183,11 @@ func (k *KubeWrapper) InitCluster(
return nil, fmt.Errorf("setting up kms: %w", err) return nil, fmt.Errorf("setting up kms: %w", err)
} }
if err := k.setupJoinService(k.cloudProvider, k.initialMeasurementsJSON, measurementSalt, enforcedPCRs, k.initialIdKeyDigest, enforceIdKeyDigest); err != nil { if err := k.setupInternalConfigMap(ctx, strconv.FormatBool(azureCVM)); err != nil {
return nil, fmt.Errorf("failed to setup internal ConfigMap: %w", err)
}
if err := k.setupJoinService(k.cloudProvider, k.initialMeasurementsJSON, measurementSalt, enforcedPCRs, idKeyDigest, enforceIdKeyDigest); err != nil {
return nil, fmt.Errorf("setting up join service failed: %w", err) return nil, fmt.Errorf("setting up join service failed: %w", err)
} }
@ -224,7 +226,7 @@ func (k *KubeWrapper) InitCluster(
// Store the received k8sVersion in a ConfigMap, overwriting existing values (there shouldn't be any). // Store the received k8sVersion in a ConfigMap, overwriting existing values (there shouldn't be any).
// Joining nodes determine the kubernetes version they will install based on this ConfigMap. // Joining nodes determine the kubernetes version they will install based on this ConfigMap.
if err := k.setupK8sVersionConfigMap(ctx, k8sVersion); err != nil { if err := k.setupK8sVersionConfigMap(ctx, k8sVersion); err != nil {
return nil, fmt.Errorf("failed to setup k8s version ConfigMap: %v", err) return nil, fmt.Errorf("failed to setup k8s version ConfigMap: %w", err)
} }
k.clusterUtil.FixCilium(nodeName, log) k.clusterUtil.FixCilium(nodeName, log)
@ -414,7 +416,32 @@ func (k *KubeWrapper) setupK8sVersionConfigMap(ctx context.Context, k8sVersion v
// We do not use the client's Apply method here since we are handling a kubernetes-native type. // We do not use the client's Apply method here since we are handling a kubernetes-native type.
// These types don't implement our custom Marshaler interface. // These types don't implement our custom Marshaler interface.
if err := k.client.CreateConfigMap(ctx, config); err != nil { if err := k.client.CreateConfigMap(ctx, config); err != nil {
return fmt.Errorf("apply in KubeWrapper.setupK8sVersionConfigMap(..) failed with: %v", err) return fmt.Errorf("apply in KubeWrapper.setupK8sVersionConfigMap(..) failed with: %w", err)
}
return nil
}
// setupInternalConfigMap applies a ConfigMap (cf. server-side apply) to store information that is not supposed to be user-editable.
func (k *KubeWrapper) setupInternalConfigMap(ctx context.Context, azureCVM string) error {
config := corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: constants.InternalConfigMap,
Namespace: "kube-system",
},
Data: map[string]string{
constants.AzureCVM: azureCVM,
},
}
// We do not use the client's Apply method here since we are handling a kubernetes-native type.
// These types don't implement our custom Marshaler interface.
if err := k.client.CreateConfigMap(ctx, config); err != nil {
return fmt.Errorf("apply in KubeWrapper.setupInternalConfigMap failed with: %w", err)
} }
return nil return nil

View file

@ -308,7 +308,7 @@ func TestInitCluster(t *testing.T) {
_, err := kube.InitCluster( _, err := kube.InitCluster(
context.Background(), autoscalingNodeGroups, serviceAccountURI, string(tc.k8sVersion), context.Background(), autoscalingNodeGroups, serviceAccountURI, string(tc.k8sVersion),
nil, nil, false, resources.KMSConfig{MasterSecret: masterSecret}, nil, nil, logger.NewTest(t), nil, nil, false, nil, true, resources.KMSConfig{MasterSecret: masterSecret}, nil, nil, logger.NewTest(t),
) )
if tc.wantErr { if tc.wantErr {

View file

@ -14,7 +14,8 @@ import (
"fmt" "fmt"
"github.com/edgelesssys/constellation/internal/atls" "github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/attestation/azure" "github.com/edgelesssys/constellation/internal/attestation/azure/snp"
"github.com/edgelesssys/constellation/internal/attestation/azure/trustedlaunch"
"github.com/edgelesssys/constellation/internal/attestation/gcp" "github.com/edgelesssys/constellation/internal/attestation/gcp"
"github.com/edgelesssys/constellation/internal/attestation/qemu" "github.com/edgelesssys/constellation/internal/attestation/qemu"
"github.com/edgelesssys/constellation/internal/attestation/vtpm" "github.com/edgelesssys/constellation/internal/attestation/vtpm"
@ -29,6 +30,7 @@ type Validator struct {
enforcedPCRs []uint32 enforcedPCRs []uint32
idkeydigest []byte idkeydigest []byte
enforceIdKeyDigest bool enforceIdKeyDigest bool
azureCVM bool
validator atls.Validator validator atls.Validator
} }
@ -43,6 +45,8 @@ func NewValidator(provider cloudprovider.Provider, config *config.Config) (*Vali
} }
if v.provider == cloudprovider.Azure { if v.provider == cloudprovider.Azure {
v.azureCVM = *config.Provider.Azure.ConfidentialVM
if v.azureCVM {
idkeydigest, err := hex.DecodeString(config.Provider.Azure.IdKeyDigest) idkeydigest, err := hex.DecodeString(config.Provider.Azure.IdKeyDigest)
if err != nil { if err != nil {
return nil, fmt.Errorf("bad config: decoding idkeydigest from config: %w", err) return nil, fmt.Errorf("bad config: decoding idkeydigest from config: %w", err)
@ -50,6 +54,7 @@ func NewValidator(provider cloudprovider.Provider, config *config.Config) (*Vali
v.enforceIdKeyDigest = *config.Provider.Azure.EnforceIdKeyDigest v.enforceIdKeyDigest = *config.Provider.Azure.EnforceIdKeyDigest
v.idkeydigest = idkeydigest v.idkeydigest = idkeydigest
} }
}
return &v, nil return &v, nil
} }
@ -140,7 +145,11 @@ func (v *Validator) updateValidator(cmd *cobra.Command) {
case cloudprovider.GCP: case cloudprovider.GCP:
v.validator = gcp.NewValidator(v.pcrs, v.enforcedPCRs, log) v.validator = gcp.NewValidator(v.pcrs, v.enforcedPCRs, log)
case cloudprovider.Azure: case cloudprovider.Azure:
v.validator = azure.NewValidator(v.pcrs, v.enforcedPCRs, v.idkeydigest, v.enforceIdKeyDigest, log) if v.azureCVM {
v.validator = snp.NewValidator(v.pcrs, v.enforcedPCRs, v.idkeydigest, v.enforceIdKeyDigest, log)
} else {
v.validator = trustedlaunch.NewValidator(v.pcrs, v.enforcedPCRs, log)
}
case cloudprovider.QEMU: case cloudprovider.QEMU:
v.validator = qemu.NewValidator(v.pcrs, v.enforcedPCRs, log) v.validator = qemu.NewValidator(v.pcrs, v.enforcedPCRs, log)
} }

View file

@ -12,7 +12,8 @@ import (
"testing" "testing"
"github.com/edgelesssys/constellation/internal/atls" "github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/attestation/azure" "github.com/edgelesssys/constellation/internal/attestation/azure/snp"
"github.com/edgelesssys/constellation/internal/attestation/azure/trustedlaunch"
"github.com/edgelesssys/constellation/internal/attestation/gcp" "github.com/edgelesssys/constellation/internal/attestation/gcp"
"github.com/edgelesssys/constellation/internal/attestation/qemu" "github.com/edgelesssys/constellation/internal/attestation/qemu"
"github.com/edgelesssys/constellation/internal/attestation/vtpm" "github.com/edgelesssys/constellation/internal/attestation/vtpm"
@ -40,15 +41,22 @@ func TestNewValidator(t *testing.T) {
pcrs map[uint32][]byte pcrs map[uint32][]byte
enforceIdKeyDigest bool enforceIdKeyDigest bool
idkeydigest string idkeydigest string
azureCVM bool
wantErr bool wantErr bool
}{ }{
"gcp": { "gcp": {
provider: cloudprovider.GCP, provider: cloudprovider.GCP,
pcrs: testPCRs, pcrs: testPCRs,
}, },
"azure": { "azure cvm": {
provider: cloudprovider.Azure, provider: cloudprovider.Azure,
pcrs: testPCRs, pcrs: testPCRs,
azureCVM: true,
},
"azure trusted launch": {
provider: cloudprovider.Azure,
pcrs: testPCRs,
azureCVM: false,
}, },
"qemu": { "qemu": {
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
@ -80,6 +88,7 @@ func TestNewValidator(t *testing.T) {
pcrs: testPCRs, pcrs: testPCRs,
idkeydigest: "41414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414", idkeydigest: "41414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414",
enforceIdKeyDigest: true, enforceIdKeyDigest: true,
azureCVM: true,
wantErr: true, wantErr: true,
}, },
} }
@ -95,7 +104,7 @@ func TestNewValidator(t *testing.T) {
} }
if tc.provider == cloudprovider.Azure { if tc.provider == cloudprovider.Azure {
measurements := config.Measurements(tc.pcrs) measurements := config.Measurements(tc.pcrs)
conf.Provider.Azure = &config.AzureConfig{Measurements: measurements, EnforceIdKeyDigest: &tc.enforceIdKeyDigest, IdKeyDigest: tc.idkeydigest} conf.Provider.Azure = &config.AzureConfig{Measurements: measurements, EnforceIdKeyDigest: &tc.enforceIdKeyDigest, IdKeyDigest: tc.idkeydigest, ConfidentialVM: &tc.azureCVM}
} }
if tc.provider == cloudprovider.QEMU { if tc.provider == cloudprovider.QEMU {
measurements := config.Measurements(tc.pcrs) measurements := config.Measurements(tc.pcrs)
@ -140,16 +149,23 @@ func TestValidatorV(t *testing.T) {
provider cloudprovider.Provider provider cloudprovider.Provider
pcrs map[uint32][]byte pcrs map[uint32][]byte
wantVs atls.Validator wantVs atls.Validator
azureCVM bool
}{ }{
"gcp": { "gcp": {
provider: cloudprovider.GCP, provider: cloudprovider.GCP,
pcrs: newTestPCRs(), pcrs: newTestPCRs(),
wantVs: gcp.NewValidator(newTestPCRs(), nil, nil), wantVs: gcp.NewValidator(newTestPCRs(), nil, nil),
}, },
"azure": { "azure cvm": {
provider: cloudprovider.Azure, provider: cloudprovider.Azure,
pcrs: newTestPCRs(), pcrs: newTestPCRs(),
wantVs: azure.NewValidator(newTestPCRs(), nil, nil, false, nil), wantVs: snp.NewValidator(newTestPCRs(), nil, nil, false, nil),
azureCVM: true,
},
"azure trusted launch": {
provider: cloudprovider.Azure,
pcrs: newTestPCRs(),
wantVs: trustedlaunch.NewValidator(newTestPCRs(), nil, nil),
}, },
"qemu": { "qemu": {
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
@ -162,7 +178,7 @@ func TestValidatorV(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
validators := &Validator{provider: tc.provider, pcrs: tc.pcrs} validators := &Validator{provider: tc.provider, pcrs: tc.pcrs, azureCVM: tc.azureCVM}
resultValidator := validators.V(&cobra.Command{}) resultValidator := validators.V(&cobra.Command{})

View file

@ -73,6 +73,9 @@ func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler,
if config.IsAzureNonCVM() { if config.IsAzureNonCVM() {
cmd.Println("Disabling Confidential VMs is insecure. Use only for evaluation purposes.") cmd.Println("Disabling Confidential VMs is insecure. Use only for evaluation purposes.")
if config.EnforcesIdKeyDigest() {
cmd.Println("Your config asks for enforcing the idkeydigest. This is only available on Confidential VMs. It will not be enforced.")
}
} }
var instanceType string var instanceType string

View file

@ -1,62 +0,0 @@
//go:build azure
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package azure
import (
"encoding/json"
"testing"
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestAttestation(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
issuer := NewIssuer()
validator := NewValidator(map[uint32][]byte{}, nil, nil, false, nil) // TODO: check for list of expected Azure PCRs
nonce := []byte{2, 3, 4}
challenge := []byte("Constellation")
attDocRaw, err := issuer.Issue(challenge, nonce)
assert.NoError(err)
var attDoc vtpm.AttestationDocument
err = json.Unmarshal(attDocRaw, &attDoc)
require.NoError(err)
assert.Equal(challenge, attDoc.UserData)
originalPCR := attDoc.Attestation.Quotes[1].Pcrs.Pcrs[uint32(vtpm.PCRIndexOwnerID)]
out, err := validator.Validate(attDocRaw, nonce)
require.NoError(err)
assert.Equal(challenge, out)
// Mark node as intialized. We should still be abe to validate
assert.NoError(vtpm.MarkNodeAsBootstrapped(vtpm.OpenVTPM, []byte("Test")))
attDocRaw, err = issuer.Issue(challenge, nonce)
assert.NoError(err)
// Make sure the PCR changed
err = json.Unmarshal(attDocRaw, &attDoc)
require.NoError(err)
assert.NotEqual(originalPCR, attDoc.Attestation.Quotes[1].Pcrs.Pcrs[uint32(vtpm.PCRIndexOwnerID)])
out, err = validator.Validate(attDocRaw, nonce)
require.NoError(err)
assert.Equal(challenge, out)
}

View file

@ -7,147 +7,18 @@ SPDX-License-Identifier: AGPL-3.0-only
package azure package azure
import ( import (
"bytes" "github.com/edgelesssys/constellation/internal/atls"
"context" "github.com/edgelesssys/constellation/internal/attestation/azure/snp"
"encoding/json" "github.com/edgelesssys/constellation/internal/attestation/azure/trustedlaunch"
"fmt"
"io"
"net/http"
"github.com/edgelesssys/constellation/internal/attestation/vtpm" "github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/internal/oid"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm/tpm2"
) )
const ( // NewIssuer returns an SNP issuer if it can successfully read the idkeydigest from the TPM.
lenHclHeader = 0x20 // Otherwise returns a Trusted Launch issuer.
lenSnpReport = 0x4a0 func NewIssuer() atls.Issuer {
lenSnpReportRuntimeDataPadding = 0x14 if _, err := snp.GetIdKeyDigest(vtpm.OpenVTPM); err == nil {
tpmReportIdx = 0x01400001 return snp.NewIssuer()
) } else {
return trustedlaunch.NewIssuer()
// Issuer for Azure TPM attestation.
type Issuer struct {
oid.Azure
*vtpm.Issuer
}
// NewIssuer initializes a new Azure Issuer.
func NewIssuer() *Issuer {
imdsAPI := imdsClient{
client: &http.Client{Transport: &http.Transport{Proxy: nil}},
}
return &Issuer{
Issuer: vtpm.NewIssuer(
vtpm.OpenVTPM,
getHCLAttestationKey,
getSNPAttestation(&tpmReport{}, imdsAPI),
),
} }
} }
// GetIdKeyDigest reads the idkeydigest from the snp report saved in the TPM's non-volatile memory.
func GetIdKeyDigest(open vtpm.TPMOpenFunc) ([]byte, error) {
tpm, err := open()
if err != nil {
return nil, err
}
defer tpm.Close()
reportRaw, err := tpm2.NVReadEx(tpm, tpmReportIdx, tpm2.HandleOwner, "", 0)
if err != nil {
return nil, fmt.Errorf("reading idx %x from TMP: %w", tpmReportIdx, err)
}
report, err := newSNPReportFromBytes(reportRaw[lenHclHeader:])
if err != nil {
return nil, fmt.Errorf("creating snp report: %w", err)
}
return report.IdKeyDigest[:], nil
}
func hclAkTemplate() tpm2.Public {
akFlags := tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagNoDA | tpm2.FlagRestricted | tpm2.FlagSign
return tpm2.Public{
Type: tpm2.AlgRSA,
NameAlg: tpm2.AlgSHA256,
Attributes: akFlags,
RSAParameters: &tpm2.RSAParams{
Sign: &tpm2.SigScheme{
Alg: tpm2.AlgRSASSA,
Hash: tpm2.AlgSHA256,
},
KeyBits: 2048,
},
}
}
// getHCLAttestationKey reads the attesation key put into the TPM during early boot.
func getHCLAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) {
// A minor drawback of `NewCachedKey` is that it will transparently create/overwrite a key if it does not find one matching the template at the given index.
// We actually wouldn't want to continue at this point if we realize that the key at the index is not present, due to
// easier debuggability. If `NewCachedKey` creates a new key, attestation will fail at the validator.
// The function in tpmclient that doesn't create a new key, ReadPublic, can't be used as we would have to create
// a tpmclient.Key object manually, which we can't since there is no constructor exported.
ak, err := tpmclient.NewCachedKey(tpm, tpm2.HandleOwner, hclAkTemplate(), 0x81000003)
if err != nil {
return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err)
}
return ak, nil
}
// getSNPAttestation loads and returns the SEV-SNP attestation report [1] and the
// AMD VCEK certificate chain.
// The attestation report is loaded from the TPM, the certificate chain is queried
// from the cloud metadata API.
// [1] https://github.com/AMDESE/sev-guest/blob/main/include/attestation.h
func getSNPAttestation(reportGetter tpmReportGetter, imdsAPI imdsApi) func(tpm io.ReadWriteCloser) ([]byte, error) {
return func(tpm io.ReadWriteCloser) ([]byte, error) {
hclReport, err := reportGetter.get(tpm)
if err != nil {
return nil, fmt.Errorf("reading report from TPM: %w", err)
}
if len(hclReport) < lenHclHeader+lenSnpReport+lenSnpReportRuntimeDataPadding {
return nil, fmt.Errorf("report read from TPM is shorter then expected: %x", hclReport)
}
hclReport = hclReport[lenHclHeader:]
runtimeData, _, _ := bytes.Cut(hclReport[lenSnpReport+lenSnpReportRuntimeDataPadding:], []byte{0})
vcekResponse, err := imdsAPI.getVcek(context.TODO())
if err != nil {
return nil, fmt.Errorf("getVcekFromIMDS: %w", err)
}
instanceInfo := azureInstanceInfo{
Vcek: []byte(vcekResponse.VcekCert),
CertChain: []byte(vcekResponse.CertificateChain),
AttestationReport: hclReport[:0x4a0],
RuntimeData: runtimeData,
}
statement, err := json.Marshal(instanceInfo)
if err != nil {
return nil, fmt.Errorf("marshalling AzureInstanceInfo: %w", err)
}
return statement, nil
}
}
type tpmReport struct{}
func (s *tpmReport) get(tpm io.ReadWriteCloser) ([]byte, error) {
return tpm2.NVReadEx(tpm, tpmReportIdx, tpm2.HandleOwner, "", 0)
}
type tpmReportGetter interface {
get(tpm io.ReadWriteCloser) ([]byte, error)
}
type imdsApi interface {
getVcek(ctx context.Context) (vcekResponse, error)
}

View file

@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
package azure package snp
import ( import (
"context" "context"

View file

@ -0,0 +1,153 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package snp
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/internal/oid"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm/tpm2"
)
const (
lenHclHeader = 0x20
lenSnpReport = 0x4a0
lenSnpReportRuntimeDataPadding = 0x14
tpmReportIdx = 0x01400001
)
// GetIdKeyDigest reads the idkeydigest from the snp report saved in the TPM's non-volatile memory.
func GetIdKeyDigest(open vtpm.TPMOpenFunc) ([]byte, error) {
tpm, err := open()
if err != nil {
return nil, err
}
defer tpm.Close()
reportRaw, err := tpm2.NVReadEx(tpm, tpmReportIdx, tpm2.HandleOwner, "", 0)
if err != nil {
return nil, fmt.Errorf("reading idx %x from TMP: %w", tpmReportIdx, err)
}
report, err := newSNPReportFromBytes(reportRaw[lenHclHeader:])
if err != nil {
return nil, fmt.Errorf("creating snp report: %w", err)
}
return report.IdKeyDigest[:], nil
}
// Issuer for Azure TPM attestation.
type Issuer struct {
oid.AzureSNP
*vtpm.Issuer
}
// NewIssuer initializes a new Azure Issuer.
func NewIssuer() *Issuer {
imdsAPI := imdsClient{
client: &http.Client{Transport: &http.Transport{Proxy: nil}},
}
return &Issuer{
Issuer: vtpm.NewIssuer(
vtpm.OpenVTPM,
getAttestationKey,
getInstanceInfo(&tpmReport{}, imdsAPI),
),
}
}
// getInstanceInfo loads and returns the SEV-SNP attestation report [1] and the
// AMD VCEK certificate chain.
// The attestation report is loaded from the TPM, the certificate chain is queried
// from the cloud metadata API.
// [1] https://github.com/AMDESE/sev-guest/blob/main/include/attestation.h
func getInstanceInfo(reportGetter tpmReportGetter, imdsAPI imdsApi) func(tpm io.ReadWriteCloser) ([]byte, error) {
return func(tpm io.ReadWriteCloser) ([]byte, error) {
hclReport, err := reportGetter.get(tpm)
if err != nil {
return nil, fmt.Errorf("reading report from TPM: %w", err)
}
if len(hclReport) < lenHclHeader+lenSnpReport+lenSnpReportRuntimeDataPadding {
return nil, fmt.Errorf("report read from TPM is shorter then expected: %x", hclReport)
}
hclReport = hclReport[lenHclHeader:]
runtimeData, _, _ := bytes.Cut(hclReport[lenSnpReport+lenSnpReportRuntimeDataPadding:], []byte{0})
vcekResponse, err := imdsAPI.getVcek(context.TODO())
if err != nil {
return nil, fmt.Errorf("getVcekFromIMDS: %w", err)
}
instanceInfo := azureInstanceInfo{
Vcek: []byte(vcekResponse.VcekCert),
CertChain: []byte(vcekResponse.CertificateChain),
AttestationReport: hclReport[:0x4a0],
RuntimeData: runtimeData,
}
statement, err := json.Marshal(instanceInfo)
if err != nil {
return nil, fmt.Errorf("marshalling AzureInstanceInfo: %w", err)
}
return statement, nil
}
}
func hclAkTemplate() tpm2.Public {
akFlags := tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagNoDA | tpm2.FlagRestricted | tpm2.FlagSign
return tpm2.Public{
Type: tpm2.AlgRSA,
NameAlg: tpm2.AlgSHA256,
Attributes: akFlags,
RSAParameters: &tpm2.RSAParams{
Sign: &tpm2.SigScheme{
Alg: tpm2.AlgRSASSA,
Hash: tpm2.AlgSHA256,
},
KeyBits: 2048,
},
}
}
// getAttestationKey reads the attesation key put into the TPM during early boot.
func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) {
// A minor drawback of `NewCachedKey` is that it will transparently create/overwrite a key if it does not find one matching the template at the given index.
// We actually wouldn't want to continue at this point if we realize that the key at the index is not present, due to
// easier debuggability. If `NewCachedKey` creates a new key, attestation will fail at the validator.
// The function in tpmclient that doesn't create a new key, ReadPublic, can't be used as we would have to create
// a tpmclient.Key object manually, which we can't since there is no constructor exported.
ak, err := tpmclient.NewCachedKey(tpm, tpm2.HandleOwner, hclAkTemplate(), 0x81000003)
if err != nil {
return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err)
}
return ak, nil
}
type tpmReport struct{}
func (s *tpmReport) get(tpm io.ReadWriteCloser) ([]byte, error) {
return tpm2.NVReadEx(tpm, tpmReportIdx, tpm2.HandleOwner, "", 0)
}
type tpmReportGetter interface {
get(tpm io.ReadWriteCloser) ([]byte, error)
}
type imdsApi interface {
getVcek(ctx context.Context) (vcekResponse, error)
}

View file

@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
package azure package snp
import ( import (
"context" "context"
@ -64,7 +64,8 @@ func TestGetSNPAttestation(t *testing.T) {
tpmContent: reportDecoded, tpmContent: reportDecoded,
err: nil, err: nil,
} }
attestationJson, err := getSNPAttestation(&snpAttestationReport, imdsClient)(tpm)
attestationJson, err := getInstanceInfo(&snpAttestationReport, imdsClient)(tpm)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
return return
@ -96,7 +97,7 @@ func TestGetHCLAttestationKey(t *testing.T) {
assert.NoError(err) assert.NoError(err)
defer tpm.Close() defer tpm.Close()
_, err = getHCLAttestationKey(tpm) _, err = getAttestationKey(tpm)
assert.NoError(err) assert.NoError(err)
} }

View file

@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
package azure package snp
import ( import (
"bytes" "bytes"
@ -30,7 +30,7 @@ const arkPEM = "-----BEGIN CERTIFICATE-----\nMIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb
// Validator for Azure confidential VM attestation. // Validator for Azure confidential VM attestation.
type Validator struct { type Validator struct {
oid.Azure oid.AzureSNP
*vtpm.Validator *vtpm.Validator
} }
@ -40,65 +40,37 @@ func NewValidator(pcrs map[uint32][]byte, enforcedPCRs []uint32, idkeydigest []b
Validator: vtpm.NewValidator( Validator: vtpm.NewValidator(
pcrs, pcrs,
enforcedPCRs, enforcedPCRs,
trustedKeyFromSNP(&azureInstanceInfo{}, idkeydigest, enforceIdKeyDigest, log), getTrustedKey(&azureInstanceInfo{}, idkeydigest, enforceIdKeyDigest, log),
validateAzureCVM, validateCVM,
vtpm.VerifyPKCS1v15, vtpm.VerifyPKCS1v15,
log, log,
), ),
} }
} }
type signatureError struct { // validateCVM is a stub, since SEV-SNP attestation is already verified in trustedKeyFromSNP().
innerError error func validateCVM(attestation vtpm.AttestationDocument) error {
}
func (e *signatureError) Unwrap() error {
return e.innerError
}
func (e *signatureError) Error() string {
return fmt.Sprintf("signature validation failed: %v", e.innerError)
}
type askError struct {
innerError error
}
func (e *askError) Unwrap() error {
return e.innerError
}
func (e *askError) Error() string {
return fmt.Sprintf("validating ASK: %v", e.innerError)
}
type vcekError struct {
innerError error
}
func (e *vcekError) Unwrap() error {
return e.innerError
}
func (e *vcekError) Error() string {
return fmt.Sprintf("validating VCEK: %v", e.innerError)
}
type idkeyError struct {
expectedValue []byte
}
func (e *idkeyError) Unwrap() error {
return nil return nil
} }
func (e *idkeyError) Error() string { func newSNPReportFromBytes(reportRaw []byte) (snpAttestationReport, error) {
return fmt.Sprintf("configured idkeydigest does not match reported idkeydigest: %x", e.expectedValue) var report snpAttestationReport
if err := binary.Read(bytes.NewReader(reportRaw), binary.LittleEndian, &report); err != nil {
return snpAttestationReport{}, fmt.Errorf("reading attestation report: %w", err)
}
return report, nil
} }
// trustedKeyFromSNP establishes trust in the given public key. func reverseEndian(b []byte) {
for i := 0; i < len(b)/2; i++ {
b[i], b[len(b)-i-1] = b[len(b)-i-1], b[i]
}
}
// getTrustedKey establishes trust in the given public key.
// It does so by verifying the SNP attestation statement in instanceInfo. // It does so by verifying the SNP attestation statement in instanceInfo.
func trustedKeyFromSNP(hclAk HCLAkValidator, idkeydigest []byte, enforceIdKeyDigest bool, log vtpm.WarnLogger) func(akPub, instanceInfoRaw []byte) (crypto.PublicKey, error) { func getTrustedKey(hclAk HCLAkValidator, idkeydigest []byte, enforceIdKeyDigest bool, log vtpm.WarnLogger) func(akPub, instanceInfoRaw []byte) (crypto.PublicKey, error) {
return func(akPub, instanceInfoRaw []byte) (crypto.PublicKey, error) { return func(akPub, instanceInfoRaw []byte) (crypto.PublicKey, error) {
var instanceInfo azureInstanceInfo var instanceInfo azureInstanceInfo
if err := json.Unmarshal(instanceInfoRaw, &instanceInfo); err != nil { if err := json.Unmarshal(instanceInfoRaw, &instanceInfo); err != nil {
@ -132,26 +104,6 @@ func trustedKeyFromSNP(hclAk HCLAkValidator, idkeydigest []byte, enforceIdKeyDig
} }
} }
func reverseEndian(b []byte) {
for i := 0; i < len(b)/2; i++ {
b[i], b[len(b)-i-1] = b[len(b)-i-1], b[i]
}
}
// validateAzureCVM is a stub, since SEV-SNP attestation is already verified in trustedKeyFromSNP().
func validateAzureCVM(attestation vtpm.AttestationDocument) error {
return nil
}
func newSNPReportFromBytes(reportRaw []byte) (snpAttestationReport, error) {
var report snpAttestationReport
if err := binary.Read(bytes.NewReader(reportRaw), binary.LittleEndian, &report); err != nil {
return snpAttestationReport{}, fmt.Errorf("reading attestation report: %w", err)
}
return report, nil
}
// validateVCEK takes the PEM-encoded X509 certificate VCEK, ASK and ARK and verifies the integrity of the chain. // validateVCEK takes the PEM-encoded X509 certificate VCEK, ASK and ARK and verifies the integrity of the chain.
// ARK (hardcoded) validates ASK (cloud metadata API) validates VCEK (cloud metadata API). // ARK (hardcoded) validates ASK (cloud metadata API) validates VCEK (cloud metadata API).
func validateVCEK(vcekRaw []byte, certChain []byte) (*x509.Certificate, error) { func validateVCEK(vcekRaw []byte, certChain []byte) (*x509.Certificate, error) {
@ -191,9 +143,9 @@ func validateSNPReport(cert *x509.Certificate, expectedIdKeyDigest []byte, enfor
reverseEndian(sig_r) reverseEndian(sig_r)
reverseEndian(sig_s) reverseEndian(sig_s)
r := new(big.Int).SetBytes(sig_r) rParam := new(big.Int).SetBytes(sig_r)
s := new(big.Int).SetBytes(sig_s) sParam := new(big.Int).SetBytes(sig_s)
sequence := ecdsaSig{r, s} sequence := ecdsaSig{rParam, sParam}
sigEncoded, err := asn1.Marshal(sequence) sigEncoded, err := asn1.Marshal(sequence)
if err != nil { if err != nil {
return fmt.Errorf("marshalling ecdsa signature: %w", err) return fmt.Errorf("marshalling ecdsa signature: %w", err)
@ -213,7 +165,7 @@ func validateSNPReport(cert *x509.Certificate, expectedIdKeyDigest []byte, enfor
return &idkeyError{report.IdKeyDigest[:]} return &idkeyError{report.IdKeyDigest[:]}
} }
if log != nil { if log != nil {
log.Warnf("Encountered different than configured idkeydigest value: %x.\n", report.IdKeyDigest[:]) log.Warnf("Encountered different than configured idkeydigest value: %x", report.IdKeyDigest[:])
} }
} }
@ -276,6 +228,54 @@ type HCLAkValidator interface {
validateAk(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error validateAk(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error
} }
type signatureError struct {
innerError error
}
func (e *signatureError) Unwrap() error {
return e.innerError
}
func (e *signatureError) Error() string {
return fmt.Sprintf("signature validation failed: %v", e.innerError)
}
type askError struct {
innerError error
}
func (e *askError) Unwrap() error {
return e.innerError
}
func (e *askError) Error() string {
return fmt.Sprintf("validating ASK: %v", e.innerError)
}
type vcekError struct {
innerError error
}
func (e *vcekError) Unwrap() error {
return e.innerError
}
func (e *vcekError) Error() string {
return fmt.Sprintf("validating VCEK: %v", e.innerError)
}
type idkeyError struct {
expectedValue []byte
}
func (e *idkeyError) Unwrap() error {
return nil
}
func (e *idkeyError) Error() string {
return fmt.Sprintf("configured idkeydigest does not match reported idkeydigest: %x", e.expectedValue)
}
type snpSignature struct { type snpSignature struct {
R [72]byte R [72]byte
S [72]byte S [72]byte

View file

@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
package azure package snp
import ( import (
"bytes" "bytes"
@ -145,7 +145,7 @@ func TestTrustedKeyFromSNP(t *testing.T) {
idkeydigest, err := hex.DecodeString(tc.idkeydigest) idkeydigest, err := hex.DecodeString(tc.idkeydigest)
assert.NoError(err) assert.NoError(err)
key, err := trustedKeyFromSNP(&instanceInfo, idkeydigest, tc.enforceIdKeyDigest, nil)(akPub, statement) key, err := getTrustedKey(&instanceInfo, idkeydigest, tc.enforceIdKeyDigest, nil)(akPub, statement)
if tc.wantErr { if tc.wantErr {
tc.assertCorrectError(err) tc.assertCorrectError(err)
} else { } else {
@ -171,7 +171,7 @@ func TestValidateAzureCVM(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
err := validateAzureCVM(tc.attDoc) err := validateCVM(tc.attDoc)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
} else { } else {

View file

@ -0,0 +1,37 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package trustedlaunch
import (
"io"
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/internal/oid"
tpmclient "github.com/google/go-tpm-tools/client"
)
// Issuer for Azure trusted launch TPM attestation.
type Issuer struct {
oid.AzureTrustedLaunch
*vtpm.Issuer
}
// NewIssuer initializes a new Azure Issuer.
func NewIssuer() *Issuer {
return &Issuer{
Issuer: vtpm.NewIssuer(
vtpm.OpenVTPM,
tpmclient.AttestationKeyRSA,
getAttestation,
),
}
}
// getAttestation returns nil.
func getAttestation(tpm io.ReadWriteCloser) ([]byte, error) {
return nil, nil
}

View file

@ -0,0 +1,46 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package trustedlaunch
import (
"testing"
"github.com/edgelesssys/constellation/internal/attestation/simulator"
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetSNPAttestation(t *testing.T) {
testCases := map[string]struct {
tpmFunc vtpm.TPMOpenFunc
wantErr bool
}{
"success": {
tpmFunc: simulator.OpenSimulatedTPM,
wantErr: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
tpm, err := tc.tpmFunc()
require.NoError(err)
defer tpm.Close()
_, err = getAttestation(tpm)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}

View file

@ -0,0 +1,49 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package trustedlaunch
import (
"crypto"
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/internal/oid"
"github.com/google/go-tpm/tpm2"
)
// Validator for Azure trusted launch VM attestation.
type Validator struct {
oid.AzureTrustedLaunch
*vtpm.Validator
}
// NewValidator initializes a new Azure validator with the provided PCR values.
func NewValidator(pcrs map[uint32][]byte, enforcedPCRs []uint32, log vtpm.WarnLogger) *Validator {
return &Validator{
Validator: vtpm.NewValidator(
pcrs,
enforcedPCRs,
trustedKey,
validateVM,
vtpm.VerifyPKCS1v15,
log,
),
}
}
// trustedKey returns the key encoded in the given TPMT_PUBLIC message.
func trustedKey(akPub, instanceInfo []byte) (crypto.PublicKey, error) {
pubArea, err := tpm2.DecodePublic(akPub)
if err != nil {
return nil, err
}
return pubArea.Key()
}
// validateVM returns nil.
func validateVM(attestation vtpm.AttestationDocument) error {
return nil
}

View file

@ -0,0 +1,81 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package trustedlaunch
import (
"testing"
"github.com/edgelesssys/constellation/internal/attestation/simulator"
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/google/go-tpm-tools/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTrustedKeyFromSNP(t *testing.T) {
require := require.New(t)
tpm, err := simulator.OpenSimulatedTPM()
require.NoError(err)
defer tpm.Close()
key, err := client.AttestationKeyRSA(tpm)
require.NoError(err)
defer key.Close()
akPub, err := key.PublicArea().Encode()
require.NoError(err)
testCases := map[string]struct {
key []byte
instanceInfo []byte
wantErr bool
}{
"success": {
key: akPub,
instanceInfo: []byte{},
wantErr: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
key, err := trustedKey(tc.key, tc.instanceInfo)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.NotNil(key)
}
})
}
}
func TestValidateAzureCVM(t *testing.T) {
testCases := map[string]struct {
attDoc vtpm.AttestationDocument
wantErr bool
}{
"success": {
attDoc: vtpm.AttestationDocument{},
wantErr: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := validateVM(tc.attDoc)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}

View file

@ -1,63 +0,0 @@
//go:build gcp
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package gcp
import (
"encoding/json"
"testing"
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestAttestation(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
PCR0 := []byte{0x0f, 0x35, 0xc2, 0x14, 0x60, 0x8d, 0x93, 0xc7, 0xa6, 0xe6, 0x8a, 0xe7, 0x35, 0x9b, 0x4a, 0x8b, 0xe5, 0xa0, 0xe9, 0x9e, 0xea, 0x91, 0x07, 0xec, 0xe4, 0x27, 0xc4, 0xde, 0xa4, 0xe4, 0x39, 0xcf}
issuer := NewIssuer()
validator := NewValidator(map[uint32][]byte{0: PCR0}, nil, nil)
nonce := []byte{2, 3, 4}
challenge := []byte("Constellation")
attDocRaw, err := issuer.Issue(challenge, nonce)
assert.NoError(err)
var attDoc vtpm.AttestationDocument
err = json.Unmarshal(attDocRaw, &attDoc)
require.NoError(err)
assert.Equal(challenge, attDoc.UserData)
originalPCR := attDoc.Attestation.Quotes[1].Pcrs.Pcrs[uint32(vtpm.PCRIndexOwnerID)]
out, err := validator.Validate(attDocRaw, nonce)
assert.NoError(err)
assert.Equal(challenge, out)
// Mark node as intialized. We should still be abe to validate
assert.NoError(vtpm.MarkNodeAsBootstrapped(vtpm.OpenVTPM, []byte("Test")))
attDocRaw, err = issuer.Issue(challenge, nonce)
assert.NoError(err)
// Make sure the PCR changed
err = json.Unmarshal(attDocRaw, &attDoc)
require.NoError(err)
assert.NotEqual(originalPCR, attDoc.Attestation.Quotes[1].Pcrs.Pcrs[uint32(vtpm.PCRIndexOwnerID)])
out, err = validator.Validate(attDocRaw, nonce)
assert.NoError(err)
assert.Equal(challenge, out)
}

View file

@ -0,0 +1,33 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package vmtype
import "strings"
//go:generate stringer -type=VMType
// VMType describes different vm types we support. Introduced for Azure SNP / Trusted Launch attestation.
type VMType uint32
const (
Unknown VMType = iota
AzureCVM
AzureTrustedLaunch
)
// FromString returns a VMType from a string.
func FromString(s string) VMType {
s = strings.ToLower(s)
switch s {
case "azurecvm":
return AzureCVM
case "azuretrustedlaunch":
return AzureTrustedLaunch
default:
return Unknown
}
}

View file

@ -0,0 +1,25 @@
// Code generated by "stringer -type=VMType"; DO NOT EDIT.
package vmtype
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Unknown-0]
_ = x[AzureCVM-1]
_ = x[AzureTrustedLaunch-2]
}
const _VMType_name = "UnknownAzureCVMAzureTrustedLaunch"
var _VMType_index = [...]uint8{0, 7, 15, 33}
func (i VMType) String() string {
if i >= VMType(len(_VMType_index)-1) {
return "VMType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _VMType_name[_VMType_index[i]:_VMType_index[i+1]]
}

View file

@ -487,6 +487,10 @@ func (c *Config) IsAzureNonCVM() bool {
return c.Provider.Azure != nil && c.Provider.Azure.ConfidentialVM != nil && !*c.Provider.Azure.ConfidentialVM return c.Provider.Azure != nil && c.Provider.Azure.ConfidentialVM != nil && !*c.Provider.Azure.ConfidentialVM
} }
func (c *Config) EnforcesIdKeyDigest() bool {
return c.Provider.Azure != nil && c.Provider.Azure.EnforceIdKeyDigest != nil && *c.Provider.Azure.EnforceIdKeyDigest
}
// FromFile returns config file with `name` read from `fileHandler` by parsing // FromFile returns config file with `name` read from `fileHandler` by parsing
// it as YAML. // it as YAML.
func FromFile(fileHandler file.Handler, name string) (*Config, error) { func FromFile(fileHandler file.Handler, name string) (*Config, error) {

View file

@ -84,6 +84,8 @@ const (
IdKeyDigestFilename = "idkeydigest" IdKeyDigestFilename = "idkeydigest"
// EnforceIdKeyDigestFilename is the name of the file configuring whether idkeydigest is enforced or not. // EnforceIdKeyDigestFilename is the name of the file configuring whether idkeydigest is enforced or not.
EnforceIdKeyDigestFilename = "enforceIdKeyDigest" EnforceIdKeyDigestFilename = "enforceIdKeyDigest"
// AzureCVM is the name of the file indicating whether the cluster is expected to run on CVMs or not.
AzureCVM = "azureCVM"
// K8sVersion is the filename of the mapped "k8s-version" configMap file. // K8sVersion is the filename of the mapped "k8s-version" configMap file.
K8sVersion = "k8s-version" K8sVersion = "k8s-version"
@ -101,6 +103,7 @@ const (
KubernetesJoinTokenTTL = 15 * time.Minute KubernetesJoinTokenTTL = 15 * time.Minute
ConstellationNamespace = "kube-system" ConstellationNamespace = "kube-system"
JoinConfigMap = "join-config" JoinConfigMap = "join-config"
InternalConfigMap = "internal-config"
// //
// Helm. // Helm.

View file

@ -2,8 +2,23 @@
Copyright (c) Edgeless Systems GmbH Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/
Package oid defines OIDs for different CSPs. Currently this is used in attested TLS to distinguish the attestation documents.
OIDs beginning with 1.3.9900 are reserved and can be used without registration.
* The 1.3.9900.1 branch is reserved for placeholder values and testing.
* The 1.3.9900.2 branch is reserved for AWS.
* The 1.3.9900.3 branch is reserved for GCP.
* The 1.3.9900.4 branch is reserved for Azure.
* The 1.3.9900.5 branch is reserved for QEMU.
Deprecated OIDs should never be reused for different purposes.
Instead, new OIDs should be added in the appropriate branch at the next available index.
*/
package oid package oid
import ( import (
@ -15,15 +30,12 @@ type Getter interface {
OID() asn1.ObjectIdentifier OID() asn1.ObjectIdentifier
} }
// Here we define OIDs for different CSPs. Currently this is used in attested TLS to distinguish the attestation documents.
// OIDs beginning with 1.3.9900 are reserved and can be used without registration.
// Dummy OID for testing. // Dummy OID for testing.
type Dummy struct{} type Dummy struct{}
// OID returns the struct's object identifier. // OID returns the struct's object identifier.
func (Dummy) OID() asn1.ObjectIdentifier { func (Dummy) OID() asn1.ObjectIdentifier {
return asn1.ObjectIdentifier{1, 3, 9900, 1} return asn1.ObjectIdentifier{1, 3, 9900, 1, 1}
} }
// AWS holds the AWS OID. // AWS holds the AWS OID.
@ -31,7 +43,7 @@ type AWS struct{}
// OID returns the struct's object identifier. // OID returns the struct's object identifier.
func (AWS) OID() asn1.ObjectIdentifier { func (AWS) OID() asn1.ObjectIdentifier {
return asn1.ObjectIdentifier{1, 3, 9900, 2} return asn1.ObjectIdentifier{1, 3, 9900, 2, 1}
} }
// GCP holds the GCP OID. // GCP holds the GCP OID.
@ -39,15 +51,23 @@ type GCP struct{}
// OID returns the struct's object identifier. // OID returns the struct's object identifier.
func (GCP) OID() asn1.ObjectIdentifier { func (GCP) OID() asn1.ObjectIdentifier {
return asn1.ObjectIdentifier{1, 3, 9900, 3} return asn1.ObjectIdentifier{1, 3, 9900, 3, 1}
} }
// Azure holds the Azure OID. // AzureSNP holds the OID for Azure SNP CVMs.
type Azure struct{} type AzureSNP struct{}
// OID returns the struct's object identifier. // OID returns the struct's object identifier.
func (Azure) OID() asn1.ObjectIdentifier { func (AzureSNP) OID() asn1.ObjectIdentifier {
return asn1.ObjectIdentifier{1, 3, 9900, 4} return asn1.ObjectIdentifier{1, 3, 9900, 4, 1}
}
// Azure holds the OID for Azure TrustedLaunch VMs.
type AzureTrustedLaunch struct{}
// OID returns the struct's object identifier.
func (AzureTrustedLaunch) OID() asn1.ObjectIdentifier {
return asn1.ObjectIdentifier{1, 3, 9900, 4, 2}
} }
// QEMU holds the QEMU OID. // QEMU holds the QEMU OID.
@ -55,5 +75,5 @@ type QEMU struct{}
// OID returns the struct's object identifier. // OID returns the struct's object identifier.
func (QEMU) OID() asn1.ObjectIdentifier { func (QEMU) OID() asn1.ObjectIdentifier {
return asn1.ObjectIdentifier{1, 3, 9900, 5} return asn1.ObjectIdentifier{1, 3, 9900, 5, 1}
} }

View file

@ -36,10 +36,10 @@ func IsSupportedK8sVersion(version string) bool {
const ( const (
// Constellation images. // Constellation images.
// These images are built in a way that they support all versions currently listed in VersionConfigs. // These images are built in a way that they support all versions currently listed in VersionConfigs.
JoinImage = "ghcr.io/edgelesssys/constellation/join-service:v0.0.1-0.20220831112436-10766c6049b8" JoinImage = "ghcr.io/edgelesssys/constellation/join-service:v0.0.2-0.20220901132202-b08ce24b6b40"
AccessManagerImage = "ghcr.io/edgelesssys/constellation/access-manager:v0.0.1" AccessManagerImage = "ghcr.io/edgelesssys/constellation/access-manager:v0.0.1"
KmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:v0.0.1" KmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:v0.0.2-0.20220901132202-b08ce24b6b40"
VerificationImage = "ghcr.io/edgelesssys/constellation/verification-service:v0.0.1-0.20220831112436-10766c6049b8" VerificationImage = "ghcr.io/edgelesssys/constellation/verification-service:v0.0.2-0.20220901132202-b08ce24b6b40"
GcpGuestImage = "ghcr.io/edgelesssys/gcp-guest-agent:20220713.00" GcpGuestImage = "ghcr.io/edgelesssys/gcp-guest-agent:20220713.00"
NodeOperatorCatalogImage = "ghcr.io/edgelesssys/constellation/node-operator-catalog" NodeOperatorCatalogImage = "ghcr.io/edgelesssys/constellation/node-operator-catalog"
NodeOperatorVersion = "v0.0.1" NodeOperatorVersion = "v0.0.1"

View file

@ -15,7 +15,8 @@ import (
"sync" "sync"
"github.com/edgelesssys/constellation/internal/atls" "github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/attestation/azure" "github.com/edgelesssys/constellation/internal/attestation/azure/snp"
"github.com/edgelesssys/constellation/internal/attestation/azure/trustedlaunch"
"github.com/edgelesssys/constellation/internal/attestation/gcp" "github.com/edgelesssys/constellation/internal/attestation/gcp"
"github.com/edgelesssys/constellation/internal/attestation/qemu" "github.com/edgelesssys/constellation/internal/attestation/qemu"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
@ -31,16 +32,23 @@ type Updatable struct {
newValidator newValidatorFunc newValidator newValidatorFunc
fileHandler file.Handler fileHandler file.Handler
csp cloudprovider.Provider csp cloudprovider.Provider
azureCVM bool
atls.Validator atls.Validator
} }
// NewValidator initializes a new updatable validator. // NewValidator initializes a new updatable validator.
func NewValidator(log *logger.Logger, csp string, fileHandler file.Handler) (*Updatable, error) { func NewValidator(log *logger.Logger, csp string, fileHandler file.Handler, azureCVM bool) (*Updatable, error) {
var newValidator newValidatorFunc var newValidator newValidatorFunc
switch cloudprovider.FromString(csp) { switch cloudprovider.FromString(csp) {
case cloudprovider.Azure: case cloudprovider.Azure:
if azureCVM {
newValidator = func(m map[uint32][]byte, e []uint32, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator { newValidator = func(m map[uint32][]byte, e []uint32, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator {
return azure.NewValidator(m, e, idkeydigest, enforceIdKeyDigest, log) return snp.NewValidator(m, e, idkeydigest, enforceIdKeyDigest, log)
}
} else {
newValidator = func(m map[uint32][]byte, e []uint32, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator {
return trustedlaunch.NewValidator(m, e, log)
}
} }
case cloudprovider.GCP: case cloudprovider.GCP:
newValidator = func(m map[uint32][]byte, e []uint32, _ []byte, _ bool, log *logger.Logger) atls.Validator { newValidator = func(m map[uint32][]byte, e []uint32, _ []byte, _ bool, log *logger.Logger) atls.Validator {
@ -59,6 +67,7 @@ func NewValidator(log *logger.Logger, csp string, fileHandler file.Handler) (*Up
newValidator: newValidator, newValidator: newValidator,
fileHandler: fileHandler, fileHandler: fileHandler,
csp: cloudprovider.FromString(csp), csp: cloudprovider.FromString(csp),
azureCVM: azureCVM,
} }
if err := u.Update(); err != nil { if err := u.Update(); err != nil {
@ -100,7 +109,7 @@ func (u *Updatable) Update() error {
var idkeydigest []byte var idkeydigest []byte
var enforceIdKeyDigest bool var enforceIdKeyDigest bool
if u.csp == cloudprovider.Azure { if u.csp == cloudprovider.Azure && u.azureCVM {
u.log.Infof("Updating encforceIdKeyDigest value") u.log.Infof("Updating encforceIdKeyDigest value")
enforceRaw, err := u.fileHandler.Read(filepath.Join(constants.ServiceBasePath, constants.EnforceIdKeyDigestFilename)) enforceRaw, err := u.fileHandler.Read(filepath.Join(constants.ServiceBasePath, constants.EnforceIdKeyDigestFilename))
if err != nil { if err != nil {

View file

@ -91,12 +91,17 @@ func TestNewUpdateableValidator(t *testing.T) {
filepath.Join(constants.ServiceBasePath, constants.EnforceIdKeyDigestFilename), filepath.Join(constants.ServiceBasePath, constants.EnforceIdKeyDigestFilename),
[]byte("false"), []byte("false"),
)) ))
require.NoError(handler.Write(
filepath.Join(constants.ServiceBasePath, constants.AzureCVM),
[]byte("true"),
))
} }
_, err := NewValidator( _, err := NewValidator(
logger.NewTest(t), logger.NewTest(t),
tc.provider, tc.provider,
handler, handler,
false,
) )
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
@ -147,6 +152,10 @@ func TestUpdate(t *testing.T) {
filepath.Join(constants.ServiceBasePath, constants.EnforceIdKeyDigestFilename), filepath.Join(constants.ServiceBasePath, constants.EnforceIdKeyDigestFilename),
[]byte("false"), []byte("false"),
)) ))
require.NoError(handler.Write(
filepath.Join(constants.ServiceBasePath, constants.AzureCVM),
[]byte("true"),
))
// call update once to initialize the server's validator // call update once to initialize the server's validator
require.NoError(validator.Update()) require.NoError(validator.Update())
@ -213,6 +222,10 @@ func TestUpdateConcurrency(t *testing.T) {
filepath.Join(constants.ServiceBasePath, constants.EnforceIdKeyDigestFilename), filepath.Join(constants.ServiceBasePath, constants.EnforceIdKeyDigestFilename),
[]byte("false"), []byte("false"),
)) ))
require.NoError(handler.Write(
filepath.Join(constants.ServiceBasePath, constants.AzureCVM),
[]byte("true"),
))
var wg sync.WaitGroup var wg sync.WaitGroup

View file

@ -49,7 +49,16 @@ func main() {
handler := file.NewHandler(afero.NewOsFs()) handler := file.NewHandler(afero.NewOsFs())
validator, err := watcher.NewValidator(log.Named("validator"), *provider, handler) cvmRaw, err := handler.Read(filepath.Join(constants.ServiceBasePath, constants.AzureCVM))
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to get azureCVM from config map")
}
azureCVM, err := strconv.ParseBool(string(cvmRaw))
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to parse content of AzureCVM: %s", cvmRaw)
}
validator, err := watcher.NewValidator(log.Named("validator"), *provider, handler, azureCVM)
if err != nil { if err != nil {
flag.Usage() flag.Usage()
log.With(zap.Error(err)).Fatalf("Failed to create validator") log.With(zap.Error(err)).Fatalf("Failed to create validator")

View file

@ -67,6 +67,7 @@ func main() {
if err != nil { if err != nil {
log.With(zap.Error).Fatalf("Failed to create Azure metadata API") log.With(zap.Error).Fatalf("Failed to create Azure metadata API")
} }
issuer = azure.NewIssuer() issuer = azure.NewIssuer()
case "gcp": case "gcp":