diff --git a/.github/actions/constellation_create/action.yml b/.github/actions/constellation_create/action.yml index db56cab7d..67793d2aa 100644 --- a/.github/actions/constellation_create/action.yml +++ b/.github/actions/constellation_create/action.yml @@ -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 diff --git a/.github/workflows/build-micro-service-manual.yml b/.github/workflows/build-micro-service-manual.yml index 1d2a46693..c36fde629 100644 --- a/.github/workflows/build-micro-service-manual.yml +++ b/.github/workflows/build-micro-service-manual.yml @@ -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 }} diff --git a/.github/workflows/build-verification-service.yml b/.github/workflows/build-verification-service.yml new file mode 100644 index 000000000..032ccc356 --- /dev/null +++ b/.github/workflows/build-verification-service.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 078615d6e..b7391dc0e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ go.work.sum build admin.conf coordinator-* -util/pcr-reader/pcrs/ # VS Code configuration folder .vscode diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index bc779b5b7..5a08e501c 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -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) +} diff --git a/cli/internal/cmd/verify_test.go b/cli/internal/cmd/verify_test.go index 02ef083f5..e184f773a 100644 --- a/cli/internal/cmd/verify_test.go +++ b/cli/internal/cmd/verify_test.go @@ -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 +} diff --git a/coordinator/kubernetes/k8sapi/resources/access_manager.go b/coordinator/kubernetes/k8sapi/resources/access_manager.go index 06311de61..283301500 100644 --- a/coordinator/kubernetes/k8sapi/resources/access_manager.go +++ b/coordinator/kubernetes/k8sapi/resources/access_manager.go @@ -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", diff --git a/coordinator/kubernetes/k8sapi/resources/activation.go b/coordinator/kubernetes/k8sapi/resources/activation.go index 556fa4d88..c1dc0b218 100644 --- a/coordinator/kubernetes/k8sapi/resources/activation.go +++ b/coordinator/kubernetes/k8sapi/resources/activation.go @@ -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, diff --git a/coordinator/kubernetes/k8sapi/resources/cloud_controller_manager.go b/coordinator/kubernetes/k8sapi/resources/cloud_controller_manager.go index e5971c01f..a224736af 100644 --- a/coordinator/kubernetes/k8sapi/resources/cloud_controller_manager.go +++ b/coordinator/kubernetes/k8sapi/resources/cloud_controller_manager.go @@ -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, diff --git a/coordinator/kubernetes/k8sapi/resources/cloudnodemanager.go b/coordinator/kubernetes/k8sapi/resources/cloudnodemanager.go index e47f5a49a..8a511cb0c 100644 --- a/coordinator/kubernetes/k8sapi/resources/cloudnodemanager.go +++ b/coordinator/kubernetes/k8sapi/resources/cloudnodemanager.go @@ -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, diff --git a/coordinator/kubernetes/k8sapi/resources/cluster_autoscaler.go b/coordinator/kubernetes/k8sapi/resources/cluster_autoscaler.go index d6c3670ca..6ffca5c0f 100644 --- a/coordinator/kubernetes/k8sapi/resources/cluster_autoscaler.go +++ b/coordinator/kubernetes/k8sapi/resources/cluster_autoscaler.go @@ -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, diff --git a/coordinator/kubernetes/k8sapi/resources/images.go b/coordinator/kubernetes/k8sapi/resources/images.go new file mode 100644 index 000000000..17f5b00b8 --- /dev/null +++ b/coordinator/kubernetes/k8sapi/resources/images.go @@ -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" +) diff --git a/coordinator/kubernetes/k8sapi/resources/kms.go b/coordinator/kubernetes/k8sapi/resources/kms.go index ef2060af7..b63591cd5 100644 --- a/coordinator/kubernetes/k8sapi/resources/kms.go +++ b/coordinator/kubernetes/k8sapi/resources/kms.go @@ -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, diff --git a/coordinator/kubernetes/k8sapi/resources/verification.go b/coordinator/kubernetes/k8sapi/resources/verification.go new file mode 100644 index 000000000..77e8bf55d --- /dev/null +++ b/coordinator/kubernetes/k8sapi/resources/verification.go @@ -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) +} diff --git a/coordinator/kubernetes/k8sapi/resources/verification_test.go b/coordinator/kubernetes/k8sapi/resources/verification_test.go new file mode 100644 index 000000000..8c15f7ed7 --- /dev/null +++ b/coordinator/kubernetes/k8sapi/resources/verification_test.go @@ -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) +} diff --git a/coordinator/kubernetes/k8sapi/util.go b/coordinator/kubernetes/k8sapi/util.go index 64a3a3d52..5349ec558 100644 --- a/coordinator/kubernetes/k8sapi/util.go +++ b/coordinator/kubernetes/k8sapi/util.go @@ -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) diff --git a/coordinator/kubernetes/k8sutil.go b/coordinator/kubernetes/k8sutil.go index 899cc1487..74e6803c6 100644 --- a/coordinator/kubernetes/k8sutil.go +++ b/coordinator/kubernetes/k8sutil.go @@ -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) diff --git a/coordinator/kubernetes/kubernetes.go b/coordinator/kubernetes/kubernetes.go index c71fbe344..0810c7cb6 100644 --- a/coordinator/kubernetes/kubernetes.go +++ b/coordinator/kubernetes/kubernetes.go @@ -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) } diff --git a/coordinator/kubernetes/kubernetes_test.go b/coordinator/kubernetes/kubernetes_test.go index 004078993..02e16c4fd 100644 --- a/coordinator/kubernetes/kubernetes_test.go +++ b/coordinator/kubernetes/kubernetes_test.go @@ -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 diff --git a/hack/pcr-reader/README.md b/hack/pcr-reader/README.md index f4f82279b..52d542cde 100644 --- a/hack/pcr-reader/README.md +++ b/hack/pcr-reader/README.md @@ -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 -coord-port +go run main.go -constell-ip -constell-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 diff --git a/hack/pcr-reader/fetch_pcrs.sh b/hack/pcr-reader/fetch_pcrs.sh deleted file mode 100755 index c927f0756..000000000 --- a/hack/pcr-reader/fetch_pcrs.sh +++ /dev/null @@ -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 diff --git a/hack/pcr-reader/main.go b/hack/pcr-reader/main.go index 7530496df..dfd169c6e 100644 --- a/hack/pcr-reader/main.go +++ b/hack/pcr-reader/main.go @@ -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. diff --git a/hack/pcr-reader/main_test.go b/hack/pcr-reader/main_test.go index bda0e7546..f4e8a4f28 100644 --- a/hack/pcr-reader/main_test.go +++ b/hack/pcr-reader/main_test.go @@ -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 diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 09b26d918..4a95dbc4d 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -24,6 +24,10 @@ const ( ActivationServicePort = 9090 ActivationServiceNodePort = 30090 + VerifyServicePortHTTP = 8080 + VerifyServicePortGRPC = 9090 + VerifyServiceNodePortHTTP = 30080 + VerifyServiceNodePortGRPC = 30081 KMSPort = 9000 CoordinatorPort = 9000 EnclaveSSHPort = 2222 diff --git a/proto/Dockerfile.gen-proto b/proto/Dockerfile.gen-proto index 724fabafe..d9081bb23 100644 --- a/proto/Dockerfile.gen-proto +++ b/proto/Dockerfile.gen-proto @@ -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/ diff --git a/verify/Dockerfile b/verify/Dockerfile new file mode 100644 index 000000000..7ec25fb67 --- /dev/null +++ b/verify/Dockerfile @@ -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" ] diff --git a/verify/cmd/main.go b/verify/cmd/main.go new file mode 100644 index 000000000..9f71d24fe --- /dev/null +++ b/verify/cmd/main.go @@ -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") + } +} diff --git a/verify/server/server.go b/verify/server/server.go new file mode 100644 index 000000000..a420a9647 --- /dev/null +++ b/verify/server/server.go @@ -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) +} diff --git a/verify/server/server_test.go b/verify/server/server_test.go new file mode 100644 index 000000000..da3ac9a66 --- /dev/null +++ b/verify/server/server_test.go @@ -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 +} diff --git a/verify/verifyproto/verify.pb.go b/verify/verifyproto/verify.pb.go new file mode 100644 index 000000000..5d4ea3de5 --- /dev/null +++ b/verify/verifyproto/verify.pb.go @@ -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 +} diff --git a/verify/verifyproto/verify.proto b/verify/verifyproto/verify.proto new file mode 100644 index 000000000..6e23098f8 --- /dev/null +++ b/verify/verifyproto/verify.proto @@ -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; +} diff --git a/verify/verifyproto/verify_grpc.pb.go b/verify/verifyproto/verify_grpc.pb.go new file mode 100644 index 000000000..8a9dc540b --- /dev/null +++ b/verify/verifyproto/verify_grpc.pb.go @@ -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", +}