2022-09-05 03:06:08 -04:00
|
|
|
/*
|
|
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
2022-03-22 11:03:15 -04:00
|
|
|
package gcp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/json"
|
|
|
|
"encoding/pem"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
compute "cloud.google.com/go/compute/apiv1"
|
2022-12-08 05:26:51 -05:00
|
|
|
"cloud.google.com/go/compute/apiv1/computepb"
|
2022-11-15 09:40:49 -05:00
|
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
2022-09-21 07:47:57 -04:00
|
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/oid"
|
2022-03-22 11:03:15 -04:00
|
|
|
"github.com/google/go-tpm-tools/proto/attest"
|
|
|
|
"github.com/google/go-tpm-tools/server"
|
|
|
|
"github.com/googleapis/gax-go/v2"
|
|
|
|
"google.golang.org/api/option"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Validator for GCP confidential VM attestation.
|
|
|
|
type Validator struct {
|
|
|
|
oid.GCP
|
|
|
|
*vtpm.Validator
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewValidator initializes a new GCP validator with the provided PCR values.
|
2022-11-24 04:57:58 -05:00
|
|
|
func NewValidator(pcrs measurements.M, log vtpm.AttestationLogger) *Validator {
|
2022-03-22 11:03:15 -04:00
|
|
|
return &Validator{
|
|
|
|
Validator: vtpm.NewValidator(
|
|
|
|
pcrs,
|
|
|
|
trustedKeyFromGCEAPI(newInstanceClient),
|
|
|
|
gceNonHostInfoEvent,
|
|
|
|
vtpm.VerifyPKCS1v15,
|
2022-08-29 10:41:09 -04:00
|
|
|
log,
|
2022-03-22 11:03:15 -04:00
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type gcpRestClient interface {
|
|
|
|
GetShieldedInstanceIdentity(ctx context.Context, req *computepb.GetShieldedInstanceIdentityInstanceRequest, opts ...gax.CallOption) (*computepb.ShieldedInstanceIdentity, error)
|
|
|
|
Close() error
|
|
|
|
}
|
|
|
|
|
|
|
|
type instanceClient struct {
|
|
|
|
*compute.InstancesClient
|
|
|
|
}
|
|
|
|
|
|
|
|
func newInstanceClient(ctx context.Context, opts ...option.ClientOption) (gcpRestClient, error) {
|
|
|
|
c, err := compute.NewInstancesRESTClient(ctx, opts...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &instanceClient{c}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// trustedKeyFromGCEAPI queries the GCE API for a shieldedVM's public signing key.
|
|
|
|
// This key can be used to verify attestation statements issued by the VM.
|
|
|
|
func trustedKeyFromGCEAPI(getClient func(ctx context.Context, opts ...option.ClientOption) (gcpRestClient, error)) func(akPub []byte, instanceInfoRaw []byte) (crypto.PublicKey, error) {
|
|
|
|
return func(akPub, instanceInfoRaw []byte) (crypto.PublicKey, error) {
|
|
|
|
var instanceInfo attest.GCEInstanceInfo
|
|
|
|
if err := json.Unmarshal(instanceInfoRaw, &instanceInfo); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
|
|
defer cancel()
|
|
|
|
client, err := getClient(ctx)
|
|
|
|
if err != nil {
|
2022-06-09 10:04:30 -04:00
|
|
|
return nil, fmt.Errorf("creating GCE client: %w", err)
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
instance, err := client.GetShieldedInstanceIdentity(ctx, &computepb.GetShieldedInstanceIdentityInstanceRequest{
|
|
|
|
Instance: instanceInfo.GetInstanceName(),
|
|
|
|
Project: instanceInfo.GetProjectId(),
|
|
|
|
Zone: instanceInfo.GetZone(),
|
|
|
|
})
|
|
|
|
if err != nil {
|
2022-06-09 10:04:30 -04:00
|
|
|
return nil, fmt.Errorf("retrieving VM identity: %w", err)
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if instance.SigningKey == nil || instance.SigningKey.EkPub == nil {
|
|
|
|
return nil, fmt.Errorf("received no signing key from GCP API")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the signing key return by GetShieldedInstanceIdentity
|
|
|
|
block, _ := pem.Decode([]byte(*instance.SigningKey.EkPub))
|
|
|
|
if block == nil || block.Type != "PUBLIC KEY" {
|
|
|
|
return nil, fmt.Errorf("failed to decode PEM block containing public key")
|
|
|
|
}
|
|
|
|
|
|
|
|
return x509.ParsePKIXPublicKey(block.Bytes)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// gceNonHostInfoEvent looks for the GCE Non-Host info event in an event log.
|
|
|
|
// Returns an error if the event is not found, or if the event is missing the required flag to mark the VM confidential.
|
|
|
|
func gceNonHostInfoEvent(attDoc vtpm.AttestationDocument) error {
|
|
|
|
if attDoc.Attestation == nil {
|
|
|
|
return errors.New("missing attestation in attestation document")
|
|
|
|
}
|
|
|
|
// The event log of a GCE VM contains the GCE Non-Host info event
|
|
|
|
// This event is 32-bytes, followed by one byte 0x01 if it is confidential, 0x00 otherwise,
|
|
|
|
// followed by 15 reserved bytes.
|
|
|
|
// See https://pkg.go.dev/github.com/google/go-tpm-tools@v0.3.1/server#pkg-variables
|
|
|
|
idx := bytes.Index(attDoc.Attestation.EventLog, server.GCENonHostInfoSignature)
|
|
|
|
if idx <= 0 {
|
|
|
|
return fmt.Errorf("event log is missing GCE Non-Host info event")
|
|
|
|
}
|
|
|
|
if attDoc.Attestation.EventLog[idx+len(server.GCENonHostInfoSignature)] != 0x01 {
|
|
|
|
return fmt.Errorf("GCE Non-Host info is missing confidential bit")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|