AB#2190 Verification service (#232)

* Add verification service

* Update verify command to use new Constellation verification service

* Deploy verification service on cluster init

* Update pcr-reader to use verification service

* Add verification service build workflow

Signed-off-by: Daniel Weiße <dw@edgeless.systems>

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2022-06-28 17:03:28 +02:00 committed by GitHub
parent b10b13b173
commit 042f668d20
32 changed files with 1365 additions and 281 deletions

View File

@ -68,17 +68,24 @@ runs:
- name: Read Coordinator IP (Azure)
run: |
echo COORD_IP=$(jq -r .azurecoordinators[].PublicIP constellation-state.json) >> $GITHUB_ENV
echo CONSTELL_IP=$(jq -r .azurecoordinators[].PublicIP constellation-state.json) >> $GITHUB_ENV
shell: bash
if: ${{ inputs.cloudProvider == 'azure' }}
- name: Read Coordinator IP (GCP)
run: |
echo COORD_IP=$(jq -r .gcpcoordinators[].PublicIP constellation-state.json) >> $GITHUB_ENV
echo CONSTELL_IP=$(jq -r .gcpcoordinators[].PublicIP constellation-state.json) >> $GITHUB_ENV
shell: bash
if: ${{ inputs.cloudProvider == 'gcp' }}
- name: Constellation init
run: |
if [ ${{ inputs.autoscale }} = true ]; then autoscale=--autoscale; fi
constellation init ${autoscale}
shell: bash
- name: Fetch PCRs
run: |
pcr-reader --coord-ip ${{ env.COORD_IP }} -o measurements.go
pcr-reader --constell-ip ${{ env.CONSTELL_IP }} -o measurements.go
shell: bash
- name: Upload measurements
uses: actions/upload-artifact@v3
@ -87,12 +94,6 @@ runs:
path: measurements.go
if: ${{ !env.ACT }}
- name: Constellation init
run: |
if [ ${{ inputs.autoscale }} = true ]; then autoscale=--autoscale; fi
constellation init ${autoscale}
shell: bash
- name: Configure VPN connection
run: wg-quick up ./wg0.conf
shell: bash

View File

@ -10,6 +10,7 @@ on:
- 'access-manager'
- 'activation-service'
- 'kmsserver'
- 'verification-service'
required: true
default: 'access-manager'
imageTag:
@ -43,6 +44,8 @@ jobs:
echo "microServiceDockerfile=activation/Dockerfile" >> $GITHUB_ENV ;;
"kmsserver" )
echo "microServiceDockerfile=Dockerfile.kms" >> $GITHUB_ENV ;;
"verification-service" )
echo "microServiceDockerfile=verify/Dockerfile" >> $GITHUB_ENV ;;
esac
- name: Build and upload activation-service container image
@ -50,7 +53,7 @@ jobs:
uses: ./.github/actions/build_micro-service
with:
name: ${{ inputs.microService }}
projectVersion: '0.0.0'
projectVersion: ${{ inputs.version }}
dockerfile: ${{ env.microServiceDockerfile }}
pushTag: ${{ inputs.imageTag }}
githubToken: ${{ secrets.GITHUB_TOKEN }}

View File

@ -0,0 +1,31 @@
name: Build and upload verification-service image
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "verify/**"
- "internal/attestation/**"
- "internal/constants/**"
jobs:
build-activation-service:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out repository
id: checkout
uses: actions/checkout@v3
- name: Build and upload verification-service container image
id: build-and-upload
uses: ./.github/actions/build_micro-service
with:
name: verification-service
projectVersion: '0.0.0'
dockerfile: verify/Dockerfile
githubToken: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@ -21,7 +21,6 @@ go.work.sum
build
admin.conf
coordinator-*
util/pcr-reader/pcrs/
# VS Code configuration folder
.vscode

View File

