diff --git a/.gitignore b/.gitignore index c798eb44..467d86a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ **.o /azmount /remotefs -/tools/get-snp-report/bin + /bin +/docker/skr/bin +/docker/encfs/bin +/tools/get-snp-report/bin diff --git a/cmd/remotefs/azurefs.go b/cmd/remotefs/azurefs.go index d1f227bb..7d91670f 100644 --- a/cmd/remotefs/azurefs.go +++ b/cmd/remotefs/azurefs.go @@ -1,373 +1,388 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//go:build linux -// +build linux - -package main - -import ( - "crypto/rsa" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strconv" - "time" - - "github.com/Microsoft/confidential-sidecar-containers/pkg/common" - "github.com/Microsoft/confidential-sidecar-containers/pkg/skr" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" - - "golang.org/x/crypto/hkdf" -) - -// Test dependencies -var ( - _azmountRun = azmountRun - _containerMountAzureFilesystem = containerMountAzureFilesystem - _cryptsetupOpen = cryptsetupOpen - ioutilWriteFile = ioutil.WriteFile - osGetenv = os.Getenv - osMkdirAll = os.MkdirAll - osRemoveAll = os.RemoveAll - osStat = os.Stat - timeSleep = time.Sleep - unixMount = unix.Mount -) - -var ( - Identity common.Identity - EncodedUvmInformation common.UvmInformation - // for testing encrypted filesystems without releasing secrets from - // AKV allowTestingWithRawKey needs to be set to true and a raw key - // needs to have been provided. Default mode is that such testing is - // disabled. - allowTestingWithRawKey = false -) - -// azmountRun starts azmount with the specified arguments, and leaves it running -// in the background. -func azmountRun(imageLocalFolder string, azureImageUrl string, azureImageUrlPrivate bool, azmountLogFile string, cacheBlockSize string, numBlocks string) error { - - identityJson, err := json.Marshal(Identity) - if err != nil { - logrus.Debugf("failed to marshal identity...") - return err - } - - encodedIdentity := base64.StdEncoding.EncodeToString(identityJson) - - logrus.Debugf("Starting azmount: %s %s %s %s %s KB", imageLocalFolder, azureImageUrl, strconv.FormatBool(azureImageUrlPrivate), azmountLogFile, cacheBlockSize) - cmd := exec.Command("/bin/azmount", "-mountpoint", imageLocalFolder, "-url", azureImageUrl, "-private", strconv.FormatBool(azureImageUrlPrivate), "-identity", encodedIdentity, "-logfile", azmountLogFile, "-blocksize", cacheBlockSize, "-numblocks", numBlocks) - if err := cmd.Start(); err != nil { - errorString := "azmount failed to start" - logrus.WithError(err).Error(errorString) - return errors.Wrapf(err, errorString) - } - logrus.Debugf("azmount running...") - return nil -} - -// cryptsetupCommand runs cryptsetup with the provided arguments -func cryptsetupCommand(args []string) error { - // --debug and -v are used to increase the information printed by - // cryptsetup. By default, it doesn't print much information, which makes it - // hard to debug it when there are problems. - cmd := exec.Command("cryptsetup", append([]string{"--debug", "-v"}, args...)...) - output, err := cmd.CombinedOutput() - if err != nil { - return errors.Wrapf(err, "failed to execute cryptsetup: %s", string(output)) - } - return nil -} - -// cryptsetupOpen runs "cryptsetup luksOpen" with the right arguments. -func cryptsetupOpen(source string, deviceName string, keyFilePath string) error { - openArgs := []string{ - // Open device with the key passed to luksFormat - "luksOpen", source, deviceName, "--key-file", keyFilePath, - // Don't use a journal to increase performance - "--integrity-no-journal", - "--persistent"} - - return cryptsetupCommand(openArgs) -} - -func mountAzureFile(tempDir string, index int, azureImageUrl string, azureImageUrlPrivate bool, cacheBlockSize string, numBlocks string) (string, error) { - - imageLocalFolder := filepath.Join(tempDir, fmt.Sprintf("%d", index)) - if err := osMkdirAll(imageLocalFolder, 0755); err != nil { - return "", errors.Wrapf(err, "mkdir failed: %s", imageLocalFolder) - } - - // Location in the UVM of the encrypted filesystem image. - imageLocalFile := filepath.Join(imageLocalFolder, "data") - - // Location of log file generated by azmount - azmountLogFile := filepath.Join(tempDir, fmt.Sprintf("log-%d.txt", index)) - - // Any program that sets up a FUSE filesystem becomes a server that listens - // to requests from the kernel, and it gets stuck in the loop that serves - // requests, so it is needed to run it in a different process so that the - // execution can continue in this one. - _azmountRun(imageLocalFolder, azureImageUrl, azureImageUrlPrivate, azmountLogFile, cacheBlockSize, numBlocks) - - // Wait until the file is available - count := 0 - for { - _, err := osStat(imageLocalFile) - if err == nil { - // Found - break - } - // Timeout after 10 seconds - count++ - if count == 1000 { - return "", errors.New("timed out") - } - timeSleep(60 * time.Millisecond) - } - logrus.Debugf("Found file") - - return imageLocalFile, nil -} - -// rawRemoteFilesystemKey sets up the key file path using the raw key passed -func rawRemoteFilesystemKey(tempDir string, rawKeyHexString string) (keyFilePath string, err error) { - - keyFilePath = filepath.Join(tempDir, "keyfile") - - keyBytes := make([]byte, 64) - - keyBytes, err = hex.DecodeString(rawKeyHexString) - if err != nil { - logrus.WithError(err).Debugf("failed to decode raw key: %s", rawKeyHexString) - return "", errors.Wrapf(err, "failed to write keyfile") - } - - // dm-crypt expects a key file, so create a key file using the key released in - // previous step - err = ioutilWriteFile(keyFilePath, keyBytes, 0644) - if err != nil { - logrus.WithError(err).Debugf("failed to delete keyfile: %s", keyFilePath) - return "", errors.Wrapf(err, "failed to write keyfile") - } - - return keyFilePath, nil -} - -// releaseRemoteFilesystemKey releases the key identified by keyBlob from AKV -// -// 1) Retrieve encoded security policy by reading the environment variable -// -// 2) Perform secure key release -// -// 3) Prepare the key file path using the released key -func releaseRemoteFilesystemKey(tempDir string, keyDerivationBlob skr.KeyDerivationBlob, keyBlob skr.KeyBlob) (keyFilePath string, err error) { - - keyFilePath = filepath.Join(tempDir, "keyfile") - - if EncodedUvmInformation.EncodedSecurityPolicy == "" { - err = errors.New("EncodedSecurityPolicy is empty") - logrus.WithError(err).Debugf("Make sure the environment is correct") // only helpful running outside of a UVM - return "", err - } - - // 2) release key identified by keyBlob using encoded security policy - jwKey, err := skr.SecureKeyRelease(Identity, keyBlob, EncodedUvmInformation) - if err != nil { - logrus.WithError(err).Debugf("failed to release key: %v", keyBlob) - return "", errors.Wrapf(err, "failed to release key") - } - logrus.Debugf("Key Type: %s", jwKey.KeyType()) - - octetKeyBytes := make([]byte, 32) - var rawKey interface{} - err = jwKey.Raw(&rawKey) - if err != nil { - logrus.WithError(err).Debugf("failed to extract raw key") - return "", errors.Wrapf(err, "failed to extract raw key") - } - - if jwKey.KeyType() == "oct" { - rawOctetKeyBytes, ok := rawKey.([]byte) - if !ok || len(rawOctetKeyBytes) != 32 { - logrus.WithError(err).Debugf("expected 32-byte octet key") - return "", errors.Wrapf(err, "expected 32-byte octet key") - } - octetKeyBytes = rawOctetKeyBytes - } else if jwKey.KeyType() == "RSA" { - rawKey, ok := rawKey.(*rsa.PrivateKey) - if !ok { - logrus.WithError(err).Debugf("expected RSA key") - return "", errors.Wrapf(err, "expected RSA key") - } - // use sha256 as hashing function for HKDF - hash := sha256.New - - // public salt and label - var labelString string - if keyDerivationBlob.Label != "" { - labelString = keyDerivationBlob.Label - } else { - labelString = "Symmetric Encryption Key" - } - - // decode public salt hexstring - salt, err := hex.DecodeString(keyDerivationBlob.Salt) - if err != nil { - logrus.WithError(err).Debugf("failed to decode salt hexstring") - return "", errors.Wrapf(err, "failed to decode salt hexstring") - } - - // setup derivation function using secret D exponent, salt, and label - hkdf := hkdf.New(hash, rawKey.D.Bytes(), salt, []byte(labelString)) - - // derive key - if _, err := io.ReadFull(hkdf, octetKeyBytes); err != nil { - logrus.WithError(err).Debugf("failed to derive oct key") - return "", errors.Wrapf(err, "failed to derive oct key") - } - - logrus.Debugf("Symmetric key %s (salt: %s label: %s)", hex.EncodeToString(octetKeyBytes), keyDerivationBlob.Salt, labelString) - } else { - logrus.WithError(err).Debugf("key type %snot supported", jwKey.KeyType()) - return "", errors.Wrapf(err, "key type %s not supported", jwKey.KeyType()) - } - - // 3) dm-crypt expects a key file, so create a key file using the key released in - // previous step - err = ioutilWriteFile(keyFilePath, octetKeyBytes, 0644) - if err != nil { - logrus.WithError(err).Debugf("failed to delete keyfile: %s", keyFilePath) - return "", errors.Wrapf(err, "failed to write keyfile") - } - - return keyFilePath, nil -} - -// containerMountAzureFilesystem mounts a remote filesystems specified in the -// policy of a given container. -// -// 1. Get the actual filesystem image. This is done by starting a new azmount -// process. The file is then exposed at “/[tempDir]/[index]/data“ and the -// log of azmount is saved to “/[tempDir]/log-[index].txt“. -// -// 2. Obtain keyfile. This is hardcoded at the moment and needs to be replaced -// by the actual code that gets the key. It is saved to a temporary file so -// that it can be passed to cryptsetup. It can be removed afterwards. -// -// 3. Open encrypted filesystem with cryptsetup. The result is a block device in -// “/dev/mapper/remote-crypt-[filesystem-index]“. -// -// 4) Mount block device as a read-only filesystem. -// -// 5. Create a symlink to the filesystem in the path shared between the UVM and -// the container. -func containerMountAzureFilesystem(tempDir string, index int, fs AzureFilesystem) (err error) { - - cacheBlockSize := "512" - numBlocks := "32" - - // 1) Mount remote image - imageLocalFile, err := mountAzureFile(tempDir, index, fs.AzureUrl, fs.AzureUrlPrivate, cacheBlockSize, numBlocks) - if err != nil { - return errors.Wrapf(err, "failed to mount remote file: %s", fs.AzureUrl) - } - - // 2) Obtain keyfile - - var keyFilePath string - if fs.KeyBlob.KID != "" { - keyFilePath, err = releaseRemoteFilesystemKey(tempDir, fs.KeyDerivationBlob, fs.KeyBlob) - if err != nil { - return errors.Wrapf(err, "failed to obtain keyfile") - } - } else if allowTestingWithRawKey { - keyFilePath, err = rawRemoteFilesystemKey(tempDir, fs.RawKeyHexString) - if err != nil { - return errors.Wrapf(err, "failed to obtain keyfile") - } - } - - defer func() { - // Delete keyfile on exit - if inErr := osRemoveAll(keyFilePath); inErr != nil { - logrus.WithError(inErr).Debugf("failed to delete keyfile: %s", keyFilePath) - } else { - logrus.Debugf("Deleted keyfile: %s", keyFilePath) - } - }() - - // 3) Open encrypted filesystem with cryptsetup. The result is a block - // device in /dev/mapper/remote-crypt-[filesystem-index] so that it is - // unique from all other filesystems. - var deviceName = fmt.Sprintf("remote-crypt-%d", index) - var deviceNamePath = "/dev/mapper/" + deviceName - - err = _cryptsetupOpen(imageLocalFile, deviceName, keyFilePath) - if err != nil { - return errors.Wrapf(err, "luksOpen failed: %s", deviceName) - } - logrus.Debugf("Device opened: %s", deviceName) - - // 4) Mount block device as a read-only filesystem. - tempMountFolder, err := filepath.Abs(filepath.Join(fs.MountPoint, fmt.Sprintf("../.filesystem-%d", index))) - if err != nil { - return errors.Wrapf(err, "failed to resolve absolute path") - } - - logrus.Debugf("Mounting to: %s", tempMountFolder) - - var flags uintptr = unix.MS_RDONLY - data := "noload" - - if err := osMkdirAll(tempMountFolder, 0755); err != nil { - return errors.Wrapf(err, "mkdir failed: %s", tempMountFolder) - } - - if err := unixMount(deviceNamePath, tempMountFolder, "ext4", flags, data); err != nil { - return errors.Wrapf(err, "failed to mount filesystem: %s", deviceNamePath) - } - - // 5) Create a symlink to the folder where the filesystem is mounted. - destPath := fs.MountPoint - logrus.Debugf("Linking to: %s", destPath) - - if err := os.Symlink(fmt.Sprintf(".filesystem-%d", index), destPath); err != nil { - return errors.Wrapf(err, "failed to symlink filesystem: %s", destPath) - } - - return nil -} - -func MountAzureFilesystems(tempDir string, info RemoteFilesystemsInformation) (err error) { - - Identity = info.AzureInfo.Identity - - // Retrieve the incoming encoded security policy, cert and uvm endorsement - EncodedUvmInformation, err = common.GetUvmInfomation() - if err != nil { - logrus.Fatalf("Failed to extract UVM_* environment variables: %s", err.Error()) - } - - for i, fs := range info.AzureFilesystems { - logrus.Debugf("Mounting Azure Storage blob %d...", i) - - err = _containerMountAzureFilesystem(tempDir, i, fs) - if err != nil { - return errors.Wrapf(err, "failed to mount filesystem index %d", i) - } - } - - return nil -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//go:build linux +// +build linux + +package main + +import ( + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "time" + + "github.com/Microsoft/confidential-sidecar-containers/pkg/attest" + "github.com/Microsoft/confidential-sidecar-containers/pkg/common" + "github.com/Microsoft/confidential-sidecar-containers/pkg/skr" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + + "golang.org/x/crypto/hkdf" +) + +// Test dependencies +var ( + _azmountRun = azmountRun + _containerMountAzureFilesystem = containerMountAzureFilesystem + _cryptsetupOpen = cryptsetupOpen + ioutilWriteFile = ioutil.WriteFile + osGetenv = os.Getenv + osMkdirAll = os.MkdirAll + osRemoveAll = os.RemoveAll + osStat = os.Stat + timeSleep = time.Sleep + unixMount = unix.Mount +) + +var ( + Identity common.Identity + CertState attest.CertState + EncodedUvmInformation common.UvmInformation + // for testing encrypted filesystems without releasing secrets from + // AKV allowTestingWithRawKey needs to be set to true and a raw key + // needs to have been provided. Default mode is that such testing is + // disabled. + allowTestingWithRawKey = false +) + +// azmountRun starts azmount with the specified arguments, and leaves it running +// in the background. +func azmountRun(imageLocalFolder string, azureImageUrl string, azureImageUrlPrivate bool, azmountLogFile string, cacheBlockSize string, numBlocks string) error { + + identityJson, err := json.Marshal(Identity) + if err != nil { + logrus.Debugf("failed to marshal identity...") + return err + } + + encodedIdentity := base64.StdEncoding.EncodeToString(identityJson) + + logrus.Debugf("Starting azmount: %s %s %s %s %s KB", imageLocalFolder, azureImageUrl, strconv.FormatBool(azureImageUrlPrivate), azmountLogFile, cacheBlockSize) + cmd := exec.Command("/bin/azmount", "-mountpoint", imageLocalFolder, "-url", azureImageUrl, "-private", strconv.FormatBool(azureImageUrlPrivate), "-identity", encodedIdentity, "-logfile", azmountLogFile, "-blocksize", cacheBlockSize, "-numblocks", numBlocks) + if err := cmd.Start(); err != nil { + errorString := "azmount failed to start" + logrus.WithError(err).Error(errorString) + return errors.Wrapf(err, errorString) + } + logrus.Debugf("azmount running...") + return nil +} + +// cryptsetupCommand runs cryptsetup with the provided arguments +func cryptsetupCommand(args []string) error { + // --debug and -v are used to increase the information printed by + // cryptsetup. By default, it doesn't print much information, which makes it + // hard to debug it when there are problems. + cmd := exec.Command("cryptsetup", append([]string{"--debug", "-v"}, args...)...) + output, err := cmd.CombinedOutput() + if err != nil { + return errors.Wrapf(err, "failed to execute cryptsetup: %s", string(output)) + } + return nil +} + +// cryptsetupOpen runs "cryptsetup luksOpen" with the right arguments. +func cryptsetupOpen(source string, deviceName string, keyFilePath string) error { + openArgs := []string{ + // Open device with the key passed to luksFormat + "luksOpen", source, deviceName, "--key-file", keyFilePath, + // Don't use a journal to increase performance + "--integrity-no-journal", + "--persistent"} + + return cryptsetupCommand(openArgs) +} + +func mountAzureFile(tempDir string, index int, azureImageUrl string, azureImageUrlPrivate bool, cacheBlockSize string, numBlocks string) (string, error) { + + imageLocalFolder := filepath.Join(tempDir, fmt.Sprintf("%d", index)) + if err := osMkdirAll(imageLocalFolder, 0755); err != nil { + return "", errors.Wrapf(err, "mkdir failed: %s", imageLocalFolder) + } + + // Location in the UVM of the encrypted filesystem image. + imageLocalFile := filepath.Join(imageLocalFolder, "data") + + // Location of log file generated by azmount + azmountLogFile := filepath.Join(tempDir, fmt.Sprintf("log-%d.txt", index)) + + // Any program that sets up a FUSE filesystem becomes a server that listens + // to requests from the kernel, and it gets stuck in the loop that serves + // requests, so it is needed to run it in a different process so that the + // execution can continue in this one. + _azmountRun(imageLocalFolder, azureImageUrl, azureImageUrlPrivate, azmountLogFile, cacheBlockSize, numBlocks) + + // Wait until the file is available + count := 0 + for { + _, err := osStat(imageLocalFile) + if err == nil { + // Found + break + } + // Timeout after 10 seconds + count++ + if count == 1000 { + return "", errors.New("timed out") + } + timeSleep(60 * time.Millisecond) + } + logrus.Debugf("Found file") + + return imageLocalFile, nil +} + +// rawRemoteFilesystemKey sets up the key file path using the raw key passed +func rawRemoteFilesystemKey(tempDir string, rawKeyHexString string) (keyFilePath string, err error) { + + keyFilePath = filepath.Join(tempDir, "keyfile") + + keyBytes := make([]byte, 64) + + keyBytes, err = hex.DecodeString(rawKeyHexString) + if err != nil { + logrus.WithError(err).Debugf("failed to decode raw key: %s", rawKeyHexString) + return "", errors.Wrapf(err, "failed to write keyfile") + } + + // dm-crypt expects a key file, so create a key file using the key released in + // previous step + err = ioutilWriteFile(keyFilePath, keyBytes, 0644) + if err != nil { + logrus.WithError(err).Debugf("failed to delete keyfile: %s", keyFilePath) + return "", errors.Wrapf(err, "failed to write keyfile") + } + + return keyFilePath, nil +} + +// releaseRemoteFilesystemKey releases the key identified by keyBlob from AKV +// +// 1) Retrieve encoded security policy by reading the environment variable +// +// 2) Perform secure key release +// +// 3) Prepare the key file path using the released key +func releaseRemoteFilesystemKey(tempDir string, keyDerivationBlob skr.KeyDerivationBlob, keyBlob skr.KeyBlob) (keyFilePath string, err error) { + + keyFilePath = filepath.Join(tempDir, "keyfile") + + if EncodedUvmInformation.EncodedSecurityPolicy == "" { + err = errors.New("EncodedSecurityPolicy is empty") + logrus.WithError(err).Debugf("Make sure the environment is correct") // only helpful running outside of a UVM + return "", err + } + + // 2) release key identified by keyBlob using encoded security policy and certfetcher (contained in CertState object) + // certfetcher is required for validating the attestation report against the cert + // chain of the chip identified in the attestation report + jwKey, err := skr.SecureKeyRelease(Identity, CertState, keyBlob, EncodedUvmInformation) + if err != nil { + logrus.WithError(err).Debugf("failed to release key: %v", keyBlob) + return "", errors.Wrapf(err, "failed to release key") + } + logrus.Debugf("Key Type: %s", jwKey.KeyType()) + + octetKeyBytes := make([]byte, 32) + var rawKey interface{} + err = jwKey.Raw(&rawKey) + if err != nil { + logrus.WithError(err).Debugf("failed to extract raw key") + return "", errors.Wrapf(err, "failed to extract raw key") + } + + if jwKey.KeyType() == "oct" { + rawOctetKeyBytes, ok := rawKey.([]byte) + if !ok || len(rawOctetKeyBytes) != 32 { + logrus.WithError(err).Debugf("expected 32-byte octet key") + return "", errors.Wrapf(err, "expected 32-byte octet key") + } + octetKeyBytes = rawOctetKeyBytes + } else if jwKey.KeyType() == "RSA" { + rawKey, ok := rawKey.(*rsa.PrivateKey) + if !ok { + logrus.WithError(err).Debugf("expected RSA key") + return "", errors.Wrapf(err, "expected RSA key") + } + // use sha256 as hashing function for HKDF + hash := sha256.New + + // public salt and label + var labelString string + if keyDerivationBlob.Label != "" { + labelString = keyDerivationBlob.Label + } else { + labelString = "Symmetric Encryption Key" + } + + // decode public salt hexstring + salt, err := hex.DecodeString(keyDerivationBlob.Salt) + if err != nil { + logrus.WithError(err).Debugf("failed to decode salt hexstring") + return "", errors.Wrapf(err, "failed to decode salt hexstring") + } + + // setup derivation function using secret D exponent, salt, and label + hkdf := hkdf.New(hash, rawKey.D.Bytes(), salt, []byte(labelString)) + + // derive key + if _, err := io.ReadFull(hkdf, octetKeyBytes); err != nil { + logrus.WithError(err).Debugf("failed to derive oct key") + return "", errors.Wrapf(err, "failed to derive oct key") + } + + logrus.Debugf("Symmetric key %s (salt: %s label: %s)", hex.EncodeToString(octetKeyBytes), keyDerivationBlob.Salt, labelString) + } else { + logrus.WithError(err).Debugf("key type %snot supported", jwKey.KeyType()) + return "", errors.Wrapf(err, "key type %s not supported", jwKey.KeyType()) + } + + // 3) dm-crypt expects a key file, so create a key file using the key released in + // previous step + err = ioutilWriteFile(keyFilePath, octetKeyBytes, 0644) + if err != nil { + logrus.WithError(err).Debugf("failed to delete keyfile: %s", keyFilePath) + return "", errors.Wrapf(err, "failed to write keyfile") + } + + return keyFilePath, nil +} + +// containerMountAzureFilesystem mounts a remote filesystems specified in the +// policy of a given container. +// +// 1. Get the actual filesystem image. This is done by starting a new azmount +// process. The file is then exposed at “/[tempDir]/[index]/data“ and the +// log of azmount is saved to “/[tempDir]/log-[index].txt“. +// +// 2. Obtain keyfile. This is hardcoded at the moment and needs to be replaced +// by the actual code that gets the key. It is saved to a temporary file so +// that it can be passed to cryptsetup. It can be removed afterwards. +// +// 3. Open encrypted filesystem with cryptsetup. The result is a block device in +// “/dev/mapper/remote-crypt-[filesystem-index]“. +// +// 4) Mount block device as a read-only filesystem. +// +// 5. Create a symlink to the filesystem in the path shared between the UVM and +// the container. +func containerMountAzureFilesystem(tempDir string, index int, fs AzureFilesystem) (err error) { + + cacheBlockSize := "512" + numBlocks := "32" + + // 1) Mount remote image + imageLocalFile, err := mountAzureFile(tempDir, index, fs.AzureUrl, fs.AzureUrlPrivate, cacheBlockSize, numBlocks) + if err != nil { + return errors.Wrapf(err, "failed to mount remote file: %s", fs.AzureUrl) + } + + // 2) Obtain keyfile + + var keyFilePath string + if fs.KeyBlob.KID != "" { + keyFilePath, err = releaseRemoteFilesystemKey(tempDir, fs.KeyDerivationBlob, fs.KeyBlob) + if err != nil { + return errors.Wrapf(err, "failed to obtain keyfile") + } + } else if allowTestingWithRawKey { + keyFilePath, err = rawRemoteFilesystemKey(tempDir, fs.RawKeyHexString) + if err != nil { + return errors.Wrapf(err, "failed to obtain keyfile") + } + } + + defer func() { + // Delete keyfile on exit + if inErr := osRemoveAll(keyFilePath); inErr != nil { + logrus.WithError(inErr).Debugf("failed to delete keyfile: %s", keyFilePath) + } else { + logrus.Debugf("Deleted keyfile: %s", keyFilePath) + } + }() + + // 3) Open encrypted filesystem with cryptsetup. The result is a block + // device in /dev/mapper/remote-crypt-[filesystem-index] so that it is + // unique from all other filesystems. + var deviceName = fmt.Sprintf("remote-crypt-%d", index) + var deviceNamePath = "/dev/mapper/" + deviceName + + err = _cryptsetupOpen(imageLocalFile, deviceName, keyFilePath) + if err != nil { + return errors.Wrapf(err, "luksOpen failed: %s", deviceName) + } + logrus.Debugf("Device opened: %s", deviceName) + + // 4) Mount block device as a read-only filesystem. + tempMountFolder, err := filepath.Abs(filepath.Join(fs.MountPoint, fmt.Sprintf("../.filesystem-%d", index))) + if err != nil { + return errors.Wrapf(err, "failed to resolve absolute path") + } + + logrus.Debugf("Mounting to: %s", tempMountFolder) + + var flags uintptr = unix.MS_RDONLY + data := "noload" + + if err := osMkdirAll(tempMountFolder, 0755); err != nil { + return errors.Wrapf(err, "mkdir failed: %s", tempMountFolder) + } + + if err := unixMount(deviceNamePath, tempMountFolder, "ext4", flags, data); err != nil { + return errors.Wrapf(err, "failed to mount filesystem: %s", deviceNamePath) + } + + // 5) Create a symlink to the folder where the filesystem is mounted. + destPath := fs.MountPoint + logrus.Debugf("Linking to: %s", destPath) + + if err := os.Symlink(fmt.Sprintf(".filesystem-%d", index), destPath); err != nil { + return errors.Wrapf(err, "failed to symlink filesystem: %s", destPath) + } + + return nil +} + +func MountAzureFilesystems(tempDir string, info RemoteFilesystemsInformation) (err error) { + + Identity = info.AzureInfo.Identity + + // Retrieve the incoming encoded security policy, cert and uvm endorsement + EncodedUvmInformation, err = common.GetUvmInformation() + if err != nil { + logrus.Fatalf("Failed to extract UVM_* environment variables: %s", err.Error()) + } + + logrus.Debugf("EncodedUvmInformation.InitialCerts.Tcbm: %s\n", EncodedUvmInformation.InitialCerts.Tcbm) + thimTcbm, err := strconv.ParseUint(EncodedUvmInformation.InitialCerts.Tcbm, 16, 64) + if err != nil { + logrus.Fatal("Unable to convert Initial Certs TCBM to a uint64") + } + + CertState = attest.CertState{ + CertFetcher: info.AzureInfo.CertFetcher, + Tcbm: thimTcbm, + } + + for i, fs := range info.AzureFilesystems { + logrus.Debugf("Mounting Azure Storage blob %d...", i) + + err = _containerMountAzureFilesystem(tempDir, i, fs) + if err != nil { + return errors.Wrapf(err, "failed to mount filesystem index %d", i) + } + } + + return nil +} diff --git a/cmd/remotefs/main.go b/cmd/remotefs/main.go index 0ef4d9c5..9b3ed4e0 100644 --- a/cmd/remotefs/main.go +++ b/cmd/remotefs/main.go @@ -1,119 +1,121 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package main - -import ( - "encoding/base64" - "encoding/json" - "flag" - "fmt" - "io/ioutil" - "os" - - "github.com/Microsoft/confidential-sidecar-containers/pkg/common" - "github.com/Microsoft/confidential-sidecar-containers/pkg/skr" - "github.com/sirupsen/logrus" -) - -type AzureInfo struct { - Identity common.Identity `json:"identity,omitempty"` -} - -type RemoteFilesystemsInformation struct { - AzureInfo AzureInfo `json:"azure_info"` - AzureFilesystems []AzureFilesystem `json:"azure_filesystems"` -} - -// AzureFilesystem contains information about a filesystem image stored in Azure -// Blob Storage. -type AzureFilesystem struct { - // This is the URL of the image - AzureUrl string `json:"azure_url"` - // This is a private AzureUrl - AzureUrlPrivate bool `json:"azure_url_private"` - // This is the path where the filesystem will be exposed in the container. - MountPoint string `json:"mount_point"` - // This is the information used by encfs to derive the encryption key of the filesystem - // if the key being released is a private RSA key - KeyDerivationBlob skr.KeyDerivationBlob `json:"key_derivation,omitempty"` - // This is the information used by skr to release the encryption key of the filesystem - KeyBlob skr.KeyBlob `json:"key,omitempty"` - // This is a testing key hexstring encoded to be used against the filesystem. This should - // be used only for testing. - RawKeyHexString string `json:"raw_key, omitempty"` -} - -func usage() { - fmt.Printf("Usage of %s:\n", os.Args[0]) - flag.PrintDefaults() -} - -func main() { - base64string := flag.String("base64", "", "base64-encoded json string with all information") - logLevel := flag.String("loglevel", "debug", "Logging Level: trace, debug, info, warning, error, fatal, panic.") - logFile := flag.String("logfile", "", "Logging Target: An optional file name/path. Omit for console output.") - - flag.Usage = usage - - flag.Parse() - - if *logFile != "" { - // If the file doesn't exist, create it. If it exists, append to it. - file, err := os.OpenFile(*logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) - if err != nil { - logrus.Fatal(err) - } - defer file.Close() - logrus.SetOutput(file) - } - - level, err := logrus.ParseLevel(*logLevel) - if err != nil { - logrus.Fatal(err) - } - logrus.SetLevel(level) - - logrus.Infof("Starting %s...", os.Args[0]) - - logrus.Infof("Args:") - logrus.Debugf(" Log Level: %s", *logLevel) - logrus.Debugf(" Log File: %s", *logFile) - logrus.Debugf(" base64: %s", *base64string) - - logrus.Infof("Creating temporary directory") - tempDir, err := ioutil.TempDir("", "remotefs") - if err != nil { - logrus.Fatalf("Failed to create temp dir: %s", err.Error()) - } - logrus.Infof("Temporary directory: %s", tempDir) - - // Decode information - bytes, err := base64.StdEncoding.DecodeString(*base64string) - if err != nil { - logrus.Fatalf("Failed to decode base64: %s", err.Error()) - } - - info := RemoteFilesystemsInformation{} - err = json.Unmarshal(bytes, &info) - if err != nil { - logrus.Fatalf("Failed to unmarshal: %s", err.Error()) - } - - // populate missing attributes in KeyBlob - for i, _ := range info.AzureFilesystems { - // set the api versions and the tee type for which the authority will authorize secure key release - info.AzureFilesystems[i].KeyBlob.AKV.APIVersion = "api-version=7.3-preview" - info.AzureFilesystems[i].KeyBlob.Authority.APIVersion = "api-version=2020-10-01" - info.AzureFilesystems[i].KeyBlob.Authority.TEEType = "SevSnpVM" - } - - logrus.Debugf("JSON = %+v", info) - - err = MountAzureFilesystems(tempDir, info) - if err != nil { - logrus.Fatalf("Failed to mount filesystems: %s", err.Error()) - } - - os.Exit(0) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package main + +import ( + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + + "github.com/Microsoft/confidential-sidecar-containers/pkg/attest" + "github.com/Microsoft/confidential-sidecar-containers/pkg/common" + "github.com/Microsoft/confidential-sidecar-containers/pkg/skr" + "github.com/sirupsen/logrus" +) + +type AzureInfo struct { + CertFetcher attest.CertFetcher `json:"certcache,omitempty"` + Identity common.Identity `json:"identity,omitempty"` +} + +type RemoteFilesystemsInformation struct { + AzureInfo AzureInfo `json:"azure_info"` + AzureFilesystems []AzureFilesystem `json:"azure_filesystems"` +} + +// AzureFilesystem contains information about a filesystem image stored in Azure +// Blob Storage. +type AzureFilesystem struct { + // This is the URL of the image + AzureUrl string `json:"azure_url"` + // This is a private AzureUrl + AzureUrlPrivate bool `json:"azure_url_private"` + // This is the path where the filesystem will be exposed in the container. + MountPoint string `json:"mount_point"` + // This is the information used by encfs to derive the encryption key of the filesystem + // if the key being released is a private RSA key + KeyDerivationBlob skr.KeyDerivationBlob `json:"key_derivation,omitempty"` + // This is the information used by skr to release the encryption key of the filesystem + KeyBlob skr.KeyBlob `json:"key,omitempty"` + // This is a testing key hexstring encoded to be used against the filesystem. This should + // be used only for testing. + RawKeyHexString string `json:"raw_key, omitempty"` +} + +func usage() { + fmt.Printf("Usage of %s:\n", os.Args[0]) + flag.PrintDefaults() +} + +func main() { + base64string := flag.String("base64", "", "base64-encoded json string with all information") + logLevel := flag.String("loglevel", "debug", "Logging Level: trace, debug, info, warning, error, fatal, panic.") + logFile := flag.String("logfile", "", "Logging Target: An optional file name/path. Omit for console output.") + + flag.Usage = usage + + flag.Parse() + + if *logFile != "" { + // If the file doesn't exist, create it. If it exists, append to it. + file, err := os.OpenFile(*logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + logrus.Fatal(err) + } + defer file.Close() + logrus.SetOutput(file) + } + + level, err := logrus.ParseLevel(*logLevel) + if err != nil { + logrus.Fatal(err) + } + logrus.SetLevel(level) + + logrus.Infof("Starting %s...", os.Args[0]) + + logrus.Infof("Args:") + logrus.Debugf(" Log Level: %s", *logLevel) + logrus.Debugf(" Log File: %s", *logFile) + logrus.Debugf(" base64: %s", *base64string) + + logrus.Infof("Creating temporary directory") + tempDir, err := ioutil.TempDir("", "remotefs") + if err != nil { + logrus.Fatalf("Failed to create temp dir: %s", err.Error()) + } + logrus.Infof("Temporary directory: %s", tempDir) + + // Decode information + bytes, err := base64.StdEncoding.DecodeString(*base64string) + if err != nil { + logrus.Fatalf("Failed to decode base64: %s", err.Error()) + } + + info := RemoteFilesystemsInformation{} + err = json.Unmarshal(bytes, &info) + if err != nil { + logrus.Fatalf("Failed to unmarshal: %s", err.Error()) + } + + // populate missing attributes in KeyBlob + for i, _ := range info.AzureFilesystems { + // set the api versions and the tee type for which the authority will authorize secure key release + info.AzureFilesystems[i].KeyBlob.AKV.APIVersion = "api-version=7.3-preview" + info.AzureFilesystems[i].KeyBlob.Authority.APIVersion = "api-version=2020-10-01" + info.AzureFilesystems[i].KeyBlob.Authority.TEEType = "SevSnpVM" + } + + logrus.Debugf("JSON = %+v", info) + + err = MountAzureFilesystems(tempDir, info) + if err != nil { + logrus.Fatalf("Failed to mount filesystems: %s", err.Error()) + } + + os.Exit(0) +} diff --git a/cmd/skr/main.go b/cmd/skr/main.go index 41488064..a44fdcc5 100644 --- a/cmd/skr/main.go +++ b/cmd/skr/main.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "os" + "strconv" "github.com/Microsoft/confidential-sidecar-containers/pkg/attest" "github.com/Microsoft/confidential-sidecar-containers/pkg/common" @@ -21,11 +22,17 @@ import ( var ( Identity common.Identity + ServerCertState attest.CertState EncodedUvmInformation common.UvmInformation ready bool ) type AzureInformation struct { + // Endpoint of the certificate cache service from which + // the certificate chain endorsing hardware attestations + // can be retrieved. This is optional only when the container + // will expose attest/maa and key/release APIs. + CertFetcher attest.CertFetcher `json:"certcache,omitempty"` // Identifier of the managed identity to be used // for authenticating with AKV. This is optional and // useful only when the container group has been assigned @@ -143,7 +150,7 @@ func postMAAAttest(c *gin.Context) { c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) } - maaToken, err := attest.Attest(maa, runtimeDataBytes, EncodedUvmInformation) + maaToken, err := ServerCertState.Attest(maa, runtimeDataBytes, EncodedUvmInformation) if err != nil { c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) } @@ -186,7 +193,7 @@ func postKeyRelease(c *gin.Context) { AKV: akv, } - jwKey, err := skr.SecureKeyRelease(Identity, skrKeyBlob, EncodedUvmInformation) + jwKey, err := skr.SecureKeyRelease(Identity, ServerCertState, skrKeyBlob, EncodedUvmInformation) if err != nil { c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return @@ -203,14 +210,15 @@ func postKeyRelease(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"key": string(jwkJSONBytes)}) } -func setupServer(identity common.Identity) *gin.Engine { - +func setupServer(certState attest.CertState, identity common.Identity) *gin.Engine { + ServerCertState = certState Identity = identity + certString := EncodedUvmInformation.InitialCerts.VcekCert + EncodedUvmInformation.InitialCerts.CertificateChain + logrus.Debugf("Setting security policy to %s", EncodedUvmInformation.EncodedSecurityPolicy) - logrus.Debugf("Setting platform certs to %s", EncodedUvmInformation.CertChain) + logrus.Debugf("Setting platform certs to %s", certString) logrus.Debugf("Setting uvm reference to %s", EncodedUvmInformation.EncodedUvmReferenceInfo) - r := gin.Default() r.GET("/status", getStatus) @@ -221,7 +229,7 @@ func setupServer(identity common.Identity) *gin.Engine { // the certificate chain endording the signing key of the hardware attestation. // Hence, these APIs are exposed only if the platform certificate information // has been provided at startup time. - if EncodedUvmInformation.CertChain != "" { + if certState.CertFetcher.Endpoint != "" || certString != "" { r.POST("/attest/maa", postMAAAttest) r.POST("/key/release", postKeyRelease) } @@ -237,6 +245,11 @@ func main() { logLevel := flag.String("loglevel", "debug", "Logging Level: trace, debug, info, warning, error, fatal, panic.") logFile := flag.String("logfile", "", "Logging Target: An optional file name/path. Omit for console output.") port := flag.String("port", "8080", "Port on which to listen") + allowTestingMismatchedTCB := flag.Bool("allowTestingMismatchedTCB", false, "For TESTING purposes only. Corrupts the TCB value") + + // for testing mis-matched TCB versions allowTestingWithMismatchedTCB + // and CorruptedTCB + CorruptedTCB := "ffffffff" // WARNING!!! // If the security policy does not control the arguments to this process then @@ -277,8 +290,9 @@ func main() { logrus.Debugf(" Port: %s", *port) logrus.Debugf(" Hostname: %s", *hostname) logrus.Debugf(" azure info: %s", *azureInfoBase64string) + logrus.Debugf(" corrupt tcbm: %t", *allowTestingMismatchedTCB) - EncodedUvmInformation, err = common.GetUvmInfomation() // from the env. + EncodedUvmInformation, err = common.GetUvmInformation() // from the env. if err != nil { logrus.Fatalf("Failed to extract UVM_* environment variables: %s", err.Error()) } @@ -301,5 +315,24 @@ func main() { // See above comment about hostname and risk of breaking confidentiality url := *hostname + ":" + *port - setupServer(info.Identity).Run(url) + var tcbm string + if *allowTestingMismatchedTCB { + logrus.Debugf("setting tcbm to CorruptedTCB value: %s\n", CorruptedTCB) + tcbm = CorruptedTCB + } else { + logrus.Debugf("setting tcbm to EncodedUvmInformation.InitialCerts.Tcbm value: %s\n", EncodedUvmInformation.InitialCerts.Tcbm) + tcbm = EncodedUvmInformation.InitialCerts.Tcbm + } + + thimTcbm, err := strconv.ParseUint(tcbm, 16, 64) + if err != nil { + logrus.Fatal("Unable to convert intial TCBM to a uint64") + } + + certState := attest.CertState{ + CertFetcher: info.CertFetcher, + Tcbm: thimTcbm, + } + + setupServer(certState, info.Identity).Run(url) } diff --git a/docker/skr/Dockerfile.debug b/docker/skr/Dockerfile.debug index c38ce9f3..6f39b428 100755 --- a/docker/skr/Dockerfile.debug +++ b/docker/skr/Dockerfile.debug @@ -1,22 +1,22 @@ -FROM ubuntu:18.04 -RUN apt update -RUN apt install --fix-missing -y net-tools wget curl bc jq bash vim ssh - -# clearly this is extremely insecure but is only for debugging -# do not copy this. -RUN useradd --uid 1000 --gid 0 --non-unique -ms /bin/bash auserwithalongname -RUN echo "auserwithalongname:shortpassword" | chpasswd -RUN mkdir /run/sshd - -# set the start command which will be used by default by ACI -# note that this script exposes attestation on an external port -# NEVER DO THIS IN PRODUCTION as it exposes the attestations -# which can be used to trick an attestation agent or relying party - -COPY ./bin/skr ./bin/get-snp-report ./bin/verbose-report /bin/ -COPY skr.sh skr-debug.sh tests/*_client.sh / -RUN mkdir -p /tests/skr; mv *_client.sh /tests/skr -RUN chmod +x /*.sh /tests/skr/*.sh; date > /made-date - -# set the start command -CMD [ "sleep", "1000000" ] +FROM ubuntu:18.04 +RUN apt update +RUN apt install --fix-missing -y net-tools wget curl bc jq bash vim ssh + +# clearly this is extremely insecure but is only for debugging +# do not copy this. +RUN useradd --uid 1000 --gid 0 --non-unique -ms /bin/bash auserwithalongname +RUN echo "auserwithalongname:shortpassword" | chpasswd +RUN mkdir /run/sshd + +# set the start command which will be used by default by ACI +# note that this script exposes attestation on an external port +# NEVER DO THIS IN PRODUCTION as it exposes the attestations +# which can be used to trick an attestation agent or relying party + +COPY ./bin/skr ./bin/get-snp-report ./bin/verbose-report /bin/ +COPY skr.sh skr-debug.sh tests/*_client.sh tests/skr_test.sh / +RUN mkdir -p /tests/skr; mv *_client.sh /tests/skr; mv skr_test.sh /tests/skr +RUN chmod +x /*.sh /tests/skr/*.sh; date > /made-date + +# set the start command +CMD [ "sleep", "1000000" ] diff --git a/docker/skr/Dockerfile.skr b/docker/skr/Dockerfile.skr index 762ba02e..3d4c1e32 100644 --- a/docker/skr/Dockerfile.skr +++ b/docker/skr/Dockerfile.skr @@ -3,8 +3,8 @@ FROM alpine:3.17.1 RUN apk update && apk add curl COPY ./bin/skr ./bin/get-snp-report /bin/ -COPY skr.sh tests/*_client.sh / -RUN mkdir -p /tests/skr; mv *_client.sh /tests/skr +COPY skr.sh tests/*_client.sh tests/skr_test.sh / +RUN mkdir -p /tests/skr; mv *_client.sh /tests/skr; mv skr_test.sh /tests/skr RUN chmod +x /*.sh /tests/skr/*.sh; date > /made-date # set the start command diff --git a/docker/skr/tests/skr_test.sh b/docker/skr/tests/skr_test.sh new file mode 100644 index 00000000..063a156b --- /dev/null +++ b/docker/skr/tests/skr_test.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Important note: This script is meant to run from inside the container + +CmdlineArgs="-logfile ./log.txt" + +if [ -z "${SkrSideCarArgs}" ]; then + SkrSideCarArgs=$1 +fi + +echo SkrSideCarArgs = $SkrSideCarArgs + +if [ -n "${SkrSideCarArgs}" ]; then + CmdlineArgs="${CmdlineArgs} -base64 ${SkrSideCarArgs}" +fi + +if [ -z "${TestingMismatchedTCB}" ]; then + TestingMismatchedTCB=$2 +fi + +echo CorruptedTcbm = $TestingMismatchedTCB + +if [ -n "${TestingMismatchedTCB}" ]; then + CmdlineArgs="${CmdlineArgs} -allowTestingMismatchedTCB=${TestingMismatchedTCB}" +fi + +if [ -z "${Port}" ]; then + Port=$3 +fi + +echo Port = $Port + +if [ -n "${Port}" ]; then + CmdlineArgs="${CmdlineArgs} -port ${Port}" +fi + +echo CmdlineArgs = $CmdlineArgs + +if /bin/skr $CmdlineArgs; then + echo "1" > result +else + echo "0" > result +fi diff --git a/examples/skr/README.md b/examples/skr/README.md index b40a6860..9bbaddd5 100644 --- a/examples/skr/README.md +++ b/examples/skr/README.md @@ -1,11 +1,8 @@ # Attestation and Secure Key Release Sidecar Example ## Table of Contents -- [Managed identity](#managed-identity) - [Policy generation](#policy-generation) -- [Import key](#import-key) -- [Deployment](#deployment) -- [Step by step example](#step-by-step-example) +- [Step by Step Example](#step-by-step-example) In our confidential container group example, we will deploy the skr sidecar along with a set of test containers that exercise and test the REST API. - **skr sidecar.** The sidecar’s entry point is /skr.sh which uses the SkrSideCarArgs environment variable to pass the certificate cache endpoint information. @@ -14,86 +11,127 @@ In our confidential container group example, we will deploy the skr sidecar alon - **key/release test.** The sidecar’s entry point is /tests/skr_client.sh which uses the three environment variables: (i) SkrClientKID passes the key identifier of the key to be released from the key vault, (ii) SkrClientAKVEndpoint passes the key vault endpoint from which the key will be released, and (iii) SkrClientMAAEndpoint passes the Microsoft Azure Attestation endpoint shall author the attestation token required for releasing the secret. The MAA endpoint shall be the same as the one specified in the SKR policy during the key import to the key vault. -### Managed identity -The user needs to generate a user-assigned managed idenity which will be attached to the container group so that the containers can have the right access permissions to Azure services and resources. More information about creating identities can be found [here.](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/) - ### Policy generation -Deploying a confidential container group requires generating a security policy that restricts what containers can run within the container group. There is an az tool available for generating policies. See [here](https://github.com/Azure/azure-cli-extensions/tree/main/src/confcom/azext_confcom#microsoft-azure-cli-confcom-extension-examples) for installing Azure `confcom` CLI extension. +Deploying a confidential container group requires generating a security policy that restricts what containers can run within the container group. To generate security policies, install the Azure `confcom` CLI extension by following the instructions [here](https://github.com/Azure/azure-cli-extensions/tree/main/src/confcom/azext_confcom#microsoft-azure-cli-confcom-extension-examples). The ARM template can be used directly to generate a security policy. The following command generates a security policy and automatically injects it into the template. Make sure `--debug-mode` option is included so that the generated policy allows shelling into container to see the released key in this example. NOTE: the current image used in the ARM template is built upon commit id a82b530. - `az confcom acipolicygen -a aci-skr-arm-template.json --debug-mode` +``` +az confcom acipolicygen -a aci-skr-arm-template.json --debug-mode +``` The ARM template file includes three entries: (i) skr sidecar container which whitelists the /skr.sh as entry point command and the environment variable SkrSideCarArgs used by the script, (ii) attest_client container which whitelists the /tests/attest_client.sh as entry point command and a set of environment variables used by the script and whose names begin with AttestClient, and (iii) skr_client container which whitelists the /tests/skr_client.sh as entry point command and a set of environment variables used by the script and whose names begin with SkrClient. Please note that: - The skr sidecar must be allowed to execute as elevated because it needs access to the PSP which is mounted as a device at /dev/sev. - The policy includes one entry for both attestation tests, as both tests use the same entry point and a superset of environment variables whitelisted by the AttestClient regular expression. -### Import key -The user needs to instantiate an Azure Key Vault resource that supports storing keys in an HSM: a [Premium vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) or an [MHSM resource](https://docs.microsoft.com/en-us/azure/key-vault/managed-hsm/overview). For the former, the user needs to assign -the *Key Vault Crypto Officer* and *Key Vault Crypto User* roles to the user-assigned managed identity and for the latter, the user needs to assign *Managed HSM Crypto Officer* and *Managed HSM Crypto User* roles for /keys to the user-assigned managed identity. -Once the key vault resource is ready, the user can import `RSA-HSM` or `oct-HSM` keys into it using the `importkey` tool placed under `/tools/importkey` after updating the `importkeyconfig.json` with the required information as discussed in the tools' readme file. For instance, the hostdata claim value needs to be set to the hash digest of the security policy, which can be obtained by executing the following command: +### Step by Step Example -`go run /tools/securitypolicydigest/main.go -p ` +Here is an example of running skr sidecar on confidential ACI. The MAA endpoint is the value of env var [`SkrClientMAAEndpoint`](aci-arm-template.json?plain=1#L55). +The managed HSM instance endpoint corresponds to [`SkrClientAKVEndpoint`](aci-arm-template.json?plain=1#L59). We will also import a key into managed HSM under the name [`doc-sample-key-release`](aci-arm-template.json?plain=1#L64) -And the AAD token with permission to AKV/mHSM can be obtained with the following command: -`az account get-access-token --resource https://managedhsm.azure.net` +#### 1. Obtain an Attestation Endpoint -Once the `importkeyconfig.json` is updated, execute the following command: +If you don't already have a valid attestation endpoint, create a [Microsoft Azure Attestation](https://learn.microsoft.com/en-us/azure/attestation/overview) endpoint to author the attestation token and run the following command to get the endpoint value: -`cd /tools/importkey` +``` +az attestation show --name "" --resource-group "" +``` -`go run main.go -c /examples/skr/importkeyconfig.json +Copy the AttestURI endpoint value (sans https://) to the [Attestation Authority endpoint](importkeyconfig.json#L6) in `importkeyconfig.json` and to [SkrClientMAAEndpoint](aci-arm-template.json#L56) and [AttestClientMAAEndpoint](aci-arm-template.json#L106) in `aci-arm-template.json`. -### Deployment -The `aci-arm-template.json` provides an ARM template which can be parametrized using the security policy obtained above, the registry name (and credentials if private), the user-assigned managed identity, and the URIs to the endpoints required by the sidecar and test containers, discussed above. -### Step by step example -Here is an example of running skr sidecar on confidential ACI. The MAA endpoint is the value of env var [`SkrClientMAAEndpoint`](aci-arm-template.json?plain=1#L55). -The managed HSM instance endpoint corresponds to [`SkrClientAKVEndpoint`](aci-arm-template.json?plain=1#L59). We will also import a key into managed HSM under the name [`doc-sample-key-release`](aci-arm-template.json?plain=1#L64) +#### 2. Generate User Managed Identity + +The user needs to instantiate an Azure Key Vault resource that supports storing keys in an HSM: a [Premium vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) or an [MHSM resource](https://docs.microsoft.com/en-us/azure/key-vault/managed-hsm/overview). + +After setting up an Azure Key Vault resource, generate a user-assigned managed identity that will be attached to the container group so that the containers have the correct access permissions to Azure services and resources. The managed identity needs *Key Vault Crypto Officer* and *Key Vault Crypto User* roles if using AKV key vault or *Managed HSM Crypto Officer* and *Managed HSM Crypto User* roles for /keys if using AKV managed HSM. More information about creating identities can be found [here.](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/) -**Preparation**: +If you already have a user-assigned managed identity with the appropriate access permissions, run the following command to list the managed identities for a resource group: -Update the following [ARM template managed identity](aci-arm-template.json?plain=1#L22) that has the correct role based access. The managed identity needs *Key Vault Crypto Officer* and *Key Vault Crypto User* roles if using AKV key vault. *Managed HSM Crypto Officer* and *Managed HSM Crypto User* roles for /keys if using AKV managed HSM. Follow [Managed identity](#managed-identity) for detailed instruction. Update the [image registry credentials](aci-arm-template.json?plain=1#L123) on the ARM template in order to access the private container registry. The credential could be either a managed identity or username/password. In our case, you do not need this section because we are using a public image. +``` +az identity list -g +``` + +Or you can use the following command if you know the name of the managed identity and the resource group: + +``` +az identity show -g -n +``` +Replace [managed-identity-with-right-permissions-to-key-vault](aci-arm-template.json#:~:text=%22%3Cmanaged%2Didentity%2Dwith%2Dright%2Dpermissions%2Dto%2Dkey%2Dvault%3E%22) of `aci-arm-template.json` with the identity ID. -**Generate security policy**: -Run the following command to generate the security policy and make sure you include the `--debug-mode` option so that the policy allows users to shell into the container. +#### 3. Populate Image Registry Credentials - az confcom acipolicygen -a aci-arm-template.json --debug-mode +Update the [image registry credentials](aci-arm-template.json?plain=1#L123) on the ARM template in order to access a private container registry. The credential could be either a managed identity or username/password. This section is not needed for public images. -**Key import**: +#### 4. Obtain the AAD token + +The AAD token with permission to AKV/mHSM can be obtained with the following command: + +``` +az account get-access-token --resource https://managedhsm.azure.net +``` + +Replace [AAD token](importkeyconfig.json#L11) in `importkeyconfig.json` and [SkrClientAKVEndpoint](aci-arm-template.json#L60) in `aci-arm-template.json` with the output accessToken. - git clone git@github.com:microsoft/confidential-sidecar-containers.git -Use the tools in this repository to obtain the security hash of the generated policy and to import key into the AKV/mHSM. Copy the value of the generated `ccePolicy` from the ARM template. At the root of the clone repo, obtain the security hash of the policy by running: +#### 5. Fill in Key Information - go run tools/securitypolicydigest/main.go -p ccePolicyValue +After setting up an [Azure Key Vault resource](#import-key), fill in the `importkeyconfig.json` file with the name of the key to be created and imported into the key vault [Key name](importkeyconfig.json#L3). + +Additionally, fill in the optional [key derivation](importkeyconfig.json#L14) for RSA keys and [Key type: `RSA-HSM` or `oct-HSM`](importkeyconfig.json#L4) fields or remove these fields from the `importkeyconfig.json` file. + +Copy the key name into [SkrClientKID](aci-arm-template.json#L64) in the `aci-arm-template.json`. + + +#### 6. Generate Security Policy + +At this point, the `aci-arm-template.json` file should be filled out except for the `ccepolicy` field. After installing the [Azure `confcom` CLI extension](#policy-generation), run the following command to generate the security policy and include the `--debug-mode` option so that the policy allows users to shell into the container. + +``` +az confcom acipolicygen -a aci-arm-template.json --debug-mode +``` + +This should prompt you to automatically populate the [cce policy](aci-arm-template.json#L142) field of `aci-arm-template.json.` + + +#### 7. Generate Security Policy Hash + +Use the tools in this repository to obtain the security hash of the generated policy and the key to be imported into AKV/mHSM. Start by cloning the repository locally: + +``` +git clone git@github.com:microsoft/confidential-sidecar-containers.git +``` + +Copy the value of the generated `ccePolicy` from the ARM template and at the root of the cloned repo, obtain the sha256 hash of the security policy by running: + +``` +go run tools/securitypolicydigest/main.go -p ccePolicyValue +``` At the end of the command output, you should see something similar to the following: inittimeData sha-256 digest **aaa4e****cc09d** -**Obtain the AAD token**: +Copy the digest and replace the [hash-digest-of-the-security-policy](importkeyconfig.json#L22) string of the `importkeyconfig.json` file. - az account get-access-token --resource https://managedhsm.azure.net +#### 8. Import Keys into mHSM/AKV -Fill in the `keyimportconfig.json` file with the following information: +Once the key vault resource is ready and the `importkeyconfig.json` file is completely filled out, the user can import `RSA-HSM` or `oct-HSM` keys into it using the `importkey` tool placed under `/tools/importkey` as discussed in the tools' [readme file](https://github.com/microsoft/confidential-sidecar-containers/tree/main/tools/importkey). -[imported key name](importkeyconfig.json?plain=1#L3)
-[MAA endpoint](importkeyconfig.json?plain=1#L6)
-[mHSM endpoint](importkeyconfig.json?plain=1#L9)
-[AAD token](importkeyconfig.json?plain=1#L11)
-[security hash of policy](importkeyconfig.json?plain=1#L22)
+A fake encryption key is used in the command below to see the key get released. To import the key into AKV/mHSM, use the following command: -Import the key into mHSM with the following command. I'm using a fake encryption key here because I just want to see the key gets released. Upon successful import completion, you should see something similar to the following: +``` +go run /tools/importkey/main.go -c importkeyconfig.json -kh encryptionKey +``` - go run /tools/importkey/main.go -c keyimportconfig.json -kh encryptionKey +Upon successful import completion, you should see something similar to the following: ``` [34 71 33 117 113 25 191 84 199 236 137 166 201 103 83 20 203 233 66 236 121 110 223 2 122 99 106 20 22 212 49 224] @@ -101,16 +139,16 @@ https://accmhsm.managedhsm.azure.net/keys/doc-sample-key-release/8659****0cdff08 {"version":"0.2","anyOf":[{"authority":"https://sharedeus2.eus2.test.attest.azure.net","allOf":[{"claim":"x-ms-sevsnpvm-hostdata","equals":"aaa7***7cc09d"},{"claim":"x-ms-compliance-status","equals":"azure-compliant-uvm"},{"claim":"x-ms-sevsnpvm-is-debuggable","equals":"false"}]}]} ``` -In this case, I use the following command to verify my key has been successfully imported: +In this case, use the following commands to verify the key has been successfully imported: ``` -az account set --subscription "my subscription" -az keyvault key list --hsm-name mhsm-name -o table +az account set --subscription "" +az keyvault key list --hsm-name -o table ``` -**Deployment**: +#### 9. Deployment -Go to Azure portal and click on `deploy a custom template`, then click `Build your own template in the editor`. By this time, the `ccePolicy` field should have been generated and filled in the previous `az confcom acipolicygen` command. Copy and paste the ARM template into the field start a deployment. Once deployment is done, verify the key has been successful released, shell into the `skr-sidecar-container` container and see the log.txt and you should see the following log message: +Go to Azure portal and click on `deploy a custom template`, then click `Build your own template in the editor`. By this time, the `aci-arm-template.json` file should be completely filled out. Copy and paste the ARM template into the field start a deployment. Once deployment is done, to verify the key has been successful released, shell into the `skr-sidecar-container` container and see the log.txt and you should see the following log message: ``` level=debug msg=Releasing key blob: {doc-sample-key-release} diff --git a/examples/skr/importkeyconfig.json b/examples/skr/importkeyconfig.json index 43a64d3b..b3063200 100644 --- a/examples/skr/importkeyconfig.json +++ b/examples/skr/importkeyconfig.json @@ -3,12 +3,12 @@ "kid": "", "kty": "", "authority": { - "endpoint": "" + "endpoint": "" }, "akv": { - "endpoint": "key-vault-endpoint", + "endpoint": "", "api_version": "api-version=7.3-preview", - "bearer_token": "" } }, "key_derivation": { diff --git a/pkg/attest/attest.go b/pkg/attest/attest.go index 4a065555..5dd82c48 100644 --- a/pkg/attest/attest.go +++ b/pkg/attest/attest.go @@ -6,6 +6,7 @@ package attest import ( "encoding/base64" "encoding/hex" + "fmt" "io/ioutil" "os" @@ -15,14 +16,63 @@ import ( "github.com/sirupsen/logrus" ) +// CertState contains information about the certificate cache service +// that provides access to the certificate chain required upon attestation +type CertState struct { + CertFetcher CertFetcher `json:"cert_cache"` + Tcbm uint64 `json:"tcbm"` +} + +func GetSNPReport(securityPolicy string, runtimeDataBytes []byte) ([]byte, []byte, error) { + // check if sev device exists on the platform; if not fetch fake snp report + fetchRealSNPReport := true + if _, err := os.Stat("/dev/sev"); errors.Is(err, os.ErrNotExist) { + // dev/sev doesn't exist, check dev/sev-guest + if _, err := os.Stat("/dev/sev-guest"); errors.Is(err, os.ErrNotExist) { + // dev/sev-guest doesn't exist + fetchRealSNPReport = false + } + } + + inittimeDataBytes, err := base64.StdEncoding.DecodeString(securityPolicy) + if err != nil { + return nil, nil, errors.Wrap(err, "decoding policy from Base64 format failed") + } + logrus.Debugf(" inittimeDataBytes: %v", inittimeDataBytes) + + SNPReportBytes, err := FetchSNPReport(fetchRealSNPReport, runtimeDataBytes, inittimeDataBytes) + if err != nil { + return nil, nil, errors.Wrapf(err, "fetching snp report failed") + } + + if common.GenerateTestData { + ioutil.WriteFile("snp_report.bin", SNPReportBytes, 0644) + } + + logrus.Debugf(" SNPReportBytes: %v", SNPReportBytes) + return SNPReportBytes, inittimeDataBytes, nil +} + +func (certState *CertState) RefreshCertChain(SNPReport SNPAttestationReport) ([]byte, error) { + // TCB values not the same, try refreshing cert first + vcekCertChain, thimTcbm, err := certState.CertFetcher.GetCertChain(SNPReport.ChipID, SNPReport.ReportedTCB) + if err != nil { + return nil, errors.Wrap(err, "refreshing CertChain failed") + } + certState.Tcbm = thimTcbm + return vcekCertChain, nil +} + // RawAttest returns the raw attestation report in hex string format func RawAttest(inittimeDataBytes []byte, runtimeDataBytes []byte) (string, error) { // check if sev device exists on the platform; if not fetch fake snp report - var fetchRealSNPReport bool - if _, err := os.Stat("/dev/sev"); os.IsNotExist(err) { - fetchRealSNPReport = false - } else { - fetchRealSNPReport = true + fetchRealSNPReport := true + if _, err := os.Stat("/dev/sev"); errors.Is(err, os.ErrNotExist) { + // dev/sev doesn't exist, check dev/sev-guest + if _, err := os.Stat("/dev/sev-guest"); errors.Is(err, os.ErrNotExist) { + // dev/sev-guest doesn't exist + fetchRealSNPReport = false + } } SNPReportBytes, err := FetchSNPReport(fetchRealSNPReport, runtimeDataBytes, inittimeDataBytes) @@ -47,52 +97,58 @@ func RawAttest(inittimeDataBytes []byte, runtimeDataBytes []byte) (string, error // (E) runtime data: for example it may be a wrapping key blob that has been hashed during the attestation report // // retrieval and has been reported by the PSP in the attestation report as REPORT DATA -func Attest(maa MAA, runtimeDataBytes []byte, uvmInformation common.UvmInformation) (string, error) { +func (certState *CertState) Attest(maa MAA, runtimeDataBytes []byte, uvmInformation common.UvmInformation) (string, error) { // Fetch the attestation report - - // check if sev device exists on the platform; if not fetch fake snp report - var fetchRealSNPReport bool - if _, err := os.Stat("/dev/sev"); os.IsNotExist(err) { - fetchRealSNPReport = false - } else { - fetchRealSNPReport = true - } - - inittimeDataBytes, err := base64.StdEncoding.DecodeString(uvmInformation.EncodedSecurityPolicy) - if err != nil { - return "", errors.Wrap(err, "decoding policy from Base64 format failed") - } - logrus.Debugf(" inittimeDataBytes: %v", inittimeDataBytes) - - SNPReportBytes, err := FetchSNPReport(fetchRealSNPReport, runtimeDataBytes, inittimeDataBytes) + SNPReportBytes, inittimeDataBytes, err := GetSNPReport(uvmInformation.EncodedSecurityPolicy, runtimeDataBytes) if err != nil { - return "", errors.Wrapf(err, "fetching snp report failed") + return "", errors.Wrapf(err, "failed to retrieve attestation report") } - /* - TODO: - - At this point check that the TCB of the cert chain matches that reported so we fail early or - fetch fresh certs by other means. - - */ - - if common.GenerateTestData { - ioutil.WriteFile("snp_report.bin", SNPReportBytes, 0644) - } - - logrus.Debugf(" SNPReportBytes: %v", SNPReportBytes) - // Retrieve the certificate chain using the chip identifier and platform version // fields of the attestation report var SNPReport SNPAttestationReport - if err := SNPReport.DeserializeReport(SNPReportBytes); err != nil { + if err = SNPReport.DeserializeReport(SNPReportBytes); err != nil { return "", errors.Wrapf(err, "failed to deserialize attestation report") } - vcekCertChain := []byte(uvmInformation.CertChain) - - /* TODO: to support use outside of Azure add code to fetch the AMD certs here */ + logrus.Debugf("SNP Report Reported TCB: %d\nCert Chain TCBM Value: %d\n", SNPReport.ReportedTCB, certState.Tcbm) + + // At this point check that the TCB of the cert chain matches that reported so we fail early or + // fetch fresh certs by other means. + var vcekCertChain []byte + if SNPReport.ReportedTCB != certState.Tcbm { + // TCB values not the same, try refreshing cert cache first + vcekCertChain, err = certState.RefreshCertChain(SNPReport) + if err != nil { + return "", err + } + + if SNPReport.ReportedTCB != certState.Tcbm { + // TCB values still don't match, try retrieving the SNP report again + SNPReportBytes, inittimeDataBytes, err = GetSNPReport(uvmInformation.EncodedSecurityPolicy, runtimeDataBytes) + if err != nil { + return "", errors.Wrapf(err, "failed to retrieve new attestation report") + } + + if err = SNPReport.DeserializeReport(SNPReportBytes); err != nil { + return "", errors.Wrapf(err, "failed to deserialize new attestation report") + } + + // refresh certs again + vcekCertChain, err = certState.RefreshCertChain(SNPReport) + if err != nil { + return "", err + } + + // if no match after refreshing certs and attestation report, fail + if SNPReport.ReportedTCB != certState.Tcbm { + return "", errors.New(fmt.Sprintf("SNP reported TCB value: %d doesn't match Certificate TCB value: %d", SNPReport.ReportedTCB, certState.Tcbm)) + } + } + } else { + certString := uvmInformation.InitialCerts.VcekCert + uvmInformation.InitialCerts.CertificateChain + vcekCertChain = []byte(certString) + } uvmReferenceInfoBytes, err := base64.StdEncoding.DecodeString(uvmInformation.EncodedUvmReferenceInfo) diff --git a/pkg/attest/attest_test.go b/pkg/attest/attest_test.go index c84c8e07..e6eadbdd 100644 --- a/pkg/attest/attest_test.go +++ b/pkg/attest/attest_test.go @@ -168,23 +168,26 @@ func Test_SNP(t *testing.T) { } } -func Test_CertCache(t *testing.T) { +func Test_CertFetcher(t *testing.T) { TestSNPReportBytes, _ := hex.DecodeString("01000000010000001f00030000000000010000000000000000000000000000000200000000000000000000000000000000000000010000000000000000000031010000000000000000000000000000007ab000a323b3c873f5b81bbe584e7c1a26bcf40dc27e00f8e0d144b1ed2d14f10000000000000000000000000000000000000000000000000000000000000000b579c7d6b89f3914659abe09a004a58a1e77846b65bbdac9e29bd8f2f31b31af445a5dd40f76f71ecdd73117f1d592a38c19f1b6eee8658fbf8ff1b37f603c38929896b1cc813583bbfb21015b7aa66dd188ac79386022aec7aa4e72a7e87b0a8e0e8009183334bb0fe4f97ed89436f360b3644cd8382c7a14531a87b81a8f360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e880add9a31077e5e8f3568b4c4451f0fea4372f66e3df3c0ca3ba26f447db2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000031000000000000000000000000000000000000000000000000e6c86796cd44b0bc6b7c0d4fdab33e2807e14b5fc4538b3750921169d97bcf4447c7d3ab2a7c25f74c1641e2885c1011d025cc536f5c9a2504713136c7877f48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247c7525e84623db9868fccf00faab22229d60aaa380213108f8875011a8f456231c5371277cc706733f4a483338fb59000000000000000000000000000000000000000000000000ed8c62254022f64630ebf97d66254dee04f708ecbe22387baf8018752fadc2b763f64bded65c94a325b6b9f22ebbb0d80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") var TestSNPReport SNPAttestationReport if err := TestSNPReport.DeserializeReport(TestSNPReportBytes); err != nil { t.Fatalf("failed to deserialize attestation report") } + ValidEndpointType := "AzCache" ValidEndpoint := "americas.test.acccache.azure.net" ValidTEEType := "SevSnpVM" ValidAPIVersion := "api-version=2020-10-15-preview" ValidChipID := TestSNPReport.ChipID ValidPlatformVersion := TestSNPReport.PlatformVersion + // To-Do: add tests for local THIM endpoints + type testcase struct { name string - certCache CertCache + certFetcher CertFetcher chipID string platformVersion uint64 @@ -195,13 +198,14 @@ func Test_CertCache(t *testing.T) { } testcases := []*testcase{ - // CertCache_Success passes the testing if it does not receive an error and the certchain mathces the expected content + // CertFetcher_Success passes the testing if it does not receive an error and the certchain mathces the expected content { - name: "CertCache_Success", - certCache: CertCache{ - Endpoint: ValidEndpoint, - TEEType: ValidTEEType, - APIVersion: ValidAPIVersion, + name: "CertFetcher_Success", + certFetcher: CertFetcher{ + EndpointType: ValidEndpointType, + Endpoint: ValidEndpoint, + TEEType: ValidTEEType, + APIVersion: ValidAPIVersion, }, chipID: ValidChipID, platformVersion: ValidPlatformVersion, @@ -209,50 +213,67 @@ func Test_CertCache(t *testing.T) { expectErr: false, expectedContent: "-----BEGIN CERTIFICATE-----\nMIIFTDCCAvugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA\noRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATB7MRQwEgYD\nVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDASBgNVBAcMC1NhbnRhIENs\nYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5jZWQgTWljcm8gRGV2aWNl\nczESMBAGA1UEAwwJU0VWLU1pbGFuMB4XDTIyMDIwOTIxMDYwMFoXDTI5MDIwOTIx\nMDYwMFowejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYD\nVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2Vk\nIE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WQ0VLMHYwEAYHKoZIzj0CAQYF\nK4EEACIDYgAEv8TQmmW1BdCCyeVpj9C6Y2IW3rv3swTxZJNHBYGd9hRvWAyan82O\nCrX4tPiRzxmFRzY7e1RcGWYDhgQw2p8UokXGVz9fKNJKqBMdvEQf7vGUocRHhM83\nJTJrxmq0QxBXo4IBFjCCARIwEAYJKwYBBAGceAEBBAMCAQAwFwYJKwYBBAGceAEC\nBAoWCE1pbGFuLUEwMBEGCisGAQQBnHgBAwEEAwIBADARBgorBgEEAZx4AQMCBAMC\nAQAwEQYKKwYBBAGceAEDBAQDAgEAMBEGCisGAQQBnHgBAwUEAwIBADARBgorBgEE\nAZx4AQMGBAMCAQAwEQYKKwYBBAGceAEDBwQDAgEAMBEGCisGAQQBnHgBAwMEAwIB\nADARBgorBgEEAZx4AQMIBAMCATEwTQYJKwYBBAGceAEEBEDmyGeWzUSwvGt8DU/a\nsz4oB+FLX8RTizdQkhFp2XvPREfH06sqfCX3TBZB4ohcEBHQJcxTb1yaJQRxMTbH\nh39IMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0B\nAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBA4ICAQBbwYA2balTK5o3cPAQarZT\ngIRuX4nXWJDHpDJ2YikPK2Te48O49i9r5+1Voza/JauOI+u8h3Rg3fZSCbemLutw\nDzp6vRv9LJOKhy20HcBl2xYKL1zDX2MTnxg2PrHEE++PeMVf4nbhZkbnyfOXE9Ui\nkatdAdpjpSVhIe78c+IipM1rI1weCdRckmPidZHrJvIARYDyUZjHWoqV28O0reaE\nGXPO7k4VyfRh9hBYhwaFyVsAc1yhdU8Fi1jVzXwnyh7xlGfoakIC1oTiv2OF8w8C\nTKir+JXWqndF3BVNK4vMpdCoXO76KELA+gCPOsbbOQ2LMo7ZlwrwrXElFONwyQdi\ncM56hZ7pdGeGe4DoVWQMR/0LJ3b53r90zrvK+2rT65TffeYkt9OUhn5EkCYEGwOI\nb0/f41VTcN2eyh3OUXhucuCsNGCkqSp4sT79W9TOwUHB2oCricjMoLo9nNmdjIWj\nrA0cAae74AbSMmrVEavEdm10zb2lvFshYp5L4reqOqs5DKM+ksKp1u/SsiELYu+i\n3SMDsP/+3eM+7MgJj05rRaGSoYm/mbTUtDl1zZTRVPaW7eQmJBrzClHTd47N+hzD\n/2rJ4hcJnQLVSyYeNxi6zWAv83B95elvyD35CFh8M0L8GjHu+iww3/i6d5rSPLEp\n94cYfdFPuj/5HEjXDN5eww==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGiTCCBDigAwIBAgIDAQABMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC\nBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS\nBgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg\nQ2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp\nY2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTgyNDIwWhcNNDUxMDIy\nMTgyNDIwWjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS\nBgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j\nZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJU0VWLU1pbGFuMIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEAnU2drrNTfbhNQIllf+W2y+ROCbSzId1aKZft\n2T9zjZQOzjGccl17i1mIKWl7NTcB0VYXt3JxZSzOZjsjLNVAEN2MGj9TiedL+Qew\nKZX0JmQEuYjm+WKksLtxgdLp9E7EZNwNDqV1r0qRP5tB8OWkyQbIdLeu4aCz7j/S\nl1FkBytev9sbFGzt7cwnjzi9m7noqsk+uRVBp3+In35QPdcj8YflEmnHBNvuUDJh\nLCJMW8KOjP6++Phbs3iCitJcANEtW4qTNFoKW3CHlbcSCjTM8KsNbUx3A8ek5EVL\njZWH1pt9E3TfpR6XyfQKnY6kl5aEIPwdW3eFYaqCFPrIo9pQT6WuDSP4JCYJbZne\nKKIbZjzXkJt3NQG32EukYImBb9SCkm9+fS5LZFg9ojzubMX3+NkBoSXI7OPvnHMx\njup9mw5se6QUV7GqpCA2TNypolmuQ+cAaxV7JqHE8dl9pWf+Y3arb+9iiFCwFt4l\nAlJw5D0CTRTC1Y5YWFDBCrA/vGnmTnqG8C+jjUAS7cjjR8q4OPhyDmJRPnaC/ZG5\nuP0K0z6GoO/3uen9wqshCuHegLTpOeHEJRKrQFr4PVIwVOB0+ebO5FgoyOw43nyF\nD5UKBDxEB4BKo/0uAiKHLRvvgLbORbU8KARIs1EoqEjmF8UtrmQWV2hUjwzqwvHF\nei8rPxMCAwEAAaOBozCBoDAdBgNVHQ4EFgQUO8ZuGCrD/T1iZEib47dHLLT8v/gw\nHwYDVR0jBBgwFoAUhawa0UP3yKxV1MUdQUir1XhK1FMwEgYDVR0TAQH/BAgwBgEB\n/wIBADAOBgNVHQ8BAf8EBAMCAQQwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cHM6Ly9r\nZHNpbnRmLmFtZC5jb20vdmNlay92MS9NaWxhbi9jcmwwRgYJKoZIhvcNAQEKMDmg\nDzANBglghkgBZQMEAgIFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgIFAKID\nAgEwowMCAQEDggIBAIgeUQScAf3lDYqgWU1VtlDbmIN8S2dC5kmQzsZ/HtAjQnLE\nPI1jh3gJbLxL6gf3K8jxctzOWnkYcbdfMOOr28KT35IaAR20rekKRFptTHhe+DFr\n3AFzZLDD7cWK29/GpPitPJDKCvI7A4Ug06rk7J0zBe1fz/qe4i2/F12rvfwCGYhc\nRxPy7QF3q8fR6GCJdB1UQ5SlwCjFxD4uezURztIlIAjMkt7DFvKRh+2zK+5plVGG\nFsjDJtMz2ud9y0pvOE4j3dH5IW9jGxaSGStqNrabnnpF236ETr1/a43b8FFKL5QN\nmt8Vr9xnXRpznqCRvqjr+kVrb6dlfuTlliXeQTMlBoRWFJORL8AcBJxGZ4K2mXft\nl1jU5TLeh5KXL9NW7a/qAOIUs2FiOhqrtzAhJRg9Ij8QkQ9Pk+cKGzw6El3T3kFr\nEg6zkxmvMuabZOsdKfRkWfhH2ZKcTlDfmH1H0zq0Q2bG3uvaVdiCtFY1LlWyB38J\nS2fNsR/Py6t5brEJCFNvzaDky6KeC4ion/cVgUai7zzS3bGQWzKDKU35SqNU2WkP\nI8xCZ00WtIiKKFnXWUQxvlKmmgZBIYPe01zD0N8atFxmWiSnfJl690B9rJpNR/fI\najxCW3Seiws6r1Zm+tCuVbMiNtpS9ThjNX4uve5thyfE2DgoxRFvY1CsoF5M\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC\nBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS\nBgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg\nQ2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp\nY2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy\nMTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS\nBgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j\nZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg\nW41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta\n1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2\nSzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0\n60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05\ngmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg\nbKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs\n+gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi\nQi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ\neTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18\nfHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j\nWhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI\nrFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG\nKWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG\nSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI\nAWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel\nETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw\nSTjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK\ndHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq\nzT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp\nKGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e\npmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq\nHnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh\n3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn\nJZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH\nCViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4\nAFZEAwoKCQ==\n-----END CERTIFICATE-----\n", }, - // CertCache_Invalid_PlatformVersion passes if the uri associated with the requested certificate was not found + // CertFetcher_Invalid_PlatformVersion passes if the uri associated with the requested certificate was not found { - name: "CertCache_Invalid_PlatformVersion", - certCache: CertCache{ - Endpoint: ValidEndpoint, - TEEType: ValidTEEType, - APIVersion: ValidAPIVersion, + name: "CertFetcher_Invalid_PlatformVersion", + certFetcher: CertFetcher{ + EndpointType: ValidEndpointType, + Endpoint: ValidEndpoint, + TEEType: ValidTEEType, + APIVersion: ValidAPIVersion, }, chipID: ValidChipID, platformVersion: 0xdeadbeef, - expectedError: errors.Errorf("http response status equal to 404 Not Found"), + expectedError: errors.Errorf("pulling certchain response from AzCache get request failed: http response status equal to 404 Not Found"), expectErr: true, }, - // CertCache_Invalid_ChipID passes if the uri associated with the requested certificate was not found + // CertFetcher_Invalid_ChipID passes if the uri associated with the requested certificate was not found { - name: "CertCache_Invalid_ChipID", - certCache: CertCache{ - Endpoint: ValidEndpoint, - TEEType: ValidTEEType, - APIVersion: ValidAPIVersion, + name: "CertFetcher_Invalid_ChipID", + certFetcher: CertFetcher{ + EndpointType: ValidEndpointType, + Endpoint: ValidEndpoint, + TEEType: ValidTEEType, + APIVersion: ValidAPIVersion, }, chipID: "0xdeadbeef", platformVersion: ValidPlatformVersion, - expectedError: errors.Errorf("http response status equal to 404 Not Found"), + expectedError: errors.Errorf("pulling certchain response from AzCache get request failed: http response status equal to 404 Not Found"), + expectErr: true, + }, + // CertFetcher_Invalid_TEEType passes if the uri associated with the requested tee_type and certificate was not found + { + name: "CertFetcher_Invalid_TEEType", + certFetcher: CertFetcher{ + EndpointType: ValidEndpointType, + Endpoint: ValidEndpoint, + TEEType: "InvalidTEEType", + APIVersion: ValidAPIVersion, + }, + chipID: ValidChipID, + platformVersion: ValidPlatformVersion, + expectedError: errors.Errorf("pulling certchain response from AzCache get request failed: http response status equal to 404 Not Found"), expectErr: true, }, - // CertCache_Invalid_TEEType passes if the uri associated with the requested tee_type and certificate was not found + // CertFetcher_Invalid_EndpointType passes if the uri associated with the requested tee_type and certificate was not found { - name: "CertCache_Invalid_TEEType", - certCache: CertCache{ - Endpoint: ValidEndpoint, - TEEType: "InvalidTEEType", - APIVersion: ValidAPIVersion, + name: "CertFetcher_Invalid_EndpointType", + certFetcher: CertFetcher{ + EndpointType: "InvalidEndpointType", + Endpoint: ValidEndpoint, + TEEType: ValidTEEType, + APIVersion: ValidAPIVersion, }, chipID: ValidChipID, platformVersion: ValidPlatformVersion, - expectedError: errors.Errorf("http response status equal to 404 Not Found"), + expectedError: errors.Errorf("invalid endpoint type: InvalidEndpointType"), expectErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - certchain, err := tc.certCache.retrieveCertChain(tc.chipID, tc.platformVersion) + certchain, _, err := tc.certFetcher.retrieveCertChain(tc.chipID, tc.platformVersion) if tc.expectErr { if err == nil { @@ -313,28 +334,30 @@ func Test_MAA(t *testing.T) { t.Fatalf("failed to deserialize attestation report") } - ValidMAAEndpoint := "sharedeus2.eus2.test.attest.azure.net" + ValidMAAEndpoint := "sharedeus2.eus2.test.attest.azure.net" ValidTEEType := "SevSnpVM" ValidMAAAPIVersion := "api-version=2020-10-01" - certCache := CertCache{ - Endpoint: "americas.test.acccache.azure.net", - TEEType: "SevSnpVM", - APIVersion: "api-version=2020-10-15-preview", + certFetcher := CertFetcher{ + EndpointType: "AzCache", + Endpoint: "americas.test.acccache.azure.net", + TEEType: "SevSnpVM", + APIVersion: "api-version=2020-10-15-preview", } - ValidCertChain, err := certCache.retrieveCertChain(TestSNPReport.ChipID, TestSNPReport.PlatformVersion) + ValidCertChain, _, err := certFetcher.retrieveCertChain(TestSNPReport.ChipID, TestSNPReport.PlatformVersion) if err != nil { t.Fatalf("retrieving cert chain failed") } - ProductionCertCache := CertCache{ - Endpoint: "americas.acccache.azure.net", - TEEType: "SevSnpVM", - APIVersion: "api-version=2020-10-15-preview", + ProductionCertCache := CertFetcher{ + EndpointType: "AzCache", + Endpoint: "americas.acccache.azure.net", + TEEType: "SevSnpVM", + APIVersion: "api-version=2020-10-15-preview", } - ProductionValidCertChain, err := ProductionCertCache.retrieveCertChain(ProductionTestSNPReport.ChipID, ProductionTestSNPReport.PlatformVersion) + ProductionValidCertChain, _, err := ProductionCertCache.retrieveCertChain(ProductionTestSNPReport.ChipID, ProductionTestSNPReport.PlatformVersion) if err != nil { t.Fatalf("retrieving cert chain failed") } diff --git a/pkg/attest/certcache.go b/pkg/attest/certcache.go index fbf8439d..571579b5 100644 --- a/pkg/attest/certcache.go +++ b/pkg/attest/certcache.go @@ -16,6 +16,8 @@ import ( "fmt" "strconv" + "crypto/x509" + "github.com/Microsoft/confidential-sidecar-containers/pkg/common" "github.com/pkg/errors" ) @@ -24,68 +26,184 @@ const ( AzureCertCacheRequestURITemplate = "https://%s/%s/certificates/%s/%s?%s" AmdVCEKRequestURITemplate = "https://%s/%s/%s?ucodeSPL=%d&snpSPL=%d&teeSPL=%d&blSPL=%d" AmdCertChainRequestURITemplate = "https://%s/%s/cert_chain" + LocalTHIMUriTemplate = "https://%s" // To-Do update once we know what this looks like +) + +const ( + BlSplTcbmByteIndex = 0 + TeeSplTcbmByteIndex = 1 + TcbSpl_4TcbmByteIndex = 2 + TcbSpl_5TcbmByteIndex = 3 + TcbSpl_6TcbmByteIndex = 4 + TcbSpl_7TcbmByteIndex = 5 + SnpSplTcbmByteIndex = 6 + UcodeSplTcbmByteIndex = 7 ) -// CertCache contains information about the certificate cache service +// can't find any documentation on why the 3rd byte of the Certificate.Extensions byte array is the one that matters +// all the byte arrays for the tcb values are of length 3 and fit the format [2 1 IMPORTANT_VALUE] +const x509CertExtensionsValuePos = 2 + +// parses the cached CertChain and returns the VCEK leaf certificate +// Subject of the (x509) VCEK certificate (CN=SEV-VCEK) +func GetVCEKFromCertChain(certChain []byte) (*x509.Certificate, error) { + currChain := certChain + // iterate through the certificates in the chain + for len(currChain) > 0 { + var block *pem.Block + block, currChain = pem.Decode(currChain) + if block.Type == "CERTIFICATE" { + certificate, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + // check if this is the correct certificate + if certificate.Subject.CommonName == "SEV-VCEK" { + return certificate, nil + } + } + } + return nil, errors.New("No certificate chain found") +} + +// parses the VCEK certificate to return the TCB version +// fields reprocessed back into a uint64 to compare against the `ReportedTCB` in the fetched attestation report +func ParseVCEK(certChain []byte) ( /*tcbVersion*/ uint64, error) { + vcekCert, err := GetVCEKFromCertChain(certChain) + if err != nil { + return 0, err + } + + tcbValues := make([]byte, 8) // TCB version is 8 bytes + // parse extensions to update the THIM URL and get TCB Version + for _, ext := range vcekCert.Extensions { + switch ext.Id.String() { + + // blSPL + case "1.3.6.1.4.1.3704.1.3.1": + tcbValues[BlSplTcbmByteIndex] = ext.Value[x509CertExtensionsValuePos] + // teeSPL + case "1.3.6.1.4.1.3704.1.3.2": + tcbValues[TeeSplTcbmByteIndex] = ext.Value[x509CertExtensionsValuePos] + // spl_4 + case "1.3.6.1.4.1.3704.1.3.4": + tcbValues[TcbSpl_4TcbmByteIndex] = ext.Value[x509CertExtensionsValuePos] + // spl_5 + case "1.3.6.1.4.1.3704.1.3.5": + tcbValues[TcbSpl_5TcbmByteIndex] = ext.Value[x509CertExtensionsValuePos] + // spl_6 + case "1.3.6.1.4.1.3704.1.3.6": + tcbValues[TcbSpl_6TcbmByteIndex] = ext.Value[x509CertExtensionsValuePos] + // spl_7 + case "1.3.6.1.4.1.3704.1.3.7": + tcbValues[TcbSpl_7TcbmByteIndex] = ext.Value[x509CertExtensionsValuePos] + // snpSPL + case "1.3.6.1.4.1.3704.1.3.3": + tcbValues[SnpSplTcbmByteIndex] = ext.Value[x509CertExtensionsValuePos] + // ucodeSPL + case "1.3.6.1.4.1.3704.1.3.8": + tcbValues[UcodeSplTcbmByteIndex] = ext.Value[x509CertExtensionsValuePos] + } + } + + return binary.LittleEndian.Uint64(tcbValues), nil +} + +// CertFetcher contains information about the certificate cache service // that provides access to the certificate chain required upon attestation -type CertCache struct { - AMD bool `json:"amd,omitempty"` - Endpoint string `json:"endpoint"` - TEEType string `json:"tee_type,omitempty"` - APIVersion string `json:"api_version,omitempty"` +type CertFetcher struct { + EndpointType string `json:"endpoint_type"` // AMD, AzCache, LocalTHIM + Endpoint string `json:"endpoint"` + TEEType string `json:"tee_type,omitempty"` + APIVersion string `json:"api_version,omitempty"` } // retrieveCertChain interacts with the cert cache service to fetch the cert chain of the // chip identified by chipId running firmware identified by reportedTCB. These attributes // are retrived from the attestation report. -func (certCache CertCache) retrieveCertChain(chipID string, reportedTCB uint64) ([]byte, error) { +// Returns the cert chain as a bytes array, the TCBM from the local THIM cert cache is as a string +// (only in the case of a local THIM endpoint), and any errors encountered +func (certFetcher CertFetcher) retrieveCertChain(chipID string, reportedTCB uint64) ([]byte, uint64, error) { // HTTP GET request to cert cache service var uri string + var thimTcbm uint64 + var thimCerts common.THIMCerts reportedTCBBytes := make([]byte, 8) binary.LittleEndian.PutUint64(reportedTCBBytes, reportedTCB) - if certCache.AMD { - // AMD cert cache endpoint returns the VCEK certificate in DER format - uri = fmt.Sprintf(AmdVCEKRequestURITemplate, certCache.Endpoint, certCache.TEEType, chipID, reportedTCBBytes[7], reportedTCBBytes[6], reportedTCBBytes[1], reportedTCBBytes[0]) - httpResponse, err := common.HTTPGetRequest(uri, false) - if err != nil { - return nil, errors.Wrapf(err, "certcache http get request failed") - } - derBytes, err := common.HTTPResponseBody(httpResponse) - if err != nil { - return nil, err - } - // encode the VCEK cert in PEM format - vcekPEMBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) - - // now retrieve the cert chain - uri = fmt.Sprintf(AmdCertChainRequestURITemplate, certCache.Endpoint, certCache.TEEType) - httpResponse, err = common.HTTPGetRequest(uri, false) - if err != nil { - return nil, errors.Wrapf(err, "certcache http get request failed") - } - certChainPEMBytes, err := common.HTTPResponseBody(httpResponse) - if err != nil { - return nil, errors.Wrapf(err, "pulling certchain response from get request failed") - } + if certFetcher.Endpoint != "" { + switch certFetcher.EndpointType { + case "AMD": + // AMD cert cache endpoint returns the VCEK certificate in DER format + uri = fmt.Sprintf(AmdVCEKRequestURITemplate, certFetcher.Endpoint, certFetcher.TEEType, chipID, reportedTCBBytes[UcodeSplTcbmByteIndex], reportedTCBBytes[SnpSplTcbmByteIndex], reportedTCBBytes[TeeSplTcbmByteIndex], reportedTCBBytes[BlSplTcbmByteIndex]) + httpResponse, err := common.HTTPGetRequest(uri, false) + if err != nil { + return nil, reportedTCB, errors.Wrapf(err, "AMD certcache http get request failed") + } + derBytes, err := common.HTTPResponseBody(httpResponse) + if err != nil { + return nil, reportedTCB, err + } + // encode the VCEK cert in PEM format + vcekPEMBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) - // constuct full chain by appending the VCEK cert to the cert chain - var fullCertChain []byte - fullCertChain = append(fullCertChain, vcekPEMBytes[:]...) - fullCertChain = append(fullCertChain, certChainPEMBytes[:]...) + // now retrieve the cert chain + uri = fmt.Sprintf(AmdCertChainRequestURITemplate, certFetcher.Endpoint, certFetcher.TEEType) + httpResponse, err = common.HTTPGetRequest(uri, false) + if err != nil { + return nil, reportedTCB, errors.Wrapf(err, "AMD certcache http get request failed") + } + certChainPEMBytes, err := common.HTTPResponseBody(httpResponse) + if err != nil { + return nil, reportedTCB, errors.Wrapf(err, "pulling AMD certchain response from get request failed") + } - return fullCertChain, nil - } else { - uri = fmt.Sprintf(AzureCertCacheRequestURITemplate, certCache.Endpoint, certCache.TEEType, chipID, strconv.FormatUint(reportedTCB, 16), certCache.APIVersion) - httpResponse, err := common.HTTPGetRequest(uri, false) - if err != nil { - return nil, errors.Wrapf(err, "certcache http get request failed") + // constuct full chain by appending the VCEK cert to the cert chain + fullCertChain := append(vcekPEMBytes, certChainPEMBytes[:]...) + + return fullCertChain, reportedTCB, nil + case "LocalTHIM": + uri = fmt.Sprintf(LocalTHIMUriTemplate, certFetcher.Endpoint) + // local THIM cert cache endpoint returns THIM Certs object + httpResponse, err := common.HTTPGetRequest(uri, false) + if err != nil { + return nil, thimTcbm, errors.Wrapf(err, "certcache http get request failed") + } + THIMCertsBytes, err := common.HTTPResponseBody(httpResponse) + if err != nil { + return nil, thimTcbm, errors.Wrapf(err, "pulling certchain response from get request failed") + } + + thimCerts, thimTcbm, err := thimCerts.GetLocalCerts(string(THIMCertsBytes)) + if err != nil { + return nil, thimTcbm, errors.Wrapf(err, "certcache failed to get local certs") + } + + return []byte(thimCerts), thimTcbm, nil + case "AzCache": + uri = fmt.Sprintf(AzureCertCacheRequestURITemplate, certFetcher.Endpoint, certFetcher.TEEType, chipID, strconv.FormatUint(reportedTCB, 16), certFetcher.APIVersion) + httpResponse, err := common.HTTPGetRequest(uri, false) + if err != nil { + return nil, thimTcbm, errors.Wrapf(err, "AzCache http get request failed") + } + certChain, err := common.HTTPResponseBody(httpResponse) + if err != nil { + return nil, thimTcbm, errors.Wrapf(err, "pulling certchain response from AzCache get request failed") + } + thimTcbm, err = ParseVCEK(certChain) + if err != nil { + return nil, thimTcbm, errors.Wrapf(err, "AzCache failed to parse VCEK from cert chain") + } + return certChain, thimTcbm, nil + default: + return nil, thimTcbm, errors.Errorf("invalid endpoint type: %s", certFetcher.EndpointType) } - return common.HTTPResponseBody(httpResponse) + } else { + return nil, thimTcbm, errors.Errorf("failed to retrieve cert chain: certificate endpoint not set") } } -func (certCache CertCache) GetCertChain(chipID string, reportedTCB uint64) ([]byte, error) { - return certCache.retrieveCertChain(chipID, reportedTCB) +func (certFetcher CertFetcher) GetCertChain(chipID string, reportedTCB uint64) ([]byte, uint64, error) { + return certFetcher.retrieveCertChain(chipID, reportedTCB) } diff --git a/pkg/common/info.go b/pkg/common/info.go index d83c8c61..23a93768 100644 --- a/pkg/common/info.go +++ b/pkg/common/info.go @@ -1,173 +1,177 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package common - -import ( - "encoding/base64" - "encoding/json" - - "io/ioutil" - "os" - "path/filepath" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// Set to true to regenerate test files at every request. -// Also useful to debug the various steps, especially encoding -// to the correct base64url encoding. - -const GenerateTestData = false - -// Information supplied by the UVM specific to running Pod - -type UvmInformation struct { - EncodedSecurityPolicy string // customer security policy - CertChain string // platform certificates for the actual physical host, ascii PEM - EncodedUvmReferenceInfo string // endorsements for the particular UVM image -} - -// format of the json provided to the UVM by hcsshim. Comes fro the THIM endpoint -// and is a base64 encoded json string - -type THIMCerts struct { - VcekCert string `json:"vcekCert"` - Tcbm string `json:"tcbm"` - CertificateChain string `json:"certificateChain"` - CacheControl string `json:"cacheControl"` -} - -func THIMtoPEM(encodedHostCertsFromTHIM string) (string, error) { - hostCertsFromTHIM, err := base64.StdEncoding.DecodeString(encodedHostCertsFromTHIM) - if err != nil { - return "", errors.Wrapf(err, "base64 decoding platform certs failed") - } - - if GenerateTestData { - ioutil.WriteFile("uvm_host_amd_certificate.json", hostCertsFromTHIM, 0644) - } - - var certsFromTHIM THIMCerts - err = json.Unmarshal(hostCertsFromTHIM, &certsFromTHIM) - if err != nil { - return "", errors.Wrapf(err, "json unmarshal platform certs failed") - } - - certsString := certsFromTHIM.VcekCert + certsFromTHIM.CertificateChain - - if GenerateTestData { - ioutil.WriteFile("uvm_host_amd_certificate.pem", []byte(certsString), 0644) - } - - logrus.Debugf("certsFromTHIM:\n\n%s\n\n", certsString) - - return certsString, nil -} - -// Late in Public Preview, we made a change to pass the UVM information -// via files instead of environment variables. -// This code detects which method is being used and calls the appropriate -// function to get the UVM information. - -// The environment variable scheme will go away by "General Availability" -// but we handle both to decouple this code and the hcsshim/gcs code. - -// Matching PR https://github.com/microsoft/hcsshim/pull/1708 - -func GetUvmInfomation() (UvmInformation, error) { - securityContextDir := os.Getenv("UVM_SECURITY_CONTEXT_DIR") - if securityContextDir != "" { - return GetUvmInfomationFromFiles() - } else { - return GetUvmInfomationFromEnv() - } -} - -func GetUvmInfomationFromEnv() (UvmInformation, error) { - var encodedUvmInformation UvmInformation - encodedHostCertsFromTHIM := os.Getenv("UVM_HOST_AMD_CERTIFICATE") - - if GenerateTestData { - ioutil.WriteFile("uvm_host_amd_certificate.base64", []byte(encodedHostCertsFromTHIM), 0644) - } - - if encodedHostCertsFromTHIM != "" { - certChain, err := THIMtoPEM(encodedHostCertsFromTHIM) - if err != nil { - return encodedUvmInformation, err - } - encodedUvmInformation.CertChain = certChain - } - encodedUvmInformation.EncodedSecurityPolicy = os.Getenv("UVM_SECURITY_POLICY") - encodedUvmInformation.EncodedUvmReferenceInfo = os.Getenv("UVM_REFERENCE_INFO") - - if GenerateTestData { - ioutil.WriteFile("uvm_security_policy.base64", []byte(encodedUvmInformation.EncodedSecurityPolicy), 0644) - ioutil.WriteFile("uvm_reference_info.base64", []byte(encodedUvmInformation.EncodedUvmReferenceInfo), 0644) - } - - return encodedUvmInformation, nil -} - -// From hcsshim pkg/securitypolicy/securitypolicy.go - -const ( - SecurityContextDirTemplate = "security-context-*" - PolicyFilename = "security-policy-base64" - HostAMDCertFilename = "host-amd-cert-base64" - ReferenceInfoFilename = "reference-info-base64" -) - -func readSecurityContextFile(dir string, filename string) (string, error) { - targetFilename := filepath.Join(dir, filename) - blob, err := os.ReadFile(targetFilename) - if err != nil { - return "", err - } - return string(blob), nil -} - -func GetUvmInfomationFromFiles() (UvmInformation, error) { - var encodedUvmInformation UvmInformation - - securityContextDir := os.Getenv("UVM_SECURITY_CONTEXT_DIR") - if securityContextDir == "" { - return encodedUvmInformation, errors.New("UVM_SECURITY_CONTEXT_DIR not set") - } - - encodedHostCertsFromTHIM, err := readSecurityContextFile(securityContextDir, HostAMDCertFilename) - if err != nil { - return encodedUvmInformation, errors.Wrapf(err, "reading host amd cert failed") - } - - if GenerateTestData { - ioutil.WriteFile("uvm_host_amd_certificate.base64", []byte(encodedHostCertsFromTHIM), 0644) - } - - if encodedHostCertsFromTHIM != "" { - certChain, err := THIMtoPEM(encodedHostCertsFromTHIM) - if err != nil { - return encodedUvmInformation, err - } - encodedUvmInformation.CertChain = certChain - } - - encodedUvmInformation.EncodedSecurityPolicy, err = readSecurityContextFile(securityContextDir, PolicyFilename) - if err != nil { - return encodedUvmInformation, errors.Wrapf(err, "reading security policy failed") - } - - encodedUvmInformation.EncodedUvmReferenceInfo, err = readSecurityContextFile(securityContextDir, ReferenceInfoFilename) - if err != nil { - return encodedUvmInformation, errors.Wrapf(err, "reading uvm reference info failed") - } - - if GenerateTestData { - ioutil.WriteFile("uvm_security_policy.base64", []byte(encodedUvmInformation.EncodedSecurityPolicy), 0644) - ioutil.WriteFile("uvm_reference_info.base64", []byte(encodedUvmInformation.EncodedUvmReferenceInfo), 0644) - } - - return encodedUvmInformation, nil -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package common + +import ( + "encoding/base64" + "encoding/json" + "io/ioutil" + "strconv" + + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// Set to true to regenerate test files at every request. +// Also useful to debug the various steps, especially encoding +// to the correct base64url encoding. + +const GenerateTestData = false + +// format of the json provided to the UVM by hcsshim. Comes from the THIM endpoint +// and is a base64 encoded json string +type THIMCerts struct { + VcekCert string `json:"vcekCert"` + Tcbm string `json:"tcbm"` + CertificateChain string `json:"certificateChain"` + CacheControl string `json:"cacheControl"` +} + +func (thimCerts *THIMCerts) GetLocalCerts(encodedHostCertsFromTHIM string) (string, uint64, error) { + var thimTcbm uint64 + hostCertsFromTHIM, err := base64.StdEncoding.DecodeString(encodedHostCertsFromTHIM) + if err != nil { + return "", thimTcbm, errors.Wrapf(err, "base64 decoding platform certs failed") + } + + if GenerateTestData { + ioutil.WriteFile("uvm_host_amd_certificate.json", hostCertsFromTHIM, 0644) + } + + //var certsFromTHIM THIMCerts + err = json.Unmarshal(hostCertsFromTHIM, &thimCerts) + if err != nil { + return "", thimTcbm, errors.Wrapf(err, "json unmarshal platform certs failed") + } + + certsString := thimCerts.VcekCert + thimCerts.CertificateChain + + if GenerateTestData { + ioutil.WriteFile("uvm_host_amd_certificate.pem", []byte(certsString), 0644) + } + + logrus.Debugf("certsFromTHIM:\n\n%s\n\n", certsString) + logrus.Debugf("thimTcbm: %s\n\n", thimCerts.Tcbm) + + thimTcbm, err = strconv.ParseUint(thimCerts.Tcbm, 16, 64) + if err != nil { + return "", thimTcbm, errors.Wrap(err, "Unable to convert TCBM from THIM certificates to a uint64") + } + + return certsString, thimTcbm, nil +} + +type UvmInformation struct { + EncodedSecurityPolicy string // customer security policy + InitialCerts THIMCerts // platform certificates for the actual physical host + EncodedUvmReferenceInfo string // endorsements for the particular UVM image +} + +// Late in Public Preview, we made a change to pass the UVM information +// via files instead of environment variables. +// This code detects which method is being used and calls the appropriate +// function to get the UVM information. + +// The environment variable scheme will go away by "General Availability" +// but we handle both to decouple this code and the hcsshim/gcs code. + +// Matching PR https://github.com/microsoft/hcsshim/pull/1708 + +func GetUvmInformation() (UvmInformation, error) { + securityContextDir := os.Getenv("UVM_SECURITY_CONTEXT_DIR") + if securityContextDir != "" { + return GetUvmInformationFromFiles() + } else { + return GetUvmInformationFromEnv() + } +} + +func GetUvmInformationFromEnv() (UvmInformation, error) { + var encodedUvmInformation UvmInformation + + encodedHostCertsFromTHIM := os.Getenv("UVM_HOST_AMD_CERTIFICATE") + + if GenerateTestData { + ioutil.WriteFile("uvm_host_amd_certificate.base64", []byte(encodedHostCertsFromTHIM), 0644) + } + + if encodedHostCertsFromTHIM != "" { + _, _, err := encodedUvmInformation.InitialCerts.GetLocalCerts(encodedHostCertsFromTHIM) + if err != nil { + return encodedUvmInformation, err + } + } + encodedUvmInformation.EncodedSecurityPolicy = os.Getenv("UVM_SECURITY_POLICY") + encodedUvmInformation.EncodedUvmReferenceInfo = os.Getenv("UVM_REFERENCE_INFO") + + if GenerateTestData { + ioutil.WriteFile("uvm_security_policy.base64", []byte(encodedUvmInformation.EncodedSecurityPolicy), 0644) + ioutil.WriteFile("uvm_reference_info.base64", []byte(encodedUvmInformation.EncodedUvmReferenceInfo), 0644) + } + + return encodedUvmInformation, nil +} + +// From hcsshim pkg/securitypolicy/securitypolicy.go + +const ( + SecurityContextDirTemplate = "security-context-*" + PolicyFilename = "security-policy-base64" + HostAMDCertFilename = "host-amd-cert-base64" + ReferenceInfoFilename = "reference-info-base64" +) + +func readSecurityContextFile(dir string, filename string) (string, error) { + targetFilename := filepath.Join(dir, filename) + blob, err := os.ReadFile(targetFilename) + if err != nil { + return "", err + } + return string(blob), nil +} + +func GetUvmInformationFromFiles() (UvmInformation, error) { + var encodedUvmInformation UvmInformation + + securityContextDir := os.Getenv("UVM_SECURITY_CONTEXT_DIR") + if securityContextDir == "" { + return encodedUvmInformation, errors.New("UVM_SECURITY_CONTEXT_DIR not set") + } + + encodedHostCertsFromTHIM, err := readSecurityContextFile(securityContextDir, HostAMDCertFilename) + if err != nil { + return encodedUvmInformation, errors.Wrapf(err, "reading host amd cert failed") + } + + if GenerateTestData { + ioutil.WriteFile("uvm_host_amd_certificate.base64", []byte(encodedHostCertsFromTHIM), 0644) + } + + if encodedHostCertsFromTHIM != "" { + _, _, err := encodedUvmInformation.InitialCerts.GetLocalCerts(encodedHostCertsFromTHIM) + if err != nil { + return encodedUvmInformation, err + } + } + + encodedUvmInformation.EncodedSecurityPolicy, err = readSecurityContextFile(securityContextDir, PolicyFilename) + if err != nil { + return encodedUvmInformation, errors.Wrapf(err, "reading security policy failed") + } + + encodedUvmInformation.EncodedUvmReferenceInfo, err = readSecurityContextFile(securityContextDir, ReferenceInfoFilename) + if err != nil { + return encodedUvmInformation, errors.Wrapf(err, "reading uvm reference info failed") + } + + if GenerateTestData { + ioutil.WriteFile("uvm_security_policy.base64", []byte(encodedUvmInformation.EncodedSecurityPolicy), 0644) + ioutil.WriteFile("uvm_reference_info.base64", []byte(encodedUvmInformation.EncodedUvmReferenceInfo), 0644) + } + + return encodedUvmInformation, nil +} diff --git a/pkg/skr/skr.go b/pkg/skr/skr.go index 53ef079e..335d6d2e 100644 --- a/pkg/skr/skr.go +++ b/pkg/skr/skr.go @@ -57,7 +57,7 @@ type KeyBlob struct { // information about the AKV, authority and the key to be released. // // The return type is a JWK key -func SecureKeyRelease(identity common.Identity, SKRKeyBlob KeyBlob, uvmInformation common.UvmInformation) (_ jwk.Key, err error) { +func SecureKeyRelease(identity common.Identity, certState attest.CertState, SKRKeyBlob KeyBlob, uvmInformation common.UvmInformation) (_ jwk.Key, err error) { logrus.Debugf("Releasing key blob: %v", SKRKeyBlob) @@ -81,7 +81,7 @@ func SecureKeyRelease(identity common.Identity, SKRKeyBlob KeyBlob, uvmInformati } // Attest - maaToken, err = attest.Attest(SKRKeyBlob.Authority, jwkSetBytes, uvmInformation) + maaToken, err = certState.Attest(SKRKeyBlob.Authority, jwkSetBytes, uvmInformation) if err != nil { return nil, errors.Wrapf(err, "attestation failed") } diff --git a/tools/importkey/main.go b/tools/importkey/main.go index b0ae373f..84c02333 100644 --- a/tools/importkey/main.go +++ b/tools/importkey/main.go @@ -283,7 +283,7 @@ func main() { fmt.Println(AKVResponse.Key.KID) releasePolicyJSON, err := json.Marshal(releasePolicy) if err != nil { - fmt.Println("marshalling releasy policy failed") + fmt.Println("marshalling release policy failed") } else { fmt.Println(string(releasePolicyJSON)) }