diff --git a/cli/internal/helm/charts/edgeless/constellation-services/charts/join-service/templates/clusterrole.yaml b/cli/internal/helm/charts/edgeless/constellation-services/charts/join-service/templates/clusterrole.yaml index 1ead07241..2ff069711 100644 --- a/cli/internal/helm/charts/edgeless/constellation-services/charts/join-service/templates/clusterrole.yaml +++ b/cli/internal/helm/charts/edgeless/constellation-services/charts/join-service/templates/clusterrole.yaml @@ -28,6 +28,7 @@ rules: - configmaps verbs: - get + - create - apiGroups: - "update.edgeless.systems" resources: diff --git a/cli/internal/helm/testdata/AWS/constellation-services/charts/join-service/templates/clusterrole.yaml b/cli/internal/helm/testdata/AWS/constellation-services/charts/join-service/templates/clusterrole.yaml index 1ead07241..2ff069711 100644 --- a/cli/internal/helm/testdata/AWS/constellation-services/charts/join-service/templates/clusterrole.yaml +++ b/cli/internal/helm/testdata/AWS/constellation-services/charts/join-service/templates/clusterrole.yaml @@ -28,6 +28,7 @@ rules: - configmaps verbs: - get + - create - apiGroups: - "update.edgeless.systems" resources: diff --git a/cli/internal/helm/testdata/Azure/constellation-services/charts/join-service/templates/clusterrole.yaml b/cli/internal/helm/testdata/Azure/constellation-services/charts/join-service/templates/clusterrole.yaml index 1ead07241..2ff069711 100644 --- a/cli/internal/helm/testdata/Azure/constellation-services/charts/join-service/templates/clusterrole.yaml +++ b/cli/internal/helm/testdata/Azure/constellation-services/charts/join-service/templates/clusterrole.yaml @@ -28,6 +28,7 @@ rules: - configmaps verbs: - get + - create - apiGroups: - "update.edgeless.systems" resources: diff --git a/cli/internal/helm/testdata/GCP/constellation-services/charts/join-service/templates/clusterrole.yaml b/cli/internal/helm/testdata/GCP/constellation-services/charts/join-service/templates/clusterrole.yaml index 1ead07241..2ff069711 100644 --- a/cli/internal/helm/testdata/GCP/constellation-services/charts/join-service/templates/clusterrole.yaml +++ b/cli/internal/helm/testdata/GCP/constellation-services/charts/join-service/templates/clusterrole.yaml @@ -28,6 +28,7 @@ rules: - configmaps verbs: - get + - create - apiGroups: - "update.edgeless.systems" resources: diff --git a/cli/internal/helm/testdata/OpenStack/constellation-services/charts/join-service/templates/clusterrole.yaml b/cli/internal/helm/testdata/OpenStack/constellation-services/charts/join-service/templates/clusterrole.yaml index 1ead07241..2ff069711 100644 --- a/cli/internal/helm/testdata/OpenStack/constellation-services/charts/join-service/templates/clusterrole.yaml +++ b/cli/internal/helm/testdata/OpenStack/constellation-services/charts/join-service/templates/clusterrole.yaml @@ -28,6 +28,7 @@ rules: - configmaps verbs: - get + - create - apiGroups: - "update.edgeless.systems" resources: diff --git a/cli/internal/helm/testdata/QEMU/constellation-services/charts/join-service/templates/clusterrole.yaml b/cli/internal/helm/testdata/QEMU/constellation-services/charts/join-service/templates/clusterrole.yaml index 1ead07241..2ff069711 100644 --- a/cli/internal/helm/testdata/QEMU/constellation-services/charts/join-service/templates/clusterrole.yaml +++ b/cli/internal/helm/testdata/QEMU/constellation-services/charts/join-service/templates/clusterrole.yaml @@ -28,6 +28,7 @@ rules: - configmaps verbs: - get + - create - apiGroups: - "update.edgeless.systems" resources: diff --git a/internal/attestation/azure/snp/BUILD.bazel b/internal/attestation/azure/snp/BUILD.bazel index 42b54de5f..8eed5efed 100644 --- a/internal/attestation/azure/snp/BUILD.bazel +++ b/internal/attestation/azure/snp/BUILD.bazel @@ -19,6 +19,7 @@ go_library( "//internal/attestation/vtpm", "//internal/cloud/azure", "//internal/config", + "//internal/constants", "@com_github_edgelesssys_go_azguestattestation//maa", "@com_github_google_go_sev_guest//abi", "@com_github_google_go_sev_guest//kds", diff --git a/internal/attestation/azure/snp/validator.go b/internal/attestation/azure/snp/validator.go index 93bdafd48..c40fa2eca 100644 --- a/internal/attestation/azure/snp/validator.go +++ b/internal/attestation/azure/snp/validator.go @@ -24,6 +24,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/google/go-sev-guest/abi" "github.com/google/go-sev-guest/kds" spb "github.com/google/go-sev-guest/proto/sevsnp" @@ -101,19 +102,28 @@ func NewValidator(cfg *config.AzureSEVSNP, log attestation.Logger) *Validator { // getTrustedKey establishes trust in the given public key. // It does so by verifying the SNP attestation document. func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDocument, extraData []byte) (crypto.PublicKey, error) { + trustedAsk := (*x509.Certificate)(&v.config.AMDSigningKey) // ASK, cached by the Join-Service + trustedArk := (*x509.Certificate)(&v.config.AMDRootKey) // ARK, specified in the Constellation config + + // fallback certificates, used if not present in THIM response. + cachedCerts := sevSnpCerts{ + ask: trustedAsk, + ark: trustedArk, + } + // transform the instanceInfo received from Microsoft into a verifiable attestation report format. var instanceInfo azureInstanceInfo if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil { return nil, fmt.Errorf("unmarshalling instanceInfo: %w", err) } - att, err := instanceInfo.attestationWithCerts(v.log, v.getter) + + att, err := instanceInfo.attestationWithCerts(v.log, v.getter, cachedCerts) if err != nil { return nil, fmt.Errorf("parsing attestation report: %w", err) } - // Verify the attestation report's certificates. - trustedArk := x509.Certificate(v.config.AMDRootKey) // ARK, specified in Constellation config. - ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) // ASK, as reported from THIM / KDS. + // ASK, as cached in joinservice or reported from THIM / KDS. + ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) if err != nil { return nil, fmt.Errorf("parsing ASK certificate: %w", err) } @@ -125,7 +135,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo Product: "Milan", ProductCerts: &trust.ProductCerts{ Ask: ask, - Ark: &trustedArk, + Ark: trustedArk, }, }, }, @@ -246,9 +256,13 @@ type azureInstanceInfo struct { } // attestationWithCerts returns a formatted version of the attestation report and its certificates from the instanceInfo. -// if the VCEK certificate or the certificate chain is not present, the given getter is used to retrieve them -// from the AMD KDS. -func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, getter trust.HTTPSGetter) (*spb.Attestation, error) { +// Certificates are retrieved in the following precedence: +// 1. ASK or ARK from THIM +// 2. ASK or ARK from fallbackCerts +// 3. ASK or ARK from AMD KDS. +func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, getter trust.HTTPSGetter, + fallbackCerts sevSnpCerts, +) (*spb.Attestation, error) { report, err := abi.ReportToProto(a.AttestationReport) if err != nil { return nil, fmt.Errorf("converting report to proto: %w", err) @@ -282,17 +296,29 @@ func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, gett att.CertificateChain.VcekCert = vcek } - // If the certificate chain is present, parse it and format it. + // If the certificate chain from THIM is present, parse it and format it. ask, ark, err := a.parseCertChain() if err != nil { logger.Warnf("Error parsing certificate chain: %v", err) } if ask != nil { + logger.Infof("Using ASK certificate from Azure THIM") att.CertificateChain.AskCert = ask.Raw } if ark != nil { + logger.Infof("Using ARK certificate from Azure THIM") att.CertificateChain.ArkCert = ark.Raw } + + // If a cached ASK or an ARK from the Constellation config is present, use it. + if att.CertificateChain.AskCert == nil && fallbackCerts.ask != nil { + logger.Infof("Using cached ASK certificate") + att.CertificateChain.AskCert = fallbackCerts.ask.Raw + } + if att.CertificateChain.ArkCert == nil && fallbackCerts.ark != nil { + logger.Infof("Using ARK certificate from %s", constants.ConfigFilename) + att.CertificateChain.ArkCert = fallbackCerts.ark.Raw + } // Otherwise, retrieve it from AMD KDS. if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil { logger.Infof( @@ -304,10 +330,12 @@ func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, gett if err != nil { return nil, fmt.Errorf("retrieving certificate chain from AMD KDS: %w", err) } - if att.CertificateChain.AskCert == nil { + if att.CertificateChain.AskCert == nil && kdsCertChain.Ask != nil { + logger.Infof("Using ASK certificate from AMD KDS") att.CertificateChain.AskCert = kdsCertChain.Ask.Raw } - if att.CertificateChain.ArkCert == nil { + if att.CertificateChain.ArkCert == nil && kdsCertChain.Ask != nil { + logger.Infof("Using ARK certificate from AMD KDS") att.CertificateChain.ArkCert = kdsCertChain.Ark.Raw } } @@ -315,6 +343,11 @@ func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, gett return att, nil } +type sevSnpCerts struct { + ask *x509.Certificate + ark *x509.Certificate +} + // parseCertChain parses the certificate chain from the instanceInfo into x509-formatted ASK and ARK certificates. // If less than 2 certificates are present, only the present certificate is returned. // If more than 2 certificates are present, an error is returned. diff --git a/internal/attestation/azure/snp/validator_test.go b/internal/attestation/azure/snp/validator_test.go index 03f36ba9d..a68776a34 100644 --- a/internal/attestation/azure/snp/validator_test.go +++ b/internal/attestation/azure/snp/validator_test.go @@ -10,6 +10,7 @@ import ( "bytes" "context" "crypto/sha256" + "crypto/x509" "encoding/base64" "encoding/binary" "encoding/hex" @@ -169,22 +170,31 @@ func TestParseVCEK(t *testing.T) { } } -// TestInstanceInfoAttestation tests the basic unmarshalling of the attestation report. +// TestInstanceInfoAttestation tests the basic unmarshalling of the attestation report and the ASK / ARK precedence. func TestInstanceInfoAttestation(t *testing.T) { defaultReport := testdata.AttestationReport + testdataArk, testdataAsk := mustCertChainToPem(t, testdata.CertChain) + exampleCert := &x509.Certificate{ + Raw: []byte{1, 2, 3}, + } cfg := config.DefaultForAzureSEVSNP() testCases := map[string]struct { - report []byte - vcek []byte - certChain []byte - getter *stubHTTPSGetter - wantErr bool + report []byte + vcek []byte + certChain []byte + fallbackCerts sevSnpCerts + getter *stubHTTPSGetter + expectedArk *x509.Certificate + expectedAsk *x509.Certificate + wantErr bool }{ "success": { - report: defaultReport, - vcek: testdata.AzureThimVCEK, - certChain: testdata.CertChain, + report: defaultReport, + vcek: testdata.AzureThimVCEK, + certChain: testdata.CertChain, + expectedArk: testdataArk, + expectedAsk: testdataAsk, }, "retrieve vcek": { report: defaultReport, @@ -196,6 +206,8 @@ func TestInstanceInfoAttestation(t *testing.T) { }, nil, ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, }, "retrieve certchain": { report: defaultReport, @@ -207,6 +219,37 @@ func TestInstanceInfoAttestation(t *testing.T) { }, nil, ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "use fallback certs": { + report: defaultReport, + vcek: testdata.AzureThimVCEK, + fallbackCerts: sevSnpCerts{ + ask: exampleCert, + ark: exampleCert, + }, + getter: newStubHTTPSGetter( + &urlResponseMatcher{}, + nil, + ), + expectedArk: exampleCert, + expectedAsk: exampleCert, + }, + "use certchain with fallback certs": { + report: defaultReport, + certChain: testdata.CertChain, + vcek: testdata.AzureThimVCEK, + fallbackCerts: sevSnpCerts{ + ask: &x509.Certificate{}, + ark: &x509.Certificate{}, + }, + getter: newStubHTTPSGetter( + &urlResponseMatcher{}, + nil, + ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, }, "retrieve vcek and certchain": { report: defaultReport, @@ -219,6 +262,8 @@ func TestInstanceInfoAttestation(t *testing.T) { }, nil, ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, }, "report too short": { report: defaultReport[:len(defaultReport)-100], @@ -245,7 +290,7 @@ func TestInstanceInfoAttestation(t *testing.T) { VCEK: tc.vcek, } - att, err := instanceInfo.attestationWithCerts(logger.NewTest(t), tc.getter) + att, err := instanceInfo.attestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts) if tc.wantErr { assert.Error(err) } else { @@ -263,11 +308,21 @@ func TestInstanceInfoAttestation(t *testing.T) { assert.True(tcbValues.TeeSpl >= cfg.TEEVersion.Value) assert.True(tcbValues.SnpSpl >= cfg.SNPVersion.Value) assert.True(tcbValues.UcodeSpl >= cfg.MicrocodeVersion.Value) + assert.Equal(tc.expectedArk.Raw, att.CertificateChain.ArkCert) + assert.Equal(tc.expectedAsk.Raw, att.CertificateChain.AskCert) } }) } } +func mustCertChainToPem(t *testing.T, certchain []byte) (ark, ask *x509.Certificate) { + t.Helper() + a := azureInstanceInfo{CertChain: certchain} + ask, ark, err := a.parseCertChain() + require.NoError(t, err) + return ark, ask +} + type stubHTTPSGetter struct { urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs err error diff --git a/internal/attestation/choose/choose.go b/internal/attestation/choose/choose.go index 965645b3c..a74551860 100644 --- a/internal/attestation/choose/choose.go +++ b/internal/attestation/choose/choose.go @@ -66,6 +66,6 @@ func Validator(cfg config.AttestationCfg, log attestation.Logger) (atls.Validato case *config.DummyCfg: return atls.NewFakeValidator(variant.Dummy{}), nil default: - return nil, fmt.Errorf("unknown attestation variant") + return nil, fmt.Errorf("unknown attestation variant: %s", cfg.GetVariant()) } } diff --git a/internal/attestation/variant/variant.go b/internal/attestation/variant/variant.go index b1aead8b7..82fd1f1c9 100644 --- a/internal/attestation/variant/variant.go +++ b/internal/attestation/variant/variant.go @@ -134,7 +134,7 @@ func ValidProvider(provider cloudprovider.Provider, variant Variant) bool { return false } -// Dummy OID for testfing. +// Dummy OID for testing. type Dummy struct{} // OID returns the struct's object identifier. diff --git a/internal/config/attestation.go b/internal/config/attestation.go index 03e301c57..f5ee019a1 100644 --- a/internal/config/attestation.go +++ b/internal/config/attestation.go @@ -65,18 +65,27 @@ type Certificate x509.Certificate // MarshalJSON marshals the certificate to PEM. func (c Certificate) MarshalJSON() ([]byte, error) { + if len(c.Raw) == 0 { + return json.Marshal(new(string)) + } pem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}) return json.Marshal(string(pem)) } // MarshalYAML marshals the certificate to PEM. func (c Certificate) MarshalYAML() (any, error) { + if len(c.Raw) == 0 { + return "", nil + } pem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}) return string(pem), nil } // UnmarshalJSON unmarshals the certificate from PEM. func (c *Certificate) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } return c.unmarshal(func(val any) error { return json.Unmarshal(data, val) }) @@ -92,6 +101,9 @@ func (c *Certificate) unmarshal(unmarshalFunc func(any) error) error { if err := unmarshalFunc(&pemData); err != nil { return err } + if pemData == "" { + return nil + } block, _ := pem.Decode([]byte(pemData)) cert, err := x509.ParseCertificate(block.Bytes) if err != nil { diff --git a/internal/config/attestation_test.go b/internal/config/attestation_test.go index a0333234d..e0e3492dc 100644 --- a/internal/config/attestation_test.go +++ b/internal/config/attestation_test.go @@ -77,6 +77,19 @@ func TestCertificateMarshalJSON(t *testing.T) { assert.JSONEq(jsonCert, string(out)) } +func TestEmptyCertificateMarshalJSON(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + jsonCert := "\"\"" + var cert Certificate + require.NoError(json.Unmarshal([]byte(jsonCert), &cert)) + + out, err := json.Marshal(cert) + require.NoError(err) + assert.JSONEq(jsonCert, string(out)) +} + func TestCertificateMarshalYAML(t *testing.T) { require := require.New(t) assert := assert.New(t) @@ -89,3 +102,16 @@ func TestCertificateMarshalYAML(t *testing.T) { require.NoError(err) assert.YAMLEq(yamlCert, string(out)) } + +func TestEmptyCertificateMarshalYAML(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + yamlCert := "\"\"" + var cert Certificate + require.NoError(yaml.Unmarshal([]byte(yamlCert), &cert)) + + out, err := yaml.Marshal(cert) + require.NoError(err) + assert.YAMLEq(yamlCert, string(out)) +} diff --git a/internal/config/config.go b/internal/config/config.go index 33a00323a..020101b31 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1118,6 +1118,9 @@ type AzureSEVSNP struct { // description: | // AMD Root Key certificate used to verify the SEV-SNP certificate chain. AMDRootKey Certificate `json:"amdRootKey" yaml:"amdRootKey"` + // description: | + // AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate. + AMDSigningKey Certificate `json:"amdSigningKey,omitempty" yaml:"amdSigningKey,omitempty" validate:"len=0"` } // setWantLatestToFalse sets the WantLatest field to false for all versions in order to unmarshal the numerical versions instead of the string "latest". diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index f47adb876..443fd9657 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -590,7 +590,7 @@ func init() { FieldName: "azureSEVSNP", }, } - AzureSEVSNPDoc.Fields = make([]encoder.Doc, 7) + AzureSEVSNPDoc.Fields = make([]encoder.Doc, 8) AzureSEVSNPDoc.Fields[0].Name = "measurements" AzureSEVSNPDoc.Fields[0].Type = "M" AzureSEVSNPDoc.Fields[0].Note = "" @@ -626,6 +626,11 @@ func init() { AzureSEVSNPDoc.Fields[6].Note = "" AzureSEVSNPDoc.Fields[6].Description = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." AzureSEVSNPDoc.Fields[6].Comments[encoder.LineComment] = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." + AzureSEVSNPDoc.Fields[7].Name = "amdSigningKey" + AzureSEVSNPDoc.Fields[7].Type = "Certificate" + AzureSEVSNPDoc.Fields[7].Note = "" + AzureSEVSNPDoc.Fields[7].Description = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." + AzureSEVSNPDoc.Fields[7].Comments[encoder.LineComment] = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." AzureTrustedLaunchDoc.Type = "AzureTrustedLaunch" AzureTrustedLaunchDoc.Comments[encoder.LineComment] = "AzureTrustedLaunch is the configuration for Azure Trusted Launch attestation." diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 565584cf7..ad7ee7d42 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -128,6 +128,12 @@ const ( K8sVersionFieldName = "cluster-version" // ComponentsListKey is the name of the key holding the list of components in the components configMap. ComponentsListKey = "components" + // SevSnpCertCacheConfigMapName is the name of the configMap holding the SEV-SNP certificate cache in the join service. + SevSnpCertCacheConfigMapName = "sev-snp-cert-cache" + // CertCacheAskKey is the name of the key holding the ASK certificate in the SEV-SNP certificate cache. + CertCacheAskKey = "ask" + // CertCacheArkKey is the name of the key holding the ARK certificate in the SEV-SNP certificate cache. + CertCacheArkKey = "ark" // NodeVersionResourceName resource name used for NodeVersion in constellation-operator and CLI. NodeVersionResourceName = "constellation-version" // NodeKubernetesComponentsAnnotationKey is the name of the annotation holding the reference to the ConfigMap listing all K8s components. diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index 64ddee3a6..081e25d71 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only package crypto import ( + "bytes" "crypto/rand" "crypto/sha256" "crypto/x509" @@ -61,7 +62,8 @@ func GenerateRandomBytes(length int) ([]byte, error) { return nonce, nil } -// PemToX509Cert takes a list of PEM encoded certificates, parses the first one and returns it. +// PemToX509Cert takes a list of PEM-encoded certificates, parses the first one and returns it +// as an x.509 certificate. func PemToX509Cert(raw []byte) (*x509.Certificate, error) { decoded, _ := pem.Decode(raw) if decoded == nil { @@ -73,3 +75,13 @@ func PemToX509Cert(raw []byte) (*x509.Certificate, error) { } return cert, nil } + +// X509CertToPem takes an x.509 certificate and returns it as a PEM-encoded certificate. +func X509CertToPem(cert *x509.Certificate) ([]byte, error) { + outWriter := &bytes.Buffer{} + err := pem.Encode(outWriter, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, fmt.Errorf("encode certificate: %w", err) + } + return outWriter.Bytes(), nil +} diff --git a/internal/crypto/crypto_test.go b/internal/crypto/crypto_test.go index d814f568b..be7b0aa77 100644 --- a/internal/crypto/crypto_test.go +++ b/internal/crypto/crypto_test.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only package crypto import ( + "crypto/x509" "testing" "github.com/edgelesssys/constellation/v2/internal/crypto/testvector" @@ -119,3 +120,58 @@ func TestGenerateRandomBytes(t *testing.T) { require.NoError(err) assert.Len(n3, 16) } + +func TestPemToX509Cert(t *testing.T) { + testCases := map[string]struct { + pemCert []byte + wantErr bool + }{ + // TODO(msanft): Add more test cases with testdata + "invalid cert": { + pemCert: []byte("invalid"), + wantErr: true, + }, + "empty cert": { + pemCert: []byte{}, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + _, err := PemToX509Cert(tc.pemCert) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + }) + } +} + +func TestX509ToPemCert(t *testing.T) { + testCases := map[string]struct { + cert *x509.Certificate + wantErr bool + }{ + "success": { + cert: &x509.Certificate{}, + }, + // TODO(msanft): Add more test cases with testdata + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + _, err := X509CertToPem(tc.cert) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + }) + } +} diff --git a/joinservice/cmd/BUILD.bazel b/joinservice/cmd/BUILD.bazel index 0a439eeb8..c55b08cc7 100644 --- a/joinservice/cmd/BUILD.bazel +++ b/joinservice/cmd/BUILD.bazel @@ -21,8 +21,10 @@ go_library( "//internal/file", "//internal/grpc/atlscredentials", "//internal/logger", + "//joinservice/internal/certcache", "//joinservice/internal/kms", "//joinservice/internal/kubeadm", + "//joinservice/internal/kubernetes", "//joinservice/internal/kubernetesca", "//joinservice/internal/server", "//joinservice/internal/watcher", diff --git a/joinservice/cmd/main.go b/joinservice/cmd/main.go index fe767c9ca..435577dd7 100644 --- a/joinservice/cmd/main.go +++ b/joinservice/cmd/main.go @@ -28,8 +28,10 @@ import ( "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/grpc/atlscredentials" "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/joinservice/internal/certcache" "github.com/edgelesssys/constellation/v2/joinservice/internal/kms" "github.com/edgelesssys/constellation/v2/joinservice/internal/kubeadm" + "github.com/edgelesssys/constellation/v2/joinservice/internal/kubernetes" "github.com/edgelesssys/constellation/v2/joinservice/internal/kubernetesca" "github.com/edgelesssys/constellation/v2/joinservice/internal/server" "github.com/edgelesssys/constellation/v2/joinservice/internal/watcher" @@ -56,11 +58,23 @@ func main() { handler := file.NewHandler(afero.NewOsFs()) - variant, err := variant.FromString(*attestationVariant) + kubeClient, err := kubernetes.New() + if err != nil { + log.With(zap.Error(err)).Fatalf("Failed to create Kubernetes client") + } + + attVariant, err := variant.FromString(*attestationVariant) if err != nil { log.With(zap.Error(err)).Fatalf("Failed to parse attestation variant") } - validator, err := watcher.NewValidator(log.Named("validator"), variant, handler) + + certCacheClient := certcache.NewClient(log.Named("certcache"), kubeClient, attVariant) + cachedCerts, err := certCacheClient.CreateCertChainCache(context.Background()) + if err != nil { + log.With(zap.Error(err)).Fatalf("Failed to create certificate chain cache") + } + + validator, err := watcher.NewValidator(log.Named("validator"), attVariant, handler, cachedCerts) if err != nil { flag.Usage() log.With(zap.Error(err)).Fatalf("Failed to create validator") @@ -68,9 +82,10 @@ func main() { creds := atlscredentials.New(nil, []atls.Validator{validator}) - ctx, cancel := context.WithTimeout(context.Background(), vpcIPTimeout) + vpcCtx, cancel := context.WithTimeout(context.Background(), vpcIPTimeout) defer cancel() - vpcIP, err := getVPCIP(ctx, *provider) + + vpcIP, err := getVPCIP(vpcCtx, *provider) if err != nil { log.With(zap.Error(err)).Fatalf("Failed to get IP in VPC") } @@ -91,6 +106,7 @@ func main() { kubernetesca.New(log.Named("certificateAuthority"), handler), kubeadm, keyServiceClient, + kubeClient, log.Named("server"), ) if err != nil { diff --git a/joinservice/internal/certcache/BUILD.bazel b/joinservice/internal/certcache/BUILD.bazel new file mode 100644 index 000000000..c5ba663dd --- /dev/null +++ b/joinservice/internal/certcache/BUILD.bazel @@ -0,0 +1,37 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") + +go_library( + name = "certcache", + srcs = ["certcache.go"], + importpath = "github.com/edgelesssys/constellation/v2/joinservice/internal/certcache", + visibility = ["//joinservice:__subpackages__"], + deps = [ + "//internal/attestation/variant", + "//internal/constants", + "//internal/crypto", + "//internal/logger", + "//joinservice/internal/certcache/amdkds", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//verify/trust", + "@io_k8s_apimachinery//pkg/api/errors", + ], +) + +go_test( + name = "certcache_test", + srcs = ["certcache_test.go"], + embed = [":certcache"], + deps = [ + "//internal/attestation/variant", + "//internal/constants", + "//internal/crypto", + "//internal/logger", + "//joinservice/internal/certcache/testdata", + "@com_github_google_go_sev_guest//abi", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + "@io_k8s_apimachinery//pkg/api/errors", + "@io_k8s_apimachinery//pkg/runtime/schema", + ], +) diff --git a/joinservice/internal/certcache/amdkds/BUILD.bazel b/joinservice/internal/certcache/amdkds/BUILD.bazel new file mode 100644 index 000000000..79df7c806 --- /dev/null +++ b/joinservice/internal/certcache/amdkds/BUILD.bazel @@ -0,0 +1,26 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") + +go_library( + name = "amdkds", + srcs = ["amdkds.go"], + importpath = "github.com/edgelesssys/constellation/v2/joinservice/internal/certcache/amdkds", + visibility = ["//joinservice:__subpackages__"], + deps = [ + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//verify/trust", + ], +) + +go_test( + name = "amdkds_test", + srcs = ["amdkds_test.go"], + embed = [":amdkds"], + deps = [ + "//internal/logger", + "//joinservice/internal/certcache/amdkds/testdata", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//verify/trust", + "@com_github_stretchr_testify//assert", + ], +) diff --git a/joinservice/internal/certcache/amdkds/amdkds.go b/joinservice/internal/certcache/amdkds/amdkds.go new file mode 100644 index 000000000..8b1a9b131 --- /dev/null +++ b/joinservice/internal/certcache/amdkds/amdkds.go @@ -0,0 +1,38 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// The AMDKDS package implements interaction with the AMD KDS (Key Distribution Service). +package amdkds + +import ( + "crypto/x509" + "fmt" + + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/verify/trust" +) + +// KDSClient is a client for interacting with the AMD KDS. +type KDSClient struct { + getter trust.HTTPSGetter +} + +// NewKDSClient creates a new KDS Client. +func NewKDSClient(getter trust.HTTPSGetter) *KDSClient { + return &KDSClient{ + getter: getter, + } +} + +// CertChain queries the AMD KDS for the certificate chain for given signing type (VCEK / VLEK). +func (c *KDSClient) CertChain(signingType abi.ReportSigner) (ask, ark *x509.Certificate, err error) { + askark, err := trust.GetProductChain("Milan", signingType, c.getter) + if err != nil { + return nil, nil, fmt.Errorf("retrieving certificate chain: %w", err) + } + + return askark.Ask, askark.Ark, nil +} diff --git a/joinservice/internal/certcache/amdkds/amdkds_test.go b/joinservice/internal/certcache/amdkds/amdkds_test.go new file mode 100644 index 000000000..e3b878729 --- /dev/null +++ b/joinservice/internal/certcache/amdkds/amdkds_test.go @@ -0,0 +1,74 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package amdkds + +import ( + "testing" + + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/joinservice/internal/certcache/amdkds/testdata" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/verify/trust" + "github.com/stretchr/testify/assert" +) + +func TestCertChain(t *testing.T) { + testCases := map[string]struct { + getter *stubGetter + wantErr bool + }{ + "success": { + getter: &stubGetter{ + log: logger.NewTest(t), + ret: testdata.CertChain, + }, + }, + "getter error": { + getter: &stubGetter{ + log: logger.NewTest(t), + err: assert.AnError, + }, + wantErr: true, + }, + "empty cert chain": { + getter: &stubGetter{ + log: logger.NewTest(t), + ret: nil, + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // Clear the product cert cache to ensure that the test cases are independent. + trust.ClearProductCertCache() + + assert := assert.New(t) + + kdsClient := NewKDSClient(tc.getter) + + _, _, err := kdsClient.CertChain(abi.NoneReportSigner) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + }) + } +} + +type stubGetter struct { + log *logger.Logger + ret []byte + err error +} + +func (s *stubGetter) Get(url string) ([]byte, error) { + s.log.Debugf("Request to %s", url) + return s.ret, s.err +} diff --git a/joinservice/internal/certcache/amdkds/testdata/BUILD.bazel b/joinservice/internal/certcache/amdkds/testdata/BUILD.bazel new file mode 100644 index 000000000..b29e07e8c --- /dev/null +++ b/joinservice/internal/certcache/amdkds/testdata/BUILD.bazel @@ -0,0 +1,9 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "testdata", + srcs = ["testdata.go"], + embedsrcs = ["certchain.pem"], + importpath = "github.com/edgelesssys/constellation/v2/joinservice/internal/certcache/amdkds/testdata", + visibility = ["//joinservice:__subpackages__"], +) diff --git a/joinservice/internal/certcache/amdkds/testdata/certchain.pem b/joinservice/internal/certcache/amdkds/testdata/certchain.pem new file mode 100644 index 000000000..148ca4678 --- /dev/null +++ b/joinservice/internal/certcache/amdkds/testdata/certchain.pem @@ -0,0 +1,74 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC +BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS +BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg +Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp +Y2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy +MTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS +BgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j +ZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg +W41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta +1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2 +SzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0 +60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05 +gmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg +bKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs ++gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi +Qi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ +eTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18 +fHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j +WhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI +rFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG +KWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG +SIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI +AWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel +ETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw +STjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK +dHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq +zT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp +KGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e +pmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq +HnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh +3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn +JZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH +CViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4 +AFZEAwoKCQ== +-----END CERTIFICATE----- diff --git a/joinservice/internal/certcache/amdkds/testdata/testdata.go b/joinservice/internal/certcache/amdkds/testdata/testdata.go new file mode 100644 index 000000000..4e4d4a40e --- /dev/null +++ b/joinservice/internal/certcache/amdkds/testdata/testdata.go @@ -0,0 +1,15 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// Package testdata contains testing data for an attestation process. +package testdata + +import _ "embed" + +// CertChain is a valid certificate chain (PEM, as returned from Azure THIM) for the VCEK certificate. +// +//go:embed certchain.pem +var CertChain []byte diff --git a/joinservice/internal/certcache/certcache.go b/joinservice/internal/certcache/certcache.go new file mode 100644 index 000000000..bffa596e9 --- /dev/null +++ b/joinservice/internal/certcache/certcache.go @@ -0,0 +1,203 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// Package certcache implements an in-cluster SEV-SNP certificate cache. +package certcache + +import ( + "context" + "crypto/x509" + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/crypto" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/joinservice/internal/certcache/amdkds" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/verify/trust" + k8serrors "k8s.io/apimachinery/pkg/api/errors" +) + +// Client is a client for interacting with the certificate chain cache. +type Client struct { + log *logger.Logger + attVariant variant.Variant + kdsClient + kubeClient kubeClient +} + +// NewClient creates a new CertCacheClient. +func NewClient(log *logger.Logger, kubeClient kubeClient, attVariant variant.Variant) *Client { + kdsClient := amdkds.NewKDSClient(trust.DefaultHTTPSGetter()) + + return &Client{ + attVariant: attVariant, + log: log, + kubeClient: kubeClient, + kdsClient: kdsClient, + } +} + +// CreateCertChainCache creates a certificate chain cache for the given attestation variant +// and returns the cached certificates, if applicable. +// If the certificate chain cache already exists, nothing is done. +func (c *Client) CreateCertChainCache(ctx context.Context) (*CachedCerts, error) { + switch c.attVariant { + case variant.AzureSEVSNP{}: + c.log.Debugf("Creating Azure SEV-SNP certificate chain cache") + ask, ark, err := c.createCertChainCache(ctx, abi.VcekReportSigner) + if err != nil { + return nil, fmt.Errorf("creating Azure SEV-SNP certificate chain cache: %w", err) + } + return &CachedCerts{ + ask: ask, + ark: ark, + }, nil + // TODO(derpsteb): Add AWS + default: + c.log.Debugf("No certificate chain caching possible for attestation variant %s", c.attVariant) + return nil, nil + } +} + +// CachedCerts contains the cached certificates. +type CachedCerts struct { + ask *x509.Certificate + ark *x509.Certificate +} + +// SevSnpCerts returns the cached SEV-SNP ASK and ARK certificates. +func (c *CachedCerts) SevSnpCerts() (ask, ark *x509.Certificate) { + return c.ask, c.ark +} + +// createCertChainCache creates a certificate chain cache configmap with the ASK and ARK +// retrieved from the KDS and returns ASK and ARK. If the configmap already exists and both ASK and ARK are present, +// nothing is done and the existing ASK and ARK are returned. If the configmap already exists but either ASK or ARK +// are missing, the missing certificate is retrieved from the KDS and the configmap is updated with the missing value. +func (c *Client) createCertChainCache(ctx context.Context, signingType abi.ReportSigner) (ask, ark *x509.Certificate, err error) { + c.log.Debugf("Creating certificate chain cache") + var shouldCreateConfigMap bool + + // Check if ASK or ARK is already cached. + // If both are cached, return them. + // If only one is cached, retrieve the other one from the + // KDS and update the configmap with the missing value. + cacheAsk, cacheArk, err := c.getCertChainCache(ctx) + if k8serrors.IsNotFound(err) { + c.log.Debugf("Certificate chain cache does not exist") + shouldCreateConfigMap = true + } else if err != nil { + return nil, nil, fmt.Errorf("getting certificate chain cache: %w", err) + } + if cacheAsk != nil && cacheArk != nil { + c.log.Debugf("ASK and ARK present in cache, returning cached values") + return cacheAsk, cacheArk, nil + } + if cacheAsk != nil { + ask = cacheAsk + } + if cacheArk != nil { + ark = cacheArk + } + + // If only one certificate is cached, retrieve the other one from the KDS. + c.log.Debugf("Retrieving certificate chain from KDS") + kdsAsk, kdsArk, err := c.kdsClient.CertChain(signingType) + if err != nil { + return nil, nil, fmt.Errorf("retrieving certificate chain from KDS: %w", err) + } + if ask == nil && kdsAsk != nil { + ask = kdsAsk + } + if ark == nil && kdsArk != nil { + ark = kdsArk + } + + askPem, err := crypto.X509CertToPem(ask) + if err != nil { + return nil, nil, fmt.Errorf("encoding ASK: %w", err) + } + arkPem, err := crypto.X509CertToPem(ark) + if err != nil { + return nil, nil, fmt.Errorf("encoding ARK: %w", err) + } + + if shouldCreateConfigMap { + // ConfigMap does not exist, create it. + c.log.Debugf("Creating certificate chain cache configmap") + if err := c.kubeClient.CreateConfigMap(ctx, constants.SevSnpCertCacheConfigMapName, map[string]string{ + constants.CertCacheAskKey: string(askPem), + constants.CertCacheArkKey: string(arkPem), + }); err != nil { + // If the ConfigMap already exists, another JoinService instance created the certificate cache while this operation was running. + // Calling this function again should now retrieve the cached certificates. + if k8serrors.IsAlreadyExists(err) { + c.log.Debugf("Certificate chain cache configmap already exists, retrieving cached certificates") + return c.getCertChainCache(ctx) + } + return nil, nil, fmt.Errorf("creating certificate chain cache configmap: %w", err) + } + } else { + // ConfigMap already exists but either ASK or ARK are missing. Update the according value. + if cacheAsk == nil { + if err := c.kubeClient.UpdateConfigMap(ctx, constants.SevSnpCertCacheConfigMapName, + constants.CertCacheAskKey, string(askPem)); err != nil { + return nil, nil, fmt.Errorf("updating ASK in certificate chain cache configmap: %w", err) + } + } + if cacheArk == nil { + if err := c.kubeClient.UpdateConfigMap(ctx, constants.SevSnpCertCacheConfigMapName, + constants.CertCacheArkKey, string(arkPem)); err != nil { + return nil, nil, fmt.Errorf("updating ARK in certificate chain cache configmap: %w", err) + } + } + } + + return ask, ark, nil +} + +// getCertChainCache returns the cached ASK and ARK certificate, if available. If either of the keys +// is not present in the configmap, no error is returned. +func (c *Client) getCertChainCache(ctx context.Context) (ask, ark *x509.Certificate, err error) { + c.log.Debugf("Retrieving certificate chain from cache") + askRaw, err := c.kubeClient.GetConfigMapData(ctx, constants.SevSnpCertCacheConfigMapName, constants.CertCacheAskKey) + if err != nil { + return nil, nil, fmt.Errorf("getting ASK from configmap: %w", err) + } + if askRaw != "" { + c.log.Debugf("ASK cache hit") + ask, err = crypto.PemToX509Cert([]byte(askRaw)) + if err != nil { + return nil, nil, fmt.Errorf("parsing ASK: %w", err) + } + } + + arkRaw, err := c.kubeClient.GetConfigMapData(ctx, constants.SevSnpCertCacheConfigMapName, constants.CertCacheArkKey) + if err != nil { + return nil, nil, fmt.Errorf("getting ARK from configmap: %w", err) + } + if arkRaw != "" { + c.log.Debugf("ARK cache hit") + ark, err = crypto.PemToX509Cert([]byte(arkRaw)) + if err != nil { + return nil, nil, fmt.Errorf("parsing ARK: %w", err) + } + } + + return ask, ark, nil +} + +type kubeClient interface { + CreateConfigMap(ctx context.Context, name string, data map[string]string) error + GetConfigMapData(ctx context.Context, name, key string) (string, error) + UpdateConfigMap(ctx context.Context, name, key, value string) error +} + +type kdsClient interface { + CertChain(signingType abi.ReportSigner) (ask, ark *x509.Certificate, err error) +} diff --git a/joinservice/internal/certcache/certcache_test.go b/joinservice/internal/certcache/certcache_test.go new file mode 100644 index 000000000..a742d43c6 --- /dev/null +++ b/joinservice/internal/certcache/certcache_test.go @@ -0,0 +1,247 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package certcache + +import ( + "context" + "crypto/x509" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/crypto" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/joinservice/internal/certcache/testdata" + "github.com/google/go-sev-guest/abi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestCreateCertChainCache(t *testing.T) { + notFoundErr := k8serrors.NewNotFound(schema.GroupResource{}, "test") + + testCases := map[string]struct { + kubeClient *stubKubeClient + kdsClient *stubKdsClient + expectedArk *x509.Certificate + expectedAsk *x509.Certificate + wantErr bool + }{ + "available in configmap": { + kubeClient: &stubKubeClient{ + askResponse: string(testdata.Ask), + arkResponse: string(testdata.Ark), + }, + kdsClient: &stubKdsClient{}, + expectedArk: mustParsePEM(testdata.Ark), + expectedAsk: mustParsePEM(testdata.Ask), + }, + "query from kds": { + kubeClient: &stubKubeClient{ + getConfigMapDataErr: notFoundErr, + }, + kdsClient: &stubKdsClient{ + askResponse: testdata.Ask, + arkResponse: testdata.Ark, + }, + expectedArk: mustParsePEM(testdata.Ark), + expectedAsk: mustParsePEM(testdata.Ask), + }, + "only replace uncached cert": { + kubeClient: &stubKubeClient{ + askResponse: string(testdata.Ark), // on purpose + }, + kdsClient: &stubKdsClient{ + arkResponse: testdata.Ark, + askResponse: testdata.Ask, + }, + expectedArk: mustParsePEM(testdata.Ark), + expectedAsk: mustParsePEM(testdata.Ark), // on purpose + }, + "only ask available in configmap": { + kubeClient: &stubKubeClient{ + askResponse: string(testdata.Ask), + }, + kdsClient: &stubKdsClient{ + arkResponse: testdata.Ark, + }, + expectedArk: mustParsePEM(testdata.Ark), + expectedAsk: mustParsePEM(testdata.Ask), + }, + "only ark available in configmap": { + kubeClient: &stubKubeClient{ + arkResponse: string(testdata.Ark), + }, + kdsClient: &stubKdsClient{ + askResponse: testdata.Ask, + }, + expectedArk: mustParsePEM(testdata.Ark), + expectedAsk: mustParsePEM(testdata.Ask), + }, + "get config map data err": { + kubeClient: &stubKubeClient{ + getConfigMapDataErr: assert.AnError, + }, + wantErr: true, + }, + "update configmap err": { + kubeClient: &stubKubeClient{ + askResponse: string(testdata.Ask), + updateConfigMapErr: assert.AnError, + }, + kdsClient: &stubKdsClient{ + arkResponse: testdata.Ark, + }, + wantErr: true, + }, + "kds cert chain err": { + kubeClient: &stubKubeClient{ + getConfigMapDataErr: notFoundErr, + }, + kdsClient: &stubKdsClient{ + certChainErr: assert.AnError, + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + ctx := context.Background() + + c := &Client{ + attVariant: variant.Dummy{}, + log: logger.NewTest(t), + kubeClient: tc.kubeClient, + kdsClient: tc.kdsClient, + } + + ask, ark, err := c.createCertChainCache(ctx, abi.NoneReportSigner) + if tc.wantErr { + assert.Error(err) + } else { + require.NoError(err) + assert.Equal(tc.expectedArk, ark) + assert.Equal(tc.expectedAsk, ask) + } + }) + } +} + +type stubKdsClient struct { + askResponse []byte + arkResponse []byte + certChainErr error +} + +func (c *stubKdsClient) CertChain(abi.ReportSigner) (ask, ark *x509.Certificate, err error) { + if c.askResponse != nil { + ask = mustParsePEM(c.askResponse) + } + if c.arkResponse != nil { + ark = mustParsePEM(c.arkResponse) + } + return ask, ark, c.certChainErr +} + +func mustParsePEM(pemBytes []byte) *x509.Certificate { + cert, err := crypto.PemToX509Cert(pemBytes) + if err != nil { + panic(err) + } + return cert +} + +func TestGetCertChainCache(t *testing.T) { + testCases := map[string]struct { + kubeClient *stubKubeClient + expectedAsk *x509.Certificate + expectedArk *x509.Certificate + wantErr bool + }{ + "success": { + kubeClient: &stubKubeClient{ + askResponse: string(testdata.Ask), + arkResponse: string(testdata.Ark), + }, + expectedAsk: mustParsePEM(testdata.Ask), + expectedArk: mustParsePEM(testdata.Ark), + }, + "empty ask": { + kubeClient: &stubKubeClient{ + askResponse: "", + arkResponse: string(testdata.Ark), + }, + expectedAsk: nil, + expectedArk: mustParsePEM(testdata.Ark), + }, + "empty ark": { + kubeClient: &stubKubeClient{ + askResponse: string(testdata.Ask), + arkResponse: "", + }, + expectedAsk: mustParsePEM(testdata.Ask), + expectedArk: nil, + }, + "error getting config map data": { + kubeClient: &stubKubeClient{ + getConfigMapDataErr: assert.AnError, + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + ctx := context.Background() + + c := NewClient(logger.NewTest(t), tc.kubeClient, variant.Dummy{}) + + ask, ark, err := c.getCertChainCache(ctx) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.expectedArk, ark) + assert.Equal(tc.expectedAsk, ask) + } + }) + } +} + +type stubKubeClient struct { + askResponse string + arkResponse string + createConfigMapErr error + updateConfigMapErr error + getConfigMapDataErr error +} + +func (s *stubKubeClient) CreateConfigMap(context.Context, string, map[string]string) error { + return s.createConfigMapErr +} + +func (s *stubKubeClient) GetConfigMapData(_ context.Context, _ string, key string) (string, error) { + if key == constants.CertCacheAskKey { + return s.askResponse, s.getConfigMapDataErr + } + if key == constants.CertCacheArkKey { + return s.arkResponse, s.getConfigMapDataErr + } + return "", s.getConfigMapDataErr +} + +func (s *stubKubeClient) UpdateConfigMap(context.Context, string, string, string) error { + return s.updateConfigMapErr +} diff --git a/joinservice/internal/certcache/testdata/BUILD.bazel b/joinservice/internal/certcache/testdata/BUILD.bazel new file mode 100644 index 000000000..849203857 --- /dev/null +++ b/joinservice/internal/certcache/testdata/BUILD.bazel @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "testdata", + srcs = ["testdata.go"], + embedsrcs = [ + "ark.pem", + "ask.pem", + ], + importpath = "github.com/edgelesssys/constellation/v2/joinservice/internal/certcache/testdata", + visibility = ["//joinservice:__subpackages__"], +) diff --git a/joinservice/internal/certcache/testdata/ark.pem b/joinservice/internal/certcache/testdata/ark.pem new file mode 100644 index 000000000..838631734 --- /dev/null +++ b/joinservice/internal/certcache/testdata/ark.pem @@ -0,0 +1,37 @@ +-----BEGIN CERTIFICATE----- +MIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC +BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS +BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg +Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp +Y2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy +MTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS +BgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j +ZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg +W41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta +1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2 +SzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0 +60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05 +gmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg +bKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs ++gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi +Qi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ +eTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18 +fHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j +WhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI +rFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG +KWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG +SIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI +AWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel +ETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw +STjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK +dHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq +zT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp +KGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e +pmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq +HnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh +3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn +JZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH +CViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4 +AFZEAwoKCQ== +-----END CERTIFICATE----- diff --git a/joinservice/internal/certcache/testdata/ask.pem b/joinservice/internal/certcache/testdata/ask.pem new file mode 100644 index 000000000..26c059c70 --- /dev/null +++ b/joinservice/internal/certcache/testdata/ask.pem @@ -0,0 +1,37 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/joinservice/internal/certcache/testdata/testdata.go b/joinservice/internal/certcache/testdata/testdata.go new file mode 100644 index 000000000..3830ccb16 --- /dev/null +++ b/joinservice/internal/certcache/testdata/testdata.go @@ -0,0 +1,20 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// Package testdata contains testing data for an attestation process. +package testdata + +import _ "embed" + +// Ark is a valid ARK certificate, as returned from the AMD KDS. +// +//go:embed ark.pem +var Ark []byte + +// Ask is a valid ASK certificate, as returned from the AMD KDS. +// +//go:embed ask.pem +var Ask []byte diff --git a/joinservice/internal/kubernetes/BUILD.bazel b/joinservice/internal/kubernetes/BUILD.bazel index ac2f37183..9fbc8868e 100644 --- a/joinservice/internal/kubernetes/BUILD.bazel +++ b/joinservice/internal/kubernetes/BUILD.bazel @@ -9,6 +9,7 @@ go_library( deps = [ "//internal/constants", "//internal/versions/components", + "@io_k8s_api//core/v1:core", "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", "@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured", "@io_k8s_apimachinery//pkg/runtime/schema", diff --git a/joinservice/internal/kubernetes/kubernetes.go b/joinservice/internal/kubernetes/kubernetes.go index 6e2913eb7..c71e4a801 100644 --- a/joinservice/internal/kubernetes/kubernetes.go +++ b/joinservice/internal/kubernetes/kubernetes.go @@ -17,6 +17,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/versions/components" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -54,7 +55,7 @@ func New() (*Client, error) { // GetComponents returns the components of the cluster. func (c *Client) GetComponents(ctx context.Context, configMapName string) (components.Components, error) { - componentsRaw, err := c.getConfigMapData(ctx, configMapName, constants.ComponentsListKey) + componentsRaw, err := c.GetConfigMapData(ctx, configMapName, constants.ComponentsListKey) if err != nil { return components.Components{}, fmt.Errorf("failed to get components: %w", err) } @@ -65,8 +66,9 @@ func (c *Client) GetComponents(ctx context.Context, configMapName string) (compo return clusterComponents, nil } -func (c *Client) getConfigMapData(ctx context.Context, name, key string) (string, error) { - cm, err := c.client.CoreV1().ConfigMaps("kube-system").Get(ctx, name, metav1.GetOptions{}) +// GetConfigMapData returns the data for the given key in the configmap with the given name. +func (c *Client) GetConfigMapData(ctx context.Context, name, key string) (string, error) { + cm, err := c.client.CoreV1().ConfigMaps(constants.ConstellationNamespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return "", fmt.Errorf("failed to get configmap: %w", err) } @@ -159,3 +161,37 @@ func k8sCompliantHostname(in string) (string, error) { } return hostname, nil } + +// CreateConfigMap creates a configmap in the kube-system namespace with the provided name and data. +func (c *Client) CreateConfigMap(ctx context.Context, name string, data map[string]string) error { + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: constants.ConstellationNamespace, + }, + Data: data, + } + _, err := c.client.CoreV1().ConfigMaps(constants.ConstellationNamespace).Create(ctx, cm, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create configmap: %w", err) + } + return nil +} + +// UpdateConfigMap updates the configmap with the provided name by writing the provided key and value. +func (c *Client) UpdateConfigMap(ctx context.Context, name, key, value string) error { + cm, err := c.client.CoreV1().ConfigMaps(constants.ConstellationNamespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get configmap: %w", err) + } + cm.Data[key] = value + _, err = c.client.CoreV1().ConfigMaps(constants.ConstellationNamespace).Update(ctx, cm, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update configmap: %w", err) + } + return nil +} diff --git a/joinservice/internal/server/BUILD.bazel b/joinservice/internal/server/BUILD.bazel index 8b7ed9da3..f8ffb5dfb 100644 --- a/joinservice/internal/server/BUILD.bazel +++ b/joinservice/internal/server/BUILD.bazel @@ -13,7 +13,6 @@ go_library( "//internal/grpc/grpclog", "//internal/logger", "//internal/versions/components", - "//joinservice/internal/kubernetes", "//joinservice/joinproto", "@io_k8s_kubernetes//cmd/kubeadm/app/apis/kubeadm/v1beta3", "@org_golang_google_grpc//:go_default_library", diff --git a/joinservice/internal/server/server.go b/joinservice/internal/server/server.go index 7d8fd6932..9f3f149e4 100644 --- a/joinservice/internal/server/server.go +++ b/joinservice/internal/server/server.go @@ -19,7 +19,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/grpc/grpclog" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/versions/components" - "github.com/edgelesssys/constellation/v2/joinservice/internal/kubernetes" "github.com/edgelesssys/constellation/v2/joinservice/joinproto" "go.uber.org/zap" "google.golang.org/grpc" @@ -45,12 +44,8 @@ type Server struct { // New initializes a new Server. func New( measurementSalt []byte, ca certificateAuthority, - joinTokenGetter joinTokenGetter, dataKeyGetter dataKeyGetter, log *logger.Logger, + joinTokenGetter joinTokenGetter, dataKeyGetter dataKeyGetter, kubeClient kubeClient, log *logger.Logger, ) (*Server, error) { - kubeClient, err := kubernetes.New() - if err != nil { - return nil, fmt.Errorf("failed to create kubernetes client: %w", err) - } return &Server{ measurementSalt: measurementSalt, log: log, diff --git a/joinservice/internal/watcher/validator.go b/joinservice/internal/watcher/validator.go index 505a6e49c..1cb204174 100644 --- a/joinservice/internal/watcher/validator.go +++ b/joinservice/internal/watcher/validator.go @@ -8,6 +8,7 @@ package watcher import ( "context" + "crypto/x509" "encoding/asn1" "fmt" "path/filepath" @@ -28,21 +29,25 @@ type Updatable struct { mux sync.Mutex fileHandler file.Handler variant variant.Variant + cachedCerts cachedCerts atls.Validator } -// NewValidator initializes a new updatable validator. -func NewValidator(log *logger.Logger, variant variant.Variant, fileHandler file.Handler) (*Updatable, error) { +// NewValidator initializes a new updatable validator and performs an initial update (aka. initialization). +func NewValidator(log *logger.Logger, variant variant.Variant, fileHandler file.Handler, cachedCerts cachedCerts) (*Updatable, error) { u := &Updatable{ log: log, fileHandler: fileHandler, variant: variant, + cachedCerts: cachedCerts, } + err := u.Update() - if err := u.Update(); err != nil { - return nil, err - } - return u, nil + return u, err +} + +type cachedCerts interface { + SevSnpCerts() (ask *x509.Certificate, ark *x509.Certificate) } // Validate calls the validators Validate method, and prevents any updates during the call. @@ -76,11 +81,44 @@ func (u *Updatable) Update() error { } u.log.Debugf("New expected measurements: %+v", cfg.GetMeasurements()) - validator, err := choose.Validator(cfg, u.log) + cfgWithCerts, err := u.configWithCerts(cfg) if err != nil { - return fmt.Errorf("updating validator: %w", err) + return fmt.Errorf("adding cached certificates: %w", err) + } + + validator, err := choose.Validator(cfgWithCerts, u.log) + if err != nil { + return fmt.Errorf("choosing validator: %w", err) } u.Validator = validator return nil } + +// addCachedCerts adds the certificates cached by the validator to the attestation config, if applicable. +func (u *Updatable) configWithCerts(cfg config.AttestationCfg) (config.AttestationCfg, error) { + switch c := cfg.(type) { + case *config.AzureSEVSNP: + ask, err := u.getCachedAskCert() + if err != nil { + return nil, fmt.Errorf("getting cached ASK certificate: %w", err) + } + c.AMDSigningKey = config.Certificate(ask) + return c, nil + } + // TODO(derpsteb): Add AWS SEV-SNP + + return cfg, nil +} + +// getCachedAskCert returns the cached SEV-SNP ASK certificate. +func (u *Updatable) getCachedAskCert() (x509.Certificate, error) { + if u.cachedCerts == nil { + return x509.Certificate{}, fmt.Errorf("no cached certs available") + } + ask, _ := u.cachedCerts.SevSnpCerts() + if ask == nil { + return x509.Certificate{}, fmt.Errorf("no ASK available") + } + return *ask, nil +} diff --git a/joinservice/internal/watcher/validator_test.go b/joinservice/internal/watcher/validator_test.go index c0ff4b145..c6e87db94 100644 --- a/joinservice/internal/watcher/validator_test.go +++ b/joinservice/internal/watcher/validator_test.go @@ -8,6 +8,7 @@ package watcher import ( "context" + "crypto/x509" "encoding/asn1" "encoding/json" "io" @@ -39,13 +40,18 @@ func TestMain(m *testing.M) { func TestNewUpdateableValidator(t *testing.T) { testCases := map[string]struct { - variant variant.Variant - config config.AttestationCfg - wantErr bool + variant variant.Variant + config config.AttestationCfg + snpCerts *stubSnpCerts + wantErr bool }{ "azure": { variant: variant.AzureSEVSNP{}, config: config.DefaultForAzureSEVSNP(), + snpCerts: &stubSnpCerts{ + ask: &x509.Certificate{}, + ark: &x509.Certificate{}, + }, }, "gcp": { variant: variant.GCPSEVES{}, @@ -83,6 +89,7 @@ func TestNewUpdateableValidator(t *testing.T) { logger.NewTest(t), tc.variant, handler, + tc.snpCerts, ) if tc.wantErr { assert.Error(err) @@ -93,6 +100,15 @@ func TestNewUpdateableValidator(t *testing.T) { } } +type stubSnpCerts struct { + ask *x509.Certificate + ark *x509.Certificate +} + +func (s *stubSnpCerts) SevSnpCerts() (ask *x509.Certificate, ark *x509.Certificate) { + return s.ask, s.ark +} + func TestUpdate(t *testing.T) { assert := assert.New(t) require := require.New(t)