mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-11 07:29:29 -05:00
joinservice: cache certificates for Azure SEV-SNP attestation (#2336)
* add ASK caching in joinservice Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use cached ASK in Azure SEV-SNP attestation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * update test charts Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix linter Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix typ Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * make caching mechanism less provider-specific Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * update buildfiles Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add `omitempty` flag Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * frontload certificate getter Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * rename frontloaded function Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * pass cached certificates to constructor Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix race condition Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix marshalling of empty certs Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix validator usage Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip] add certcache tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add certcache tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix validator test Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * remove unused fields in validator Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix certificate precedence Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use separate context Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * linter fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * linter fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Remove unnecessary comment Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com> * use background context Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Use error format directive Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com> * `azure` -> `Azure` Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com> * improve error messages Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add x509 -> PEM util function Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use crypto util functions Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix certificate replacement logic Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * only require ASK from certcache Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix comment typo Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com>
This commit is contained in:
parent
68d8b29335
commit
a5021c52d3
@ -28,6 +28,7 @@ rules:
|
|||||||
- configmaps
|
- configmaps
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
|
- create
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- "update.edgeless.systems"
|
- "update.edgeless.systems"
|
||||||
resources:
|
resources:
|
||||||
|
@ -28,6 +28,7 @@ rules:
|
|||||||
- configmaps
|
- configmaps
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
|
- create
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- "update.edgeless.systems"
|
- "update.edgeless.systems"
|
||||||
resources:
|
resources:
|
||||||
|
@ -28,6 +28,7 @@ rules:
|
|||||||
- configmaps
|
- configmaps
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
|
- create
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- "update.edgeless.systems"
|
- "update.edgeless.systems"
|
||||||
resources:
|
resources:
|
||||||
|
@ -28,6 +28,7 @@ rules:
|
|||||||
- configmaps
|
- configmaps
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
|
- create
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- "update.edgeless.systems"
|
- "update.edgeless.systems"
|
||||||
resources:
|
resources:
|
||||||
|
@ -28,6 +28,7 @@ rules:
|
|||||||
- configmaps
|
- configmaps
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
|
- create
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- "update.edgeless.systems"
|
- "update.edgeless.systems"
|
||||||
resources:
|
resources:
|
||||||
|
@ -28,6 +28,7 @@ rules:
|
|||||||
- configmaps
|
- configmaps
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
|
- create
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- "update.edgeless.systems"
|
- "update.edgeless.systems"
|
||||||
resources:
|
resources:
|
||||||
|
@ -19,6 +19,7 @@ go_library(
|
|||||||
"//internal/attestation/vtpm",
|
"//internal/attestation/vtpm",
|
||||||
"//internal/cloud/azure",
|
"//internal/cloud/azure",
|
||||||
"//internal/config",
|
"//internal/config",
|
||||||
|
"//internal/constants",
|
||||||
"@com_github_edgelesssys_go_azguestattestation//maa",
|
"@com_github_edgelesssys_go_azguestattestation//maa",
|
||||||
"@com_github_google_go_sev_guest//abi",
|
"@com_github_google_go_sev_guest//abi",
|
||||||
"@com_github_google_go_sev_guest//kds",
|
"@com_github_google_go_sev_guest//kds",
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
"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/abi"
|
||||||
"github.com/google/go-sev-guest/kds"
|
"github.com/google/go-sev-guest/kds"
|
||||||
spb "github.com/google/go-sev-guest/proto/sevsnp"
|
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.
|
// getTrustedKey establishes trust in the given public key.
|
||||||
// It does so by verifying the SNP attestation document.
|
// It does so by verifying the SNP attestation document.
|
||||||
func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDocument, extraData []byte) (crypto.PublicKey, error) {
|
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.
|
// transform the instanceInfo received from Microsoft into a verifiable attestation report format.
|
||||||
var instanceInfo azureInstanceInfo
|
var instanceInfo azureInstanceInfo
|
||||||
if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil {
|
if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshalling instanceInfo: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing attestation report: %w", err)
|
return nil, fmt.Errorf("parsing attestation report: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the attestation report's certificates.
|
// ASK, as cached in joinservice or reported from THIM / KDS.
|
||||||
trustedArk := x509.Certificate(v.config.AMDRootKey) // ARK, specified in Constellation config.
|
ask, err := x509.ParseCertificate(att.CertificateChain.AskCert)
|
||||||
ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) // ASK, as reported from THIM / KDS.
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing ASK certificate: %w", err)
|
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",
|
Product: "Milan",
|
||||||
ProductCerts: &trust.ProductCerts{
|
ProductCerts: &trust.ProductCerts{
|
||||||
Ask: ask,
|
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.
|
// 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
|
// Certificates are retrieved in the following precedence:
|
||||||
// from the AMD KDS.
|
// 1. ASK or ARK from THIM
|
||||||
func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, getter trust.HTTPSGetter) (*spb.Attestation, error) {
|
// 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)
|
report, err := abi.ReportToProto(a.AttestationReport)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("converting report to proto: %w", err)
|
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
|
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()
|
ask, ark, err := a.parseCertChain()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warnf("Error parsing certificate chain: %v", err)
|
logger.Warnf("Error parsing certificate chain: %v", err)
|
||||||
}
|
}
|
||||||
if ask != nil {
|
if ask != nil {
|
||||||
|
logger.Infof("Using ASK certificate from Azure THIM")
|
||||||
att.CertificateChain.AskCert = ask.Raw
|
att.CertificateChain.AskCert = ask.Raw
|
||||||
}
|
}
|
||||||
if ark != nil {
|
if ark != nil {
|
||||||
|
logger.Infof("Using ARK certificate from Azure THIM")
|
||||||
att.CertificateChain.ArkCert = ark.Raw
|
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.
|
// Otherwise, retrieve it from AMD KDS.
|
||||||
if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil {
|
if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil {
|
||||||
logger.Infof(
|
logger.Infof(
|
||||||
@ -304,10 +330,12 @@ func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, gett
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("retrieving certificate chain from AMD KDS: %w", err)
|
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
|
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
|
att.CertificateChain.ArkCert = kdsCertChain.Ark.Raw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,6 +343,11 @@ func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, gett
|
|||||||
return att, nil
|
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.
|
// 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 less than 2 certificates are present, only the present certificate is returned.
|
||||||
// If more than 2 certificates are present, an error is returned.
|
// If more than 2 certificates are present, an error is returned.
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"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) {
|
func TestInstanceInfoAttestation(t *testing.T) {
|
||||||
defaultReport := testdata.AttestationReport
|
defaultReport := testdata.AttestationReport
|
||||||
|
testdataArk, testdataAsk := mustCertChainToPem(t, testdata.CertChain)
|
||||||
|
exampleCert := &x509.Certificate{
|
||||||
|
Raw: []byte{1, 2, 3},
|
||||||
|
}
|
||||||
cfg := config.DefaultForAzureSEVSNP()
|
cfg := config.DefaultForAzureSEVSNP()
|
||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
report []byte
|
report []byte
|
||||||
vcek []byte
|
vcek []byte
|
||||||
certChain []byte
|
certChain []byte
|
||||||
getter *stubHTTPSGetter
|
fallbackCerts sevSnpCerts
|
||||||
wantErr bool
|
getter *stubHTTPSGetter
|
||||||
|
expectedArk *x509.Certificate
|
||||||
|
expectedAsk *x509.Certificate
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
"success": {
|
"success": {
|
||||||
report: defaultReport,
|
report: defaultReport,
|
||||||
vcek: testdata.AzureThimVCEK,
|
vcek: testdata.AzureThimVCEK,
|
||||||
certChain: testdata.CertChain,
|
certChain: testdata.CertChain,
|
||||||
|
expectedArk: testdataArk,
|
||||||
|
expectedAsk: testdataAsk,
|
||||||
},
|
},
|
||||||
"retrieve vcek": {
|
"retrieve vcek": {
|
||||||
report: defaultReport,
|
report: defaultReport,
|
||||||
@ -196,6 +206,8 @@ func TestInstanceInfoAttestation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
),
|
),
|
||||||
|
expectedArk: testdataArk,
|
||||||
|
expectedAsk: testdataAsk,
|
||||||
},
|
},
|
||||||
"retrieve certchain": {
|
"retrieve certchain": {
|
||||||
report: defaultReport,
|
report: defaultReport,
|
||||||
@ -207,6 +219,37 @@ func TestInstanceInfoAttestation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
nil,
|
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": {
|
"retrieve vcek and certchain": {
|
||||||
report: defaultReport,
|
report: defaultReport,
|
||||||
@ -219,6 +262,8 @@ func TestInstanceInfoAttestation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
),
|
),
|
||||||
|
expectedArk: testdataArk,
|
||||||
|
expectedAsk: testdataAsk,
|
||||||
},
|
},
|
||||||
"report too short": {
|
"report too short": {
|
||||||
report: defaultReport[:len(defaultReport)-100],
|
report: defaultReport[:len(defaultReport)-100],
|
||||||
@ -245,7 +290,7 @@ func TestInstanceInfoAttestation(t *testing.T) {
|
|||||||
VCEK: tc.vcek,
|
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 {
|
if tc.wantErr {
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
} else {
|
} else {
|
||||||
@ -263,11 +308,21 @@ func TestInstanceInfoAttestation(t *testing.T) {
|
|||||||
assert.True(tcbValues.TeeSpl >= cfg.TEEVersion.Value)
|
assert.True(tcbValues.TeeSpl >= cfg.TEEVersion.Value)
|
||||||
assert.True(tcbValues.SnpSpl >= cfg.SNPVersion.Value)
|
assert.True(tcbValues.SnpSpl >= cfg.SNPVersion.Value)
|
||||||
assert.True(tcbValues.UcodeSpl >= cfg.MicrocodeVersion.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 {
|
type stubHTTPSGetter struct {
|
||||||
urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs
|
urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs
|
||||||
err error
|
err error
|
||||||
|
@ -66,6 +66,6 @@ func Validator(cfg config.AttestationCfg, log attestation.Logger) (atls.Validato
|
|||||||
case *config.DummyCfg:
|
case *config.DummyCfg:
|
||||||
return atls.NewFakeValidator(variant.Dummy{}), nil
|
return atls.NewFakeValidator(variant.Dummy{}), nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown attestation variant")
|
return nil, fmt.Errorf("unknown attestation variant: %s", cfg.GetVariant())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ func ValidProvider(provider cloudprovider.Provider, variant Variant) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy OID for testfing.
|
// Dummy OID for testing.
|
||||||
type Dummy struct{}
|
type Dummy struct{}
|
||||||
|
|
||||||
// OID returns the struct's object identifier.
|
// OID returns the struct's object identifier.
|
||||||
|
@ -65,18 +65,27 @@ type Certificate x509.Certificate
|
|||||||
|
|
||||||
// MarshalJSON marshals the certificate to PEM.
|
// MarshalJSON marshals the certificate to PEM.
|
||||||
func (c Certificate) MarshalJSON() ([]byte, error) {
|
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})
|
pem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw})
|
||||||
return json.Marshal(string(pem))
|
return json.Marshal(string(pem))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalYAML marshals the certificate to PEM.
|
// MarshalYAML marshals the certificate to PEM.
|
||||||
func (c Certificate) MarshalYAML() (any, error) {
|
func (c Certificate) MarshalYAML() (any, error) {
|
||||||
|
if len(c.Raw) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
pem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw})
|
pem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw})
|
||||||
return string(pem), nil
|
return string(pem), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON unmarshals the certificate from PEM.
|
// UnmarshalJSON unmarshals the certificate from PEM.
|
||||||
func (c *Certificate) UnmarshalJSON(data []byte) error {
|
func (c *Certificate) UnmarshalJSON(data []byte) error {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return c.unmarshal(func(val any) error {
|
return c.unmarshal(func(val any) error {
|
||||||
return json.Unmarshal(data, val)
|
return json.Unmarshal(data, val)
|
||||||
})
|
})
|
||||||
@ -92,6 +101,9 @@ func (c *Certificate) unmarshal(unmarshalFunc func(any) error) error {
|
|||||||
if err := unmarshalFunc(&pemData); err != nil {
|
if err := unmarshalFunc(&pemData); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if pemData == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
block, _ := pem.Decode([]byte(pemData))
|
block, _ := pem.Decode([]byte(pemData))
|
||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -77,6 +77,19 @@ func TestCertificateMarshalJSON(t *testing.T) {
|
|||||||
assert.JSONEq(jsonCert, string(out))
|
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) {
|
func TestCertificateMarshalYAML(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
@ -89,3 +102,16 @@ func TestCertificateMarshalYAML(t *testing.T) {
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
assert.YAMLEq(yamlCert, string(out))
|
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))
|
||||||
|
}
|
||||||
|
@ -1118,6 +1118,9 @@ type AzureSEVSNP struct {
|
|||||||
// description: |
|
// description: |
|
||||||
// AMD Root Key certificate used to verify the SEV-SNP certificate chain.
|
// AMD Root Key certificate used to verify the SEV-SNP certificate chain.
|
||||||
AMDRootKey Certificate `json:"amdRootKey" yaml:"amdRootKey"`
|
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".
|
// setWantLatestToFalse sets the WantLatest field to false for all versions in order to unmarshal the numerical versions instead of the string "latest".
|
||||||
|
@ -590,7 +590,7 @@ func init() {
|
|||||||
FieldName: "azureSEVSNP",
|
FieldName: "azureSEVSNP",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
AzureSEVSNPDoc.Fields = make([]encoder.Doc, 7)
|
AzureSEVSNPDoc.Fields = make([]encoder.Doc, 8)
|
||||||
AzureSEVSNPDoc.Fields[0].Name = "measurements"
|
AzureSEVSNPDoc.Fields[0].Name = "measurements"
|
||||||
AzureSEVSNPDoc.Fields[0].Type = "M"
|
AzureSEVSNPDoc.Fields[0].Type = "M"
|
||||||
AzureSEVSNPDoc.Fields[0].Note = ""
|
AzureSEVSNPDoc.Fields[0].Note = ""
|
||||||
@ -626,6 +626,11 @@ func init() {
|
|||||||
AzureSEVSNPDoc.Fields[6].Note = ""
|
AzureSEVSNPDoc.Fields[6].Note = ""
|
||||||
AzureSEVSNPDoc.Fields[6].Description = "AMD Root Key certificate used to verify the SEV-SNP certificate chain."
|
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[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.Type = "AzureTrustedLaunch"
|
||||||
AzureTrustedLaunchDoc.Comments[encoder.LineComment] = "AzureTrustedLaunch is the configuration for Azure Trusted Launch attestation."
|
AzureTrustedLaunchDoc.Comments[encoder.LineComment] = "AzureTrustedLaunch is the configuration for Azure Trusted Launch attestation."
|
||||||
|
@ -128,6 +128,12 @@ const (
|
|||||||
K8sVersionFieldName = "cluster-version"
|
K8sVersionFieldName = "cluster-version"
|
||||||
// ComponentsListKey is the name of the key holding the list of components in the components configMap.
|
// ComponentsListKey is the name of the key holding the list of components in the components configMap.
|
||||||
ComponentsListKey = "components"
|
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 resource name used for NodeVersion in constellation-operator and CLI.
|
||||||
NodeVersionResourceName = "constellation-version"
|
NodeVersionResourceName = "constellation-version"
|
||||||
// NodeKubernetesComponentsAnnotationKey is the name of the annotation holding the reference to the ConfigMap listing all K8s components.
|
// NodeKubernetesComponentsAnnotationKey is the name of the annotation holding the reference to the ConfigMap listing all K8s components.
|
||||||
|
@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
@ -61,7 +62,8 @@ func GenerateRandomBytes(length int) ([]byte, error) {
|
|||||||
return nonce, nil
|
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) {
|
func PemToX509Cert(raw []byte) (*x509.Certificate, error) {
|
||||||
decoded, _ := pem.Decode(raw)
|
decoded, _ := pem.Decode(raw)
|
||||||
if decoded == nil {
|
if decoded == nil {
|
||||||
@ -73,3 +75,13 @@ func PemToX509Cert(raw []byte) (*x509.Certificate, error) {
|
|||||||
}
|
}
|
||||||
return cert, nil
|
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
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/crypto/testvector"
|
"github.com/edgelesssys/constellation/v2/internal/crypto/testvector"
|
||||||
@ -119,3 +120,58 @@ func TestGenerateRandomBytes(t *testing.T) {
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
assert.Len(n3, 16)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,8 +21,10 @@ go_library(
|
|||||||
"//internal/file",
|
"//internal/file",
|
||||||
"//internal/grpc/atlscredentials",
|
"//internal/grpc/atlscredentials",
|
||||||
"//internal/logger",
|
"//internal/logger",
|
||||||
|
"//joinservice/internal/certcache",
|
||||||
"//joinservice/internal/kms",
|
"//joinservice/internal/kms",
|
||||||
"//joinservice/internal/kubeadm",
|
"//joinservice/internal/kubeadm",
|
||||||
|
"//joinservice/internal/kubernetes",
|
||||||
"//joinservice/internal/kubernetesca",
|
"//joinservice/internal/kubernetesca",
|
||||||
"//joinservice/internal/server",
|
"//joinservice/internal/server",
|
||||||
"//joinservice/internal/watcher",
|
"//joinservice/internal/watcher",
|
||||||
|
@ -28,8 +28,10 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/grpc/atlscredentials"
|
"github.com/edgelesssys/constellation/v2/internal/grpc/atlscredentials"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
"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/kms"
|
||||||
"github.com/edgelesssys/constellation/v2/joinservice/internal/kubeadm"
|
"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/kubernetesca"
|
||||||
"github.com/edgelesssys/constellation/v2/joinservice/internal/server"
|
"github.com/edgelesssys/constellation/v2/joinservice/internal/server"
|
||||||
"github.com/edgelesssys/constellation/v2/joinservice/internal/watcher"
|
"github.com/edgelesssys/constellation/v2/joinservice/internal/watcher"
|
||||||
@ -56,11 +58,23 @@ func main() {
|
|||||||
|
|
||||||
handler := file.NewHandler(afero.NewOsFs())
|
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 {
|
if err != nil {
|
||||||
log.With(zap.Error(err)).Fatalf("Failed to parse attestation variant")
|
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 {
|
if err != nil {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
log.With(zap.Error(err)).Fatalf("Failed to create validator")
|
log.With(zap.Error(err)).Fatalf("Failed to create validator")
|
||||||
@ -68,9 +82,10 @@ func main() {
|
|||||||
|
|
||||||
creds := atlscredentials.New(nil, []atls.Validator{validator})
|
creds := atlscredentials.New(nil, []atls.Validator{validator})
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), vpcIPTimeout)
|
vpcCtx, cancel := context.WithTimeout(context.Background(), vpcIPTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
vpcIP, err := getVPCIP(ctx, *provider)
|
|
||||||
|
vpcIP, err := getVPCIP(vpcCtx, *provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.With(zap.Error(err)).Fatalf("Failed to get IP in VPC")
|
log.With(zap.Error(err)).Fatalf("Failed to get IP in VPC")
|
||||||
}
|
}
|
||||||
@ -91,6 +106,7 @@ func main() {
|
|||||||
kubernetesca.New(log.Named("certificateAuthority"), handler),
|
kubernetesca.New(log.Named("certificateAuthority"), handler),
|
||||||
kubeadm,
|
kubeadm,
|
||||||
keyServiceClient,
|
keyServiceClient,
|
||||||
|
kubeClient,
|
||||||
log.Named("server"),
|
log.Named("server"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
37
joinservice/internal/certcache/BUILD.bazel
Normal file
37
joinservice/internal/certcache/BUILD.bazel
Normal file
@ -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",
|
||||||
|
],
|
||||||
|
)
|
26
joinservice/internal/certcache/amdkds/BUILD.bazel
Normal file
26
joinservice/internal/certcache/amdkds/BUILD.bazel
Normal file
@ -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",
|
||||||
|
],
|
||||||
|
)
|
38
joinservice/internal/certcache/amdkds/amdkds.go
Normal file
38
joinservice/internal/certcache/amdkds/amdkds.go
Normal file
@ -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
|
||||||
|
}
|
74
joinservice/internal/certcache/amdkds/amdkds_test.go
Normal file
74
joinservice/internal/certcache/amdkds/amdkds_test.go
Normal file
@ -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
|
||||||
|
}
|
9
joinservice/internal/certcache/amdkds/testdata/BUILD.bazel
vendored
Normal file
9
joinservice/internal/certcache/amdkds/testdata/BUILD.bazel
vendored
Normal file
@ -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__"],
|
||||||
|
)
|
74
joinservice/internal/certcache/amdkds/testdata/certchain.pem
vendored
Normal file
74
joinservice/internal/certcache/amdkds/testdata/certchain.pem
vendored
Normal file
@ -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-----
|
15
joinservice/internal/certcache/amdkds/testdata/testdata.go
vendored
Normal file
15
joinservice/internal/certcache/amdkds/testdata/testdata.go
vendored
Normal file
@ -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
|
203
joinservice/internal/certcache/certcache.go
Normal file
203
joinservice/internal/certcache/certcache.go
Normal file
@ -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)
|
||||||
|
}
|
247
joinservice/internal/certcache/certcache_test.go
Normal file
247
joinservice/internal/certcache/certcache_test.go
Normal file
@ -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
|
||||||
|
}
|
12
joinservice/internal/certcache/testdata/BUILD.bazel
vendored
Normal file
12
joinservice/internal/certcache/testdata/BUILD.bazel
vendored
Normal file
@ -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__"],
|
||||||
|
)
|
37
joinservice/internal/certcache/testdata/ark.pem
vendored
Normal file
37
joinservice/internal/certcache/testdata/ark.pem
vendored
Normal file
@ -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-----
|
37
joinservice/internal/certcache/testdata/ask.pem
vendored
Normal file
37
joinservice/internal/certcache/testdata/ask.pem
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
20
joinservice/internal/certcache/testdata/testdata.go
vendored
Normal file
20
joinservice/internal/certcache/testdata/testdata.go
vendored
Normal file
@ -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
|
@ -9,6 +9,7 @@ go_library(
|
|||||||
deps = [
|
deps = [
|
||||||
"//internal/constants",
|
"//internal/constants",
|
||||||
"//internal/versions/components",
|
"//internal/versions/components",
|
||||||
|
"@io_k8s_api//core/v1:core",
|
||||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||||
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured",
|
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured",
|
||||||
"@io_k8s_apimachinery//pkg/runtime/schema",
|
"@io_k8s_apimachinery//pkg/runtime/schema",
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/versions/components"
|
"github.com/edgelesssys/constellation/v2/internal/versions/components"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
@ -54,7 +55,7 @@ func New() (*Client, error) {
|
|||||||
|
|
||||||
// GetComponents returns the components of the cluster.
|
// GetComponents returns the components of the cluster.
|
||||||
func (c *Client) GetComponents(ctx context.Context, configMapName string) (components.Components, error) {
|
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 {
|
if err != nil {
|
||||||
return components.Components{}, fmt.Errorf("failed to get components: %w", err)
|
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
|
return clusterComponents, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getConfigMapData(ctx context.Context, name, key string) (string, error) {
|
// GetConfigMapData returns the data for the given key in the configmap with the given name.
|
||||||
cm, err := c.client.CoreV1().ConfigMaps("kube-system").Get(ctx, name, metav1.GetOptions{})
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get configmap: %w", err)
|
return "", fmt.Errorf("failed to get configmap: %w", err)
|
||||||
}
|
}
|
||||||
@ -159,3 +161,37 @@ func k8sCompliantHostname(in string) (string, error) {
|
|||||||
}
|
}
|
||||||
return hostname, nil
|
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
|
||||||
|
}
|
||||||
|
@ -13,7 +13,6 @@ go_library(
|
|||||||
"//internal/grpc/grpclog",
|
"//internal/grpc/grpclog",
|
||||||
"//internal/logger",
|
"//internal/logger",
|
||||||
"//internal/versions/components",
|
"//internal/versions/components",
|
||||||
"//joinservice/internal/kubernetes",
|
|
||||||
"//joinservice/joinproto",
|
"//joinservice/joinproto",
|
||||||
"@io_k8s_kubernetes//cmd/kubeadm/app/apis/kubeadm/v1beta3",
|
"@io_k8s_kubernetes//cmd/kubeadm/app/apis/kubeadm/v1beta3",
|
||||||
"@org_golang_google_grpc//:go_default_library",
|
"@org_golang_google_grpc//:go_default_library",
|
||||||
|
@ -19,7 +19,6 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/grpc/grpclog"
|
"github.com/edgelesssys/constellation/v2/internal/grpc/grpclog"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/versions/components"
|
"github.com/edgelesssys/constellation/v2/internal/versions/components"
|
||||||
"github.com/edgelesssys/constellation/v2/joinservice/internal/kubernetes"
|
|
||||||
"github.com/edgelesssys/constellation/v2/joinservice/joinproto"
|
"github.com/edgelesssys/constellation/v2/joinservice/joinproto"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@ -45,12 +44,8 @@ type Server struct {
|
|||||||
// New initializes a new Server.
|
// New initializes a new Server.
|
||||||
func New(
|
func New(
|
||||||
measurementSalt []byte, ca certificateAuthority,
|
measurementSalt []byte, ca certificateAuthority,
|
||||||
joinTokenGetter joinTokenGetter, dataKeyGetter dataKeyGetter, log *logger.Logger,
|
joinTokenGetter joinTokenGetter, dataKeyGetter dataKeyGetter, kubeClient kubeClient, log *logger.Logger,
|
||||||
) (*Server, error) {
|
) (*Server, error) {
|
||||||
kubeClient, err := kubernetes.New()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create kubernetes client: %w", err)
|
|
||||||
}
|
|
||||||
return &Server{
|
return &Server{
|
||||||
measurementSalt: measurementSalt,
|
measurementSalt: measurementSalt,
|
||||||
log: log,
|
log: log,
|
||||||
|
@ -8,6 +8,7 @@ package watcher
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -28,21 +29,25 @@ type Updatable struct {
|
|||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
fileHandler file.Handler
|
fileHandler file.Handler
|
||||||
variant variant.Variant
|
variant variant.Variant
|
||||||
|
cachedCerts cachedCerts
|
||||||
atls.Validator
|
atls.Validator
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewValidator initializes a new updatable validator.
|
// NewValidator initializes a new updatable validator and performs an initial update (aka. initialization).
|
||||||
func NewValidator(log *logger.Logger, variant variant.Variant, fileHandler file.Handler) (*Updatable, error) {
|
func NewValidator(log *logger.Logger, variant variant.Variant, fileHandler file.Handler, cachedCerts cachedCerts) (*Updatable, error) {
|
||||||
u := &Updatable{
|
u := &Updatable{
|
||||||
log: log,
|
log: log,
|
||||||
fileHandler: fileHandler,
|
fileHandler: fileHandler,
|
||||||
variant: variant,
|
variant: variant,
|
||||||
|
cachedCerts: cachedCerts,
|
||||||
}
|
}
|
||||||
|
err := u.Update()
|
||||||
|
|
||||||
if err := u.Update(); err != nil {
|
return u, err
|
||||||
return nil, err
|
}
|
||||||
}
|
|
||||||
return u, nil
|
type cachedCerts interface {
|
||||||
|
SevSnpCerts() (ask *x509.Certificate, ark *x509.Certificate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate calls the validators Validate method, and prevents any updates during the call.
|
// 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())
|
u.log.Debugf("New expected measurements: %+v", cfg.GetMeasurements())
|
||||||
|
|
||||||
validator, err := choose.Validator(cfg, u.log)
|
cfgWithCerts, err := u.configWithCerts(cfg)
|
||||||
if err != nil {
|
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
|
u.Validator = validator
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ package watcher
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
@ -39,13 +40,18 @@ func TestMain(m *testing.M) {
|
|||||||
|
|
||||||
func TestNewUpdateableValidator(t *testing.T) {
|
func TestNewUpdateableValidator(t *testing.T) {
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
variant variant.Variant
|
variant variant.Variant
|
||||||
config config.AttestationCfg
|
config config.AttestationCfg
|
||||||
wantErr bool
|
snpCerts *stubSnpCerts
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
"azure": {
|
"azure": {
|
||||||
variant: variant.AzureSEVSNP{},
|
variant: variant.AzureSEVSNP{},
|
||||||
config: config.DefaultForAzureSEVSNP(),
|
config: config.DefaultForAzureSEVSNP(),
|
||||||
|
snpCerts: &stubSnpCerts{
|
||||||
|
ask: &x509.Certificate{},
|
||||||
|
ark: &x509.Certificate{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"gcp": {
|
"gcp": {
|
||||||
variant: variant.GCPSEVES{},
|
variant: variant.GCPSEVES{},
|
||||||
@ -83,6 +89,7 @@ func TestNewUpdateableValidator(t *testing.T) {
|
|||||||
logger.NewTest(t),
|
logger.NewTest(t),
|
||||||
tc.variant,
|
tc.variant,
|
||||||
handler,
|
handler,
|
||||||
|
tc.snpCerts,
|
||||||
)
|
)
|
||||||
if tc.wantErr {
|
if tc.wantErr {
|
||||||
assert.Error(err)
|
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) {
|
func TestUpdate(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
Loading…
Reference in New Issue
Block a user