mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
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:
parent
b10b13b173
commit
042f668d20
19
.github/actions/constellation_create/action.yml
vendored
19
.github/actions/constellation_create/action.yml
vendored
@ -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
|
||||
|
@ -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 }}
|
||||
|
31
.github/workflows/build-verification-service.yml
vendored
Normal file
31
.github/workflows/build-verification-service.yml
vendored
Normal 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
1
.gitignore
vendored
@ -21,7 +21,6 @@ go.work.sum
|
||||
build
|
||||
admin.conf
|
||||
coordinator-*
|
||||
util/pcr-reader/pcrs/
|
||||
|
||||
# VS Code configuration folder
|
||||
.vscode
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
12
coordinator/kubernetes/k8sapi/resources/images.go
Normal file
12
coordinator/kubernetes/k8sapi/resources/images.go
Normal 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"
|
||||
)
|
@ -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,
|
||||
|
153
coordinator/kubernetes/k8sapi/resources/verification.go
Normal file
153
coordinator/kubernetes/k8sapi/resources/verification.go
Normal 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)
|
||||
}
|
18
coordinator/kubernetes/k8sapi/resources/verification_test.go
Normal file
18
coordinator/kubernetes/k8sapi/resources/verification_test.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -24,6 +24,10 @@ const (
|
||||
|
||||
ActivationServicePort = 9090
|
||||
ActivationServiceNodePort = 30090
|
||||
VerifyServicePortHTTP = 8080
|
||||
VerifyServicePortGRPC = 9090
|
||||
VerifyServiceNodePortHTTP = 30080
|
||||
VerifyServiceNodePortGRPC = 30081
|
||||
KMSPort = 9000
|
||||
CoordinatorPort = 9000
|
||||
EnclaveSSHPort = 2222
|
||||
|
@ -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
30
verify/Dockerfile
Normal 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
54
verify/cmd/main.go
Normal 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
160
verify/server/server.go
Normal 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)
|
||||
}
|
247
verify/server/server_test.go
Normal file
247
verify/server/server_test.go
Normal 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
|
||||
}
|
226
verify/verifyproto/verify.pb.go
Normal file
226
verify/verifyproto/verify.pb.go
Normal 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
|
||||
}
|
18
verify/verifyproto/verify.proto
Normal file
18
verify/verifyproto/verify.proto
Normal 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;
|
||||
}
|
105
verify/verifyproto/verify_grpc.pb.go
Normal file
105
verify/verifyproto/verify_grpc.pb.go
Normal 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",
|
||||
}
|
Loading…
Reference in New Issue
Block a user