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

View File

@ -20,13 +20,12 @@ import (
"github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/grpc/dialer"
"github.com/edgelesssys/constellation/internal/logger"
"github.com/edgelesssys/constellation/internal/oid"
"go.uber.org/zap"
)
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,
bindIP, bindPort string, log *logger.Logger,
cloudLogger logging.CloudLogger,
@ -58,9 +57,9 @@ func run(issuer quoteIssuer, tpm vtpm.TPMOpenFunc, fileHandler file.Handler,
}
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)
cleaner := clean.New().With(initServer).With(joinClient)
@ -92,12 +91,6 @@ type clusterInitJoiner interface {
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 {
joinclient.MetadataAPI
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.
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,
) ([]byte, error) {
return []byte{}, nil

View File

@ -18,6 +18,7 @@ import (
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
"github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/attestation"
"github.com/edgelesssys/constellation/internal/cloud/vmtype"
"github.com/edgelesssys/constellation/internal/crypto"
"github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/grpc/atlscredentials"
@ -36,12 +37,13 @@ import (
// The server handles initialization calls from the CLI and initializes the
// Kubernetes cluster.
type Server struct {
nodeLock locker
initializer ClusterInitializer
disk encryptedDisk
fileHandler file.Handler
grpcServer serveStopper
cleaner cleaner
nodeLock locker
initializer ClusterInitializer
disk encryptedDisk
fileHandler file.Handler
grpcServer serveStopper
cleaner cleaner
issuerWrapper IssuerWrapper
log *logger.Logger
@ -49,18 +51,20 @@ type Server struct {
}
// 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")
server := &Server{
nodeLock: lock,
disk: diskencryption.New(),
initializer: kube,
fileHandler: fh,
log: log,
nodeLock: lock,
disk: diskencryption.New(),
initializer: kube,
fileHandler: fh,
issuerWrapper: issuerWrapper,
log: log,
}
grpcServer := grpc.NewServer(
grpc.Creds(atlscredentials.New(issuer, nil)),
grpc.Creds(atlscredentials.New(issuerWrapper, nil)),
grpc.KeepaliveParams(keepalive.ServerParameters{Time: 15 * time.Second}),
log.Named("gRPC").GetServerUnaryInterceptor(),
)
@ -127,6 +131,8 @@ func (s *Server) Init(ctx context.Context, req *initproto.InitRequest) (*initpro
measurementSalt,
req.EnforcedPcrs,
req.EnforceIdkeydigest,
s.issuerWrapper.IdKeyDigest(),
s.issuerWrapper.VMType() == vmtype.AzureCVM,
resources.KMSConfig{
MasterSecret: req.MasterSecret,
Salt: req.Salt,
@ -175,6 +181,28 @@ func (s *Server) setupDisk(masterSecret, salt []byte) error {
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 {
keyMap := make(map[string]string)
for _, key := range keys {
@ -211,6 +239,8 @@ type ClusterInitializer interface {
measurementSalt []byte,
enforcedPcrs []uint32,
enforceIdKeyDigest bool,
idKeyDigest []byte,
azureCVM bool,
kmsConfig resources.KMSConfig,
sshUserKeys map[string]string,
helmDeployments []byte,

View File

@ -34,7 +34,7 @@ func TestNew(t *testing.T) {
assert := assert.New(t)
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.log)
assert.NotNil(server.nodeLock)
@ -289,7 +289,7 @@ type stubClusterInitializer struct {
}
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,
) ([]byte, error) {
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
providerMetadata ProviderMetadata
initialMeasurementsJSON []byte
initialIdKeyDigest []byte
getIPAddr func() (string, error)
}
// New creates a new KubeWrapper with real values.
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 {
return &KubeWrapper{
cloudProvider: cloudProvider,
@ -72,7 +71,6 @@ func New(cloudProvider string, clusterUtil clusterUtil, configProvider configura
clusterAutoscaler: clusterAutoscaler,
providerMetadata: providerMetadata,
initialMeasurementsJSON: initialMeasurementsJSON,
initialIdKeyDigest: initialIdKeyDigest,
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.
func (k *KubeWrapper) InitCluster(
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) {
k8sVersion, err := versions.NewValidK8sVersion(versionString)
if err != nil {
@ -185,7 +183,11 @@ func (k *KubeWrapper) InitCluster(
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)
}
@ -224,7 +226,7 @@ func (k *KubeWrapper) InitCluster(
// 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.
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)
@ -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.
// These types don't implement our custom Marshaler interface.
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

View File

@ -308,7 +308,7 @@ func TestInitCluster(t *testing.T) {
_, err := kube.InitCluster(
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 {

View File

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

View File

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

View File

@ -73,6 +73,9 @@ func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler,
if config.IsAzureNonCVM() {
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

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
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/attestation/azure/snp"
"github.com/edgelesssys/constellation/internal/attestation/azure/trustedlaunch"
"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
)
// 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),
),
// NewIssuer returns an SNP issuer if it can successfully read the idkeydigest from the TPM.
// Otherwise returns a Trusted Launch issuer.
func NewIssuer() atls.Issuer {
if _, err := snp.GetIdKeyDigest(vtpm.OpenVTPM); err == nil {
return snp.NewIssuer()
} else {
return trustedlaunch.NewIssuer()
}
}
// 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
*/
package azure
package snp
import (
"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
*/
package azure
package snp
import (
"context"
@ -64,7 +64,8 @@ func TestGetSNPAttestation(t *testing.T) {
tpmContent: reportDecoded,
err: nil,
}
attestationJson, err := getSNPAttestation(&snpAttestationReport, imdsClient)(tpm)
attestationJson, err := getInstanceInfo(&snpAttestationReport, imdsClient)(tpm)
if tc.wantErr {
assert.Error(err)
return
@ -96,7 +97,7 @@ func TestGetHCLAttestationKey(t *testing.T) {
assert.NoError(err)
defer tpm.Close()
_, err = getHCLAttestationKey(tpm)
_, err = getAttestationKey(tpm)
assert.NoError(err)
}

View File

@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package azure
package snp
import (
"bytes"
@ -30,7 +30,7 @@ const arkPEM = "-----BEGIN CERTIFICATE-----\nMIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb
// Validator for Azure confidential VM attestation.
type Validator struct {
oid.Azure
oid.AzureSNP
*vtpm.Validator
}
@ -40,65 +40,37 @@ func NewValidator(pcrs map[uint32][]byte, enforcedPCRs []uint32, idkeydigest []b
Validator: vtpm.NewValidator(
pcrs,
enforcedPCRs,
trustedKeyFromSNP(&azureInstanceInfo{}, idkeydigest, enforceIdKeyDigest, log),
validateAzureCVM,
getTrustedKey(&azureInstanceInfo{}, idkeydigest, enforceIdKeyDigest, log),
validateCVM,
vtpm.VerifyPKCS1v15,
log,
),
}
}
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 {
// validateCVM is a stub, since SEV-SNP attestation is already verified in trustedKeyFromSNP().
func validateCVM(attestation vtpm.AttestationDocument) error {
return nil
}
func (e *idkeyError) Error() string {
return fmt.Sprintf("configured idkeydigest does not match reported idkeydigest: %x", e.expectedValue)
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
}
// 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.
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) {
var instanceInfo azureInstanceInfo
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.
// ARK (hardcoded) validates ASK (cloud metadata API) validates VCEK (cloud metadata API).
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_s)
r := new(big.Int).SetBytes(sig_r)
s := new(big.Int).SetBytes(sig_s)
sequence := ecdsaSig{r, s}
rParam := new(big.Int).SetBytes(sig_r)
sParam := new(big.Int).SetBytes(sig_s)
sequence := ecdsaSig{rParam, sParam}
sigEncoded, err := asn1.Marshal(sequence)
if err != nil {
return fmt.Errorf("marshalling ecdsa signature: %w", err)
@ -213,7 +165,7 @@ func validateSNPReport(cert *x509.Certificate, expectedIdKeyDigest []byte, enfor
return &idkeyError{report.IdKeyDigest[:]}
}
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
}
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 {
R [72]byte
S [72]byte

View File

@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package azure
package snp
import (
"bytes"
@ -145,7 +145,7 @@ func TestTrustedKeyFromSNP(t *testing.T) {
idkeydigest, err := hex.DecodeString(tc.idkeydigest)
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 {
tc.assertCorrectError(err)
} else {
@ -171,7 +171,7 @@ func TestValidateAzureCVM(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := validateAzureCVM(tc.attDoc)
err := validateCVM(tc.attDoc)
if tc.wantErr {
assert.Error(err)
} 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
}
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
// it as YAML.
func FromFile(fileHandler file.Handler, name string) (*Config, error) {

View File

@ -84,6 +84,8 @@ const (
IdKeyDigestFilename = "idkeydigest"
// EnforceIdKeyDigestFilename is the name of the file configuring whether idkeydigest is enforced or not.
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 = "k8s-version"
@ -101,6 +103,7 @@ const (
KubernetesJoinTokenTTL = 15 * time.Minute
ConstellationNamespace = "kube-system"
JoinConfigMap = "join-config"
InternalConfigMap = "internal-config"
//
// Helm.

View File

@ -2,8 +2,23 @@
Copyright (c) Edgeless Systems GmbH
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
import (
@ -15,15 +30,12 @@ type Getter interface {
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.
type Dummy struct{}
// OID returns the struct's object identifier.
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.
@ -31,7 +43,7 @@ type AWS struct{}
// OID returns the struct's object identifier.
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.
@ -39,15 +51,23 @@ type GCP struct{}
// OID returns the struct's object identifier.
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.
type Azure struct{}
// AzureSNP holds the OID for Azure SNP CVMs.
type AzureSNP struct{}
// OID returns the struct's object identifier.
func (Azure) OID() asn1.ObjectIdentifier {
return asn1.ObjectIdentifier{1, 3, 9900, 4}
func (AzureSNP) OID() asn1.ObjectIdentifier {
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.
@ -55,5 +75,5 @@ type QEMU struct{}
// OID returns the struct's object identifier.
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 (
// Constellation images.
// 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"
KmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:v0.0.1"
VerificationImage = "ghcr.io/edgelesssys/constellation/verification-service:v0.0.1-0.20220831112436-10766c6049b8"
KmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:v0.0.2-0.20220901132202-b08ce24b6b40"
VerificationImage = "ghcr.io/edgelesssys/constellation/verification-service:v0.0.2-0.20220901132202-b08ce24b6b40"
GcpGuestImage = "ghcr.io/edgelesssys/gcp-guest-agent:20220713.00"
NodeOperatorCatalogImage = "ghcr.io/edgelesssys/constellation/node-operator-catalog"
NodeOperatorVersion = "v0.0.1"

View File

@ -15,7 +15,8 @@ import (
"sync"
"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/qemu"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
@ -31,16 +32,23 @@ type Updatable struct {
newValidator newValidatorFunc
fileHandler file.Handler
csp cloudprovider.Provider
azureCVM bool
atls.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
switch cloudprovider.FromString(csp) {
case cloudprovider.Azure:
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)
if azureCVM {
newValidator = func(m map[uint32][]byte, e []uint32, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator {
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:
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,
fileHandler: fileHandler,
csp: cloudprovider.FromString(csp),
azureCVM: azureCVM,
}
if err := u.Update(); err != nil {
@ -100,7 +109,7 @@ func (u *Updatable) Update() error {
var idkeydigest []byte
var enforceIdKeyDigest bool
if u.csp == cloudprovider.Azure {
if u.csp == cloudprovider.Azure && u.azureCVM {
u.log.Infof("Updating encforceIdKeyDigest value")
enforceRaw, err := u.fileHandler.Read(filepath.Join(constants.ServiceBasePath, constants.EnforceIdKeyDigestFilename))
if err != nil {

View File

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

View File

@ -49,7 +49,16 @@ func main() {
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 {
flag.Usage()
log.With(zap.Error(err)).Fatalf("Failed to create validator")

View File

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