constellation/cli/internal/cloudcmd/validators.go
Nils Hanke 04c4cff9f6
AB#2436: Initial support for create/terminate AWS NitroTPM instances
* Add .DS_Store to .gitignore

* Add AWS to config / supported instance types

* Move AWS terraform skeleton to cli/internal/terraform

* Move currently unused IAM to hack/terraform/aws

* Print supported AWS instance types when AWS dev flag is set

* Block everything aTLS related (e.g. init, verify) until AWS attestation is available

* Create/Terminate AWS dev cluster when dev flag is set

* Restrict Nitro instances to NitroTPM supported specifically

* Pin zone for subnets

This is not great for HA, but for now we need to avoid the two subnets
ending up in different zones, causing the load balancer to not be able
to connect to the targets.

Should be replaced later with a better implementation that just uses
multiple subnets within the same region dynamically
based on # of nodes or similar.

* Add AWS/GCP to Terraform TestLoader unit test

* Add uid tag and create log group

Co-authored-by: Daniel Weiße <dw@edgeless.systems>
Co-authored-by: Malte Poll <mp@edgeless.systems>
2022-10-21 12:24:18 +02:00

199 lines
6.0 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cloudcmd
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/trustedlaunch"
"github.com/edgelesssys/constellation/v2/internal/attestation/gcp"
"github.com/edgelesssys/constellation/v2/internal/attestation/qemu"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/spf13/cobra"
)
type Validator struct {
provider cloudprovider.Provider
pcrs map[uint32][]byte
enforcedPCRs []uint32
idkeydigest []byte
enforceIDKeyDigest bool
azureCVM bool
validator atls.Validator
}
func NewValidator(provider cloudprovider.Provider, config *config.Config) (*Validator, error) {
v := Validator{}
if provider == cloudprovider.AWS {
// TODO: Implement AWS validator
return nil, errors.New("no validator for AWS available yet")
}
if provider == cloudprovider.Unknown {
return nil, errors.New("unknown cloud provider")
}
v.provider = provider
if err := v.setPCRs(config); err != nil {
return nil, err
}
if v.provider == cloudprovider.Azure {
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
}
}
return &v, nil
}
func (v *Validator) UpdateInitPCRs(ownerID, clusterID string) error {
if err := v.updatePCR(uint32(vtpm.PCRIndexOwnerID), ownerID); err != nil {
return err
}
return v.updatePCR(uint32(vtpm.PCRIndexClusterID), clusterID)
}
// updatePCR adds a new entry to the pcr map of v, or removes the key if the input is an empty string.
//
// When adding, the input is first decoded from base64.
// We then calculate the expected PCR by hashing the input using SHA256,
// appending expected PCR for initialization, and then hashing once more.
func (v *Validator) updatePCR(pcrIndex uint32, encoded string) error {
if encoded == "" {
delete(v.pcrs, pcrIndex)
// remove enforced PCR if it exists
for i, enforcedIdx := range v.enforcedPCRs {
if enforcedIdx == pcrIndex {
v.enforcedPCRs[i] = v.enforcedPCRs[len(v.enforcedPCRs)-1]
v.enforcedPCRs = v.enforcedPCRs[:len(v.enforcedPCRs)-1]
break
}
}
return nil
}
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return fmt.Errorf("input [%s] is not base64 encoded: %w", encoded, err)
}
// new_pcr_value := hash(old_pcr_value || data_to_extend)
// Since we use the TPM2_PCR_Event call to extend the PCR, data_to_extend is the hash of our input
hashedInput := sha256.Sum256(decoded)
expectedPcr := sha256.Sum256(append(v.pcrs[pcrIndex], hashedInput[:]...))
v.pcrs[pcrIndex] = expectedPcr[:]
return nil
}
func (v *Validator) setPCRs(config *config.Config) error {
switch v.provider {
case cloudprovider.AWS:
awsPCRs := config.Provider.AWS.Measurements
enforcedPCRs := config.Provider.AWS.EnforcedMeasurements
if err := v.checkPCRs(awsPCRs, enforcedPCRs); err != nil {
return err
}
v.enforcedPCRs = enforcedPCRs
v.pcrs = awsPCRs
case cloudprovider.Azure:
azurePCRs := config.Provider.Azure.Measurements
enforcedPCRs := config.Provider.Azure.EnforcedMeasurements
if err := v.checkPCRs(azurePCRs, enforcedPCRs); err != nil {
return err
}
v.enforcedPCRs = enforcedPCRs
v.pcrs = azurePCRs
case cloudprovider.GCP:
gcpPCRs := config.Provider.GCP.Measurements
enforcedPCRs := config.Provider.GCP.EnforcedMeasurements
if err := v.checkPCRs(gcpPCRs, enforcedPCRs); err != nil {
return err
}
v.enforcedPCRs = enforcedPCRs
v.pcrs = gcpPCRs
case cloudprovider.QEMU:
qemuPCRs := config.Provider.QEMU.Measurements
enforcedPCRs := config.Provider.QEMU.EnforcedMeasurements
if err := v.checkPCRs(qemuPCRs, enforcedPCRs); err != nil {
return err
}
v.enforcedPCRs = enforcedPCRs
v.pcrs = qemuPCRs
}
return nil
}
// V returns the validator as atls.Validator.
func (v *Validator) V(cmd *cobra.Command) atls.Validator {
v.updateValidator(cmd)
return v.validator
}
// PCRS returns the validator's PCR map.
func (v *Validator) PCRS() map[uint32][]byte {
return v.pcrs
}
func (v *Validator) updateValidator(cmd *cobra.Command) {
log := warnLogger{cmd: cmd}
switch v.provider {
case cloudprovider.GCP:
v.validator = gcp.NewValidator(v.pcrs, v.enforcedPCRs, log)
case cloudprovider.Azure:
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)
}
}
func (v *Validator) checkPCRs(pcrs map[uint32][]byte, enforcedPCRs []uint32) error {
if len(pcrs) == 0 {
return errors.New("no PCR values provided")
}
for k, v := range pcrs {
if len(v) != 32 {
return fmt.Errorf("bad config: PCR[%d]: expected length: %d, but got: %d", k, 32, len(v))
}
}
for _, v := range enforcedPCRs {
if _, ok := pcrs[v]; !ok {
return fmt.Errorf("bad config: PCR[%d] is enforced, but no expected measurement is provided", v)
}
}
return nil
}
// warnLogger implements logging of warnings for validators.
type warnLogger struct {
cmd *cobra.Command
}
// Infof is a no-op since we don't want extra info messages when using the CLI.
func (wl warnLogger) Infof(format string, args ...interface{}) {}
// Warnf prints a formatted warning from the validator.
func (wl warnLogger) Warnf(fmtStr string, args ...interface{}) {
wl.cmd.PrintErrf("Warning: %s\n", fmt.Sprintf(fmtStr, args...))
}