@ -1,17 +1,23 @@
package cmd
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"github.com/edgelesssys/constellation/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/cli/internal/proto"
"github.com/edgelesssys/constellation/coordinator/util"
"github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/grpc/dialer"
"github.com/edgelesssys/constellation/verify/verifyproto"
"github.com/spf13/afero"
"github.com/spf13/cobra"
rpcStatus "google.golang.org/grpc/status"
"google.golang.org/grpc"
)
// NewVerifyCmd returns a new cobra.Command for the verify command.
@ -37,12 +43,13 @@ func NewVerifyCmd() *cobra.Command {
func runVerify(cmd *cobra.Command, args []string) error {
provider := cloudprovider.FromString(args[0])
fileHandler := file.NewHandler(afero.NewOsFs())
protoClient := &proto.Client{}
defer protoClient.Close()
return verify(cmd, provider, fileHandler, protoClient)
verifyClient := &constellationVerifier{dialer: dialer.New(nil, nil, &net.Dialer{})}
return verify(cmd, provider, fileHandler, verifyClient)
}
func verify(cmd *cobra.Command, provider cloudprovider.Provider, fileHandler file.Handler, protoClient protoClient) error {
func verify(
cmd *cobra.Command, provider cloudprovider.Provider, fileHandler file.Handler, verifyClient verifyClient,
) error {
flags, err := parseVerifyFlags(cmd)
if err != nil {
return err
@ -65,13 +72,24 @@ func verify(cmd *cobra.Command, provider cloudprovider.Provider, fileHandler fil
cmd.Print(validators.Warnings())
}
if err := protoClient.Connect(flags.endpoint, validators.V()); err != nil {
nonce, err := util.GenerateRandomBytes(32)
if err != nil {
return err
}
if _, err := protoClient.GetState(cmd.Context()); err != nil {
if err, ok := rpcStatus.FromError(err); ok {
return fmt.Errorf("verifying Constellation cluster: %s", err.Message())
}
userData, err := util.GenerateRandomBytes(32)
if err != nil {
return err
}
if err := verifyClient.Verify(
cmd.Context(),
flags.endpoint,
&verifyproto.GetAttestationRequest{
Nonce: nonce,
UserData: userData,
},
validators.V()[0],
); err != nil {
return err
}
@ -96,7 +114,7 @@ func parseVerifyFlags(cmd *cobra.Command) (verifyFlags, error) {
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing node-endpoint argument: %w", err)
}
endpoint, err = validateEndpoint(endpoint, constants.CoordinatorPort)
endpoint, err = validateEndpoint(endpoint, constants.VerifyServiceNodePortGRPC)
if err != nil {
return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err)
}
@ -121,7 +139,7 @@ type verifyFlags struct {
configPath string
}
// verifyCompletion handels the completion of CLI arguments. It is frequently called
// verifyCompletion handles the completion of CLI arguments. It is frequently called
// while the user types arguments of the command to suggest completion.
func verifyCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
@ -131,3 +149,43 @@ func verifyCompletion(cmd *cobra.Command, args []string, toComplete string) ([]s
return []string{}, cobra.ShellCompDirectiveError
}
}
type constellationVerifier struct {
dialer grpcDialer
}
// Verify retrieves an attestation statement from the Constellation and verifies it using the validator.
func (v *constellationVerifier) Verify(
ctx context.Context, endpoint string, req *verifyproto.GetAttestationRequest, validator atls.Validator,
) error {
conn, err := v.dialer.DialInsecure(ctx, endpoint)
if err != nil {
return fmt.Errorf("dialing init server: %w", err)
}
defer conn.Close()
client := verifyproto.NewAPIClient(conn)
resp, err := client.GetAttestation(ctx, req)
if err != nil {
return fmt.Errorf("getting attestation: %w", err)
}
signedData, err := validator.Validate(resp.Attestation, req.Nonce)
if err != nil {
return fmt.Errorf("validating attestation: %w", err)
}
if !bytes.Equal(signedData, req.UserData) {
return errors.New("signed data in attestation does not match provided user data")
}
return nil
}
type verifyClient interface {
Verify(ctx context.Context, endpoint string, req *verifyproto.GetAttestationRequest, validator atls.Validator) error
}
type grpcDialer interface {
DialInsecure(ctx context.Context, endpoint string) (conn *grpc.ClientConn, err error)
}

View File

@ -2,16 +2,27 @@ package cmd
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"net"
"strconv"
"testing"
"github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/grpc/dialer"
"github.com/edgelesssys/constellation/internal/grpc/testdialer"
"github.com/edgelesssys/constellation/internal/oid"
"github.com/edgelesssys/constellation/verify/verifyproto"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
rpcStatus "google.golang.org/grpc/status"
)
@ -50,7 +61,7 @@ func TestVerify(t *testing.T) {
testCases := map[string]struct {
setupFs func(*require.Assertions) afero.Fs
provider cloudprovider.Provider
protoClient protoClient
protoClient verifyClient
nodeEndpointFlag string
configFlag string
ownerIDFlag string
@ -62,28 +73,28 @@ func TestVerify(t *testing.T) {
provider: cloudprovider.GCP,
nodeEndpointFlag: "192.0.2.1:1234",
ownerIDFlag: zeroBase64,
protoClient: &stubProtoClient{},
protoClient: &stubVerifyClient{},
},
"azure": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.Azure,
nodeEndpointFlag: "192.0.2.1:1234",
ownerIDFlag: zeroBase64,
protoClient: &stubProtoClient{},
protoClient: &stubVerifyClient{},
},
"default port": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.GCP,
nodeEndpointFlag: "192.0.2.1",
ownerIDFlag: zeroBase64,
protoClient: &stubProtoClient{},
protoClient: &stubVerifyClient{},
},
"invalid endpoint": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.GCP,
nodeEndpointFlag: ":::::",
ownerIDFlag: zeroBase64,
protoClient: &stubProtoClient{},
protoClient: &stubVerifyClient{},
wantErr: true,
},
"neither owner id nor cluster id set": {
@ -100,20 +111,12 @@ func TestVerify(t *testing.T) {
configFlag: "./file",
wantErr: true,
},
"error protoClient Connect": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.Azure,
nodeEndpointFlag: "192.0.2.1:1234",
ownerIDFlag: zeroBase64,
protoClient: &stubProtoClient{connectErr: someErr},
wantErr: true,
},
"error protoClient GetState": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.Azure,
nodeEndpointFlag: "192.0.2.1:1234",
ownerIDFlag: zeroBase64,
protoClient: &stubProtoClient{getStateErr: rpcStatus.Error(codes.Internal, "failed")},
protoClient: &stubVerifyClient{verifyErr: rpcStatus.Error(codes.Internal, "failed")},
wantErr: true,
},
"error protoClient GetState not rpc": {
@ -121,7 +124,7 @@ func TestVerify(t *testing.T) {
provider: cloudprovider.Azure,
nodeEndpointFlag: "192.0.2.1:1234",
ownerIDFlag: zeroBase64,
protoClient: &stubProtoClient{getStateErr: someErr},
protoClient: &stubVerifyClient{verifyErr: someErr},
wantErr: true,
},
}
@ -132,7 +135,7 @@ func TestVerify(t *testing.T) {
require := require.New(t)
cmd := NewVerifyCmd()
cmd.Flags().String("config", "", "") // register persisten flag manually
cmd.Flags().String("config", "", "") // register persistent flag manually
out := &bytes.Buffer{}
cmd.SetOut(out)
cmd.SetErr(&bytes.Buffer{})
@ -193,3 +196,106 @@ func TestVerifyCompletion(t *testing.T) {
})
}
}
func TestVerifyClient(t *testing.T) {
testCases := map[string]struct {
attestationDoc atls.FakeAttestationDoc
userData []byte
nonce []byte
attestationErr error
wantErr bool
}{
"success": {
attestationDoc: atls.FakeAttestationDoc{
UserData: []byte("user data"),
Nonce: []byte("nonce"),
},
userData: []byte("user data"),
nonce: []byte("nonce"),
},
"attestation error": {
attestationDoc: atls.FakeAttestationDoc{
UserData: []byte("user data"),
Nonce: []byte("nonce"),
},
userData: []byte("user data"),
nonce: []byte("nonce"),
attestationErr: errors.New("error"),
wantErr: true,
},
"user data does not match": {
attestationDoc: atls.FakeAttestationDoc{
UserData: []byte("wrong user data"),
Nonce: []byte("nonce"),
},
userData: []byte("user data"),
nonce: []byte("nonce"),
wantErr: true,
},
"nonce does not match": {
attestationDoc: atls.FakeAttestationDoc{
UserData: []byte("user data"),
Nonce: []byte("wrong nonce"),
},
userData: []byte("user data"),
nonce: []byte("nonce"),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
attestation, err := json.Marshal(tc.attestationDoc)
require.NoError(err)
verifyAPI := &stubVerifyAPI{
attestation: &verifyproto.GetAttestationResponse{Attestation: attestation},
attestationErr: tc.attestationErr,
}
netDialer := testdialer.NewBufconnDialer()
dialer := dialer.New(nil, nil, netDialer)
verifyServer := grpc.NewServer()
verifyproto.RegisterAPIServer(verifyServer, verifyAPI)
addr := net.JoinHostPort("192.0.2.1", strconv.Itoa(constants.VerifyServiceNodePortGRPC))
listener := netDialer.GetListener(addr)
go verifyServer.Serve(listener)
defer verifyServer.GracefulStop()
verifier := &constellationVerifier{dialer: dialer}
request := &verifyproto.GetAttestationRequest{
UserData: tc.userData,
Nonce: tc.nonce,
}
err = verifier.Verify(context.Background(), addr, request, atls.NewFakeValidator(oid.Dummy{}))
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
type stubVerifyClient struct {
verifyErr error
}
func (c *stubVerifyClient) Verify(ctx context.Context, endpoint string, req *verifyproto.GetAttestationRequest, validator atls.Validator) error {
return c.verifyErr
}
type stubVerifyAPI struct {
attestation *verifyproto.GetAttestationResponse
attestationErr error
verifyproto.UnimplementedAPIServer
}
func (a stubVerifyAPI) GetAttestation(context.Context, *verifyproto.GetAttestationRequest) (*verifyproto.GetAttestationResponse, error) {
return a.attestation, a.attestationErr
}

View File

@ -104,7 +104,7 @@ func NewAccessManagerDeployment(sshUsers map[string]string) *accessManagerDeploy
InitContainers: []k8s.Container{
{
Name: "constellation-access-manager",
Image: "ghcr.io/edgelesssys/constellation/access-manager:v1.2",
Image: accessManagerImage,
VolumeMounts: []k8s.VolumeMount{
{
Name: "host",

View File

@ -12,8 +12,6 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
)
const activationImage = "ghcr.io/edgelesssys/constellation/activation-service:latest"
type activationDaemonset struct {
ClusterRole rbac.ClusterRole
ClusterRoleBinding rbac.ClusterRoleBinding
@ -111,6 +109,11 @@ func NewActivationDaemonset(csp, measurementsJSON, idJSON string) *activationDae
Value: "true",
Effect: k8s.TaintEffectNoSchedule,
},
{
Key: "node-role.kubernetes.io/control-plane",
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoSchedule,
},
{
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoExecute,

View File

@ -147,6 +147,11 @@ func NewDefaultCloudControllerManagerDeployment(cloudProvider, image, path, podC
Key: "node-role.kubernetes.io/master",
Effect: k8s.TaintEffectNoSchedule,
},
{
Key: "node-role.kubernetes.io/control-plane",
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoSchedule,
},
{
Key: "node.kubernetes.io/not-ready",
Effect: k8s.TaintEffectNoSchedule,

View File

@ -129,6 +129,11 @@ func NewDefaultCloudNodeManagerDeployment(image, path string, extraArgs []string
Value: "true",
Effect: k8s.TaintEffectNoSchedule,
},
{
Key: "node-role.kubernetes.io/control-plane",
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoSchedule,
},
{
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoExecute,

View File

@ -434,7 +434,7 @@ func NewDefaultAutoscalerDeployment(extraVolumes []k8s.Volume, extraVolumeMounts
Containers: []k8s.Container{
{
Name: "cluster-autoscaler",
Image: "k8s.gcr.io/autoscaling/cluster-autoscaler:v1.23.0",
Image: clusterAutoscalerImage,
ImagePullPolicy: k8s.PullIfNotPresent,
LivenessProbe: &k8s.Probe{
ProbeHandler: k8s.ProbeHandler{
@ -461,6 +461,11 @@ func NewDefaultAutoscalerDeployment(extraVolumes []k8s.Volume, extraVolumeMounts
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoSchedule,
},
{
Key: "node-role.kubernetes.io/control-plane",
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoSchedule,
},
{
Key: "node.cloudprovider.kubernetes.io/uninitialized",
Operator: k8s.TolerationOpEqual,

View File

@ -0,0 +1,12 @@
package resources
const (
// Constellation images.
activationImage = "ghcr.io/edgelesssys/constellation/activation-service:v1.2"
accessManagerImage = "ghcr.io/edgelesssys/constellation/access-manager:v1.2"
kmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:v1.2"
verificationImage = "ghcr.io/edgelesssys/constellation/verification-service:v1.2"
// external images.
clusterAutoscalerImage = "k8s.gcr.io/autoscaling/cluster-autoscaler:v1.23.0"
)

View File

@ -22,10 +22,6 @@ type kmsDeployment struct {
ImagePullSecret k8s.Secret
}
const (
kmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:latest"
)
// NewKMSDeployment creates a new *kmsDeployment to use as the key management system inside Constellation.
func NewKMSDeployment(masterSecret []byte) *kmsDeployment {
return &kmsDeployment{
@ -140,6 +136,11 @@ func NewKMSDeployment(masterSecret []byte) *kmsDeployment {
Value: "true",
Effect: k8s.TaintEffectNoSchedule,
},
{
Key: "node-role.kubernetes.io/control-plane",
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoSchedule,
},
{
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoExecute,

View File

@ -0,0 +1,153 @@
package resources
import (
"fmt"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/secrets"
apps "k8s.io/api/apps/v1"
k8s "k8s.io/api/core/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
type verificationDaemonset struct {
DaemonSet apps.DaemonSet
Service k8s.Service
}
func NewVerificationDaemonSet(csp string) *verificationDaemonset {
return &verificationDaemonset{
DaemonSet: apps.DaemonSet{
TypeMeta: meta.TypeMeta{
APIVersion: "apps/v1",
Kind: "DaemonSet",
},
ObjectMeta: meta.ObjectMeta{
Name: "verification-service",
Namespace: "kube-system",
Labels: map[string]string{
"k8s-app": "verification-service",
"component": "verification-service",
},
},
Spec: apps.DaemonSetSpec{
Selector: &meta.LabelSelector{
MatchLabels: map[string]string{
"k8s-app": "verification-service",
},
},
Template: k8s.PodTemplateSpec{
ObjectMeta: meta.ObjectMeta{
Labels: map[string]string{
"k8s-app": "verification-service",
},
},
Spec: k8s.PodSpec{
Tolerations: []k8s.Toleration{
{
Key: "node-role.kubernetes.io/master",
Operator: k8s.TolerationOpEqual,
Value: "true",
Effect: k8s.TaintEffectNoSchedule,
},
{
Key: "node-role.kubernetes.io/control-plane",
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoSchedule,
},
{
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoExecute,
},
{
Operator: k8s.TolerationOpExists,
Effect: k8s.TaintEffectNoSchedule,
},
},
ImagePullSecrets: []k8s.LocalObjectReference{
{
Name: secrets.PullSecretName,
},
},
Containers: []k8s.Container{
{
Name: "verification-service",
Image: verificationImage,
Ports: []k8s.ContainerPort{
{
Name: "http",
ContainerPort: constants.VerifyServicePortHTTP,
},
{
Name: "grpc",
ContainerPort: constants.VerifyServicePortGRPC,
},
},
SecurityContext: &k8s.SecurityContext{
Privileged: func(b bool) *bool { return &b }(true),
},
Args: []string{
fmt.Sprintf("--cloud-provider=%s", csp),
},
VolumeMounts: []k8s.VolumeMount{
{
Name: "event-log",
ReadOnly: true,
MountPath: "/sys/kernel/security/",
},
},
},
},
Volumes: []k8s.Volume{
{
Name: "event-log",
VolumeSource: k8s.VolumeSource{
HostPath: &k8s.HostPathVolumeSource{
Path: "/sys/kernel/security/",
},
},
},
},
},
},
},
},
Service: k8s.Service{
TypeMeta: meta.TypeMeta{
APIVersion: "v1",
Kind: "Service",
},
ObjectMeta: meta.ObjectMeta{
Name: "activation-service",
Namespace: "kube-system",
},
Spec: k8s.ServiceSpec{
Type: k8s.ServiceTypeNodePort,
Ports: []k8s.ServicePort{
{
Name: "http",
Protocol: k8s.ProtocolTCP,
Port: constants.VerifyServicePortHTTP,
TargetPort: intstr.FromInt(constants.VerifyServicePortHTTP),
NodePort: constants.VerifyServiceNodePortHTTP,
},
{
Name: "grpc",
Protocol: k8s.ProtocolTCP,
Port: constants.VerifyServicePortGRPC,
TargetPort: intstr.FromInt(constants.VerifyServicePortGRPC),
NodePort: constants.VerifyServiceNodePortGRPC,
},
},
Selector: map[string]string{
"k8s-app": "verification-service",
},
},
},
}
}
func (v *verificationDaemonset) Marshal() ([]byte, error) {
return MarshalK8SResources(v)
}

View File

@ -0,0 +1,18 @@
package resources
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewVerificationDaemonset(t *testing.T) {
deployment := NewVerificationDaemonSet("csp")
deploymentYAML, err := deployment.Marshal()
require.NoError(t, err)
var recreated verificationDaemonset
require.NoError(t, UnmarshalK8SResources(deploymentYAML, &recreated))
assert.Equal(t, deployment, &recreated)
}

View File

@ -261,6 +261,19 @@ func (k *KubernetesUtil) SetupAccessManager(kubectl Client, accessManagerConfigu
return kubectl.Apply(accessManagerConfiguration, true)
}
// SetupKMS deploys the KMS deployment.
func (k *KubernetesUtil) SetupKMS(kubectl Client, kmsConfiguration resources.Marshaler) error {
if err := kubectl.Apply(kmsConfiguration, true); err != nil {
return fmt.Errorf("applying KMS configuration: %w", err)
}
return nil
}
// SetupVerificationService deploys the verification service.
func (k *KubernetesUtil) SetupVerificationService(kubectl Client, verificationServiceConfiguration resources.Marshaler) error {
return kubectl.Apply(verificationServiceConfiguration, true)
}
// JoinCluster joins existing Kubernetes cluster using kubeadm join.
func (k *KubernetesUtil) JoinCluster(ctx context.Context, joinConfig []byte) error {
// TODO: audit policy should be user input
@ -295,14 +308,6 @@ func (k *KubernetesUtil) JoinCluster(ctx context.Context, joinConfig []byte) err
return nil
}
// SetupKMS deploys the KMS deployment.
func (k *KubernetesUtil) SetupKMS(kubectl Client, kmsConfiguration resources.Marshaler) error {
if err := kubectl.Apply(kmsConfiguration, true); err != nil {
return fmt.Errorf("applying KMS configuration: %w", err)
}
return nil
}
// StartKubelet enables and starts the kubelet systemd unit.
func (k *KubernetesUtil) StartKubelet() error {
ctx, cancel := context.WithTimeout(context.TODO(), kubeletStartTimeout)

View File

@ -20,6 +20,7 @@ type clusterUtil interface {
SetupCloudControllerManager(kubectl k8sapi.Client, cloudControllerManagerConfiguration resources.Marshaler, configMaps resources.Marshaler, secrets resources.Marshaler) error
SetupCloudNodeManager(kubectl k8sapi.Client, cloudNodeManagerConfiguration resources.Marshaler) error
SetupKMS(kubectl k8sapi.Client, kmsConfiguration resources.Marshaler) error
SetupVerificationService(kubectl k8sapi.Client, verificationServiceConfiguration resources.Marshaler) error
StartKubelet() error
RestartKubelet() error
GetControlPlaneJoinCertificateKey(ctx context.Context) (string, error)

View File

@ -167,6 +167,12 @@ func (k *KubeWrapper) InitCluster(
return fmt.Errorf("failed to setup access-manager: %w", err)
}
if err := k.clusterUtil.SetupVerificationService(
k.client, resources.NewVerificationDaemonSet(k.cloudProvider),
); err != nil {
return fmt.Errorf("failed to setup verification service: %w", err)
}
go k.clusterUtil.FixCilium(nodeName)
return nil
@ -256,7 +262,7 @@ func (k *KubeWrapper) setupActivationService(csp string, measurementsJSON []byte
return err
}
activationConfiguration := resources.NewActivationDaemonset(csp, string(measurementsJSON), string(idJSON)) // TODO: set kms endpoint
activationConfiguration := resources.NewActivationDaemonset(csp, string(measurementsJSON), string(idJSON))
return k.clusterUtil.SetupActivationService(k.client, activationConfiguration)
}

View File

@ -239,6 +239,17 @@ func TestInitCluster(t *testing.T) {
ClusterAutoscaler: &stubClusterAutoscaler{},
wantErr: true,
},
"kubeadm init fails when setting up verification service": {
clusterUtil: stubClusterUtil{setupVerificationServiceErr: someErr},
kubeconfigReader: &stubKubeconfigReader{
Kubeconfig: []byte("someKubeconfig"),
},
providerMetadata: &stubProviderMetadata{SupportedResp: false},
CloudControllerManager: &stubCloudControllerManager{},
CloudNodeManager: &stubCloudNodeManager{SupportedResp: false},
ClusterAutoscaler: &stubClusterAutoscaler{},
wantErr: true,
},
}
for name, tc := range testCases {
@ -515,6 +526,7 @@ type stubClusterUtil struct {
setupCloudNodeManagerError error
setupKMSError error
setupAccessManagerError error
setupVerificationServiceErr error
joinClusterErr error
startKubeletErr error
restartKubeletErr error
@ -562,6 +574,10 @@ func (s *stubClusterUtil) SetupCloudNodeManager(kubectl k8sapi.Client, cloudNode
return s.setupCloudNodeManagerError
}
func (s *stubClusterUtil) SetupVerificationService(kubectl k8sapi.Client, verificationServiceConfiguration resources.Marshaler) error {
return s.setupVerificationServiceErr
}
func (s *stubClusterUtil) JoinCluster(ctx context.Context, joinConfig []byte) error {
s.joinConfigs = append(s.joinConfigs, joinConfig)
return s.joinClusterErr

View File

@ -5,60 +5,17 @@ This utility program makes it simple to update the expected PCR values of the CL
## Usage
### Script
Run `fetch_pcrs.sh` to create Constellations on all supported cloud providers and read their PCR states.
```shell
./fetch_pcrs.sh
```
The result is printed to screen and written as Go code to files in `./pcrs`.
```bash
+ main
+ command -v constellation
+ command -v go
+ mkdir -p ./pcrs
+ constellation create azure 2 Standard_D4s_v3 --name pcr-fetch -y
Your Constellation was created successfully.
++ jq '.azurecoordinators | to_entries[] | select(.key|startswith("")) | .value.PublicIP' -rcM constellation-state.json
+ coord_ip=192.0.2.1
+ go run ../main.go -coord-ip 192.0.2.1 -o ./pcrs/azure_pcrs.go
connecting to Coordinator at 192.0.2.1:9000
PCRs:
{
"0": "q27iAZeXGAiCPdu1bqRA2gAoyMO2KrXWY4YkTCQowc4=",
...
"9": "dEGJtQe3h+SI0z42yO7TklzwPixtM3iMCUeJPGRozvg="
}
+ constellation terminate
Your Constellation was terminated successfully.
+ constellation create gcp 2 n2d-standard-2 --name pcr-fetch -y
Your Constellation was created successfully.
++ jq '.gcpcoordinators | to_entries[] | select(.key|startswith("")) | .value.PublicIP' -rcM constellation-state.json
+ coord_ip=192.0.2.2
+ go run ../main.go -coord-ip 192.0.2.2 -o ./pcrs/gcp_pcrs.go
connecting to Coordinator at 192.0.2.2:9000
PCRs:
{
"0": "DzXCFGCNk8em5ornNZtKi+Wg6Z7qkQfs5CfE3qTkOc8=",
...
"9": "gse53SjsqREEdOpImJH4KAb0b8PqIgwI+Ps/XSiFnN4="
}
+ constellation terminate
Your Constellation was terminated successfully.
```
### Manual
To read the PCR state of any running Constellation node, run the following:
```shell
go run main.go -coord-ip <NODE_IP> -coord-port <COORDINATOR_PORT>
go run main.go -constell-ip <NODE_IP> -constell-port <COORDINATOR_PORT>
```
The output is similar to the following:
```shell
$ go run main.go -coord-ip 192.0.2.3 -coord-port 12345
connecting to Coordinator at 192.0.2.3:12345
$ go run main.go -constell-ip 192.0.2.3 -constell-port 30081
connecting to Coordinator at 192.0.2.3:30081
PCRs:
{
"0": "DzXCFGCNk8em5ornNZtKi+Wg6Z7qkQfs5CfE3qTkOc8=",
@ -96,7 +53,7 @@ Optionally filter down results measurements per cloud provider:
Azure
```bash
./pcr-reader --coord-ip ${COORD_IP} --format yaml | yq e 'del(.[0,6,10,11,12,13,14,15,16,17,18,19,20,21,22,23])' -
./pcr-reader --constell-ip ${CONSTELLATION_IP} --format yaml | yq e 'del(.[0,6,10,11,12,13,14,15,16,17,18,19,20,21,22,23])' -
```
## Meaning of PCR values
@ -109,6 +66,7 @@ We use the TPM and its PCRs to verify all nodes of a Constellation run with the
PCR[0] measures the firmware volume (FV). Changes to FV also change PCR[0], making it unreliable for attestation.
PCR[6] measures the VM ID. This is unusable for cluster attestation for two reasons:
1. The Coordinator does not know the VM ID of nodes wanting to join the cluster, so it can not compute the expected PCR[6] for the joining VM
2. A user may attest any node of the cluster without knowing the VM ID

View File

@ -1,33 +0,0 @@
#!/bin/bash
set -o xtrace
trap 'terminate $?' ERR
terminate() {
echo "error: $1"
constellation terminate
exit 1
}
main() {
command -v constellation > /dev/null
command -v go > /dev/null
command -v jq > /dev/null
mkdir -p ./pcrs
# Fetch Azure PCRs
# TODO: Switch to confidential VMs
constellation create azure 2 Standard_D4s_v3 --name pcr-fetch -y
coord_ip=$(jq '.azurecoordinators | to_entries[] | select(.key|startswith("")) | .value.PublicIP' -rcM constellation-state.json)
go run main.go -coord-ip "${coord_ip}" -o ./pcrs/azure_pcrs.go
constellation terminate
# Fetch GCP PCRs
constellation create gcp 2 n2d-standard-2 --name pcr-fetch -y
coord_ip=$(jq '.gcpcoordinators | to_entries[] | select(.key|startswith("")) | .value.PublicIP' -rcM constellation-state.json)
go run main.go -coord-ip "${coord_ip}" -o ./pcrs/gcp_pcrs.go
constellation terminate
}
main

View File

@ -2,8 +2,6 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
@ -13,29 +11,26 @@ import (
"log"
"net"
"os"
"strconv"
"time"
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
"github.com/edgelesssys/constellation/coordinator/state"
"github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/attestation/azure"
"github.com/edgelesssys/constellation/internal/attestation/gcp"
"github.com/edgelesssys/constellation/coordinator/util"
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/internal/oid"
"github.com/edgelesssys/constellation/internal/statuswaiter"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/verify/verifyproto"
"github.com/spf13/afero"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"gopkg.in/yaml.v3"
)
var (
coordIP = flag.String("coord-ip", "", "IP of the VM the Coordinator is running on")
coordinatorPort = flag.String("coord-port", "9000", "Port of the Coordinator's pub API")
coordIP = flag.String("constell-ip", "", "Public IP of the Constellation")
coordinatorPort = 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 Coordinator to become available")
timeout = flag.Duration("timeout", 2*time.Minute, "Wait this duration for the verification service to become available")
)
func main() {
@ -45,27 +40,10 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
// wait for coordinator to come online
waiter := statuswaiter.New()
if err := waiter.InitializeValidators([]atls.Validator{
azure.NewValidator(map[uint32][]byte{}),
gcp.NewValidator(map[uint32][]byte{}),
}); err != nil {
log.Fatal(err)
}
if err := waiter.WaitFor(ctx, addr, state.AcceptingInit, state.ActivatingNodes, state.IsNode, state.NodeWaitingForClusterJoin); err != nil {
log.Fatal(err)
}
attDocRaw := []byte{}
tlsConfig, err := atls.CreateAttestationClientTLSConfig(nil, nil)
attDocRaw, err := getAttestation(ctx, addr)
if err != nil {
log.Fatal(err)
}
tlsConfig.VerifyPeerCertificate = getVerifyPeerCertificateFunc(&attDocRaw)
if err := connectToCoordinator(ctx, addr, tlsConfig); err != nil {
log.Fatal(err)
}
pcrs, err := validatePCRAttDoc(attDocRaw)
if err != nil {
@ -98,45 +76,27 @@ func (m Measurements) MarshalYAML() (interface{}, error) {
return base64Map, nil
}
// connectToCoordinator connects to the Constellation Coordinator and returns its attestation document.
func connectToCoordinator(ctx context.Context, addr string, tlsConfig *tls.Config) error {
// getAttestation connects to the Constellation verification service and returns its attestation document.
func getAttestation(ctx context.Context, addr string) ([]byte, error) {
conn, err := grpc.DialContext(
ctx, addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return err
return nil, fmt.Errorf("unable to connect to verification service: %w", err)
}
defer conn.Close()
client := pubproto.NewAPIClient(conn)
_, err = client.GetState(ctx, &pubproto.GetStateRequest{})
return err
}
// getVerifyPeerCertificateFunc returns a VerifyPeerCertificate function, which writes the attestation document extension to the given byte slice pointer.
func getVerifyPeerCertificateFunc(attDoc *[]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return errors.New("rawCerts is empty")
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return err
}
for _, ex := range cert.Extensions {
if ex.Id.Equal(oid.Azure{}.OID()) || ex.Id.Equal(oid.GCP{}.OID()) {
if err := json.Unmarshal(ex.Value, attDoc); err != nil {
*attDoc = ex.Value
}
}
}
if len(*attDoc) == 0 {
return errors.New("did not receive attestation document in certificate extension")
}
return nil
nonce, err := util.GenerateRandomBytes(32)
if err != nil {
return nil, err
}
client := verifyproto.NewAPIClient(conn)
res, err := client.GetAttestation(ctx, &verifyproto.GetAttestationRequest{Nonce: nonce, UserData: nonce})
if err != nil {
return nil, err
}
return res.Attestation, nil
}
// validatePCRAttDoc parses and validates PCRs of an attestation document.

View File

@ -2,19 +2,12 @@ package main
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"testing"
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/internal/oid"
"github.com/google/go-tpm-tools/proto/attest"
"github.com/google/go-tpm-tools/proto/tpm"
"github.com/spf13/afero"
@ -22,78 +15,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestGetVerifyPeerCertificateFunc(t *testing.T) {
testCases := map[string]struct {
rawCerts [][]byte
wantErr bool
}{
"no certificates": {
rawCerts: nil,
wantErr: true,
},
"invalid certificate": {
rawCerts: [][]byte{
{0x1, 0x2, 0x3},
},
wantErr: true,
},
"no extension": {
rawCerts: [][]byte{
mustGenerateTestCert(t, &x509.Certificate{
SerialNumber: big.NewInt(123),
}),
},
wantErr: true,
},
"certificate with attestation": {
rawCerts: [][]byte{
mustGenerateTestCert(t, &x509.Certificate{
SerialNumber: big.NewInt(123),
ExtraExtensions: []pkix.Extension{
{
Id: oid.GCP{}.OID(),
Value: []byte{0x1, 0x2, 0x3},
Critical: true,
},
},
}),
},
wantErr: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
attDoc := &[]byte{}
verify := getVerifyPeerCertificateFunc(attDoc)
err := verify(tc.rawCerts, nil)
if tc.wantErr {
assert.Error(err)
} else {
require.NoError(err)
assert.NotNil(attDoc)
cert, err := x509.ParseCertificate(tc.rawCerts[0])
require.NoError(err)
assert.Equal(cert.Extensions[0].Value, *attDoc)
}
})
}
}
func mustGenerateTestCert(t *testing.T, template *x509.Certificate) []byte {
require := require.New(t)
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(err)
cert, err := x509.CreateCertificate(rand.Reader, template, template, priv.Public(), priv)
require.NoError(err)
return cert
}
func TestExportToFile(t *testing.T) {
testCases := map[string]struct {
pcrs map[uint32][]byte

View File

@ -24,6 +24,10 @@ const (
ActivationServicePort = 9090
ActivationServiceNodePort = 30090
VerifyServicePortHTTP = 8080
VerifyServicePortGRPC = 9090
VerifyServiceNodePortHTTP = 30080
VerifyServiceNodePortGRPC = 30081
KMSPort = 9000
CoordinatorPort = 9000
EnclaveSSHPort = 2222

View File

@ -54,6 +54,11 @@ WORKDIR /activation
COPY activation/activationproto/*.proto /activation
RUN protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
## verify
WORKDIR /verify
COPY verify/verifyproto/*.proto /verify
RUN protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
FROM scratch as export
COPY --from=build /pubapi/*.go coordinator/pubapi/pubproto/
COPY --from=build /vpnapi/*.go coordinator/vpnapi/vpnproto/
@ -61,3 +66,4 @@ COPY --from=build /disk-mapper/*.go state/keyservice/keyproto/
COPY --from=build /service/*.go debugd/service/
COPY --from=build /kms/*.go kms/server/kmsapi/kmsproto/
COPY --from=build /activation/*.go activation/activationproto/
COPY --from=build /verify/*.go verify/verifyproto/

30
verify/Dockerfile Normal file
View File

@ -0,0 +1,30 @@
FROM fedora@sha256:36af84ba69e21c9ef86a0424a090674c433b2b80c2462e57503886f1d823abe8 as build
RUN dnf -y update && \
dnf install -y iproute iputils wget git && \
dnf clean all
# Install Go
ARG GO_VER=1.18
RUN wget https://go.dev/dl/go${GO_VER}.linux-amd64.tar.gz && \
tar -C /usr/local -xzf go${GO_VER}.linux-amd64.tar.gz && \
rm go${GO_VER}.linux-amd64.tar.gz
ENV PATH ${PATH}:/usr/local/go/bin
# Download go dependencies
WORKDIR /constellation/
COPY go.mod ./
COPY go.sum ./
RUN go mod download all
# Copy Repo
COPY . /constellation
RUN rm -rf ./hack/
WORKDIR /constellation/verify
ARG PROJECT_VERSION=0.0.0
RUN CGO_ENABLED=0 go build -o verify-service -trimpath -buildvcs=false -ldflags "-s -w -buildid='' -X github.com/edgelesssys/constellation/internal/constants.VersionInfo=${PROJECT_VERSION}" ./cmd/
FROM scratch AS release
COPY --from=build /constellation/verify/verify-service /verify
ENTRYPOINT [ "/verify" ]

54
verify/cmd/main.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"flag"
"net"
"strconv"
"github.com/edgelesssys/constellation/internal/attestation/azure"
"github.com/edgelesssys/constellation/internal/attestation/gcp"
"github.com/edgelesssys/constellation/internal/attestation/qemu"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/logger"
"github.com/edgelesssys/constellation/verify/server"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
provider := flag.String("cloud-provider", "", "cloud service provider this binary is running on")
flag.Parse()
log := logger.New(logger.JSONLog, zapcore.InfoLevel)
log.With(zap.String("version", constants.VersionInfo), zap.String("cloudProvider", *provider)).
Infof("Constellation Verification Service")
var issuer server.AttestationIssuer
switch *provider {
case "gcp":
issuer = gcp.NewIssuer()
case "azure":
issuer = azure.NewIssuer()
case "qemu":
issuer = qemu.NewIssuer()
default:
log.With(zap.String("cloudProvider", *provider)).Fatalf("Unknown cloud provider")
}
server := server.New(log.Named("server"), issuer)
httpListener, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(constants.VerifyServicePortHTTP)))
if err != nil {
log.With(zap.Error(err), zap.Int("port", constants.VerifyServicePortHTTP)).
Fatalf("Failed to listen")
}
grpcListener, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(constants.VerifyServicePortGRPC)))
if err != nil {
log.With(zap.Error(err), zap.Int("port", constants.VerifyServicePortGRPC)).
Fatalf("Failed to listen")
}
if err := server.Run(httpListener, grpcListener); err != nil {
log.With(zap.Error(err)).Fatalf("Failed to run server")
}
}

160
verify/server/server.go Normal file
View File

@ -0,0 +1,160 @@
package server
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/http"
"sync"
"github.com/edgelesssys/constellation/internal/logger"
"github.com/edgelesssys/constellation/verify/verifyproto"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
type attestation struct {
Data []byte `json:"data"`
}
// Server implements Constellation's verify API.
// The server exposes both HTTP and gRPC endpoints
// to retrieve attestation statements.
type Server struct {
log *logger.Logger
issuer AttestationIssuer
verifyproto.UnimplementedAPIServer
}
// New initializes a new verification server.
func New(log *logger.Logger, issuer AttestationIssuer) *Server {
return &Server{
log: log,
issuer: issuer,
}
}
// Run starts the HTTP and gRPC servers.
// If one of the servers fails, other server will be closed and the error will be returned.
func (s *Server) Run(httpListener, grpcListener net.Listener) error {
var err error
var wg sync.WaitGroup
var once sync.Once
s.log.WithIncreasedLevel(zapcore.WarnLevel).Named("grpc").ReplaceGRPCLogger()
grpcServer := grpc.NewServer(s.log.Named("gRPC").GetServerUnaryInterceptor())
verifyproto.RegisterAPIServer(grpcServer, s)
httpHandler := http.NewServeMux()
httpHandler.HandleFunc("/", s.getAttestationHTTP)
httpServer := &http.Server{Handler: httpHandler}
wg.Add(1)
go func() {
defer wg.Done()
defer grpcServer.GracefulStop()
s.log.Infof("Starting HTTP server on %s", httpListener.Addr().String())
httpErr := httpServer.Serve(httpListener)
if httpErr != nil && httpErr != http.ErrServerClosed {
once.Do(func() { err = httpErr })
}
}()
wg.Add(1)
go func() {
defer wg.Done()
defer func() { _ = httpServer.Shutdown(context.Background()) }()
s.log.Infof("Starting gRPC server on %s", grpcListener.Addr().String())
grpcErr := grpcServer.Serve(grpcListener)
if grpcErr != nil {
once.Do(func() { err = grpcErr })
}
}()
wg.Wait()
return err
}
// GetAttestation implements the gRPC endpoint for requesting attestation statements.
func (s *Server) GetAttestation(ctx context.Context, req *verifyproto.GetAttestationRequest) (*verifyproto.GetAttestationResponse, error) {
peerAddr := "unknown"
if peer, ok := peer.FromContext(ctx); ok {
peerAddr = peer.Addr.String()
}
log := s.log.With(zap.String("peerAddress", peerAddr)).Named("gRPC")
s.log.Infof("Received attestation request")
if len(req.Nonce) == 0 {
log.Errorf("Received attestation request with empty nonce")
return nil, status.Error(codes.InvalidArgument, "nonce is required to issue attestation")
}
if len(req.UserData) == 0 {
log.Errorf("Received attestation request with empty user data")
return nil, status.Error(codes.InvalidArgument, "user data is required to issue attestation")
}
log.Infof("Creating attestation")
statement, err := s.issuer.Issue(req.UserData, req.Nonce)
if err != nil {
return nil, status.Errorf(codes.Internal, "issuing attestation statement: %v", err)
}
log.Infof("Attestation request successful")
return &verifyproto.GetAttestationResponse{Attestation: statement}, nil
}
// getAttestationHTTP implements the HTTP endpoint for retrieving attestation statements.
func (s *Server) getAttestationHTTP(w http.ResponseWriter, r *http.Request) {
log := s.log.With(zap.String("peerAddress", r.RemoteAddr)).Named("http")
nonceB64 := r.URL.Query()["nonce"]
if len(nonceB64) != 1 || nonceB64[0] == "" {
log.Errorf("Received attestation request with empty or multiple nonce parameter")
http.Error(w, "nonce parameter is required exactly once", http.StatusBadRequest)
return
}
userDataB64 := r.URL.Query()["userData"]
if len(userDataB64) != 1 || userDataB64[0] == "" {
log.Errorf("Received attestation request with empty or multiple user data parameter")
http.Error(w, "userData parameter is required exactly once", http.StatusBadRequest)
return
}
nonce, err := base64.URLEncoding.DecodeString(nonceB64[0])
if err != nil {
log.With(zap.Error(err)).Errorf("Received attestation request with invalid nonce")
http.Error(w, fmt.Sprintf("invalid base64 encoding for nonce: %v", err), http.StatusBadRequest)
return
}
userData, err := base64.URLEncoding.DecodeString(userDataB64[0])
if err != nil {
log.With(zap.Error(err)).Errorf("Received attestation request with invalid user data")
http.Error(w, fmt.Sprintf("invalid base64 encoding for userData: %v", err), http.StatusBadRequest)
return
}
log.Infof("Creating attestation")
quote, err := s.issuer.Issue(userData, nonce)
if err != nil {
http.Error(w, fmt.Sprintf("issuing attestation statement: %v", err), http.StatusInternalServerError)
return
}
log.Infof("Attestation request successful")
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(attestation{quote}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
type AttestationIssuer interface {
Issue(userData []byte, nonce []byte) (quote []byte, err error)
}

View File

@ -0,0 +1,247 @@
package server
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/edgelesssys/constellation/internal/grpc/testdialer"
"github.com/edgelesssys/constellation/internal/logger"
"github.com/edgelesssys/constellation/verify/verifyproto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestRun(t *testing.T) {
assert := assert.New(t)
closedErr := errors.New("closed")
var err error
var wg sync.WaitGroup
s := &Server{
log: logger.NewTest(t),
issuer: stubIssuer{attestation: []byte("quote")},
}
httpListener, grpcListener := setUpTestListeners()
wg.Add(1)
go func() {
defer wg.Done()
err = s.Run(httpListener, grpcListener)
}()
assert.NoError(httpListener.Close())
wg.Wait()
assert.Equal(err, closedErr)
httpListener, grpcListener = setUpTestListeners()
wg.Add(1)
go func() {
defer wg.Done()
err = s.Run(httpListener, grpcListener)
}()
assert.NoError(grpcListener.Close())
wg.Wait()
assert.Equal(err, closedErr)
httpListener, grpcListener = setUpTestListeners()
wg.Add(1)
go func() {
defer wg.Done()
err = s.Run(httpListener, grpcListener)
}()
go assert.NoError(grpcListener.Close())
go assert.NoError(httpListener.Close())
wg.Wait()
assert.Equal(err, closedErr)
}
func TestGetAttestationGRPC(t *testing.T) {
testCases := map[string]struct {
issuer stubIssuer
request *verifyproto.GetAttestationRequest
wantErr bool
}{
"success": {
issuer: stubIssuer{attestation: []byte("quote")},
request: &verifyproto.GetAttestationRequest{
Nonce: []byte("nonce"),
UserData: []byte("userData"),
},
},
"issuer fails": {
issuer: stubIssuer{issueErr: errors.New("issuer error")},
request: &verifyproto.GetAttestationRequest{
Nonce: []byte("nonce"),
UserData: []byte("userData"),
},
wantErr: true,
},
"no nonce": {
issuer: stubIssuer{attestation: []byte("quote")},
request: &verifyproto.GetAttestationRequest{
UserData: []byte("userData"),
},
wantErr: true,
},
"no userData": {
issuer: stubIssuer{attestation: []byte("quote")},
request: &verifyproto.GetAttestationRequest{
Nonce: []byte("nonce"),
},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
server := &Server{
log: logger.NewTest(t),
issuer: tc.issuer,
}
resp, err := server.GetAttestation(context.Background(), tc.request)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.issuer.attestation, resp.Attestation)
}
})
}
}
func TestGetAttestationHTTP(t *testing.T) {
testCases := map[string]struct {
request string
issuer stubIssuer
wantErr bool
}{
"success": {
request: fmt.Sprintf(
"?nonce=%s&userData=%s",
base64.URLEncoding.EncodeToString([]byte("nonce")),
base64.URLEncoding.EncodeToString([]byte("userData")),
),
issuer: stubIssuer{attestation: []byte("quote")},
},
"invalid nonce in query": {
request: fmt.Sprintf(
"?nonce=not-base-64&userData=%s",
base64.URLEncoding.EncodeToString([]byte("userData")),
),
issuer: stubIssuer{attestation: []byte("quote")},
wantErr: true,
},
"no nonce in query": {
request: fmt.Sprintf(
"?userData=%s",
base64.URLEncoding.EncodeToString([]byte("userData")),
),
issuer: stubIssuer{attestation: []byte("quote")},
wantErr: true,
},
"empty nonce in query": {
request: fmt.Sprintf(
"?nonce=&userData=%s",
base64.URLEncoding.EncodeToString([]byte("userData")),
),
issuer: stubIssuer{attestation: []byte("quote")},
wantErr: true,
},
"invalid userData in query": {
request: fmt.Sprintf(
"?nonce=%s&userData=not-base-64",
base64.URLEncoding.EncodeToString([]byte("nonce")),
),
issuer: stubIssuer{attestation: []byte("quote")},
wantErr: true,
},
"no userData in query": {
request: fmt.Sprintf(
"?nonce=%s",
base64.URLEncoding.EncodeToString([]byte("nonce")),
),
issuer: stubIssuer{attestation: []byte("quote")},
wantErr: true,
},
"empty userData in query": {
request: fmt.Sprintf(
"?nonce=%s&userData=",
base64.URLEncoding.EncodeToString([]byte("nonce")),
),
issuer: stubIssuer{attestation: []byte("quote")},
wantErr: true,
},
"issuer fails": {
request: fmt.Sprintf(
"?nonce=%s&userData=%s",
base64.URLEncoding.EncodeToString([]byte("nonce")),
base64.URLEncoding.EncodeToString([]byte("userData")),
),
issuer: stubIssuer{issueErr: errors.New("errors")},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
server := &Server{
log: logger.NewTest(t),
issuer: tc.issuer,
}
httpServer := httptest.NewServer(http.HandlerFunc(server.getAttestationHTTP))
defer httpServer.Close()
resp, err := http.Get(httpServer.URL + tc.request)
require.NoError(err)
defer resp.Body.Close()
if tc.wantErr {
assert.NotEqual(http.StatusOK, resp.StatusCode)
return
}
assert.Equal(http.StatusOK, resp.StatusCode)
quote, err := io.ReadAll(resp.Body)
require.NoError(err)
var rawQuote attestation
require.NoError(json.Unmarshal(quote, &rawQuote))
assert.Equal(tc.issuer.attestation, rawQuote.Data)
})
}
}
func setUpTestListeners() (net.Listener, net.Listener) {
httpListener := testdialer.NewBufconnDialer().GetListener(net.JoinHostPort("192.0.2.1", "8080"))
grpcListener := testdialer.NewBufconnDialer().GetListener(net.JoinHostPort("192.0.2.1", "8081"))
return httpListener, grpcListener
}
type stubIssuer struct {
attestation []byte
issueErr error
}
func (i stubIssuer) Issue(userData []byte, nonce []byte) ([]byte, error) {
return i.attestation, i.issueErr
}

View File

@ -0,0 +1,226 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc v3.20.1
// source: verify.proto
package verifyproto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type GetAttestationRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserData []byte `protobuf:"bytes,1,opt,name=user_data,json=userData,proto3" json:"user_data,omitempty"`
Nonce []byte `protobuf:"bytes,2,opt,name=nonce,proto3" json:"nonce,omitempty"`
}
func (x *GetAttestationRequest) Reset() {
*x = GetAttestationRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_verify_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetAttestationRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAttestationRequest) ProtoMessage() {}
func (x *GetAttestationRequest) ProtoReflect() protoreflect.Message {
mi := &file_verify_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetAttestationRequest.ProtoReflect.Descriptor instead.
func (*GetAttestationRequest) Descriptor() ([]byte, []int) {
return file_verify_proto_rawDescGZIP(), []int{0}
}
func (x *GetAttestationRequest) GetUserData() []byte {
if x != nil {
return x.UserData
}
return nil
}
func (x *GetAttestationRequest) GetNonce() []byte {
if x != nil {
return x.Nonce
}
return nil
}
type GetAttestationResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Attestation []byte `protobuf:"bytes,1,opt,name=attestation,proto3" json:"attestation,omitempty"`
}
func (x *GetAttestationResponse) Reset() {
*x = GetAttestationResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_verify_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetAttestationResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAttestationResponse) ProtoMessage() {}
func (x *GetAttestationResponse) ProtoReflect() protoreflect.Message {
mi := &file_verify_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetAttestationResponse.ProtoReflect.Descriptor instead.
func (*GetAttestationResponse) Descriptor() ([]byte, []int) {
return file_verify_proto_rawDescGZIP(), []int{1}
}
func (x *GetAttestationResponse) GetAttestation() []byte {
if x != nil {
return x.Attestation
}
return nil
}
var File_verify_proto protoreflect.FileDescriptor
var file_verify_proto_rawDesc = []byte{
0x0a, 0x0c, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06,
0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x22, 0x4a, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74,
0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x1b, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0c, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05,
0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e,
0x63, 0x65, 0x22, 0x3a, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0b,
0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x56,
0x0a, 0x03, 0x41, 0x50, 0x49, 0x12, 0x4f, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x65,
0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x2e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79,
0x2e, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x2e,
0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x73, 0x79, 0x73,
0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x65, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76,
0x65, 0x72, 0x69, 0x66, 0x79, 0x2f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_verify_proto_rawDescOnce sync.Once
file_verify_proto_rawDescData = file_verify_proto_rawDesc
)
func file_verify_proto_rawDescGZIP() []byte {
file_verify_proto_rawDescOnce.Do(func() {
file_verify_proto_rawDescData = protoimpl.X.CompressGZIP(file_verify_proto_rawDescData)
})
return file_verify_proto_rawDescData
}
var file_verify_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_verify_proto_goTypes = []interface{}{
(*GetAttestationRequest)(nil), // 0: verify.GetAttestationRequest
(*GetAttestationResponse)(nil), // 1: verify.GetAttestationResponse
}
var file_verify_proto_depIdxs = []int32{
0, // 0: verify.API.GetAttestation:input_type -> verify.GetAttestationRequest
1, // 1: verify.API.GetAttestation:output_type -> verify.GetAttestationResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_verify_proto_init() }
func file_verify_proto_init() {
if File_verify_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_verify_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetAttestationRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_verify_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetAttestationResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_verify_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_verify_proto_goTypes,
DependencyIndexes: file_verify_proto_depIdxs,
MessageInfos: file_verify_proto_msgTypes,
}.Build()
File_verify_proto = out.File
file_verify_proto_rawDesc = nil
file_verify_proto_goTypes = nil
file_verify_proto_depIdxs = nil
}

View File

@ -0,0 +1,18 @@
syntax = "proto3";
package verify;
option go_package = "github.com/edgelesssys/constellation/verify/verifyproto";
service API {
rpc GetAttestation(GetAttestationRequest) returns (GetAttestationResponse);
}
message GetAttestationRequest {
bytes user_data = 1;
bytes nonce = 2;
}
message GetAttestationResponse {
bytes attestation = 1;
}

View File

@ -0,0 +1,105 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.20.1
// source: verify.proto
package verifyproto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// APIClient is the client API for API service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type APIClient interface {
GetAttestation(ctx context.Context, in *GetAttestationRequest, opts ...grpc.CallOption) (*GetAttestationResponse, error)
}
type aPIClient struct {
cc grpc.ClientConnInterface
}
func NewAPIClient(cc grpc.ClientConnInterface) APIClient {
return &aPIClient{cc}
}
func (c *aPIClient) GetAttestation(ctx context.Context, in *GetAttestationRequest, opts ...grpc.CallOption) (*GetAttestationResponse, error) {
out := new(GetAttestationResponse)
err := c.cc.Invoke(ctx, "/verify.API/GetAttestation", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// APIServer is the server API for API service.
// All implementations must embed UnimplementedAPIServer
// for forward compatibility
type APIServer interface {
GetAttestation(context.Context, *GetAttestationRequest) (*GetAttestationResponse, error)
mustEmbedUnimplementedAPIServer()
}
// UnimplementedAPIServer must be embedded to have forward compatible implementations.
type UnimplementedAPIServer struct {
}
func (UnimplementedAPIServer) GetAttestation(context.Context, *GetAttestationRequest) (*GetAttestationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAttestation not implemented")
}
func (UnimplementedAPIServer) mustEmbedUnimplementedAPIServer() {}
// UnsafeAPIServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to APIServer will
// result in compilation errors.
type UnsafeAPIServer interface {
mustEmbedUnimplementedAPIServer()
}
func RegisterAPIServer(s grpc.ServiceRegistrar, srv APIServer) {
s.RegisterService(&API_ServiceDesc, srv)
}
func _API_GetAttestation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAttestationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(APIServer).GetAttestation(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/verify.API/GetAttestation",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(APIServer).GetAttestation(ctx, req.(*GetAttestationRequest))
}
return interceptor(ctx, in, info, handler)
}
// API_ServiceDesc is the grpc.ServiceDesc for API service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var API_ServiceDesc = grpc.ServiceDesc{
ServiceName: "verify.API",
HandlerType: (*APIServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetAttestation",
Handler: _API_GetAttestation_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "verify.proto",
}