mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-24 06:59:40 -05:00
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:
parent
4bfb98d35a
commit
405db3286e
@ -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{}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -195,6 +195,13 @@ func NewJoinServiceDaemonset(csp, measurementsJSON, enforcedPCRsJSON, initialIdK
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ConfigMap: &k8s.ConfigMapProjection{
|
||||
LocalObjectReference: k8s.LocalObjectReference{
|
||||
Name: constants.InternalConfigMap,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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{})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package azure
|
||||
package snp
|
||||
|
||||
import (
|
||||
"context"
|
153
internal/attestation/azure/snp/issuer.go
Normal file
153
internal/attestation/azure/snp/issuer.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
@ -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 {
|
37
internal/attestation/azure/trustedlaunch/issuer.go
Normal file
37
internal/attestation/azure/trustedlaunch/issuer.go
Normal 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
|
||||
}
|
46
internal/attestation/azure/trustedlaunch/issuer_test.go
Normal file
46
internal/attestation/azure/trustedlaunch/issuer_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
49
internal/attestation/azure/trustedlaunch/validator.go
Normal file
49
internal/attestation/azure/trustedlaunch/validator.go
Normal 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
|
||||
}
|
81
internal/attestation/azure/trustedlaunch/validator_test.go
Normal file
81
internal/attestation/azure/trustedlaunch/validator_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
33
internal/cloud/vmtype/vmtype.go
Normal file
33
internal/cloud/vmtype/vmtype.go
Normal 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
|
||||
}
|
||||
}
|
25
internal/cloud/vmtype/vmtype_string.go
Normal file
25
internal/cloud/vmtype/vmtype_string.go
Normal 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]]
|
||||
}
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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":
|
||||
|
Loading…
Reference in New Issue
Block a user