Refactor enforced/expected PCRs (#553)

* Merge enforced and expected measurements

* Update measurement generation to new format

* Write expected measurements hex encoded by default

* Allow hex or base64 encoded expected measurements

* Allow hex or base64 encoded clusterID

* Allow security upgrades to warnOnly flag

* Upload signed measurements in JSON format

* Fetch measurements either from JSON or YAML

* Use yaml.v3 instead of yaml.v2

* Error on invalid enforced selection

* Add placeholder measurements to config

* Update e2e test to new measurement format

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2022-11-24 10:57:58 +01:00 committed by GitHub
parent 8ce954e012
commit f8001efbc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1180 additions and 801 deletions

View File

@ -75,14 +75,14 @@ runs:
(.provider | select(. | has(\"azure\")).azure.resourceGroup) = \"${{ inputs.azureResourceGroup }}\" |
(.provider | select(. | has(\"azure\")).azure.appClientID) = \"${{ inputs.azureClientID }}\" |
(.provider | select(. | has(\"azure\")).azure.clientSecretValue) = \"${{ inputs.azureClientSecret }}\" |
(.provider | select(. | has(\"azure\")).azure.enforcedMeasurements) = [15]" \
(.provider | select(. | has(\"azure\")).azure.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
constellation-conf.yaml
yq eval -i \
"(.provider | select(. | has(\"gcp\")).gcp.project) = \"${{ inputs.gcpProject }}\" |
(.provider | select(. | has(\"gcp\")).gcp.region) = \"europe-west3\" |
(.provider | select(. | has(\"gcp\")).gcp.zone) = \"europe-west3-b\" |
(.provider | select(. | has(\"gcp\")).gcp.enforcedMeasurements) = [15] |
(.provider | select(. | has(\"gcp\")).gcp.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}} |
(.provider | select(. | has(\"gcp\")).gcp.serviceAccountKeyPath) = \"serviceAccountKey.json\"" \
constellation-conf.yaml
@ -91,7 +91,7 @@ runs:
(.provider | select(. | has(\"aws\")).aws.zone) = \"eu-central-1a\" |
(.provider | select(. | has(\"aws\")).aws.iamProfileControlPlane) = \"e2e_test_control_plane_instance_profile\" |
(.provider | select(. | has(\"aws\")).aws.iamProfileWorkerNodes) = \"e2e_test_worker_node_instance_profile\" |
(.provider | select(. | has(\"aws\")).aws.enforcedMeasurements) = [15]" \
(.provider | select(. | has(\"aws\")).aws.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
constellation-conf.yaml
if [ ${{ inputs.kubernetesVersion != '' }} = true ]; then

View File

@ -51,16 +51,35 @@ runs:
run: |
KUBECONFIG="$PWD/constellation-admin.conf" kubectl rollout status ds/verification-service -n kube-system --timeout=3m
CONSTELL_IP=$(jq -r ".ip" constellation-id.json)
pcr-reader --constell-ip ${CONSTELL_IP} -format yaml > measurements.yaml
pcr-reader --constell-ip ${CONSTELL_IP} -format json > measurements.json
case $CSP in
azure)
yq e 'del(.[0,6,10,11,12,13,14,15,16,17,18,19,20,21,22,23])' -i measurements.yaml
yq e 'del(.[0,6,10,16,17,18,19,20,21,22,23])' -I 0 -o json -i measurements.json
yq '.4.warnOnly = false |
.8.warnOnly = false |
.9.warnOnly = false |
.11.warnOnly = false |
.12.warnOnly = false |
.13.warnOnly = false |
.15.warnOnly = false |
.15.expected = "0000000000000000000000000000000000000000000000000000000000000000"' \
-I 0 -o json -i measurements.json
;;
gcp)
yq e 'del(.[11,12,13,14,15,16,17,18,19,20,21,22,23])' -i measurements.yaml
yq e 'del(.[16,17,18,19,20,21,22,23])' -I 0 -o json -i measurements.json
yq '.0.warnOnly = false |
.4.warnOnly = false |
.8.warnOnly = false |
.9.warnOnly = false |
.11.warnOnly = false |
.12.warnOnly = false |
.13.warnOnly = false |
.15.warnOnly = false |
.15.expected = "0000000000000000000000000000000000000000000000000000000000000000"' \
-I 0 -o json -i measurements.json
;;
esac
cat measurements.yaml
cat measurements.json
shell: bash
env:
CSP: ${{ inputs.cloudProvider }}
@ -81,14 +100,14 @@ runs:
run: |
echo "$COSIGN_PUBLIC_KEY" > cosign.pub
# Enabling experimental mode also publishes signature to Rekor
COSIGN_EXPERIMENTAL=1 cosign sign-blob --key env://COSIGN_PRIVATE_KEY measurements.yaml > measurements.yaml.sig
COSIGN_EXPERIMENTAL=1 cosign sign-blob --key env://COSIGN_PRIVATE_KEY measurements.json > measurements.json.sig
# Verify - As documentation & check
# Local Signature (input: artifact, key, signature)
cosign verify-blob --key cosign.pub --signature measurements.yaml.sig measurements.yaml
cosign verify-blob --key cosign.pub --signature measurements.json.sig measurements.json
# Transparency Log Signature (input: artifact, key)
uuid=$(rekor-cli search --artifact measurements.yaml | tail -n 1)
uuid=$(rekor-cli search --artifact measurements.json | tail -n 1)
sig=$(rekor-cli get --uuid=$uuid --format=json | jq -r .Body.HashedRekordObj.signature.content)
cosign verify-blob --key cosign.pub --signature <(echo $sig) measurements.yaml
cosign verify-blob --key cosign.pub --signature <(echo $sig) measurements.json
shell: bash
env:
COSIGN_PUBLIC_KEY: ${{ inputs.cosignPublicKey }}
@ -100,9 +119,9 @@ runs:
run: |
IMAGE=$(yq e ".provider.${CSP}.image" constellation-conf.yaml)
S3_PATH=s3://${PUBLIC_BUCKET_NAME}/${IMAGE,,}
aws s3 cp measurements.yaml ${S3_PATH}/measurements.yaml
if test -f measurements.yaml.sig; then
aws s3 cp measurements.yaml.sig ${S3_PATH}/measurements.yaml.sig
aws s3 cp measurements.json ${S3_PATH}/measurements.json
if test -f measurements.json.sig; then
aws s3 cp measurements.json.sig ${S3_PATH}/measurements.json.sig
fi
shell: bash
env:

View File

@ -8,7 +8,6 @@ package main
import (
"context"
"encoding/json"
"flag"
"io"
"os"
@ -80,14 +79,10 @@ func main() {
switch cloudprovider.FromString(os.Getenv(constellationCSP)) {
case cloudprovider.AWS:
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.AWSPCRSelection)
measurements, err := vtpm.GetSelectedMeasurements(vtpm.OpenVTPM, vtpm.AWSPCRSelection)
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs")
}
pcrsJSON, err := json.Marshal(pcrs)
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to marshal PCRs")
}
issuer = initserver.NewIssuerWrapper(aws.NewIssuer(), vmtype.Unknown, nil)
@ -104,13 +99,13 @@ func main() {
clusterInitJoiner = kubernetes.New(
"aws", k8sapi.NewKubernetesUtil(), &k8sapi.KubdeadmConfiguration{}, kubectl.New(),
metadata, pcrsJSON, helmClient, &kubewaiter.CloudKubeAPIWaiter{},
metadata, measurements, helmClient, &kubewaiter.CloudKubeAPIWaiter{},
)
openTPM = vtpm.OpenVTPM
fs = afero.NewOsFs()
case cloudprovider.GCP:
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.GCPPCRSelection)
measurements, err := vtpm.GetSelectedMeasurements(vtpm.OpenVTPM, vtpm.GCPPCRSelection)
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs")
}
@ -129,20 +124,16 @@ func main() {
}
metadataAPI = metadata
pcrsJSON, err := json.Marshal(pcrs)
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to marshal PCRs")
}
clusterInitJoiner = kubernetes.New(
"gcp", k8sapi.NewKubernetesUtil(), &k8sapi.KubdeadmConfiguration{}, kubectl.New(),
metadata, pcrsJSON, helmClient, &kubewaiter.CloudKubeAPIWaiter{},
metadata, measurements, helmClient, &kubewaiter.CloudKubeAPIWaiter{},
)
openTPM = vtpm.OpenVTPM
fs = afero.NewOsFs()
log.Infof("Added load balancer IP to routing table")
case cloudprovider.Azure:
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.AzurePCRSelection)
measurements, err := vtpm.GetSelectedMeasurements(vtpm.OpenVTPM, vtpm.AzurePCRSelection)
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs")
}
@ -163,20 +154,16 @@ func main() {
log.With(zap.Error(err)).Fatalf("Failed to set up cloud logger")
}
metadataAPI = metadata
pcrsJSON, err := json.Marshal(pcrs)
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to marshal PCRs")
}
clusterInitJoiner = kubernetes.New(
"azure", k8sapi.NewKubernetesUtil(), &k8sapi.KubdeadmConfiguration{}, kubectl.New(),
metadata, pcrsJSON, helmClient, &kubewaiter.CloudKubeAPIWaiter{},
metadata, measurements, helmClient, &kubewaiter.CloudKubeAPIWaiter{},
)
openTPM = vtpm.OpenVTPM
fs = afero.NewOsFs()
case cloudprovider.QEMU:
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.QEMUPCRSelection)
measurements, err := vtpm.GetSelectedMeasurements(vtpm.OpenVTPM, vtpm.QEMUPCRSelection)
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to get selected PCRs")
}
@ -185,13 +172,9 @@ func main() {
cloudLogger = qemucloud.NewLogger()
metadata := qemucloud.New()
pcrsJSON, err := json.Marshal(pcrs)
if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to marshal PCRs")
}
clusterInitJoiner = kubernetes.New(
"qemu", k8sapi.NewKubernetesUtil(), &k8sapi.KubdeadmConfiguration{}, kubectl.New(),
metadata, pcrsJSON, helmClient, &kubewaiter.CloudKubeAPIWaiter{},
metadata, measurements, helmClient, &kubewaiter.CloudKubeAPIWaiter{},
)
metadataAPI = metadata

View File

@ -27,9 +27,9 @@ message InitRequest {
}
message InitResponse {
bytes kubeconfig = 1;
bytes owner_id = 2;
bytes cluster_id = 3;
bytes kubeconfig = 1;
bytes owner_id = 2;
bytes cluster_id = 3;
}
message KubernetesComponent {

View File

@ -20,6 +20,7 @@ import (
"github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/k8sapi"
kubewaiter "github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/kubeWaiter"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/azureshared"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
@ -52,33 +53,33 @@ type kubeAPIWaiter interface {
// KubeWrapper implements Cluster interface.
type KubeWrapper struct {
cloudProvider string
clusterUtil clusterUtil
helmClient helmClient
kubeAPIWaiter kubeAPIWaiter
configProvider configurationProvider
client k8sapi.Client
kubeconfigReader configReader
providerMetadata ProviderMetadata
initialMeasurementsJSON []byte
getIPAddr func() (string, error)
cloudProvider string
clusterUtil clusterUtil
helmClient helmClient
kubeAPIWaiter kubeAPIWaiter
configProvider configurationProvider
client k8sapi.Client
kubeconfigReader configReader
providerMetadata ProviderMetadata
initialMeasurements measurements.M
getIPAddr func() (string, error)
}
// New creates a new KubeWrapper with real values.
func New(cloudProvider string, clusterUtil clusterUtil, configProvider configurationProvider, client k8sapi.Client,
providerMetadata ProviderMetadata, initialMeasurementsJSON []byte, helmClient helmClient, kubeAPIWaiter kubeAPIWaiter,
providerMetadata ProviderMetadata, measurements measurements.M, helmClient helmClient, kubeAPIWaiter kubeAPIWaiter,
) *KubeWrapper {
return &KubeWrapper{
cloudProvider: cloudProvider,
clusterUtil: clusterUtil,
helmClient: helmClient,
kubeAPIWaiter: kubeAPIWaiter,
configProvider: configProvider,
client: client,
kubeconfigReader: &KubeconfigReader{fs: afero.Afero{Fs: afero.NewOsFs()}},
providerMetadata: providerMetadata,
initialMeasurementsJSON: initialMeasurementsJSON,
getIPAddr: getIPAddr,
cloudProvider: cloudProvider,
clusterUtil: clusterUtil,
helmClient: helmClient,
kubeAPIWaiter: kubeAPIWaiter,
configProvider: configProvider,
client: client,
kubeconfigReader: &KubeconfigReader{fs: afero.Afero{Fs: afero.NewOsFs()}},
providerMetadata: providerMetadata,
initialMeasurements: measurements,
getIPAddr: getIPAddr,
}
}
@ -187,7 +188,21 @@ func (k *KubeWrapper) InitCluster(
} else {
controlPlaneIP = controlPlaneEndpoint
}
serviceConfig := constellationServicesConfig{k.initialMeasurementsJSON, idKeyDigest, measurementSalt, subnetworkPodCIDR, cloudServiceAccountURI, controlPlaneIP}
if err := k.initialMeasurements.SetEnforced(enforcedPCRs); err != nil {
return nil, err
}
measurementsJSON, err := json.Marshal(k.initialMeasurements)
if err != nil {
return nil, fmt.Errorf("marshaling initial measurements: %w", err)
}
serviceConfig := constellationServicesConfig{
initialMeasurementsJSON: measurementsJSON,
idkeydigest: idKeyDigest,
measurementSalt: measurementSalt,
subnetworkPodCIDR: subnetworkPodCIDR,
cloudServiceAccountURI: cloudServiceAccountURI,
loadBalancerIP: controlPlaneIP,
}
extraVals, err := k.setupExtraVals(ctx, serviceConfig)
if err != nil {
return nil, fmt.Errorf("setting up extraVals: %w", err)

View File

@ -112,6 +112,13 @@ func (u *Upgrader) updateMeasurements(ctx context.Context, newMeasurements measu
return nil
}
// don't allow potential security downgrades by setting the warnOnly flag to true
for k, newM := range newMeasurements {
if currentM, ok := currentMeasurements[k]; ok && !currentM.WarnOnly && newM.WarnOnly {
return fmt.Errorf("setting enforced measurement %d to warn only: not allowed", k)
}
}
// backup of previous measurements
existingConf.Data["oldMeasurements"] = existingConf.Data[constants.MeasurementsFilename]

View File

@ -33,12 +33,12 @@ func TestUpdateMeasurements(t *testing.T) {
updater: &stubMeasurementsUpdater{
oldMeasurements: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":"AAAAAA=="}`,
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
},
},
newMeasurements: measurements.M{
0: []byte("1"),
0: measurements.WithAllBytes(0xBB, false),
},
wantUpdate: true,
},
@ -46,14 +46,40 @@ func TestUpdateMeasurements(t *testing.T) {
updater: &stubMeasurementsUpdater{
oldMeasurements: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":"MQ=="}`,
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
},
},
newMeasurements: measurements.M{
0: []byte("1"),
0: measurements.WithAllBytes(0xAA, false),
},
},
"trying to set warnOnly to true results in error": {
updater: &stubMeasurementsUpdater{
oldMeasurements: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
},
},
newMeasurements: measurements.M{
0: measurements.WithAllBytes(0xAA, true),
},
wantErr: true,
},
"setting warnOnly to false is allowed": {
updater: &stubMeasurementsUpdater{
oldMeasurements: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":true}}`,
},
},
},
newMeasurements: measurements.M{
0: measurements.WithAllBytes(0xAA, false),
},
wantUpdate: true,
},
"getCurrent error": {
updater: &stubMeasurementsUpdater{getErr: someErr},
wantErr: true,
@ -62,7 +88,7 @@ func TestUpdateMeasurements(t *testing.T) {
updater: &stubMeasurementsUpdater{
oldMeasurements: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":"AAAAAA=="}`,
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
},
updateErr: someErr,
@ -82,7 +108,7 @@ func TestUpdateMeasurements(t *testing.T) {
err := upgrader.updateMeasurements(context.Background(), tc.newMeasurements)
if tc.wantErr {
assert.ErrorIs(err, someErr)
assert.Error(err)
return
}

View File

@ -20,17 +20,16 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/gcp"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/attestation/qemu"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/spf13/cobra"
"go.uber.org/multierr"
)
// Validator validates Platform Configuration Registers (PCRs).
type Validator struct {
provider cloudprovider.Provider
pcrs measurements.M
enforcedPCRs []uint32
idkeydigest []byte
enforceIDKeyDigest bool
azureCVM bool
@ -65,41 +64,44 @@ func NewValidator(provider cloudprovider.Provider, conf *config.Config) (*Valida
// UpdateInitPCRs sets the owner and cluster PCR values.
func (v *Validator) UpdateInitPCRs(ownerID, clusterID string) error {
if err := v.updatePCR(uint32(vtpm.PCRIndexOwnerID), ownerID); err != nil {
if err := v.updatePCR(uint32(measurements.PCRIndexOwnerID), ownerID); err != nil {
return err
}
return v.updatePCR(uint32(vtpm.PCRIndexClusterID), clusterID)
return v.updatePCR(uint32(measurements.PCRIndexClusterID), clusterID)
}
// updatePCR adds a new entry to the pcr map of v, or removes the key if the input is an empty string.
// updatePCR adds a new entry to the measurements of v, or removes the key if the input is an empty string.
//
// When adding, the input is first decoded from base64.
// When adding, the input is first decoded from hex or base64.
// We then calculate the expected PCR by hashing the input using SHA256,
// appending expected PCR for initialization, and then hashing once more.
func (v *Validator) updatePCR(pcrIndex uint32, encoded string) error {
if encoded == "" {
delete(v.pcrs, pcrIndex)
// remove enforced PCR if it exists
for i, enforcedIdx := range v.enforcedPCRs {
if enforcedIdx == pcrIndex {
v.enforcedPCRs[i] = v.enforcedPCRs[len(v.enforcedPCRs)-1]
v.enforcedPCRs = v.enforcedPCRs[:len(v.enforcedPCRs)-1]
break
}
}
return nil
}
decoded, err := base64.StdEncoding.DecodeString(encoded)
// decode from hex or base64
decoded, err := hex.DecodeString(encoded)
if err != nil {
return fmt.Errorf("input [%s] is not base64 encoded: %w", encoded, err)
hexErr := err
decoded, err = base64.StdEncoding.DecodeString(encoded)
if err != nil {
return multierr.Append(
fmt.Errorf("input [%s] is not hex encoded: %w", encoded, hexErr),
fmt.Errorf("input [%s] is not base64 encoded: %w", encoded, err),
)
}
}
// new_pcr_value := hash(old_pcr_value || data_to_extend)
// Since we use the TPM2_PCR_Event call to extend the PCR, data_to_extend is the hash of our input
hashedInput := sha256.Sum256(decoded)
expectedPcr := sha256.Sum256(append(v.pcrs[pcrIndex], hashedInput[:]...))
v.pcrs[pcrIndex] = expectedPcr[:]
oldExpected := v.pcrs[pcrIndex].Expected
expectedPcr := sha256.Sum256(append(oldExpected[:], hashedInput[:]...))
v.pcrs[pcrIndex] = measurements.Measurement{
Expected: expectedPcr,
WarnOnly: v.pcrs[pcrIndex].WarnOnly,
}
return nil
}
@ -107,35 +109,27 @@ func (v *Validator) setPCRs(config *config.Config) error {
switch v.provider {
case cloudprovider.AWS:
awsPCRs := config.Provider.AWS.Measurements
enforcedPCRs := config.Provider.AWS.EnforcedMeasurements
if err := v.checkPCRs(awsPCRs, enforcedPCRs); err != nil {
return err
if len(awsPCRs) == 0 {
return errors.New("no expected measurement provided")
}
v.enforcedPCRs = enforcedPCRs
v.pcrs = awsPCRs
case cloudprovider.Azure:
azurePCRs := config.Provider.Azure.Measurements
enforcedPCRs := config.Provider.Azure.EnforcedMeasurements
if err := v.checkPCRs(azurePCRs, enforcedPCRs); err != nil {
return err
if len(azurePCRs) == 0 {
return errors.New("no expected measurement provided")
}
v.enforcedPCRs = enforcedPCRs
v.pcrs = azurePCRs
case cloudprovider.GCP:
gcpPCRs := config.Provider.GCP.Measurements
enforcedPCRs := config.Provider.GCP.EnforcedMeasurements
if err := v.checkPCRs(gcpPCRs, enforcedPCRs); err != nil {
return err
if len(gcpPCRs) == 0 {
return errors.New("no expected measurement provided")
}
v.enforcedPCRs = enforcedPCRs
v.pcrs = gcpPCRs
case cloudprovider.QEMU:
qemuPCRs := config.Provider.QEMU.Measurements
enforcedPCRs := config.Provider.QEMU.EnforcedMeasurements
if err := v.checkPCRs(qemuPCRs, enforcedPCRs); err != nil {
return err
if len(qemuPCRs) == 0 {
return errors.New("no expected measurement provided")
}
v.enforcedPCRs = enforcedPCRs
v.pcrs = qemuPCRs
}
return nil
@ -156,37 +150,20 @@ func (v *Validator) updateValidator(cmd *cobra.Command) {
log := warnLogger{cmd: cmd}
switch v.provider {
case cloudprovider.GCP:
v.validator = gcp.NewValidator(v.pcrs, v.enforcedPCRs, log)
v.validator = gcp.NewValidator(v.pcrs, log)
case cloudprovider.Azure:
if v.azureCVM {
v.validator = snp.NewValidator(v.pcrs, v.enforcedPCRs, v.idkeydigest, v.enforceIDKeyDigest, log)
v.validator = snp.NewValidator(v.pcrs, v.idkeydigest, v.enforceIDKeyDigest, log)
} else {
v.validator = trustedlaunch.NewValidator(v.pcrs, v.enforcedPCRs, log)
v.validator = trustedlaunch.NewValidator(v.pcrs, log)
}
case cloudprovider.AWS:
v.validator = aws.NewValidator(v.pcrs, v.enforcedPCRs, log)
v.validator = aws.NewValidator(v.pcrs, log)
case cloudprovider.QEMU:
v.validator = qemu.NewValidator(v.pcrs, v.enforcedPCRs, log)
v.validator = qemu.NewValidator(v.pcrs, log)
}
}
func (v *Validator) checkPCRs(pcrs measurements.M, enforcedPCRs []uint32) error {
if len(pcrs) == 0 {
return errors.New("no PCR values provided")
}
for k, v := range pcrs {
if len(v) != 32 {
return fmt.Errorf("bad config: PCR[%d]: expected length: %d, but got: %d", k, 32, len(v))
}
}
for _, v := range enforcedPCRs {
if _, ok := pcrs[v]; !ok {
return fmt.Errorf("bad config: PCR[%d] is enforced, but no expected measurement is provided", v)
}
}
return nil
}
// warnLogger implements logging of warnings for validators.
type warnLogger struct {
cmd *cobra.Command

View File

@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
package cloudcmd
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"testing"
"github.com/edgelesssys/constellation/v2/internal/atls"
@ -18,7 +18,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/gcp"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/attestation/qemu"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/spf13/cobra"
@ -27,12 +26,12 @@ import (
func TestNewValidator(t *testing.T) {
testPCRs := measurements.M{
0: measurements.PCRWithAllBytes(0x00),
1: measurements.PCRWithAllBytes(0xFF),
2: measurements.PCRWithAllBytes(0x00),
3: measurements.PCRWithAllBytes(0xFF),
4: measurements.PCRWithAllBytes(0x00),
5: measurements.PCRWithAllBytes(0x00),
0: measurements.WithAllBytes(0x00, false),
1: measurements.WithAllBytes(0xFF, false),
2: measurements.WithAllBytes(0x00, false),
3: measurements.WithAllBytes(0xFF, false),
4: measurements.WithAllBytes(0x00, false),
5: measurements.WithAllBytes(0x00, false),
}
testCases := map[string]struct {
@ -67,13 +66,6 @@ func TestNewValidator(t *testing.T) {
pcrs: measurements.M{},
wantErr: true,
},
"invalid pcr length": {
provider: cloudprovider.GCP,
pcrs: measurements.M{
0: bytes.Repeat([]byte{0x00}, 31),
},
wantErr: true,
},
"unknown provider": {
provider: cloudprovider.Unknown,
pcrs: testPCRs,
@ -126,19 +118,19 @@ func TestNewValidator(t *testing.T) {
func TestValidatorV(t *testing.T) {
newTestPCRs := func() measurements.M {
return measurements.M{
0: measurements.PCRWithAllBytes(0x00),
1: measurements.PCRWithAllBytes(0x00),
2: measurements.PCRWithAllBytes(0x00),
3: measurements.PCRWithAllBytes(0x00),
4: measurements.PCRWithAllBytes(0x00),
5: measurements.PCRWithAllBytes(0x00),
6: measurements.PCRWithAllBytes(0x00),
7: measurements.PCRWithAllBytes(0x00),
8: measurements.PCRWithAllBytes(0x00),
9: measurements.PCRWithAllBytes(0x00),
10: measurements.PCRWithAllBytes(0x00),
11: measurements.PCRWithAllBytes(0x00),
12: measurements.PCRWithAllBytes(0x00),
0: measurements.WithAllBytes(0x00, true),
1: measurements.WithAllBytes(0x00, true),
2: measurements.WithAllBytes(0x00, true),
3: measurements.WithAllBytes(0x00, true),
4: measurements.WithAllBytes(0x00, true),
5: measurements.WithAllBytes(0x00, true),
6: measurements.WithAllBytes(0x00, true),
7: measurements.WithAllBytes(0x00, true),
8: measurements.WithAllBytes(0x00, true),
9: measurements.WithAllBytes(0x00, true),
10: measurements.WithAllBytes(0x00, true),
11: measurements.WithAllBytes(0x00, true),
12: measurements.WithAllBytes(0x00, true),
}
}
@ -151,23 +143,23 @@ func TestValidatorV(t *testing.T) {
"gcp": {
provider: cloudprovider.GCP,
pcrs: newTestPCRs(),
wantVs: gcp.NewValidator(newTestPCRs(), nil, nil),
wantVs: gcp.NewValidator(newTestPCRs(), nil),
},
"azure cvm": {
provider: cloudprovider.Azure,
pcrs: newTestPCRs(),
wantVs: snp.NewValidator(newTestPCRs(), nil, nil, false, nil),
wantVs: snp.NewValidator(newTestPCRs(), nil, false, nil),
azureCVM: true,
},
"azure trusted launch": {
provider: cloudprovider.Azure,
pcrs: newTestPCRs(),
wantVs: trustedlaunch.NewValidator(newTestPCRs(), nil, nil),
wantVs: trustedlaunch.NewValidator(newTestPCRs(), nil),
},
"qemu": {
provider: cloudprovider.QEMU,
pcrs: newTestPCRs(),
wantVs: qemu.NewValidator(newTestPCRs(), nil, nil),
wantVs: qemu.NewValidator(newTestPCRs(), nil),
},
}
@ -185,37 +177,37 @@ func TestValidatorV(t *testing.T) {
}
func TestValidatorUpdateInitPCRs(t *testing.T) {
zero := []byte("00000000000000000000000000000000")
one := []byte("11111111111111111111111111111111")
one64 := base64.StdEncoding.EncodeToString(one)
oneHash := sha256.Sum256(one)
pcrZeroUpdatedOne := sha256.Sum256(append(zero, oneHash[:]...))
newTestPCRs := func() map[uint32][]byte {
return map[uint32][]byte{
0: zero,
1: zero,
2: zero,
3: zero,
4: zero,
5: zero,
6: zero,
7: zero,
8: zero,
9: zero,
10: zero,
11: zero,
12: zero,
13: zero,
14: zero,
15: zero,
16: zero,
17: one,
18: one,
19: one,
20: one,
21: one,
22: one,
23: zero,
zero := measurements.WithAllBytes(0x00, true)
one := measurements.WithAllBytes(0x11, true)
one64 := base64.StdEncoding.EncodeToString(one.Expected[:])
oneHash := sha256.Sum256(one.Expected[:])
pcrZeroUpdatedOne := sha256.Sum256(append(zero.Expected[:], oneHash[:]...))
newTestPCRs := func() measurements.M {
return measurements.M{
0: measurements.WithAllBytes(0x00, true),
1: measurements.WithAllBytes(0x00, true),
2: measurements.WithAllBytes(0x00, true),
3: measurements.WithAllBytes(0x00, true),
4: measurements.WithAllBytes(0x00, true),
5: measurements.WithAllBytes(0x00, true),
6: measurements.WithAllBytes(0x00, true),
7: measurements.WithAllBytes(0x00, true),
8: measurements.WithAllBytes(0x00, true),
9: measurements.WithAllBytes(0x00, true),
10: measurements.WithAllBytes(0x00, true),
11: measurements.WithAllBytes(0x00, true),
12: measurements.WithAllBytes(0x00, true),
13: measurements.WithAllBytes(0x00, true),
14: measurements.WithAllBytes(0x00, true),
15: measurements.WithAllBytes(0x00, true),
16: measurements.WithAllBytes(0x00, true),
17: measurements.WithAllBytes(0x11, true),
18: measurements.WithAllBytes(0x11, true),
19: measurements.WithAllBytes(0x11, true),
20: measurements.WithAllBytes(0x11, true),
21: measurements.WithAllBytes(0x11, true),
22: measurements.WithAllBytes(0x11, true),
23: measurements.WithAllBytes(0x00, true),
}
}
@ -285,25 +277,25 @@ func TestValidatorUpdateInitPCRs(t *testing.T) {
assert.NoError(err)
for i := 0; i < len(tc.pcrs); i++ {
switch {
case i == int(vtpm.PCRIndexClusterID) && tc.clusterID == "":
case i == int(measurements.PCRIndexClusterID) && tc.clusterID == "":
// should be deleted
_, ok := validators.pcrs[uint32(i)]
assert.False(ok)
case i == int(vtpm.PCRIndexClusterID):
case i == int(measurements.PCRIndexClusterID):
pcr, ok := validators.pcrs[uint32(i)]
assert.True(ok)
assert.Equal(pcrZeroUpdatedOne[:], pcr)
assert.Equal(pcrZeroUpdatedOne, pcr.Expected)
case i == int(vtpm.PCRIndexOwnerID) && tc.ownerID == "":
case i == int(measurements.PCRIndexOwnerID) && tc.ownerID == "":
// should be deleted
_, ok := validators.pcrs[uint32(i)]
assert.False(ok)
case i == int(vtpm.PCRIndexOwnerID):
case i == int(measurements.PCRIndexOwnerID):
pcr, ok := validators.pcrs[uint32(i)]
assert.True(ok)
assert.Equal(pcrZeroUpdatedOne[:], pcr)
assert.Equal(pcrZeroUpdatedOne, pcr.Expected)
default:
if i >= 17 && i <= 22 {
@ -320,8 +312,8 @@ func TestValidatorUpdateInitPCRs(t *testing.T) {
func TestUpdatePCR(t *testing.T) {
emptyMap := measurements.M{}
defaultMap := measurements.M{
0: measurements.PCRWithAllBytes(0xAA),
1: measurements.PCRWithAllBytes(0xBB),
0: measurements.WithAllBytes(0xAA, false),
1: measurements.WithAllBytes(0xBB, false),
}
testCases := map[string]struct {
@ -359,6 +351,20 @@ func TestUpdatePCR(t *testing.T) {
wantEntries: len(defaultMap) + 1,
wantErr: false,
},
"hex input, empty map": {
pcrMap: emptyMap,
pcrIndex: 10,
encoded: hex.EncodeToString([]byte("Constellation")),
wantEntries: 1,
wantErr: false,
},
"hex input, default map": {
pcrMap: defaultMap,
pcrIndex: 10,
encoded: hex.EncodeToString([]byte("Constellation")),
wantEntries: len(defaultMap) + 1,
wantErr: false,
},
"unencoded input, empty map": {
pcrMap: emptyMap,
pcrIndex: 10,
@ -403,9 +409,6 @@ func TestUpdatePCR(t *testing.T) {
assert.NoError(err)
}
assert.Len(pcrs, tc.wantEntries)
for _, v := range pcrs {
assert.Len(v, 32)
}
})
}
}

View File

@ -137,7 +137,7 @@ func (f *fetchMeasurementsFlags) updateURLs(ctx context.Context, conf *config.Co
if f.measurementsURL == nil {
// TODO(AB#2644): resolve image version to reference
parsedURL, err := url.Parse(constants.S3PublicBucket + imageRef + "/measurements.yaml")
parsedURL, err := url.Parse(constants.S3PublicBucket + imageRef + "/measurements.json")
if err != nil {
return err
}
@ -145,7 +145,7 @@ func (f *fetchMeasurementsFlags) updateURLs(ctx context.Context, conf *config.Co
}
if f.signatureURL == nil {
parsedURL, err := url.Parse(constants.S3PublicBucket + imageRef + "/measurements.yaml.sig")
parsedURL, err := url.Parse(constants.S3PublicBucket + imageRef + "/measurements.json.sig")
if err != nil {
return err
}

View File

@ -109,17 +109,17 @@ func TestUpdateURLs(t *testing.T) {
},
},
flags: &fetchMeasurementsFlags{},
wantMeasurementsURL: constants.S3PublicBucket + "some/image/path/image-123456/measurements.yaml",
wantMeasurementsSigURL: constants.S3PublicBucket + "some/image/path/image-123456/measurements.yaml.sig",
wantMeasurementsURL: constants.S3PublicBucket + "some/image/path/image-123456/measurements.json",
wantMeasurementsSigURL: constants.S3PublicBucket + "some/image/path/image-123456/measurements.json.sig",
},
"both set by user": {
conf: &config.Config{},
flags: &fetchMeasurementsFlags{
measurementsURL: urlMustParse("get.my/measurements.yaml"),
signatureURL: urlMustParse("get.my/measurements.yaml.sig"),
measurementsURL: urlMustParse("get.my/measurements.json"),
signatureURL: urlMustParse("get.my/measurements.json.sig"),
},
wantMeasurementsURL: "get.my/measurements.yaml",
wantMeasurementsSigURL: "get.my/measurements.yaml.sig",
wantMeasurementsURL: "get.my/measurements.json",
wantMeasurementsSigURL: "get.my/measurements.json.sig",
},
}
@ -164,14 +164,14 @@ func TestConfigFetchMeasurements(t *testing.T) {
signature := "MEUCIFdJ5dH6HDywxQWTUh9Bw77wMrq0mNCUjMQGYP+6QsVmAiEAmazj/L7rFGA4/Gz8y+kI5h5E5cDgc3brihvXBKF6qZA="
client := newTestClient(func(req *http.Request) *http.Response {
if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/someImage/measurements.yaml" {
if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/someImage/measurements.json" {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(measurements)),
Header: make(http.Header),
}
}
if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/someImage/measurements.yaml.sig" {
if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/someImage/measurements.json.sig" {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(signature)),

View File

@ -8,7 +8,7 @@ package cmd
import (
"context"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net"
@ -134,7 +134,7 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
KubernetesVersion: conf.KubernetesVersion,
KubernetesComponents: versions.VersionConfigs[k8sVersion].KubernetesComponents.ToProto(),
HelmDeployments: helmDeployments,
EnforcedPcrs: conf.GetEnforcedPCRs(),
EnforcedPcrs: conf.EnforcedPCRs(),
EnforceIdkeydigest: conf.EnforcesIDKeyDigest(),
ConformanceMode: flags.conformance,
}
@ -190,8 +190,8 @@ func (d *initDoer) Do(ctx context.Context) error {
func writeOutput(idFile clusterid.File, resp *initproto.InitResponse, wr io.Writer, fileHandler file.Handler) error {
fmt.Fprint(wr, "Your Constellation cluster was successfully initialized.\n\n")
ownerID := base64.StdEncoding.EncodeToString(resp.OwnerId)
clusterID := base64.StdEncoding.EncodeToString(resp.ClusterId)
ownerID := hex.EncodeToString(resp.OwnerId)
clusterID := hex.EncodeToString(resp.ClusterId)
tw := tabwriter.NewWriter(wr, 0, 0, 2, ' ', 0)
// writeRow(tw, "Constellation cluster's owner identifier", ownerID)

View File

@ -9,7 +9,7 @@ package cmd
import (
"bytes"
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"net"
@ -101,16 +101,6 @@ func TestInitialize(t *testing.T) {
initServerAPI: &stubInitServer{initErr: someErr},
wantErr: true,
},
"fail missing enforced PCR": {
provider: cloudprovider.GCP,
idFile: &clusterid.File{IP: "192.0.2.1"},
configMutator: func(c *config.Config) {
c.Provider.GCP.EnforcedMeasurements = append(c.Provider.GCP.EnforcedMeasurements, 10)
},
serviceAccKey: gcpServiceAccKey,
initServerAPI: &stubInitServer{initResp: testInitResp},
wantErr: true,
},
}
for name, tc := range testCases {
@ -174,7 +164,7 @@ func TestInitialize(t *testing.T) {
}
require.NoError(err)
// assert.Contains(out.String(), base64.StdEncoding.EncodeToString([]byte("ownerID")))
assert.Contains(out.String(), base64.StdEncoding.EncodeToString([]byte("clusterID")))
assert.Contains(out.String(), hex.EncodeToString([]byte("clusterID")))
var secret masterSecret
assert.NoError(fileHandler.ReadJSON(constants.MasterSecretFilename, &secret))
assert.NotEmpty(secret.Key)
@ -192,8 +182,8 @@ func TestWriteOutput(t *testing.T) {
Kubeconfig: []byte("kubeconfig"),
}
ownerID := base64.StdEncoding.EncodeToString(resp.OwnerId)
clusterID := base64.StdEncoding.EncodeToString(resp.ClusterId)
ownerID := hex.EncodeToString(resp.OwnerId)
clusterID := hex.EncodeToString(resp.ClusterId)
expectedIDFile := clusterid.File{
ClusterID: clusterID,
@ -361,11 +351,11 @@ func TestAttestation(t *testing.T) {
issuer := &testIssuer{
Getter: oid.QEMU{},
pcrs: measurements.M{
0: measurements.PCRWithAllBytes(0xFF),
1: measurements.PCRWithAllBytes(0xFF),
2: measurements.PCRWithAllBytes(0xFF),
3: measurements.PCRWithAllBytes(0xFF),
pcrs: map[uint32][]byte{
0: bytes.Repeat([]byte{0xFF}, 32),
1: bytes.Repeat([]byte{0xFF}, 32),
2: bytes.Repeat([]byte{0xFF}, 32),
3: bytes.Repeat([]byte{0xFF}, 32),
},
}
serverCreds := atlscredentials.New(issuer, nil)
@ -390,13 +380,13 @@ func TestAttestation(t *testing.T) {
cfg := config.Default()
cfg.Image = "image"
cfg.RemoveProviderExcept(cloudprovider.QEMU)
cfg.Provider.QEMU.Measurements[0] = measurements.PCRWithAllBytes(0x00)
cfg.Provider.QEMU.Measurements[1] = measurements.PCRWithAllBytes(0x11)
cfg.Provider.QEMU.Measurements[2] = measurements.PCRWithAllBytes(0x22)
cfg.Provider.QEMU.Measurements[3] = measurements.PCRWithAllBytes(0x33)
cfg.Provider.QEMU.Measurements[4] = measurements.PCRWithAllBytes(0x44)
cfg.Provider.QEMU.Measurements[9] = measurements.PCRWithAllBytes(0x99)
cfg.Provider.QEMU.Measurements[12] = measurements.PCRWithAllBytes(0xcc)
cfg.Provider.QEMU.Measurements[0] = measurements.WithAllBytes(0x00, false)
cfg.Provider.QEMU.Measurements[1] = measurements.WithAllBytes(0x11, false)
cfg.Provider.QEMU.Measurements[2] = measurements.WithAllBytes(0x22, false)
cfg.Provider.QEMU.Measurements[3] = measurements.WithAllBytes(0x33, false)
cfg.Provider.QEMU.Measurements[4] = measurements.WithAllBytes(0x44, false)
cfg.Provider.QEMU.Measurements[9] = measurements.WithAllBytes(0x99, false)
cfg.Provider.QEMU.Measurements[12] = measurements.WithAllBytes(0xcc, false)
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, cfg, file.OptNone))
ctx := context.Background()
@ -418,14 +408,14 @@ type testValidator struct {
func (v *testValidator) Validate(attDoc []byte, nonce []byte) ([]byte, error) {
var attestation struct {
UserData []byte
PCRs measurements.M
PCRs map[uint32][]byte
}
if err := json.Unmarshal(attDoc, &attestation); err != nil {
return nil, err
}
for k, pcr := range v.pcrs {
if !bytes.Equal(attestation.PCRs[k], pcr) {
if !bytes.Equal(attestation.PCRs[k], pcr.Expected[:]) {
return nil, errors.New("invalid PCR value")
}
}
@ -434,14 +424,14 @@ func (v *testValidator) Validate(attDoc []byte, nonce []byte) ([]byte, error) {
type testIssuer struct {
oid.Getter
pcrs measurements.M
pcrs map[uint32][]byte
}
func (i *testIssuer) Issue(userData []byte, nonce []byte) ([]byte, error) {
return json.Marshal(
struct {
UserData []byte
PCRs measurements.M
PCRs map[uint32][]byte
}{
UserData: userData,
PCRs: i.pcrs,
@ -474,21 +464,21 @@ func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, cs
conf.Provider.Azure.ResourceGroup = "test-resource-group"
conf.Provider.Azure.AppClientID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.ClientSecretValue = "test-client-secret"
conf.Provider.Azure.Measurements[4] = measurements.PCRWithAllBytes(0x44)
conf.Provider.Azure.Measurements[9] = measurements.PCRWithAllBytes(0x11)
conf.Provider.Azure.Measurements[12] = measurements.PCRWithAllBytes(0xcc)
conf.Provider.Azure.Measurements[4] = measurements.WithAllBytes(0x44, false)
conf.Provider.Azure.Measurements[9] = measurements.WithAllBytes(0x11, false)
conf.Provider.Azure.Measurements[12] = measurements.WithAllBytes(0xcc, false)
case cloudprovider.GCP:
conf.Provider.GCP.Region = "test-region"
conf.Provider.GCP.Project = "test-project"
conf.Provider.GCP.Zone = "test-zone"
conf.Provider.GCP.ServiceAccountKeyPath = "test-key-path"
conf.Provider.GCP.Measurements[4] = measurements.PCRWithAllBytes(0x44)
conf.Provider.GCP.Measurements[9] = measurements.PCRWithAllBytes(0x11)
conf.Provider.GCP.Measurements[12] = measurements.PCRWithAllBytes(0xcc)
conf.Provider.GCP.Measurements[4] = measurements.WithAllBytes(0x44, false)
conf.Provider.GCP.Measurements[9] = measurements.WithAllBytes(0x11, false)
conf.Provider.GCP.Measurements[12] = measurements.WithAllBytes(0xcc, false)
case cloudprovider.QEMU:
conf.Provider.QEMU.Measurements[4] = measurements.PCRWithAllBytes(0x44)
conf.Provider.QEMU.Measurements[9] = measurements.PCRWithAllBytes(0x11)
conf.Provider.QEMU.Measurements[12] = measurements.PCRWithAllBytes(0xcc)
conf.Provider.QEMU.Measurements[4] = measurements.WithAllBytes(0x44, false)
conf.Provider.QEMU.Measurements[9] = measurements.WithAllBytes(0x11, false)
conf.Provider.QEMU.Measurements[12] = measurements.WithAllBytes(0xcc, false)
}
conf.RemoveProviderExcept(csp)

View File

@ -181,12 +181,12 @@ func getCompatibleImages(csp cloudprovider.Provider, currentVersion string, imag
// getCompatibleImageMeasurements retrieves the expected measurements for each image.
func getCompatibleImageMeasurements(ctx context.Context, cmd *cobra.Command, client *http.Client, rekor rekorVerifier, pubK []byte, images map[string]config.UpgradeConfig) error {
for idx, img := range images {
measurementsURL, err := url.Parse(constants.S3PublicBucket + strings.ToLower(img.Image) + "/measurements.yaml")
measurementsURL, err := url.Parse(constants.S3PublicBucket + strings.ToLower(img.Image) + "/measurements.json")
if err != nil {
return err
}
signatureURL, err := url.Parse(constants.S3PublicBucket + strings.ToLower(img.Image) + "/measurements.yaml.sig")
signatureURL, err := url.Parse(constants.S3PublicBucket + strings.ToLower(img.Image) + "/measurements.json.sig")
if err != nil {
return err
}

View File

@ -25,7 +25,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -248,14 +248,14 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
}
client := newTestClient(func(req *http.Request) *http.Response {
if strings.HasSuffix(req.URL.String(), "/measurements.yaml") {
if strings.HasSuffix(req.URL.String(), "/measurements.json") {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n")),
Header: make(http.Header),
}
}
if strings.HasSuffix(req.URL.String(), "/measurements.yaml.sig") {
if strings.HasSuffix(req.URL.String(), "/measurements.json.sig") {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("MEUCIBs1g2/n0FsgPfJ+0uLD5TaunGhxwDcQcUGBroejKvg3AiEAzZtcLU9O6IiVhxB8tBS+ty6MXoPNwL8WRWMzyr35eKI=")),
@ -470,14 +470,14 @@ func TestUpgradePlan(t *testing.T) {
Header: make(http.Header),
}
}
if strings.HasSuffix(req.URL.String(), "/measurements.yaml") {
if strings.HasSuffix(req.URL.String(), "/measurements.json") {
return &http.Response{
StatusCode: tc.measurementsFetchStatus,
Body: io.NopCloser(strings.NewReader("0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n")),
Header: make(http.Header),
}
}
if strings.HasSuffix(req.URL.String(), "/measurements.yaml.sig") {
if strings.HasSuffix(req.URL.String(), "/measurements.json.sig") {
return &http.Response{
StatusCode: tc.measurementsFetchStatus,
Body: io.NopCloser(strings.NewReader("MEUCIBs1g2/n0FsgPfJ+0uLD5TaunGhxwDcQcUGBroejKvg3AiEAzZtcLU9O6IiVhxB8tBS+ty6MXoPNwL8WRWMzyr35eKI=")),

View File

@ -5,7 +5,6 @@ metadata:
namespace: {{ .Release.Namespace }}
data:
# mustToJson is required so the json-strings passed from go are of type string in the rendered yaml.
enforcedPCRs: {{ .Values.enforcedPCRs | mustToJson }}
measurements: {{ .Values.measurements | mustToJson }}
{{- if eq .Values.csp "Azure" }}
# ConfigMap.data is of type map[string]string. quote will not quote a quoted string.

View File

@ -5,15 +5,10 @@
"description": "CSP to which the chart is deployed.",
"enum": ["Azure", "GCP", "AWS", "QEMU"]
},
"enforcedPCRs": {
"description": "JSON-string to describe the enforced PCRs.",
"type": "string",
"examples": ["[1, 15]"]
},
"measurements": {
"description": "JSON-string to describe the expected measurements.",
"type": "string",
"examples": ["{'1':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','15':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='}"]
"examples": ["{'1':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','warnOnly':true},'15':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=','warnOnly':true}}"]
},
"enforceIdKeyDigest": {
"description": "Whether or not idkeydigest should be enforced during attestation on azure.",
@ -37,7 +32,6 @@
},
"required": [
"csp",
"enforcedPCRs",
"measurements",
"measurementSalt",
"image"

View File

@ -76,9 +76,7 @@ func New(csp cloudprovider.Provider, k8sVersion versions.ValidK8sVersion) *Chart
// Load the embedded helm charts.
func (i *ChartLoader) Load(config *config.Config, conformanceMode bool, masterSecret, salt []byte) ([]byte, error) {
csp := config.GetProvider()
ciliumRelease, err := i.loadCilium(csp, conformanceMode)
ciliumRelease, err := i.loadCilium(config.GetProvider(), conformanceMode)
if err != nil {
return nil, fmt.Errorf("loading cilium: %w", err)
}
@ -88,7 +86,7 @@ func (i *ChartLoader) Load(config *config.Config, conformanceMode bool, masterSe
return nil, fmt.Errorf("loading cilium: %w", err)
}
operatorRelease, err := i.loadOperators(csp)
operatorRelease, err := i.loadOperators(config.GetProvider())
if err != nil {
return nil, fmt.Errorf("loading operators: %w", err)
}
@ -350,11 +348,6 @@ func (i *ChartLoader) loadConstellationServicesHelper(config *config.Config, mas
return nil, nil, fmt.Errorf("loading constellation-services chart: %w", err)
}
enforcedPCRsJSON, err := json.Marshal(config.GetEnforcedPCRs())
if err != nil {
return nil, nil, fmt.Errorf("marshaling enforcedPCRs: %w", err)
}
csp := config.GetProvider()
values := map[string]any{
"global": map[string]any{
@ -374,9 +367,8 @@ func (i *ChartLoader) loadConstellationServicesHelper(config *config.Config, mas
"measurementsFilename": constants.MeasurementsFilename,
},
"join-service": map[string]any{
"csp": csp.String(),
"enforcedPCRs": string(enforcedPCRsJSON),
"image": i.joinServiceImage,
"csp": csp.String(),
"image": i.joinServiceImage,
},
"ccm": map[string]any{
"csp": csp.String(),

View File

@ -15,6 +15,7 @@ import (
"path"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/deploy/helm"
@ -56,8 +57,7 @@ func TestConstellationServices(t *testing.T) {
}{
"GCP": {
config: &config.Config{Provider: config.ProviderConfig{GCP: &config.GCPConfig{
DeployCSIDriver: func() *bool { b := true; return &b }(),
EnforcedMeasurements: []uint32{1, 11},
DeployCSIDriver: func() *bool { b := true; return &b }(),
}}},
enforceIDKeyDigest: false,
valuesModifier: prepareGCPValues,
@ -65,9 +65,8 @@ func TestConstellationServices(t *testing.T) {
},
"Azure": {
config: &config.Config{Provider: config.ProviderConfig{Azure: &config.AzureConfig{
DeployCSIDriver: func() *bool { b := true; return &b }(),
EnforcedMeasurements: []uint32{1, 11},
EnforceIDKeyDigest: func() *bool { b := true; return &b }(),
DeployCSIDriver: func() *bool { b := true; return &b }(),
EnforceIDKeyDigest: func() *bool { b := true; return &b }(),
}}},
enforceIDKeyDigest: true,
valuesModifier: prepareAzureValues,
@ -75,9 +74,7 @@ func TestConstellationServices(t *testing.T) {
cnmImage: "cnmImageForAzure",
},
"QEMU": {
config: &config.Config{Provider: config.ProviderConfig{QEMU: &config.QEMUConfig{
EnforcedMeasurements: []uint32{1, 11},
}}},
config: &config.Config{Provider: config.ProviderConfig{QEMU: &config.QEMUConfig{}}},
enforceIDKeyDigest: false,
valuesModifier: prepareQEMUValues,
},
@ -88,7 +85,14 @@ func TestConstellationServices(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
chartLoader := ChartLoader{joinServiceImage: "joinServiceImage", kmsImage: "kmsImage", ccmImage: tc.ccmImage, cnmImage: tc.cnmImage, autoscalerImage: "autoscalerImage", verificationServiceImage: "verificationImage"}
chartLoader := ChartLoader{
joinServiceImage: "joinServiceImage",
kmsImage: "kmsImage",
ccmImage: tc.ccmImage,
cnmImage: tc.cnmImage,
autoscalerImage: "autoscalerImage",
verificationServiceImage: "verificationImage",
}
chart, values, err := chartLoader.loadConstellationServicesHelper(tc.config, []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
require.NoError(err)
@ -197,7 +201,15 @@ func prepareGCPValues(values map[string]any) error {
if !ok {
return errors.New("missing 'join-service' key")
}
joinVals["measurements"] = "{'1':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','15':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='}"
m := measurements.M{
1: measurements.WithAllBytes(0xAA, false),
}
mJSON, err := json.Marshal(m)
if err != nil {
return err
}
joinVals["measurements"] = string(mJSON)
joinVals["measurementSalt"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
ccmVals, ok := values["ccm"].(map[string]any)
@ -269,7 +281,12 @@ func prepareAzureValues(values map[string]any) error {
return errors.New("missing 'join-service' key")
}
joinVals["idkeydigest"] = "baaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaad"
joinVals["measurements"] = "{'1':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','15':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='}"
m := measurements.M{1: measurements.WithAllBytes(0xAA, false)}
mJSON, err := json.Marshal(m)
if err != nil {
return err
}
joinVals["measurements"] = string(mJSON)
joinVals["measurementSalt"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
ccmVals, ok := values["ccm"].(map[string]any)
@ -311,7 +328,12 @@ func prepareQEMUValues(values map[string]any) error {
if !ok {
return errors.New("missing 'join-service' key")
}
joinVals["measurements"] = "{'1':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','15':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='}"
m := measurements.M{1: measurements.WithAllBytes(0xAA, false)}
mJSON, err := json.Marshal(m)
if err != nil {
return err
}
joinVals["measurements"] = string(mJSON)
joinVals["measurementSalt"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
verificationVals, ok := values["verification-service"].(map[string]any)

View File

@ -4,8 +4,7 @@ metadata:
name: join-config
namespace: testNamespace
data:
enforcedPCRs: "[1,11]"
measurements: "{'1':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','15':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='}"
measurements: "{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}"
enforceIdKeyDigest: "true"
idkeydigest: "baaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaad"
binaryData:

View File

@ -4,7 +4,6 @@ metadata:
name: join-config
namespace: testNamespace
data:
enforcedPCRs: "[1,11]"
measurements: "{'1':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','15':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='}"
measurements: "{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}"
binaryData:
measurementSalt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

View File

@ -4,7 +4,6 @@ metadata:
name: join-config
namespace: testNamespace
data:
enforcedPCRs: "[1,11]"
measurements: "{'1':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','15':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='}"
measurements: "{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}"
binaryData:
measurementSalt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

View File

@ -171,7 +171,7 @@ func main() {
// We can use this to calculate the PCRs of the image locally.
func exportPCRs() error {
// get TPM state
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, tpmClient.FullPcrSel(tpm2.AlgSHA256))
pcrs, err := vtpm.GetSelectedMeasurements(vtpm.OpenVTPM, tpmClient.FullPcrSel(tpm2.AlgSHA256))
if err != nil {
return err
}

2
go.mod
View File

@ -92,7 +92,6 @@ require (
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6
google.golang.org/grpc v1.51.0
google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm v2.17.0+incompatible
helm.sh/helm/v3 v3.10.2
@ -119,6 +118,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (

View File

@ -41,7 +41,6 @@ require (
github.com/go-git/go-git/v5 v5.4.2
github.com/google/go-tpm-tools v0.3.9
github.com/google/uuid v1.3.0
github.com/spf13/afero v1.9.3
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1
go.uber.org/goleak v1.2.0
@ -189,6 +188,7 @@ require (
github.com/sigstore/rekor v1.0.1 // indirect
github.com/sigstore/sigstore v1.4.5 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/talos-systems/talos/pkg/machinery v1.2.7 // indirect
github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect

View File

@ -24,24 +24,24 @@ import (
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/crypto"
"github.com/edgelesssys/constellation/v2/verify/verifyproto"
"github.com/spf13/afero"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gopkg.in/yaml.v3"
)
var (
coordIP = flag.String("constell-ip", "", "Public IP of the Constellation")
port = flag.String("constell-port", strconv.Itoa(constants.VerifyServiceNodePortGRPC), "NodePort of the Constellation's verification service")
export = flag.String("o", "", "Write PCRs, formatted as Go code, to file")
format = flag.String("format", "json", "Output format: json, yaml (default json)")
quiet = flag.Bool("q", false, "Set to disable output")
timeout = flag.Duration("timeout", 2*time.Minute, "Wait this duration for the verification service to become available")
)
func main() {
coordIP := flag.String("constell-ip", "", "Public IP of the Constellation")
port := flag.String("constell-port", strconv.Itoa(constants.VerifyServiceNodePortGRPC), "NodePort of the Constellation's verification service")
format := flag.String("format", "json", "Output format: json, yaml (default json)")
quiet := flag.Bool("q", false, "Set to disable output")
timeout := flag.Duration("timeout", 2*time.Minute, "Wait this duration for the verification service to become available")
flag.Parse()
if *coordIP == "" || *port == "" {
flag.Usage()
os.Exit(1)
}
addr := net.JoinHostPort(*coordIP, *port)
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
@ -51,18 +51,13 @@ func main() {
log.Fatal(err)
}
pcrs, err := validatePCRAttDoc(attDocRaw)
measurements, err := validatePCRAttDoc(attDocRaw)
if err != nil {
log.Fatal(err)
}
if !*quiet {
if err := printPCRs(os.Stdout, pcrs, *format); err != nil {
log.Fatal(err)
}
}
if *export != "" {
if err := exportToFile(*export, pcrs, &afero.Afero{Fs: afero.NewOsFs()}); err != nil {
if err := printPCRs(os.Stdout, measurements, *format); err != nil {
log.Fatal(err)
}
}
@ -104,16 +99,23 @@ func validatePCRAttDoc(attDocRaw []byte) (measurements.M, error) {
if err != nil {
return nil, err
}
m := measurements.M{}
for idx, pcr := range attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs {
if len(pcr) != 32 {
return nil, fmt.Errorf("incomplete PCR at index: %d", idx)
}
m[idx] = measurements.Measurement{
Expected: *(*[32]byte)(pcr),
WarnOnly: true,
}
}
return attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs, nil
return m, nil
}
// printPCRs formates and prints PCRs to the given writer.
// format can be one of 'json' or 'yaml'. If it doesnt match defaults to 'json'.
// printPCRs formats and prints PCRs to the given writer.
// format can be one of 'json' or 'yaml'. If it doesn't match defaults to 'json'.
func printPCRs(w io.Writer, pcrs measurements.M, format string) error {
switch format {
case "json":
@ -142,24 +144,3 @@ func printPCRsJSON(w io.Writer, pcrs measurements.M) error {
fmt.Fprintf(w, "%s", string(pcrJSON))
return nil
}
// exportToFile writes pcrs to a file, formatted to be valid Go code.
// Validity of the PCR map is not checked, and should be handled by the caller.
func exportToFile(path string, pcrs measurements.M, fs *afero.Afero) error {
goCode := `package pcrs
var pcrs = map[uint32][]byte{%s
}
`
pcrsFormatted := ""
for i := 0; i < len(pcrs); i++ {
pcrHex := fmt.Sprintf("%#02X", pcrs[uint32(i)][0])
for j := 1; j < len(pcrs[uint32(i)]); j++ {
pcrHex = fmt.Sprintf("%s, %#02X", pcrHex, pcrs[uint32(i)][j])
}
pcrsFormatted = pcrsFormatted + fmt.Sprintf("\n\t%d: {%s},", i, pcrHex)
}
return fs.WriteFile(path, []byte(fmt.Sprintf(goCode, pcrsFormatted)), 0o644)
}

View File

@ -8,7 +8,7 @@ package main
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"testing"
@ -17,67 +17,13 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/google/go-tpm-tools/proto/attest"
"github.com/google/go-tpm-tools/proto/tpm"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m,
// https://github.com/census-instrumentation/opencensus-go/issues/1262
goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
)
}
func TestExportToFile(t *testing.T) {
testCases := map[string]struct {
pcrs measurements.M
fs *afero.Afero
wantErr bool
}{
"file not writeable": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
fs: &afero.Afero{Fs: afero.NewReadOnlyFs(afero.NewMemMapFs())},
wantErr: true,
},
"file writeable": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
fs: &afero.Afero{Fs: afero.NewMemMapFs()},
wantErr: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
path := "test-file"
err := exportToFile(path, tc.pcrs, tc.fs)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
content, err := tc.fs.ReadFile(path)
require.NoError(err)
for _, pcr := range tc.pcrs {
for _, register := range pcr {
assert.Contains(string(content), fmt.Sprintf("%#02X", register))
}
}
}
})
}
goleak.VerifyTestMain(m)
}
func TestValidatePCRAttDoc(t *testing.T) {
@ -106,7 +52,7 @@ func TestValidatePCRAttDoc(t *testing.T) {
{
Pcrs: &tpm.PCRs{
Hash: tpm.HashAlgo_SHA256,
Pcrs: measurements.M{
Pcrs: map[uint32][]byte{
0: {0x1, 0x2, 0x3},
},
},
@ -123,8 +69,8 @@ func TestValidatePCRAttDoc(t *testing.T) {
{
Pcrs: &tpm.PCRs{
Hash: tpm.HashAlgo_SHA256,
Pcrs: measurements.M{
0: measurements.PCRWithAllBytes(0xAA),
Pcrs: map[uint32][]byte{
0: bytes.Repeat([]byte{0xAA}, 32),
},
},
},
@ -150,7 +96,10 @@ func TestValidatePCRAttDoc(t *testing.T) {
require.NoError(json.Unmarshal(tc.attDocRaw, &attDoc))
qIdx, err := vtpm.GetSHA256QuoteIndex(attDoc.Attestation.Quotes)
require.NoError(err)
assert.EqualValues(attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs, pcrs)
for pcrIdx, pcrVal := range pcrs {
assert.Equal(pcrVal.Expected[:], attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs[pcrIdx])
}
}
})
}
@ -164,31 +113,15 @@ func mustMarshalAttDoc(t *testing.T, attDoc vtpm.AttestationDocument) []byte {
func TestPrintPCRs(t *testing.T) {
testCases := map[string]struct {
pcrs measurements.M
format string
}{
"json": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
format: "json",
},
"empty format": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
format: "",
},
"yaml": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
format: "yaml",
},
}
@ -197,13 +130,19 @@ func TestPrintPCRs(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
pcrs := measurements.M{
0: measurements.WithAllBytes(0xAA, true),
1: measurements.WithAllBytes(0xBB, true),
2: measurements.WithAllBytes(0xCC, true),
}
var out bytes.Buffer
err := printPCRs(&out, tc.pcrs, tc.format)
err := printPCRs(&out, pcrs, tc.format)
assert.NoError(err)
for idx, pcr := range tc.pcrs {
for idx, pcr := range pcrs {
assert.Contains(out.String(), fmt.Sprintf("%d", idx))
assert.Contains(out.String(), base64.StdEncoding.EncodeToString(pcr))
assert.Contains(out.String(), hex.EncodeToString(pcr.Expected[:]))
}
})
}

View File

@ -307,12 +307,12 @@ func TestExportPCRs(t *testing.T) {
remoteAddr: "192.0.100.1:1234",
connect: defaultConnect,
method: http.MethodPost,
message: mustMarshal(t, measurements.M{0: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")}),
message: mustMarshal(t, measurements.M{0: measurements.WithAllBytes(0xAA, false)}),
},
"incorrect method": {
remoteAddr: "192.0.100.1:1234",
connect: defaultConnect,
message: mustMarshal(t, measurements.M{0: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")}),
message: mustMarshal(t, measurements.M{0: measurements.WithAllBytes(0xAA, false)}),
method: http.MethodGet,
wantErr: true,
},
@ -321,7 +321,7 @@ func TestExportPCRs(t *testing.T) {
connect: &stubConnect{
getNetworkErr: errors.New("error"),
},
message: mustMarshal(t, measurements.M{0: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")}),
message: mustMarshal(t, measurements.M{0: measurements.WithAllBytes(0xAA, false)}),
method: http.MethodPost,
wantErr: true,
},
@ -336,7 +336,7 @@ func TestExportPCRs(t *testing.T) {
remoteAddr: "localhost",
connect: defaultConnect,
method: http.MethodPost,
message: mustMarshal(t, measurements.M{0: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")}),
message: mustMarshal(t, measurements.M{0: measurements.WithAllBytes(0xAA, false)}),
wantErr: true,
},
}

View File

@ -29,11 +29,10 @@ type Validator struct {
}
// NewValidator create a new Validator structure and returns it.
func NewValidator(pcrs measurements.M, enforcedPCRs []uint32, log vtpm.AttestationLogger) *Validator {
func NewValidator(pcrs measurements.M, log vtpm.AttestationLogger) *Validator {
v := &Validator{}
v.Validator = vtpm.NewValidator(
pcrs,
enforcedPCRs,
getTrustedKey,
v.tpmEnabled,
vtpm.VerifyPKCS1v15,

View File

@ -42,11 +42,10 @@ type Validator struct {
}
// NewValidator initializes a new Azure validator with the provided PCR values.
func NewValidator(pcrs measurements.M, enforcedPCRs []uint32, idKeyDigest []byte, enforceIDKeyDigest bool, log vtpm.AttestationLogger) *Validator {
func NewValidator(pcrs measurements.M, idKeyDigest []byte, enforceIDKeyDigest bool, log vtpm.AttestationLogger) *Validator {
return &Validator{
Validator: vtpm.NewValidator(
pcrs,
enforcedPCRs,
getTrustedKey(&azureInstanceInfo{}, idKeyDigest, enforceIDKeyDigest, log),
validateCVM,
vtpm.VerifyPKCS1v15,

View File

@ -189,7 +189,7 @@ func TestGetAttestationCert(t *testing.T) {
}
require.NoError(err)
validator := NewValidator(measurements.M{}, []uint32{}, nil)
validator := NewValidator(measurements.M{}, nil)
cert, err := x509.ParseCertificate(rootCert.Raw)
require.NoError(err)
roots := x509.NewCertPool()

View File

@ -33,13 +33,12 @@ type Validator struct {
}
// NewValidator initializes a new Azure validator with the provided PCR values.
func NewValidator(pcrs measurements.M, enforcedPCRs []uint32, log vtpm.AttestationLogger) *Validator {
func NewValidator(pcrs measurements.M, log vtpm.AttestationLogger) *Validator {
rootPool := x509.NewCertPool()
rootPool.AddCert(ameRoot)
v := &Validator{roots: rootPool}
v.Validator = vtpm.NewValidator(
pcrs,
enforcedPCRs,
v.verifyAttestationKey,
validateVM,
vtpm.VerifyPKCS1v15,

View File

@ -35,11 +35,10 @@ type Validator struct {
}
// NewValidator initializes a new GCP validator with the provided PCR values.
func NewValidator(pcrs measurements.M, enforcedPCRs []uint32, log vtpm.AttestationLogger) *Validator {
func NewValidator(pcrs measurements.M, log vtpm.AttestationLogger) *Validator {
return &Validator{
Validator: vtpm.NewValidator(
pcrs,
enforcedPCRs,
trustedKeyFromGCEAPI(newInstanceClient),
gceNonHostInfoEvent,
vtpm.VerifyPKCS1v15,

View File

@ -12,61 +12,31 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
"gopkg.in/yaml.v2"
"github.com/google/go-tpm/tpmutil"
"go.uber.org/multierr"
"gopkg.in/yaml.v3"
)
const (
// PCRIndexClusterID is a PCR we extend to mark the node as initialized.
// The value used to extend is a random generated 32 Byte value.
PCRIndexClusterID = tpmutil.Handle(15)
// PCRIndexOwnerID is a PCR we extend to mark the node as initialized.
// The value used to extend is derived from Constellation's master key.
// TODO: move to stable, non-debug PCR before use.
PCRIndexOwnerID = tpmutil.Handle(16)
)
// M are Platform Configuration Register (PCR) values that make up the Measurements.
type M map[uint32][]byte
// PCRWithAllBytes returns a PCR value where all 32 bytes are set to b.
func PCRWithAllBytes(b byte) []byte {
return bytes.Repeat([]byte{b}, 32)
}
// DefaultsFor provides the default measurements for given cloud provider.
func DefaultsFor(provider cloudprovider.Provider) M {
switch provider {
case cloudprovider.AWS:
return M{
8: PCRWithAllBytes(0x00),
11: PCRWithAllBytes(0x00),
13: PCRWithAllBytes(0x00),
uint32(vtpm.PCRIndexClusterID): PCRWithAllBytes(0x00),
}
case cloudprovider.Azure:
return M{
8: PCRWithAllBytes(0x00),
11: PCRWithAllBytes(0x00),
13: PCRWithAllBytes(0x00),
uint32(vtpm.PCRIndexClusterID): PCRWithAllBytes(0x00),
}
case cloudprovider.GCP:
return M{
0: {0x0F, 0x35, 0xC2, 0x14, 0x60, 0x8D, 0x93, 0xC7, 0xA6, 0xE6, 0x8A, 0xE7, 0x35, 0x9B, 0x4A, 0x8B, 0xE5, 0xA0, 0xE9, 0x9E, 0xEA, 0x91, 0x07, 0xEC, 0xE4, 0x27, 0xC4, 0xDE, 0xA4, 0xE4, 0x39, 0xCF},
8: PCRWithAllBytes(0x00),
11: PCRWithAllBytes(0x00),
13: PCRWithAllBytes(0x00),
uint32(vtpm.PCRIndexClusterID): PCRWithAllBytes(0x00),
}
case cloudprovider.QEMU:
return M{
8: PCRWithAllBytes(0x00),
11: PCRWithAllBytes(0x00),
13: PCRWithAllBytes(0x00),
uint32(vtpm.PCRIndexClusterID): PCRWithAllBytes(0x00),
}
default:
return nil
}
}
type M map[uint32]Measurement
// FetchAndVerify fetches measurement and signature files via provided URLs,
// using client for download. The publicKey is used to verify the measurements.
@ -83,8 +53,14 @@ func (m *M) FetchAndVerify(ctx context.Context, client *http.Client, measurement
if err := sigstore.VerifySignature(measurements, signature, publicKey); err != nil {
return "", err
}
if err := yaml.NewDecoder(bytes.NewReader(measurements)).Decode(&m); err != nil {
return "", err
if err := json.Unmarshal(measurements, m); err != nil {
if yamlErr := yaml.Unmarshal(measurements, m); yamlErr != nil {
return "", multierr.Append(
err,
fmt.Errorf("trying yaml format: %w", yamlErr),
)
}
}
shaHash := sha256.Sum256(measurements)
@ -94,58 +70,231 @@ func (m *M) FetchAndVerify(ctx context.Context, client *http.Client, measurement
// CopyFrom copies over all values from other. Overwriting existing values,
// but keeping not specified values untouched.
func (m M) CopyFrom(other M) {
func (m *M) CopyFrom(other M) {
for idx := range other {
m[idx] = other[idx]
(*m)[idx] = other[idx]
}
}
// EqualTo tests whether the provided other Measurements are equal to these
// measurements.
func (m M) EqualTo(other M) bool {
if len(m) != len(other) {
func (m *M) EqualTo(other M) bool {
if len(*m) != len(other) {
return false
}
for k, v := range m {
if !bytes.Equal(v, other[k]) {
for k, v := range *m {
otherExpected := other[k].Expected
if !bytes.Equal(v.Expected[:], otherExpected[:]) {
return false
}
if v.WarnOnly != other[k].WarnOnly {
return false
}
}
return true
}
// MarshalYAML overwrites the default behaviour of writing out []byte not as
// single bytes, but as a single base64 encoded string.
func (m M) MarshalYAML() (any, error) {
base64Map := make(map[uint32]string)
for key, value := range m {
base64Map[key] = base64.StdEncoding.EncodeToString(value[:])
// GetEnforced returns a list of all enforced Measurements,
// i.e. all Measurements that are not marked as WarnOnly.
func (m *M) GetEnforced() []uint32 {
var enforced []uint32
for idx, measurement := range *m {
if !measurement.WarnOnly {
enforced = append(enforced, idx)
}
}
return base64Map, nil
return enforced
}
// UnmarshalYAML overwrites the default behaviour of reading []byte not as
// single bytes, but as a single base64 encoded string.
func (m *M) UnmarshalYAML(unmarshal func(any) error) error {
base64Map := make(map[uint32]string)
err := unmarshal(base64Map)
if err != nil {
return err
// SetEnforced sets the WarnOnly flag to true for all Measurements
// that are NOT included in the provided list of enforced measurements.
func (m *M) SetEnforced(enforced []uint32) error {
newM := make(M)
// set all measurements to warn only
for idx, measurement := range *m {
newM[idx] = Measurement{
Expected: measurement.Expected,
WarnOnly: true,
}
}
*m = make(M)
for key, value := range base64Map {
measurement, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return err
// set enforced measurements from list
for _, idx := range enforced {
measurement, ok := newM[idx]
if !ok {
return fmt.Errorf("measurement %d not in list, but set to enforced", idx)
}
(*m)[key] = measurement
measurement.WarnOnly = false
newM[idx] = measurement
}
*m = newM
return nil
}
// Measurement wraps expected PCR value and whether it is enforced.
type Measurement struct {
// Expected measurement value.
Expected [32]byte `json:"expected" yaml:"expected"`
// WarnOnly if set to true, a mismatching measurement will only result in a warning.
WarnOnly bool `json:"warnOnly" yaml:"warnOnly"`
}
// UnmarshalJSON reads a Measurement either as json object,
// or as a simple hex or base64 encoded string.
func (m *Measurement) UnmarshalJSON(b []byte) error {
var eM encodedMeasurement
if err := json.Unmarshal(b, &eM); err != nil {
// Unmarshalling failed, Measurement might be in legacy format,
// meaning a simple string instead of Measurement struct.
// TODO: remove with v2.4.0
if legacyErr := json.Unmarshal(b, &eM.Expected); legacyErr != nil {
return multierr.Append(
err,
fmt.Errorf("trying legacy format: %w", legacyErr),
)
}
}
if err := m.unmarshal(eM); err != nil {
return fmt.Errorf("unmarshalling json: %w", err)
}
return nil
}
// MarshalJSON writes out a Measurement with Expected encoded as a hex string.
func (m Measurement) MarshalJSON() ([]byte, error) {
return json.Marshal(encodedMeasurement{
Expected: hex.EncodeToString(m.Expected[:]),
WarnOnly: m.WarnOnly,
})
}
// UnmarshalYAML reads a Measurement either as yaml object,
// or as a simple hex or base64 encoded string.
func (m *Measurement) UnmarshalYAML(unmarshal func(any) error) error {
var eM encodedMeasurement
if err := unmarshal(&eM); err != nil {
// Unmarshalling failed, Measurement might be in legacy format,
// meaning a simple string instead of Measurement struct.
// TODO: remove with v2.4.0
if legacyErr := unmarshal(&eM.Expected); legacyErr != nil {
return multierr.Append(
err,
fmt.Errorf("trying legacy format: %w", legacyErr),
)
}
}
if err := m.unmarshal(eM); err != nil {
return fmt.Errorf("unmarshalling yaml: %w", err)
}
return nil
}
// MarshalYAML writes out a Measurement with Expected encoded as a hex string.
func (m Measurement) MarshalYAML() (any, error) {
return encodedMeasurement{
Expected: hex.EncodeToString(m.Expected[:]),
WarnOnly: m.WarnOnly,
}, nil
}
// unmarshal parses a hex or base64 encoded Measurement.
func (m *Measurement) unmarshal(eM encodedMeasurement) error {
expected, err := hex.DecodeString(eM.Expected)
if err != nil {
// expected value might be in base64 legacy format
// TODO: Remove with v2.4.0
hexErr := err
expected, err = base64.StdEncoding.DecodeString(eM.Expected)
if err != nil {
return multierr.Append(
fmt.Errorf("invalid measurement: not a hex string %w", hexErr),
fmt.Errorf("not a base64 string: %w", err),
)
}
}
if len(expected) != 32 {
return fmt.Errorf("invalid measurement: invalid length: %d", len(expected))
}
m.Expected = *(*[32]byte)(expected)
m.WarnOnly = eM.WarnOnly
return nil
}
// WithAllBytes returns a measurement value where all 32 bytes are set to b.
func WithAllBytes(b byte, warnOnly bool) Measurement {
return Measurement{
Expected: *(*[32]byte)(bytes.Repeat([]byte{b}, 32)),
WarnOnly: warnOnly,
}
}
// DefaultsFor provides the default measurements for given cloud provider.
func DefaultsFor(provider cloudprovider.Provider) M {
switch provider {
case cloudprovider.AWS:
return M{
4: PlaceHolderMeasurement(),
8: WithAllBytes(0x00, false),
9: PlaceHolderMeasurement(),
11: WithAllBytes(0x00, false),
12: PlaceHolderMeasurement(),
13: WithAllBytes(0x00, false),
uint32(PCRIndexClusterID): WithAllBytes(0x00, false),
}
case cloudprovider.Azure:
return M{
4: PlaceHolderMeasurement(),
8: WithAllBytes(0x00, false),
9: PlaceHolderMeasurement(),
11: WithAllBytes(0x00, false),
12: PlaceHolderMeasurement(),
13: WithAllBytes(0x00, false),
uint32(PCRIndexClusterID): WithAllBytes(0x00, false),
}
case cloudprovider.GCP:
return M{
0: {
Expected: [32]byte{0x0F, 0x35, 0xC2, 0x14, 0x60, 0x8D, 0x93, 0xC7, 0xA6, 0xE6, 0x8A, 0xE7, 0x35, 0x9B, 0x4A, 0x8B, 0xE5, 0xA0, 0xE9, 0x9E, 0xEA, 0x91, 0x07, 0xEC, 0xE4, 0x27, 0xC4, 0xDE, 0xA4, 0xE4, 0x39, 0xCF},
WarnOnly: false,
},
4: PlaceHolderMeasurement(),
8: WithAllBytes(0x00, false),
9: PlaceHolderMeasurement(),
11: WithAllBytes(0x00, false),
12: PlaceHolderMeasurement(),
13: WithAllBytes(0x00, false),
uint32(PCRIndexClusterID): WithAllBytes(0x00, false),
}
case cloudprovider.QEMU:
return M{
4: PlaceHolderMeasurement(),
8: WithAllBytes(0x00, false),
9: PlaceHolderMeasurement(),
11: WithAllBytes(0x00, false),
12: PlaceHolderMeasurement(),
13: WithAllBytes(0x00, false),
uint32(PCRIndexClusterID): WithAllBytes(0x00, false),
}
default:
return nil
}
}
// PlaceHolderMeasurement returns a measurement with placeholder values for Expected.
func PlaceHolderMeasurement() Measurement {
return Measurement{
Expected: *(*[32]byte)(bytes.Repeat([]byte{0x12, 0x34}, 16)),
WarnOnly: false,
}
}
func getFromURL(ctx context.Context, client *http.Client, sourceURL *url.URL) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL.String(), http.NoBody)
if err != nil {
@ -166,3 +315,8 @@ func getFromURL(ctx context.Context, client *http.Client, sourceURL *url.URL) ([
}
return content, nil
}
type encodedMeasurement struct {
Expected string `json:"expected" yaml:"expected"`
WarnOnly bool `json:"warnOnly" yaml:"warnOnly"`
}

View File

@ -8,7 +8,7 @@ package measurements
import (
"context"
"errors"
"encoding/json"
"io"
"net/http"
"net/url"
@ -17,32 +17,29 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestMarshalYAML(t *testing.T) {
func TestMarshal(t *testing.T) {
testCases := map[string]struct {
measurements M
wantBase64Map map[uint32]string
m Measurement
wantYAML string
wantJSON string
}{
"valid measurements": {
measurements: M{
2: []byte{253, 93, 233, 223, 53, 14, 59, 196, 65, 10, 192, 107, 191, 229, 204, 222, 185, 63, 83, 185, 239, 81, 35, 159, 117, 44, 230, 157, 188, 96, 15, 53},
3: []byte{213, 164, 73, 109, 33, 222, 201, 165, 37, 141, 219, 25, 198, 254, 181, 59, 180, 211, 192, 70, 63, 230, 7, 242, 72, 141, 223, 79, 16, 6, 239, 158},
},
wantBase64Map: map[uint32]string{
2: "/V3p3zUOO8RBCsBrv+XM3rk/U7nvUSOfdSzmnbxgDzU=",
3: "1aRJbSHeyaUljdsZxv61O7TTwEY/5gfySI3fTxAG754=",
"measurement": {
m: Measurement{
Expected: [32]byte{253, 93, 233, 223, 53, 14, 59, 196, 65, 10, 192, 107, 191, 229, 204, 222, 185, 63, 83, 185, 239, 81, 35, 159, 117, 44, 230, 157, 188, 96, 15, 53},
},
wantYAML: "expected: \"fd5de9df350e3bc4410ac06bbfe5ccdeb93f53b9ef51239f752ce69dbc600f35\"\nwarnOnly: false",
wantJSON: `{"expected":"fd5de9df350e3bc4410ac06bbfe5ccdeb93f53b9ef51239f752ce69dbc600f35","warnOnly":false}`,
},
"omit bytes": {
measurements: M{
2: []byte{},
3: []byte{1, 2, 3, 4},
},
wantBase64Map: map[uint32]string{
2: "",
3: "AQIDBA==",
"warn only": {
m: Measurement{
Expected: [32]byte{1, 2, 3, 4}, // implicitly padded with 0s
WarnOnly: true,
},
wantYAML: "expected: \"0102030400000000000000000000000000000000000000000000000000000000\"\nwarnOnly: true",
wantJSON: `{"expected":"0102030400000000000000000000000000000000000000000000000000000000","warnOnly":true}`,
},
}
@ -51,63 +48,99 @@ func TestMarshalYAML(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
base64Map, err := tc.measurements.MarshalYAML()
require.NoError(err)
{
// YAML
yaml, err := yaml.Marshal(tc.m)
require.NoError(err)
assert.Equal(tc.wantBase64Map, base64Map)
assert.YAMLEq(tc.wantYAML, string(yaml))
}
{
// JSON
json, err := json.Marshal(tc.m)
require.NoError(err)
assert.JSONEq(tc.wantJSON, string(json))
}
})
}
}
func TestUnmarshalYAML(t *testing.T) {
func TestUnmarshal(t *testing.T) {
testCases := map[string]struct {
inputBase64Map map[uint32]string
forceUnmarshalError bool
wantMeasurements M
wantErr bool
inputYAML string
inputJSON string
wantMeasurements M
wantErr bool
}{
"valid measurements": {
inputBase64Map: map[uint32]string{
2: "/V3p3zUOO8RBCsBrv+XM3rk/U7nvUSOfdSzmnbxgDzU=",
3: "1aRJbSHeyaUljdsZxv61O7TTwEY/5gfySI3fTxAG754=",
},
"valid measurements base64": {
inputYAML: "2:\n expected: \"/V3p3zUOO8RBCsBrv+XM3rk/U7nvUSOfdSzmnbxgDzU=\"\n3:\n expected: \"1aRJbSHeyaUljdsZxv61O7TTwEY/5gfySI3fTxAG754=\"",
inputJSON: `{"2":{"expected":"/V3p3zUOO8RBCsBrv+XM3rk/U7nvUSOfdSzmnbxgDzU="},"3":{"expected":"1aRJbSHeyaUljdsZxv61O7TTwEY/5gfySI3fTxAG754="}}`,
wantMeasurements: M{
2: []byte{253, 93, 233, 223, 53, 14, 59, 196, 65, 10, 192, 107, 191, 229, 204, 222, 185, 63, 83, 185, 239, 81, 35, 159, 117, 44, 230, 157, 188, 96, 15, 53},
3: []byte{213, 164, 73, 109, 33, 222, 201, 165, 37, 141, 219, 25, 198, 254, 181, 59, 180, 211, 192, 70, 63, 230, 7, 242, 72, 141, 223, 79, 16, 6, 239, 158},
2: {
Expected: [32]byte{253, 93, 233, 223, 53, 14, 59, 196, 65, 10, 192, 107, 191, 229, 204, 222, 185, 63, 83, 185, 239, 81, 35, 159, 117, 44, 230, 157, 188, 96, 15, 53},
},
3: {
Expected: [32]byte{213, 164, 73, 109, 33, 222, 201, 165, 37, 141, 219, 25, 198, 254, 181, 59, 180, 211, 192, 70, 63, 230, 7, 242, 72, 141, 223, 79, 16, 6, 239, 158},
},
},
},
"valid measurements hex": {
inputYAML: "2:\n expected: \"fd5de9df350e3bc4410ac06bbfe5ccdeb93f53b9ef51239f752ce69dbc600f35\"\n3:\n expected: \"d5a4496d21dec9a5258ddb19c6feb53bb4d3c0463fe607f2488ddf4f1006ef9e\"",
inputJSON: `{"2":{"expected":"fd5de9df350e3bc4410ac06bbfe5ccdeb93f53b9ef51239f752ce69dbc600f35"},"3":{"expected":"d5a4496d21dec9a5258ddb19c6feb53bb4d3c0463fe607f2488ddf4f1006ef9e"}}`,
wantMeasurements: M{
2: {
Expected: [32]byte{253, 93, 233, 223, 53, 14, 59, 196, 65, 10, 192, 107, 191, 229, 204, 222, 185, 63, 83, 185, 239, 81, 35, 159, 117, 44, 230, 157, 188, 96, 15, 53},
},
3: {
Expected: [32]byte{213, 164, 73, 109, 33, 222, 201, 165, 37, 141, 219, 25, 198, 254, 181, 59, 180, 211, 192, 70, 63, 230, 7, 242, 72, 141, 223, 79, 16, 6, 239, 158},
},
},
},
"empty bytes": {
inputBase64Map: map[uint32]string{
2: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
3: "AQIDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
},
inputYAML: "2:\n expected: \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"\n3:\n expected: \"AQIDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"",
inputJSON: `{"2":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="},"3":{"expected":"AQIDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}}`,
wantMeasurements: M{
2: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
3: []byte{1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
2: {
Expected: [32]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
3: {
Expected: [32]byte{1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
},
},
"invalid base64": {
inputBase64Map: map[uint32]string{
2: "This is not base64",
3: "AQIDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
},
wantMeasurements: M{
2: []byte{},
3: []byte{1, 2, 3, 4},
},
wantErr: true,
inputYAML: "2:\n expected: \"This is not base64\"\n3:\n expected: \"AQIDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"",
inputJSON: `{"2":{"expected":"This is not base64"},"3":{"expected":"AQIDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}}`,
wantErr: true,
},
"simulated unmarshal error": {
inputBase64Map: map[uint32]string{
2: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
3: "AQIDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
},
forceUnmarshalError: true,
"legacy format": {
inputYAML: "2: \"/V3p3zUOO8RBCsBrv+XM3rk/U7nvUSOfdSzmnbxgDzU=\"\n3: \"1aRJbSHeyaUljdsZxv61O7TTwEY/5gfySI3fTxAG754=\"",
inputJSON: `{"2":"/V3p3zUOO8RBCsBrv+XM3rk/U7nvUSOfdSzmnbxgDzU=","3":"1aRJbSHeyaUljdsZxv61O7TTwEY/5gfySI3fTxAG754="}`,
wantMeasurements: M{
2: []byte{},
3: []byte{1, 2, 3, 4},
2: {
Expected: [32]byte{253, 93, 233, 223, 53, 14, 59, 196, 65, 10, 192, 107, 191, 229, 204, 222, 185, 63, 83, 185, 239, 81, 35, 159, 117, 44, 230, 157, 188, 96, 15, 53},
},
3: {
Expected: [32]byte{213, 164, 73, 109, 33, 222, 201, 165, 37, 141, 219, 25, 198, 254, 181, 59, 180, 211, 192, 70, 63, 230, 7, 242, 72, 141, 223, 79, 16, 6, 239, 158},
},
},
wantErr: true,
},
"invalid length hex": {
inputYAML: "2:\n expected: \"fd5de9df350e3bc4410ac06bbfe5ccdeb93f53b9ef\"\n3:\n expected: \"d5a4496d21dec9a5258ddb19c6feb53bb4d3c0463f\"",
inputJSON: `{"2":{"expected":"fd5de9df350e3bc4410ac06bbfe5ccdeb93f53b9ef"},"3":{"expected":"d5a4496d21dec9a5258ddb19c6feb53bb4d3c0463f"}}`,
wantErr: true,
},
"invalid length base64": {
inputYAML: "2:\n expected: \"AA==\"\n3:\n expected: \"AQIDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\"",
inputJSON: `{"2":{"expected":"AA=="},"3":{"expected":"AQIDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="}}`,
wantErr: true,
},
"invalid format": {
inputYAML: "1:\n expected:\n someKey: 12\n anotherKey: 34",
inputJSON: `{"1":{"expected":{"someKey":12,"anotherKey":34}}}`,
wantErr: true,
},
}
@ -116,24 +149,30 @@ func TestUnmarshalYAML(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
var m M
err := m.UnmarshalYAML(func(i any) error {
if base64Map, ok := i.(map[uint32]string); ok {
for key, value := range tc.inputBase64Map {
base64Map[key] = value
}
}
if tc.forceUnmarshalError {
return errors.New("unmarshal error")
}
return nil
})
{
// YAML
var m M
err := yaml.Unmarshal([]byte(tc.inputYAML), &m)
if tc.wantErr {
assert.Error(err)
} else {
require.NoError(err)
assert.Equal(tc.wantMeasurements, m)
if tc.wantErr {
assert.Error(err, "yaml.Unmarshal should have failed")
} else {
require.NoError(err, "yaml.Unmarshal failed")
assert.Equal(tc.wantMeasurements, m)
}
}
{
// JSON
var m M
err := json.Unmarshal([]byte(tc.inputJSON), &m)
if tc.wantErr {
assert.Error(err, "json.Unmarshal should have failed")
} else {
require.NoError(err, "json.Unmarshal failed")
assert.Equal(tc.wantMeasurements, m)
}
}
})
}
@ -148,48 +187,48 @@ func TestMeasurementsCopyFrom(t *testing.T) {
"add to empty": {
current: M{},
newMeasurements: M{
1: PCRWithAllBytes(0x00),
2: PCRWithAllBytes(0x01),
3: PCRWithAllBytes(0x02),
1: WithAllBytes(0x00, true),
2: WithAllBytes(0x01, true),
3: WithAllBytes(0x02, true),
},
wantMeasurements: M{
1: PCRWithAllBytes(0x00),
2: PCRWithAllBytes(0x01),
3: PCRWithAllBytes(0x02),
1: WithAllBytes(0x00, true),
2: WithAllBytes(0x01, true),
3: WithAllBytes(0x02, true),
},
},
"keep existing": {
current: M{
4: PCRWithAllBytes(0x01),
5: PCRWithAllBytes(0x02),
4: WithAllBytes(0x01, false),
5: WithAllBytes(0x02, true),
},
newMeasurements: M{
1: PCRWithAllBytes(0x00),
2: PCRWithAllBytes(0x01),
3: PCRWithAllBytes(0x02),
1: WithAllBytes(0x00, true),
2: WithAllBytes(0x01, true),
3: WithAllBytes(0x02, true),
},
wantMeasurements: M{
1: PCRWithAllBytes(0x00),
2: PCRWithAllBytes(0x01),
3: PCRWithAllBytes(0x02),
4: PCRWithAllBytes(0x01),
5: PCRWithAllBytes(0x02),
1: WithAllBytes(0x00, true),
2: WithAllBytes(0x01, true),
3: WithAllBytes(0x02, true),
4: WithAllBytes(0x01, false),
5: WithAllBytes(0x02, true),
},
},
"overwrite existing": {
current: M{
2: PCRWithAllBytes(0x04),
3: PCRWithAllBytes(0x05),
2: WithAllBytes(0x04, false),
3: WithAllBytes(0x05, false),
},
newMeasurements: M{
1: PCRWithAllBytes(0x00),
2: PCRWithAllBytes(0x01),
3: PCRWithAllBytes(0x02),
1: WithAllBytes(0x00, true),
2: WithAllBytes(0x01, true),
3: WithAllBytes(0x02, true),
},
wantMeasurements: M{
1: PCRWithAllBytes(0x00),
2: PCRWithAllBytes(0x01),
3: PCRWithAllBytes(0x02),
1: WithAllBytes(0x00, true),
2: WithAllBytes(0x01, true),
3: WithAllBytes(0x02, true),
},
},
}
@ -224,6 +263,22 @@ func urlMustParse(raw string) *url.URL {
}
func TestMeasurementsFetchAndVerify(t *testing.T) {
// Cosign private key used to sign the measurements.
// Generated with: cosign generate-key-pair
// Password left empty.
//
// -----BEGIN ENCRYPTED COSIGN PRIVATE KEY-----
// eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6
// OCwicCI6MX0sInNhbHQiOiJlRHVYMWRQMGtIWVRnK0xkbjcxM0tjbFVJaU92eFVX
// VXgvNi9BbitFVk5BPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
// Iiwibm9uY2UiOiJwaWhLL2txNmFXa2hqSVVHR3RVUzhTVkdHTDNIWWp4TCJ9LCJj
// aXBoZXJ0ZXh0Ijoidm81SHVWRVFWcUZ2WFlQTTVPaTVaWHM5a255bndZU2dvcyth
// VklIeHcrOGFPamNZNEtvVjVmL3lHRHR0K3BHV2toanJPR1FLOWdBbmtsazFpQ0c5
// a2czUXpPQTZsU2JRaHgvZlowRVRZQ0hLeElncEdPRVRyTDlDenZDemhPZXVSOXJ6
// TDcvRjBBVy9vUDVqZXR3dmJMNmQxOEhjck9kWE8yVmYxY2w0YzNLZjVRcnFSZzlN
// dlRxQWFsNXJCNHNpY1JaMVhpUUJjb0YwNHc9PSJ9
// -----END ENCRYPTED COSIGN PRIVATE KEY-----
testCases := map[string]struct {
measurements string
measurementsStatus int
@ -237,44 +292,66 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
"simple": {
measurements: "0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n",
measurementsStatus: http.StatusOK,
signature: "MEUCIBs1g2/n0FsgPfJ+0uLD5TaunGhxwDcQcUGBroejKvg3AiEAzZtcLU9O6IiVhxB8tBS+ty6MXoPNwL8WRWMzyr35eKI=",
signature: "MEUCIQDcHS2bLls7OrLHpQKuiFGXhPrTcehPDwgVyERHl4V02wIgeIxK4J9oJpXWRBjokbog2lgifRXuJK8ljlAID26MbHk=",
signatureStatus: http.StatusOK,
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----"),
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"),
wantMeasurements: M{
0: PCRWithAllBytes(0x00),
0: WithAllBytes(0x00, false),
},
wantSHA: "4cd9d6ed8d9322150dff7738994c5e2fabff35f3bae6f5c993412d13249a5e87",
},
"404 measurements": {
measurements: "0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n",
measurementsStatus: http.StatusNotFound,
signature: "MEUCIBs1g2/n0FsgPfJ+0uLD5TaunGhxwDcQcUGBroejKvg3AiEAzZtcLU9O6IiVhxB8tBS+ty6MXoPNwL8WRWMzyr35eKI=",
"json measurements": {
measurements: `{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}`,
measurementsStatus: http.StatusOK,
signature: "MEUCIQDh3nCgrdTiYWiV4NkiaZ6vxovj79Pk8V90mdWAnmCEOwIgMAVWAx5dW0saut+8X15SgtBEiKqEixYiSICSqqhxUMg=",
signatureStatus: http.StatusOK,
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----"),
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"),
wantMeasurements: M{
0: WithAllBytes(0x00, false),
},
wantSHA: "1da09758c89537946496358f80b892e508563fcbbc695c90b6c16bf158e69c11",
},
"yaml measurements": {
measurements: "0:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n",
measurementsStatus: http.StatusOK,
signature: "MEUCIFzQdwBS92aJjY0bcIag1uQRl42lUSBmmjEvO0tM/N0ZAiEAvuWaP744qYMw5uEmc7BY4mm4Ij3TEqAWFgxNhFkckp4=",
signatureStatus: http.StatusOK,
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"),
wantMeasurements: M{
0: WithAllBytes(0x00, false),
},
wantSHA: "c651cd419fd536c63cfc5349ad44da140a09987465e31192660059d383413807",
},
"404 measurements": {
measurements: `{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}`,
measurementsStatus: http.StatusNotFound,
signature: "MEUCIQDh3nCgrdTiYWiV4NkiaZ6vxovj79Pk8V90mdWAnmCEOwIgMAVWAx5dW0saut+8X15SgtBEiKqEixYiSICSqqhxUMg=",
signatureStatus: http.StatusOK,
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"),
wantError: true,
},
"404 signature": {
measurements: "0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n",
measurements: `{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}`,
measurementsStatus: http.StatusOK,
signature: "MEUCIBs1g2/n0FsgPfJ+0uLD5TaunGhxwDcQcUGBroejKvg3AiEAzZtcLU9O6IiVhxB8tBS+ty6MXoPNwL8WRWMzyr35eKI=",
signature: "MEUCIQDh3nCgrdTiYWiV4NkiaZ6vxovj79Pk8V90mdWAnmCEOwIgMAVWAx5dW0saut+8X15SgtBEiKqEixYiSICSqqhxUMg=",
signatureStatus: http.StatusNotFound,
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----"),
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"),
wantError: true,
},
"broken signature": {
measurements: "0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n",
measurements: `{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}`,
measurementsStatus: http.StatusOK,
signature: "AAAAAAs1g2/n0FsgPfJ+0uLD5TaunGhxwDcQcUGBroejKvg3AiEAzZtcLU9O6IiVhxB8tBS+ty6MXoPNwL8WRWMzyr35eKI=",
signature: "AAAAAAAA3nCgrdTiYWiV4NkiaZ6vxovj79Pk8V90mdWAnmCEOwIgMAVWAx5dW0saut+8X15SgtBEiKqEixYiSICSqqhxUMg=",
signatureStatus: http.StatusOK,
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUs5fDUIz9aiwrfr8BK4VjN7jE6sl\ngz7UuXsOin8+dB0SGrbNHy7TJToa2fAiIKPVLTOfvY75DqRAtffhO1fpBA==\n-----END PUBLIC KEY-----"),
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"),
wantError: true,
},
"not yaml": {
"not yaml or json": {
measurements: "This is some content to be signed!\n",
measurementsStatus: http.StatusOK,
signature: "MEUCIQDzMN3yaiO9sxLGAaSA9YD8rLwzvOaZKWa/bzkcjImUFAIgXLLGzClYUd1dGbuEiY3O/g/eiwQYlyxqLQalxjFmz+8=",
signature: "MEUCIQCGA/lSu5qCJgNNvgMaTKJ9rj6vQMecUDaQo3ukaiAfUgIgWoxXRoDKLY9naN7YgxokM7r2fwnyYk3M2WKJJO1g6yo=",
signatureStatus: http.StatusOK,
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElWUhon39eAqzEC+/GP03oY4/MQg+\ngCDlEzkuOCybCHf+q766bve799L7Y5y5oRsHY1MrUCUwYF/tL7Sg7EYMsA==\n-----END PUBLIC KEY-----"),
publicKey: []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"),
wantError: true,
},
}
@ -322,30 +399,196 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
}
}
func TestPCRWithAllBytes(t *testing.T) {
func TestGetEnforced(t *testing.T) {
testCases := map[string]struct {
b byte
wantPCR []byte
input M
want map[uint32]struct{}
}{
"0x00": {
b: 0x00,
wantPCR: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
"only warnings": {
input: M{
0: WithAllBytes(0x00, true),
1: WithAllBytes(0x01, true),
},
want: map[uint32]struct{}{},
},
"0x01": {
b: 0x01,
wantPCR: []byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01},
"all enforced": {
input: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0x01, false),
},
want: map[uint32]struct{}{
0: {},
1: {},
},
},
"0xFF": {
b: 0xFF,
wantPCR: []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
"mixed": {
input: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0x01, true),
2: WithAllBytes(0x02, false),
},
want: map[uint32]struct{}{
0: {},
2: {},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
pcr := PCRWithAllBytes(tc.b)
assert.Equal(tc.wantPCR, pcr)
got := tc.input.GetEnforced()
enforced := map[uint32]struct{}{}
for _, id := range got {
enforced[id] = struct{}{}
}
assert.Equal(tc.want, enforced)
})
}
}
func TestSetEnforced(t *testing.T) {
testCases := map[string]struct {
input M
enforced []uint32
wantM M
wantErr bool
}{
"no enforced measurements": {
input: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0x01, false),
},
enforced: []uint32{},
wantM: M{
0: WithAllBytes(0x00, true),
1: WithAllBytes(0x01, true),
},
},
"all enforced measurements": {
input: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0x01, false),
},
enforced: []uint32{0, 1},
wantM: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0x01, false),
},
},
"mixed": {
input: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0x01, false),
2: WithAllBytes(0x02, false),
3: WithAllBytes(0x03, false),
},
enforced: []uint32{0, 2},
wantM: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0x01, true),
2: WithAllBytes(0x02, false),
3: WithAllBytes(0x03, true),
},
},
"warn only to enforced": {
input: M{
0: WithAllBytes(0x00, true),
1: WithAllBytes(0x01, true),
},
enforced: []uint32{0, 1},
wantM: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0x01, false),
},
},
"more enforced than measurements": {
input: M{
0: WithAllBytes(0x00, true),
1: WithAllBytes(0x01, true),
},
enforced: []uint32{0, 1, 2},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := tc.input.SetEnforced(tc.enforced)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.True(tc.input.EqualTo(tc.wantM))
})
}
}
func TestWithAllBytes(t *testing.T) {
testCases := map[string]struct {
b byte
warnOnly bool
wantMeasurement Measurement
}{
"0x00 warnOnly": {
b: 0x00,
warnOnly: true,
wantMeasurement: Measurement{
Expected: [32]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
WarnOnly: true,
},
},
"0x00": {
b: 0x00,
warnOnly: false,
wantMeasurement: Measurement{
Expected: [32]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
WarnOnly: false,
},
},
"0x01 warnOnly": {
b: 0x01,
warnOnly: true,
wantMeasurement: Measurement{
Expected: [32]byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01},
WarnOnly: true,
},
},
"0x01": {
b: 0x01,
warnOnly: false,
wantMeasurement: Measurement{
Expected: [32]byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01},
WarnOnly: false,
},
},
"0xFF warnOnly": {
b: 0xFF,
warnOnly: true,
wantMeasurement: Measurement{
Expected: [32]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
WarnOnly: true,
},
},
"0xFF": {
b: 0xFF,
warnOnly: false,
wantMeasurement: Measurement{
Expected: [32]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
WarnOnly: false,
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
measurement := WithAllBytes(tc.b, tc.warnOnly)
assert.Equal(tc.wantMeasurement, measurement)
})
}
}
@ -358,33 +601,44 @@ func TestEqualTo(t *testing.T) {
}{
"same values": {
given: M{
0: PCRWithAllBytes(0x00),
1: PCRWithAllBytes(0xFF),
0: WithAllBytes(0x00, false),
1: WithAllBytes(0xFF, false),
},
other: M{
0: PCRWithAllBytes(0x00),
1: PCRWithAllBytes(0xFF),
0: WithAllBytes(0x00, false),
1: WithAllBytes(0xFF, false),
},
wantEqual: true,
},
"different number of elements": {
given: M{
0: PCRWithAllBytes(0x00),
1: PCRWithAllBytes(0xFF),
0: WithAllBytes(0x00, false),
1: WithAllBytes(0xFF, false),
},
other: M{
0: PCRWithAllBytes(0x00),
0: WithAllBytes(0x00, false),
},
wantEqual: false,
},
"different values": {
given: M{
0: PCRWithAllBytes(0x00),
1: PCRWithAllBytes(0xFF),
0: WithAllBytes(0x00, false),
1: WithAllBytes(0xFF, false),
},
other: M{
0: PCRWithAllBytes(0xFF),
1: PCRWithAllBytes(0x00),
0: WithAllBytes(0xFF, false),
1: WithAllBytes(0x00, false),
},
wantEqual: false,
},
"different warn settings": {
given: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0xFF, false),
},
other: M{
0: WithAllBytes(0x00, false),
1: WithAllBytes(0xFF, true),
},
wantEqual: false,
},

View File

@ -22,11 +22,10 @@ type Validator struct {
}
// NewValidator initializes a new QEMU validator with the provided PCR values.
func NewValidator(pcrs measurements.M, enforcedPCRs []uint32, log vtpm.AttestationLogger) *Validator {
func NewValidator(pcrs measurements.M, log vtpm.AttestationLogger) *Validator {
return &Validator{
Validator: vtpm.NewValidator(
pcrs,
enforcedPCRs,
unconditionalTrust,
func(attestation vtpm.AttestationDocument) error { return nil },
vtpm.VerifyPKCS1v15,

View File

@ -16,6 +16,7 @@ import (
"fmt"
"io"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
tpmClient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/proto/attest"
tpmProto "github.com/google/go-tpm-tools/proto/tpm"
@ -144,8 +145,7 @@ func (i *Issuer) Issue(userData []byte, nonce []byte) ([]byte, error) {
// Validator handles validation of TPM based attestation.
type Validator struct {
expectedPCRs map[uint32][]byte
enforcedPCRs map[uint32]struct{}
expected measurements.M
getTrustedKey GetTPMTrustedAttestationPublicKey
validateCVM ValidateCVM
verifyUserData VerifyUserData
@ -154,18 +154,11 @@ type Validator struct {
}
// NewValidator returns a new Validator.
func NewValidator(expectedPCRs map[uint32][]byte, enforcedPCRs []uint32, getTrustedKey GetTPMTrustedAttestationPublicKey,
func NewValidator(expected measurements.M, getTrustedKey GetTPMTrustedAttestationPublicKey,
validateCVM ValidateCVM, verifyUserData VerifyUserData, log AttestationLogger,
) *Validator {
// Convert the enforced PCR list to a map for convenient and fast lookup
enforcedMap := make(map[uint32]struct{})
for _, pcr := range enforcedPCRs {
enforcedMap[pcr] = struct{}{}
}
return &Validator{
expectedPCRs: expectedPCRs,
enforcedPCRs: enforcedMap,
expected: expected,
getTrustedKey: getTrustedKey,
validateCVM: validateCVM,
verifyUserData: verifyUserData,
@ -212,9 +205,9 @@ func (v *Validator) Validate(attDocRaw []byte, nonce []byte) ([]byte, error) {
if err != nil {
return nil, err
}
for idx, pcr := range v.expectedPCRs {
if !bytes.Equal(pcr, attDoc.Attestation.Quotes[quoteIdx].Pcrs.Pcrs[idx]) {
if _, ok := v.enforcedPCRs[idx]; ok {
for idx, pcr := range v.expected {
if !bytes.Equal(pcr.Expected[:], attDoc.Attestation.Quotes[quoteIdx].Pcrs.Pcrs[idx]) {
if !pcr.WarnOnly {
return nil, fmt.Errorf("untrusted PCR value at PCR index %d", idx)
}
if v.log != nil {
@ -263,8 +256,8 @@ func VerifyPKCS1v15(pub crypto.PublicKey, hash crypto.Hash, hashed, sig []byte)
return rsa.VerifyPKCS1v15(key, hash, hashed, sig)
}
// GetSelectedPCRs returns a map of the selected PCR hashes.
func GetSelectedPCRs(open TPMOpenFunc, selection tpm2.PCRSelection) (map[uint32][]byte, error) {
// GetSelectedMeasurements returns a map of Measurments for the PCRs in selection.
func GetSelectedMeasurements(open TPMOpenFunc, selection tpm2.PCRSelection) (measurements.M, error) {
tpm, err := open()
if err != nil {
return nil, err
@ -276,5 +269,15 @@ func GetSelectedPCRs(open TPMOpenFunc, selection tpm2.PCRSelection) (map[uint32]
return nil, err
}
return pcrList.Pcrs, nil
m := make(measurements.M)
for i, pcr := range pcrList.Pcrs {
if len(pcr) != 32 {
return nil, fmt.Errorf("invalid measurement: invalid length: %d", len(pcr))
}
m[i] = measurements.Measurement{
Expected: *(*[32]byte)(pcr),
}
}
return m, nil
}

View File

@ -14,6 +14,7 @@ import (
"io"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
tpmsim "github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/proto/attest"
@ -64,14 +65,14 @@ func TestValidate(t *testing.T) {
return pubArea.Key()
}
testExpectedPCRs := map[uint32][]byte{
0: {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
1: {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
testExpectedPCRs := measurements.M{
0: measurements.WithAllBytes(0x00, true),
1: measurements.WithAllBytes(0x00, true),
}
warnLog := &testAttestationLogger{}
issuer := NewIssuer(newSimTPMWithEventLog, tpmclient.AttestationKeyRSA, fakeGetInstanceInfo)
validator := NewValidator(testExpectedPCRs, []uint32{0, 1}, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog)
validator := NewValidator(testExpectedPCRs, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog)
nonce := []byte{1, 2, 3, 4}
challenge := []byte("Constellation")
@ -89,18 +90,28 @@ func TestValidate(t *testing.T) {
require.NoError(err)
require.Equal(challenge, out)
enforcedPCRs := []uint32{0, 1}
expectedPCRs := map[uint32][]byte{
0: {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
1: {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
2: {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20},
3: {0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40},
4: {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60},
5: {0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80},
expectedPCRs := measurements.M{
0: measurements.WithAllBytes(0x00, true),
1: measurements.WithAllBytes(0x00, true),
2: measurements.Measurement{
Expected: [32]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20},
WarnOnly: true,
},
3: measurements.Measurement{
Expected: [32]byte{0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40},
WarnOnly: true,
},
4: measurements.Measurement{
Expected: [32]byte{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60},
WarnOnly: true,
},
5: measurements.Measurement{
Expected: [32]byte{0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80},
WarnOnly: true,
},
}
warningValidator := NewValidator(
expectedPCRs,
enforcedPCRs,
fakeGetTrustedKey,
fakeValidateCVM,
VerifyPKCS1v15,
@ -109,7 +120,7 @@ func TestValidate(t *testing.T) {
out, err = warningValidator.Validate(attDocRaw, nonce)
require.NoError(err)
assert.Equal(t, challenge, out)
assert.Len(t, warnLog.warnings, len(expectedPCRs)-len(enforcedPCRs))
assert.Len(t, warnLog.warnings, 4)
testCases := map[string]struct {
validator *Validator
@ -118,13 +129,13 @@ func TestValidate(t *testing.T) {
wantErr bool
}{
"invalid nonce": {
validator: NewValidator(testExpectedPCRs, []uint32{0, 1}, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog),
validator: NewValidator(testExpectedPCRs, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog),
attDoc: mustMarshalAttestation(attDoc, require),
nonce: []byte{4, 3, 2, 1},
wantErr: true,
},
"invalid signature": {
validator: NewValidator(testExpectedPCRs, []uint32{0, 1}, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog),
validator: NewValidator(testExpectedPCRs, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog),
attDoc: mustMarshalAttestation(AttestationDocument{
Attestation: attDoc.Attestation,
InstanceInfo: attDoc.InstanceInfo,
@ -137,7 +148,6 @@ func TestValidate(t *testing.T) {
"untrusted attestation public key": {
validator: NewValidator(
testExpectedPCRs,
[]uint32{0, 1},
func(akPub, instanceInfo []byte) (crypto.PublicKey, error) {
return nil, errors.New("untrusted")
},
@ -149,7 +159,6 @@ func TestValidate(t *testing.T) {
"not a CVM": {
validator: NewValidator(
testExpectedPCRs,
[]uint32{0, 1},
fakeGetTrustedKey,
func(attestation AttestationDocument) error {
return errors.New("untrusted")
@ -161,10 +170,12 @@ func TestValidate(t *testing.T) {
},
"untrusted PCRs": {
validator: NewValidator(
map[uint32][]byte{
0: {0xFF},
measurements.M{
0: measurements.Measurement{
Expected: [32]byte{0xFF},
WarnOnly: false,
},
},
[]uint32{0},
fakeGetTrustedKey,
fakeValidateCVM,
VerifyPKCS1v15, warnLog),
@ -173,7 +184,7 @@ func TestValidate(t *testing.T) {
wantErr: true,
},
"no sha256 quote": {
validator: NewValidator(testExpectedPCRs, []uint32{0, 1}, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog),
validator: NewValidator(testExpectedPCRs, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog),
attDoc: mustMarshalAttestation(AttestationDocument{
Attestation: &attest.Attestation{
AkPub: attDoc.Attestation.AkPub,
@ -191,7 +202,7 @@ func TestValidate(t *testing.T) {
wantErr: true,
},
"invalid attestation document": {
validator: NewValidator(testExpectedPCRs, []uint32{0, 1}, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog),
validator: NewValidator(testExpectedPCRs, fakeGetTrustedKey, fakeValidateCVM, VerifyPKCS1v15, warnLog),
attDoc: []byte("invalid attestation"),
nonce: nonce,
wantErr: true,
@ -350,7 +361,7 @@ func TestGetSHA256QuoteIndex(t *testing.T) {
}
}
func TestGetSelectedPCRs(t *testing.T) {
func TestGetSelectedMeasurements(t *testing.T) {
testCases := map[string]struct {
openFunc TPMOpenFunc
pcrSelection tpm2.PCRSelection
@ -386,17 +397,13 @@ func TestGetSelectedPCRs(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
pcrs, err := GetSelectedPCRs(tc.openFunc, tc.pcrSelection)
pcrs, err := GetSelectedMeasurements(tc.openFunc, tc.pcrSelection)
if tc.wantErr {
assert.Error(err)
} else {
require.NoError(err)
assert.Equal(len(pcrs), len(tc.pcrSelection.PCRs))
for _, pcr := range pcrs {
assert.Len(pcr, 32)
}
return
}
require.NoError(err)
assert.Len(pcrs, len(tc.pcrSelection.PCRs))
})
}
}

View File

@ -9,18 +9,8 @@ package vtpm
import (
"errors"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/google/go-tpm/tpm2"
"github.com/google/go-tpm/tpmutil"
)
const (
// PCRIndexClusterID is a PCR we extend to mark the node as initialized.
// The value used to extend is a random generated 32 Byte value.
PCRIndexClusterID = tpmutil.Handle(15)
// PCRIndexOwnerID is a PCR we extend to mark the node as initialized.
// The value used to extend is derived from Constellation's master key.
// TODO: move to stable, non-debug PCR before use.
PCRIndexOwnerID = tpmutil.Handle(16)
)
// MarkNodeAsBootstrapped marks a node as initialized by extending PCRs.
@ -32,7 +22,7 @@ func MarkNodeAsBootstrapped(openTPM TPMOpenFunc, clusterID []byte) error {
defer tpm.Close()
// clusterID is used to uniquely identify this running instance of Constellation
return tpm2.PCREvent(tpm, PCRIndexClusterID, clusterID)
return tpm2.PCREvent(tpm, measurements.PCRIndexClusterID, clusterID)
}
// IsNodeBootstrapped checks if a node is already bootstrapped by reading PCRs.
@ -43,7 +33,7 @@ func IsNodeBootstrapped(openTPM TPMOpenFunc) (bool, error) {
}
defer tpm.Close()
idxClusterID := int(PCRIndexClusterID)
idxClusterID := int(measurements.PCRIndexClusterID)
pcrs, err := tpm2.ReadPCRs(tpm, tpm2.PCRSelection{
Hash: tpm2.AlgSHA256,
PCRs: []int{idxClusterID},

View File

@ -11,6 +11,7 @@ import (
"io"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
"github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm/tpm2"
@ -45,7 +46,7 @@ func TestMarkNodeAsBootstrapped(t *testing.T) {
require.NoError(err)
for i := range pcrs {
assert.NotEqual(pcrs[i].Pcrs[uint32(PCRIndexClusterID)], pcrsInitialized[i].Pcrs[uint32(PCRIndexClusterID)])
assert.NotEqual(pcrs[i].Pcrs[uint32(measurements.PCRIndexClusterID)], pcrsInitialized[i].Pcrs[uint32(measurements.PCRIndexClusterID)])
}
}
@ -76,7 +77,7 @@ func TestIsNodeInitialized(t *testing.T) {
require.NoError(err)
defer tpm.Close()
if tc.pcrValueClusterID != nil {
require.NoError(tpm2.PCREvent(tpm, PCRIndexClusterID, tc.pcrValueClusterID))
require.NoError(tpm2.PCREvent(tpm, measurements.PCRIndexClusterID, tc.pcrValueClusterID))
}
initialized, err := IsNodeBootstrapped(func() (io.ReadWriteCloser, error) {
return &simTPMNOPCloser{tpm}, nil

View File

@ -138,10 +138,7 @@ type AWSConfig struct {
IAMProfileWorkerNodes string `yaml:"iamProfileWorkerNodes" validate:"required"`
// description: |
// Expected VM measurements.
Measurements Measurements `yaml:"measurements"`
// description: |
// List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning.
EnforcedMeasurements []uint32 `yaml:"enforcedMeasurements"`
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
}
// AzureConfig are Azure specific configuration values used by the CLI.
@ -190,10 +187,7 @@ type AzureConfig struct {
EnforceIDKeyDigest *bool `yaml:"enforceIdKeyDigest" validate:"required"`
// description: |
// Expected confidential VM measurements.
Measurements Measurements `yaml:"measurements"`
// description: |
// List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning.
EnforcedMeasurements []uint32 `yaml:"enforcedMeasurements"`
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
}
// GCPConfig are GCP specific configuration values used by the CLI.
@ -221,10 +215,7 @@ type GCPConfig struct {
DeployCSIDriver *bool `yaml:"deployCSIDriver" validate:"required"`
// description: |
// Expected confidential VM measurements.
Measurements Measurements `yaml:"measurements"`
// description: |
// List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning.
EnforcedMeasurements []uint32 `yaml:"enforcedMeasurements"`
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
}
// QEMUConfig holds config information for QEMU based Constellation deployments.
@ -255,10 +246,7 @@ type QEMUConfig struct {
Firmware string `yaml:"firmware"`
// description: |
// Measurement used to enable measured boot.
Measurements Measurements `yaml:"measurements"`
// description: |
// List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning.
EnforcedMeasurements []uint32 `yaml:"enforcedMeasurements"`
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
}
// Default returns a struct with the default config.
@ -276,7 +264,6 @@ func Default() *Config {
IAMProfileControlPlane: "",
IAMProfileWorkerNodes: "",
Measurements: measurements.DefaultsFor(cloudprovider.AWS),
EnforcedMeasurements: []uint32{4, 8, 9, 11, 12, 13, 15},
},
Azure: &AzureConfig{
SubscriptionID: "",
@ -292,7 +279,6 @@ func Default() *Config {
ConfidentialVM: func() *bool { b := true; return &b }(),
SecureBoot: func() *bool { b := false; return &b }(),
Measurements: measurements.DefaultsFor(cloudprovider.Azure),
EnforcedMeasurements: []uint32{4, 8, 9, 11, 12, 13, 15},
},
GCP: &GCPConfig{
Project: "",
@ -303,7 +289,6 @@ func Default() *Config {
StateDiskType: "pd-ssd",
DeployCSIDriver: func() *bool { b := true; return &b }(),
Measurements: measurements.DefaultsFor(cloudprovider.GCP),
EnforcedMeasurements: []uint32{0, 4, 8, 9, 11, 12, 13, 15},
},
QEMU: &QEMUConfig{
ImageFormat: "raw",
@ -314,7 +299,6 @@ func Default() *Config {
LibvirtContainerImage: versions.LibvirtImage,
NVRAM: "production",
Measurements: measurements.DefaultsFor(cloudprovider.QEMU),
EnforcedMeasurements: []uint32{4, 8, 9, 11, 12, 13, 15},
},
},
KubernetesVersion: string(versions.Default),
@ -446,18 +430,18 @@ func (c *Config) EnforcesIDKeyDigest() bool {
return c.Provider.Azure != nil && c.Provider.Azure.EnforceIDKeyDigest != nil && *c.Provider.Azure.EnforceIDKeyDigest
}
// GetEnforcedPCRs returns the list of enforced PCRs for the configured cloud provider.
func (c *Config) GetEnforcedPCRs() []uint32 {
// EnforcedPCRs returns the list of enforced PCRs for the configured cloud provider.
func (c *Config) EnforcedPCRs() []uint32 {
provider := c.GetProvider()
switch provider {
case cloudprovider.AWS:
return c.Provider.AWS.EnforcedMeasurements
return c.Provider.AWS.Measurements.GetEnforced()
case cloudprovider.Azure:
return c.Provider.Azure.EnforcedMeasurements
return c.Provider.Azure.Measurements.GetEnforced()
case cloudprovider.GCP:
return c.Provider.GCP.EnforcedMeasurements
return c.Provider.GCP.Measurements.GetEnforced()
case cloudprovider.QEMU:
return c.Provider.QEMU.EnforcedMeasurements
return c.Provider.QEMU.Measurements.GetEnforced()
default:
return nil
}
@ -499,6 +483,14 @@ func (c *Config) Validate() error {
return err
}
if err := validate.RegisterTranslation("no_placeholders", trans, registerContainsPlaceholderError, translateContainsPlaceholderError); err != nil {
return err
}
if err := validate.RegisterValidation("no_placeholders", validateNoPlaceholder); err != nil {
return err
}
if err := validate.RegisterValidation("safe_image", validateImage); err != nil {
return err
}

View File

@ -157,7 +157,7 @@ func init() {
FieldName: "aws",
},
}
AWSConfigDoc.Fields = make([]encoder.Doc, 8)
AWSConfigDoc.Fields = make([]encoder.Doc, 7)
AWSConfigDoc.Fields[0].Name = "region"
AWSConfigDoc.Fields[0].Type = "string"
AWSConfigDoc.Fields[0].Note = ""
@ -193,11 +193,6 @@ func init() {
AWSConfigDoc.Fields[6].Note = ""
AWSConfigDoc.Fields[6].Description = "Expected VM measurements."
AWSConfigDoc.Fields[6].Comments[encoder.LineComment] = "Expected VM measurements."
AWSConfigDoc.Fields[7].Name = "enforcedMeasurements"
AWSConfigDoc.Fields[7].Type = "[]uint32"
AWSConfigDoc.Fields[7].Note = ""
AWSConfigDoc.Fields[7].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AWSConfigDoc.Fields[7].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AzureConfigDoc.Type = "AzureConfig"
AzureConfigDoc.Comments[encoder.LineComment] = "AzureConfig are Azure specific configuration values used by the CLI."
@ -208,7 +203,7 @@ func init() {
FieldName: "azure",
},
}
AzureConfigDoc.Fields = make([]encoder.Doc, 16)
AzureConfigDoc.Fields = make([]encoder.Doc, 15)
AzureConfigDoc.Fields[0].Name = "subscription"
AzureConfigDoc.Fields[0].Type = "string"
AzureConfigDoc.Fields[0].Note = ""
@ -284,11 +279,6 @@ func init() {
AzureConfigDoc.Fields[14].Note = ""
AzureConfigDoc.Fields[14].Description = "Expected confidential VM measurements."
AzureConfigDoc.Fields[14].Comments[encoder.LineComment] = "Expected confidential VM measurements."
AzureConfigDoc.Fields[15].Name = "enforcedMeasurements"
AzureConfigDoc.Fields[15].Type = "[]uint32"
AzureConfigDoc.Fields[15].Note = ""
AzureConfigDoc.Fields[15].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AzureConfigDoc.Fields[15].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
GCPConfigDoc.Type = "GCPConfig"
GCPConfigDoc.Comments[encoder.LineComment] = "GCPConfig are GCP specific configuration values used by the CLI."
@ -299,7 +289,7 @@ func init() {
FieldName: "gcp",
},
}
GCPConfigDoc.Fields = make([]encoder.Doc, 9)
GCPConfigDoc.Fields = make([]encoder.Doc, 8)
GCPConfigDoc.Fields[0].Name = "project"
GCPConfigDoc.Fields[0].Type = "string"
GCPConfigDoc.Fields[0].Note = ""
@ -340,11 +330,6 @@ func init() {
GCPConfigDoc.Fields[7].Note = ""
GCPConfigDoc.Fields[7].Description = "Expected confidential VM measurements."
GCPConfigDoc.Fields[7].Comments[encoder.LineComment] = "Expected confidential VM measurements."
GCPConfigDoc.Fields[8].Name = "enforcedMeasurements"
GCPConfigDoc.Fields[8].Type = "[]uint32"
GCPConfigDoc.Fields[8].Note = ""
GCPConfigDoc.Fields[8].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
GCPConfigDoc.Fields[8].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
QEMUConfigDoc.Type = "QEMUConfig"
QEMUConfigDoc.Comments[encoder.LineComment] = "QEMUConfig holds config information for QEMU based Constellation deployments."
@ -355,7 +340,7 @@ func init() {
FieldName: "qemu",
},
}
QEMUConfigDoc.Fields = make([]encoder.Doc, 10)
QEMUConfigDoc.Fields = make([]encoder.Doc, 9)
QEMUConfigDoc.Fields[0].Name = "imageFormat"
QEMUConfigDoc.Fields[0].Type = "string"
QEMUConfigDoc.Fields[0].Note = ""
@ -401,11 +386,6 @@ func init() {
QEMUConfigDoc.Fields[8].Note = ""
QEMUConfigDoc.Fields[8].Description = "Measurement used to enable measured boot."
QEMUConfigDoc.Fields[8].Comments[encoder.LineComment] = "Measurement used to enable measured boot."
QEMUConfigDoc.Fields[9].Name = "enforcedMeasurements"
QEMUConfigDoc.Fields[9].Type = "[]uint32"
QEMUConfigDoc.Fields[9].Note = ""
QEMUConfigDoc.Fields[9].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
QEMUConfigDoc.Fields[9].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
}
func (_ Config) Doc() *encoder.Doc {

View File

@ -128,6 +128,7 @@ func TestNewWithDefaultOptions(t *testing.T) {
c.Provider.Azure.ResourceGroup = "test"
c.Provider.Azure.UserAssignedIdentity = "/subscriptions/8b8bd01f-efd9-4113-9bd1-c82137c32da7/resourcegroups/constellation-identity/providers/Microsoft.ManagedIdentity/userAssignedIdentities/constellation-identity"
c.Provider.Azure.AppClientID = "3ea4bdc1-1cc1-4237-ae78-0831eff3491e"
c.Provider.Azure.Measurements = measurements.M{15: measurements.WithAllBytes(0x00, false)}
return c
}(),
envToSet: map[string]string{
@ -147,6 +148,7 @@ func TestNewWithDefaultOptions(t *testing.T) {
c.Provider.Azure.ClientSecretValue = "other-value" // < Note secret set in config, as well.
c.Provider.Azure.UserAssignedIdentity = "/subscriptions/8b8bd01f-efd9-4113-9bd1-c82137c32da7/resourcegroups/constellation-identity/providers/Microsoft.ManagedIdentity/userAssignedIdentities/constellation-identity"
c.Provider.Azure.AppClientID = "3ea4bdc1-1cc1-4237-ae78-0831eff3491e"
c.Provider.Azure.Measurements = measurements.M{15: measurements.WithAllBytes(0x00, false)}
return c
}(),
envToSet: map[string]string{
@ -182,9 +184,9 @@ func TestNewWithDefaultOptions(t *testing.T) {
}
func TestValidate(t *testing.T) {
const defaultErrCount = 17 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const azErrCount = 8
const gcpErrCount = 5
const defaultErrCount = 21 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const azErrCount = 9
const gcpErrCount = 6
testCases := map[string]struct {
cnf *Config
@ -240,6 +242,7 @@ func TestValidate(t *testing.T) {
az.ClientSecretValue = "test-client-secret"
cnf.Provider = ProviderConfig{}
cnf.Provider.Azure = az
cnf.Provider.Azure.Measurements = measurements.M{15: measurements.WithAllBytes(0x00, false)}
return cnf
}(),
},
@ -265,6 +268,7 @@ func TestValidate(t *testing.T) {
gcp.ServiceAccountKeyPath = "test-key-path"
cnf.Provider = ProviderConfig{}
cnf.Provider.GCP = gcp
cnf.Provider.GCP.Measurements = measurements.M{15: measurements.WithAllBytes(0x00, false)}
return cnf
}(),
},
@ -364,9 +368,9 @@ func TestConfigGeneratedDocsFresh(t *testing.T) {
func TestConfig_UpdateMeasurements(t *testing.T) {
assert := assert.New(t)
newMeasurements := measurements.M{
1: measurements.PCRWithAllBytes(0x00),
2: measurements.PCRWithAllBytes(0x01),
3: measurements.PCRWithAllBytes(0x02),
1: measurements.WithAllBytes(0x00, false),
2: measurements.WithAllBytes(0x01, false),
3: measurements.WithAllBytes(0x02, false),
}
{ // AWS

View File

@ -7,9 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
package config
import (
"bytes"
"fmt"
"strings"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config/instancetypes"
"github.com/edgelesssys/constellation/v2/internal/versions"
@ -223,3 +225,35 @@ func (c *Config) translateAzureInstanceTypeError(ut ut.Translator, fe validator.
return t
}
func validateNoPlaceholder(fl validator.FieldLevel) bool {
return len(getPlaceholderEntries(fl.Field().Interface().(Measurements))) == 0
}
func registerContainsPlaceholderError(ut ut.Translator) error {
return ut.Add("no_placeholders", "{0} placeholder values (repeated 1234...)", true)
}
func translateContainsPlaceholderError(ut ut.Translator, fe validator.FieldError) string {
placeholders := getPlaceholderEntries(fe.Value().(Measurements))
msg := fmt.Sprintf("Measurements %v contain", placeholders)
if len(placeholders) == 1 {
msg = fmt.Sprintf("Measurement %v contains", placeholders)
}
t, _ := ut.T("no_placeholders", msg)
return t
}
func getPlaceholderEntries(m Measurements) []uint32 {
var placeholders []uint32
placeholder := measurements.PlaceHolderMeasurement()
for idx, measurement := range m {
if bytes.Equal(measurement.Expected[:], placeholder.Expected[:]) {
placeholders = append(placeholders, idx)
}
}
return placeholders
}

View File

@ -9,7 +9,9 @@ package watcher
import (
"encoding/asn1"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"sync"
@ -19,6 +21,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/trustedlaunch"
"github.com/edgelesssys/constellation/v2/internal/attestation/gcp"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/attestation/qemu"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
@ -42,26 +45,26 @@ func NewValidator(log *logger.Logger, csp string, fileHandler file.Handler, azur
var newValidator newValidatorFunc
switch cloudprovider.FromString(csp) {
case cloudprovider.AWS:
newValidator = func(m map[uint32][]byte, e []uint32, _ []byte, _ bool, log *logger.Logger) atls.Validator {
return aws.NewValidator(m, e, log)
newValidator = func(m measurements.M, _ []byte, _ bool, log *logger.Logger) atls.Validator {
return aws.NewValidator(m, log)
}
case cloudprovider.Azure:
if azureCVM {
newValidator = func(m map[uint32][]byte, e []uint32, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator {
return snp.NewValidator(m, e, idkeydigest, enforceIdKeyDigest, log)
newValidator = func(m measurements.M, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator {
return snp.NewValidator(m, idkeydigest, enforceIdKeyDigest, log)
}
} else {
newValidator = func(m map[uint32][]byte, e []uint32, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator {
return trustedlaunch.NewValidator(m, e, log)
newValidator = func(m measurements.M, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator {
return trustedlaunch.NewValidator(m, log)
}
}
case cloudprovider.GCP:
newValidator = func(m map[uint32][]byte, e []uint32, _ []byte, _ bool, log *logger.Logger) atls.Validator {
return gcp.NewValidator(m, e, log)
newValidator = func(m measurements.M, _ []byte, _ bool, log *logger.Logger) atls.Validator {
return gcp.NewValidator(m, log)
}
case cloudprovider.QEMU:
newValidator = func(m map[uint32][]byte, e []uint32, _ []byte, _ bool, log *logger.Logger) atls.Validator {
return qemu.NewValidator(m, e, log)
newValidator = func(m measurements.M, _ []byte, _ bool, log *logger.Logger) atls.Validator {
return qemu.NewValidator(m, log)
}
default:
return nil, fmt.Errorf("unknown cloud service provider: %q", csp)
@ -100,17 +103,24 @@ func (u *Updatable) Update() error {
u.log.Infof("Updating expected measurements")
var measurements map[uint32][]byte
var measurements measurements.M
if err := u.fileHandler.ReadJSON(filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename), &measurements); err != nil {
return err
}
u.log.Debugf("New measurements: %v", measurements)
u.log.Debugf("New measurements: %+v", measurements)
// handle legacy measurement format, where expected measurements and enforced measurements were stored in separate data structures
// TODO: remove with v2.4.0
var enforced []uint32
if err := u.fileHandler.ReadJSON(filepath.Join(constants.ServiceBasePath, constants.EnforcedPCRsFilename), &enforced); err != nil {
if err := u.fileHandler.ReadJSON(filepath.Join(constants.ServiceBasePath, constants.EnforcedPCRsFilename), &enforced); err == nil {
u.log.Debugf("Detected legacy format. Loading enforced PCRs...")
if err := measurements.SetEnforced(enforced); err != nil {
return err
}
u.log.Debugf("Merged measurements with enforced values: %+v", measurements)
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
u.log.Debugf("Enforced PCRs: %v", enforced)
var idkeydigest []byte
var enforceIDKeyDigest bool
@ -138,9 +148,9 @@ func (u *Updatable) Update() error {
u.log.Debugf("New idkeydigest: %x", idkeydigest)
}
u.Validator = u.newValidator(measurements, enforced, idkeydigest, enforceIDKeyDigest, u.log)
u.Validator = u.newValidator(measurements, idkeydigest, enforceIDKeyDigest, u.log)
return nil
}
type newValidatorFunc func(measurements map[uint32][]byte, enforcedPCRs []uint32, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator
type newValidatorFunc func(measurements measurements.M, idkeydigest []byte, enforceIdKeyDigest bool, log *logger.Logger) atls.Validator

View File

@ -20,6 +20,7 @@ import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
@ -117,7 +118,7 @@ func TestUpdate(t *testing.T) {
require := require.New(t)
oid := fakeOID{1, 3, 9900, 1}
newValidator := func(m map[uint32][]byte, e []uint32, idkeydigest []byte, enforceIdKeyDigest bool, _ *logger.Logger) atls.Validator {
newValidator := func(m measurements.M, idkeydigest []byte, enforceIdKeyDigest bool, _ *logger.Logger) atls.Validator {
return fakeValidator{fakeOID: oid}
}
handler := file.NewHandler(afero.NewMemMapFs())
@ -135,14 +136,7 @@ func TestUpdate(t *testing.T) {
// write measurement config
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename),
map[uint32][]byte{
11: {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
},
file.OptNone,
))
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.EnforcedPCRsFilename),
[]uint32{11},
measurements.M{11: measurements.WithAllBytes(0x00, false)},
))
require.NoError(handler.Write(
filepath.Join(constants.ServiceBasePath, constants.IDKeyDigestFilename),
@ -189,6 +183,23 @@ func TestUpdate(t *testing.T) {
defer resp.Body.Close()
}
assert.Error(err)
// update should work for legacy measurement format
// TODO: remove with v2.4.0
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename),
map[uint32][]byte{
11: bytes.Repeat([]byte{0x0}, 32),
12: bytes.Repeat([]byte{0x1}, 32),
},
file.OptOverwrite,
))
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.EnforcedPCRsFilename),
[]uint32{11},
))
assert.NoError(validator.Update())
}
func TestUpdateConcurrency(t *testing.T) {
@ -199,7 +210,7 @@ func TestUpdateConcurrency(t *testing.T) {
validator := &Updatable{
log: logger.NewTest(t),
fileHandler: handler,
newValidator: func(m map[uint32][]byte, e []uint32, idkeydigest []byte, enforceIdKeyDigest bool, _ *logger.Logger) atls.Validator {
newValidator: func(m measurements.M, idkeydigest []byte, enforceIdKeyDigest bool, _ *logger.Logger) atls.Validator {
return fakeValidator{fakeOID: fakeOID{1, 3, 9900, 1}}
},
}