mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-25 07:29:38 -05:00
Enable upload of TDX reports to Constellation CDN
Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
parent
9159b60331
commit
d67d0ac9df
@ -2,9 +2,9 @@ name: E2E Attestationconfig API Test
|
|||||||
description: "Test the attestationconfig CLI is functional."
|
description: "Test the attestationconfig CLI is functional."
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
csp:
|
attestationVariant:
|
||||||
description: "Cloud provider to run tests against"
|
description: "attestation variant to run tests against"
|
||||||
default: "azure"
|
default: "azure-sev-snp"
|
||||||
cosignPrivateKey:
|
cosignPrivateKey:
|
||||||
description: "Cosign private key"
|
description: "Cosign private key"
|
||||||
required: true
|
required: true
|
||||||
@ -30,4 +30,4 @@ runs:
|
|||||||
COSIGN_PRIVATE_KEY: ${{ inputs.cosignPrivateKey }}
|
COSIGN_PRIVATE_KEY: ${{ inputs.cosignPrivateKey }}
|
||||||
COSIGN_PASSWORD: ${{ inputs.cosignPassword }}
|
COSIGN_PASSWORD: ${{ inputs.cosignPassword }}
|
||||||
run: |
|
run: |
|
||||||
bazel run //internal/api/attestationconfigapi/cli:cli_e2e_test -- ${{ inputs.csp }}
|
bazel run //internal/api/attestationconfigapi/cli:cli_e2e_test -- ${{ inputs.attestationVariant }}
|
||||||
|
13
.github/actions/e2e_verify/action.yml
vendored
13
.github/actions/e2e_verify/action.yml
vendored
@ -68,9 +68,9 @@ runs:
|
|||||||
|
|
||||||
case "${{ inputs.attestationVariant }}"
|
case "${{ inputs.attestationVariant }}"
|
||||||
in
|
in
|
||||||
"azure-sev-snp"|"aws-sev-snp"|"gcp-sev-snp")
|
"azure-sev-snp"|"azure-tdx"|"aws-sev-snp"|"gcp-sev-snp")
|
||||||
echo "Extracting TCB versions for API update"
|
echo "Extracting TCB versions for API update"
|
||||||
constellation verify --cluster-id "${clusterID}" --node-endpoint localhost:9090 -o json > "snp-report-${node}.json"
|
constellation verify --cluster-id "${clusterID}" --node-endpoint localhost:9090 -o json > "attestation-report-${node}.json"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
constellation verify --cluster-id "${clusterID}" --node-endpoint localhost:9090
|
constellation verify --cluster-id "${clusterID}" --node-endpoint localhost:9090
|
||||||
@ -88,22 +88,19 @@ runs:
|
|||||||
aws-region: eu-central-1
|
aws-region: eu-central-1
|
||||||
|
|
||||||
- name: Upload extracted TCBs
|
- name: Upload extracted TCBs
|
||||||
if: github.ref_name == 'main' && (inputs.attestationVariant == 'azure-sev-snp' || inputs.attestationVariant == 'aws-sev-snp' || inputs.attestationVariant == 'gcp-sev-snp')
|
if: github.ref_name == 'main' && (inputs.attestationVariant == 'azure-sev-snp' || inputs.attestationVariant == 'azure-tdx' || inputs.attestationVariant == 'aws-sev-snp' || inputs.attestationVariant == 'gcp-sev-snp')
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
COSIGN_PASSWORD: ${{ inputs.cosignPassword }}
|
COSIGN_PASSWORD: ${{ inputs.cosignPassword }}
|
||||||
COSIGN_PRIVATE_KEY: ${{ inputs.cosignPrivateKey }}
|
COSIGN_PRIVATE_KEY: ${{ inputs.cosignPrivateKey }}
|
||||||
run: |
|
run: |
|
||||||
reports=(snp-report-*.json)
|
reports=(attestation-report-*.json)
|
||||||
if [ -z ${#reports[@]} ]; then
|
if [ -z ${#reports[@]} ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
attestationVariant=${{ inputs.attestationVariant }}
|
|
||||||
cloudProvider=${attestationVariant%%-*}
|
|
||||||
|
|
||||||
for file in "${reports[@]}"; do
|
for file in "${reports[@]}"; do
|
||||||
path=$(realpath "${file}")
|
path=$(realpath "${file}")
|
||||||
cat "${path}"
|
cat "${path}"
|
||||||
bazel run //internal/api/attestationconfigapi/cli -- upload "${cloudProvider}" snp-report "${path}"
|
bazel run //internal/api/attestationconfigapi/cli -- upload ${{ inputs.attestationVariant }} attestation-report "${path}"
|
||||||
done
|
done
|
||||||
|
@ -15,9 +15,9 @@ jobs:
|
|||||||
e2e-api:
|
e2e-api:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 1
|
max-parallel: 2
|
||||||
matrix:
|
matrix:
|
||||||
csp: ["azure", "aws", "gcp"]
|
attestationVariant: ["azure-sev-snp", "azure-tdx", "aws-sev-snp", "gcp-sev-snp"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@ -36,4 +36,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cosignPrivateKey: ${{ secrets.COSIGN_DEV_PRIVATE_KEY }}
|
cosignPrivateKey: ${{ secrets.COSIGN_DEV_PRIVATE_KEY }}
|
||||||
cosignPassword: ${{ secrets.COSIGN_DEV_PASSWORD }}
|
cosignPassword: ${{ secrets.COSIGN_DEV_PASSWORD }}
|
||||||
csp: ${{ matrix.csp }}
|
attestationVariant: ${{ matrix.attestationVariant }}
|
||||||
|
@ -204,8 +204,8 @@ func (f stubVerifyFetcher) FetchAndVerifyMeasurements(_ context.Context, _ strin
|
|||||||
|
|
||||||
type stubAttestationFetcher struct{}
|
type stubAttestationFetcher struct{}
|
||||||
|
|
||||||
func (f stubAttestationFetcher) FetchLatestVersion(_ context.Context, _ variant.Variant) (attestationconfigapi.VersionAPIEntry, error) {
|
func (f stubAttestationFetcher) FetchLatestVersion(_ context.Context, _ variant.Variant) (attestationconfigapi.Entry, error) {
|
||||||
return attestationconfigapi.VersionAPIEntry{
|
return attestationconfigapi.Entry{
|
||||||
SEVSNPVersion: testCfg,
|
SEVSNPVersion: testCfg,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -171,6 +171,6 @@ type stubConfigFetcher struct {
|
|||||||
fetchLatestErr error
|
fetchLatestErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubConfigFetcher) FetchLatestVersion(context.Context, variant.Variant) (attestationconfigapi.VersionAPIEntry, error) {
|
func (s *stubConfigFetcher) FetchLatestVersion(context.Context, variant.Variant) (attestationconfigapi.Entry, error) {
|
||||||
return attestationconfigapi.VersionAPIEntry{}, s.fetchLatestErr
|
return attestationconfigapi.Entry{}, s.fetchLatestErr
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ go_library(
|
|||||||
"//internal/api/attestationconfigapi/cli/client",
|
"//internal/api/attestationconfigapi/cli/client",
|
||||||
"//internal/api/fetcher",
|
"//internal/api/fetcher",
|
||||||
"//internal/attestation/variant",
|
"//internal/attestation/variant",
|
||||||
"//internal/cloud/cloudprovider",
|
|
||||||
"//internal/constants",
|
"//internal/constants",
|
||||||
"//internal/file",
|
"//internal/file",
|
||||||
"//internal/logger",
|
"//internal/logger",
|
||||||
@ -31,6 +30,7 @@ go_library(
|
|||||||
"@com_github_aws_aws_sdk_go_v2//aws",
|
"@com_github_aws_aws_sdk_go_v2//aws",
|
||||||
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
|
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
|
||||||
"@com_github_aws_aws_sdk_go_v2_service_s3//types",
|
"@com_github_aws_aws_sdk_go_v2_service_s3//types",
|
||||||
|
"@com_github_google_go_tdx_guest//proto/tdx",
|
||||||
"@com_github_spf13_afero//:afero",
|
"@com_github_spf13_afero//:afero",
|
||||||
"@com_github_spf13_cobra//:cobra",
|
"@com_github_spf13_cobra//:cobra",
|
||||||
],
|
],
|
||||||
|
@ -29,7 +29,6 @@ go_test(
|
|||||||
embed = [":client"],
|
embed = [":client"],
|
||||||
deps = [
|
deps = [
|
||||||
"//internal/api/attestationconfigapi",
|
"//internal/api/attestationconfigapi",
|
||||||
"//internal/attestation/variant",
|
|
||||||
"@com_github_stretchr_testify//assert",
|
"@com_github_stretchr_testify//assert",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -15,8 +15,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
apiclient "github.com/edgelesssys/constellation/v2/internal/api/client"
|
apiclient "github.com/edgelesssys/constellation/v2/internal/api/client"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
@ -35,6 +38,8 @@ type Client struct {
|
|||||||
bucketID string
|
bucketID string
|
||||||
signer sigstore.Signer
|
signer sigstore.Signer
|
||||||
cacheWindowSize int
|
cacheWindowSize int
|
||||||
|
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new Client.
|
// New returns a new Client.
|
||||||
@ -50,50 +55,34 @@ func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []b
|
|||||||
signer: sigstore.NewSigner(cosignPwd, privateKey),
|
signer: sigstore.NewSigner(cosignPwd, privateKey),
|
||||||
bucketID: cfg.Bucket,
|
bucketID: cfg.Bucket,
|
||||||
cacheWindowSize: versionWindowSize,
|
cacheWindowSize: versionWindowSize,
|
||||||
|
log: log,
|
||||||
}
|
}
|
||||||
return repo, clientClose, nil
|
return repo, clientClose, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// uploadSEVSNPVersion uploads the latest version numbers of the SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix.
|
// DeleteVersion deletes the given version (without .json suffix) from the API.
|
||||||
func (a Client) uploadSEVSNPVersion(ctx context.Context, attestation variant.Variant, version attestationconfigapi.SEVSNPVersion, date time.Time) error {
|
func (c Client) DeleteVersion(ctx context.Context, attestation variant.Variant, versionStr string) error {
|
||||||
versions, err := a.List(ctx, attestation)
|
versions, err := c.List(ctx, attestation)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fetch version list: %w", err)
|
|
||||||
}
|
|
||||||
ops := a.constructUploadCmd(attestation, version, versions, date)
|
|
||||||
|
|
||||||
return executeAllCmds(ctx, a.s3Client, ops)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSEVSNPVersion deletes the given version (without .json suffix) from the API.
|
|
||||||
func (a Client) DeleteSEVSNPVersion(ctx context.Context, attestation variant.Variant, versionStr string) error {
|
|
||||||
versions, err := a.List(ctx, attestation)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fetch version list: %w", err)
|
return fmt.Errorf("fetch version list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ops, err := a.deleteSEVSNPVersion(versions, versionStr)
|
ops, err := c.deleteVersion(versions, versionStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return executeAllCmds(ctx, a.s3Client, ops)
|
return executeAllCmds(ctx, c.s3Client, ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns the list of versions for the given attestation variant.
|
// List returns the list of versions for the given attestation variant.
|
||||||
func (a Client) List(ctx context.Context, attestation variant.Variant) (attestationconfigapi.VersionList, error) {
|
func (c Client) List(ctx context.Context, attestation variant.Variant) (attestationconfigapi.List, error) {
|
||||||
if !attestation.Equal(variant.AzureSEVSNP{}) &&
|
versions, err := apiclient.Fetch(ctx, c.s3Client, attestationconfigapi.List{Variant: attestation})
|
||||||
!attestation.Equal(variant.AWSSEVSNP{}) &&
|
|
||||||
!attestation.Equal(variant.GCPSEVSNP{}) {
|
|
||||||
return attestationconfigapi.VersionList{}, fmt.Errorf("unsupported attestation variant: %s", attestation)
|
|
||||||
}
|
|
||||||
|
|
||||||
versions, err := apiclient.Fetch(ctx, a.s3Client, attestationconfigapi.VersionList{Variant: attestation})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var notFoundErr *apiclient.NotFoundError
|
var notFoundErr *apiclient.NotFoundError
|
||||||
if errors.As(err, ¬FoundErr) {
|
if errors.As(err, ¬FoundErr) {
|
||||||
return attestationconfigapi.VersionList{Variant: attestation}, nil
|
return attestationconfigapi.List{Variant: attestation}, nil
|
||||||
}
|
}
|
||||||
return attestationconfigapi.VersionList{}, err
|
return attestationconfigapi.List{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
versions.Variant = attestation
|
versions.Variant = attestation
|
||||||
@ -101,10 +90,10 @@ func (a Client) List(ctx context.Context, attestation variant.Variant) (attestat
|
|||||||
return versions, nil
|
return versions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Client) deleteSEVSNPVersion(versions attestationconfigapi.VersionList, versionStr string) (ops []crudCmd, err error) {
|
func (c Client) deleteVersion(versions attestationconfigapi.List, versionStr string) (ops []crudCmd, err error) {
|
||||||
versionStr = versionStr + ".json"
|
versionStr = versionStr + ".json"
|
||||||
ops = append(ops, deleteCmd{
|
ops = append(ops, deleteCmd{
|
||||||
apiObject: attestationconfigapi.VersionAPIEntry{
|
apiObject: attestationconfigapi.Entry{
|
||||||
Variant: versions.Variant,
|
Variant: versions.Variant,
|
||||||
Version: versionStr,
|
Version: versionStr,
|
||||||
},
|
},
|
||||||
@ -116,47 +105,46 @@ func (a Client) deleteSEVSNPVersion(versions attestationconfigapi.VersionList, v
|
|||||||
}
|
}
|
||||||
ops = append(ops, putCmd{
|
ops = append(ops, putCmd{
|
||||||
apiObject: removedVersions,
|
apiObject: removedVersions,
|
||||||
signer: a.signer,
|
signer: c.signer,
|
||||||
})
|
})
|
||||||
return ops, nil
|
return ops, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Client) constructUploadCmd(attestation variant.Variant, version attestationconfigapi.SEVSNPVersion, versionNames attestationconfigapi.VersionList, date time.Time) []crudCmd {
|
func (c Client) listCachedVersions(ctx context.Context, attestation variant.Variant) ([]string, error) {
|
||||||
if !attestation.Equal(versionNames.Variant) {
|
list, err := c.s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
|
||||||
return nil
|
Bucket: aws.String(c.bucketID),
|
||||||
|
Prefix: aws.String(reportVersionDir(attestation)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list objects: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dateStr := date.Format(VersionFormat) + ".json"
|
var dates []string
|
||||||
var res []crudCmd
|
for _, obj := range list.Contents {
|
||||||
|
fileName := path.Base(*obj.Key)
|
||||||
|
|
||||||
res = append(res, putCmd{
|
// The cache contains signature and json files
|
||||||
apiObject: attestationconfigapi.VersionAPIEntry{Version: dateStr, Variant: attestation, SEVSNPVersion: version},
|
// We only want the json files
|
||||||
signer: a.signer,
|
if date, ok := strings.CutSuffix(fileName, ".json"); ok {
|
||||||
})
|
dates = append(dates, date)
|
||||||
|
}
|
||||||
versionNames.AddVersion(dateStr)
|
}
|
||||||
|
return dates, nil
|
||||||
res = append(res, putCmd{
|
|
||||||
apiObject: versionNames,
|
|
||||||
signer: a.signer,
|
|
||||||
})
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeVersion(list attestationconfigapi.VersionList, versionStr string) (removedVersions attestationconfigapi.VersionList, err error) {
|
func removeVersion(list attestationconfigapi.List, versionStr string) (removedVersions attestationconfigapi.List, err error) {
|
||||||
versions := list.List
|
versions := list.List
|
||||||
for i, v := range versions {
|
for i, v := range versions {
|
||||||
if v == versionStr {
|
if v == versionStr {
|
||||||
if i == len(versions)-1 {
|
if i == len(versions)-1 {
|
||||||
removedVersions = attestationconfigapi.VersionList{List: versions[:i], Variant: list.Variant}
|
removedVersions = attestationconfigapi.List{List: versions[:i], Variant: list.Variant}
|
||||||
} else {
|
} else {
|
||||||
removedVersions = attestationconfigapi.VersionList{List: append(versions[:i], versions[i+1:]...), Variant: list.Variant}
|
removedVersions = attestationconfigapi.List{List: append(versions[:i], versions[i+1:]...), Variant: list.Variant}
|
||||||
}
|
}
|
||||||
return removedVersions, nil
|
return removedVersions, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return attestationconfigapi.VersionList{}, fmt.Errorf("version %s not found in list %v", versionStr, versions)
|
return attestationconfigapi.List{}, fmt.Errorf("version %s not found in list %v", versionStr, versions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type crudCmd interface {
|
type crudCmd interface {
|
||||||
|
@ -7,60 +7,28 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUploadAzureSEVSNP(t *testing.T) {
|
|
||||||
sut := Client{
|
|
||||||
bucketID: "bucket",
|
|
||||||
signer: fakeSigner{},
|
|
||||||
}
|
|
||||||
version := attestationconfigapi.SEVSNPVersion{}
|
|
||||||
date := time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC)
|
|
||||||
ops := sut.constructUploadCmd(variant.AzureSEVSNP{}, version, attestationconfigapi.VersionList{List: []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, Variant: variant.AzureSEVSNP{}}, date)
|
|
||||||
dateStr := "2023-01-01-01-01.json"
|
|
||||||
assert := assert.New(t)
|
|
||||||
assert.Contains(ops, putCmd{
|
|
||||||
apiObject: attestationconfigapi.VersionAPIEntry{
|
|
||||||
Variant: variant.AzureSEVSNP{},
|
|
||||||
Version: dateStr,
|
|
||||||
SEVSNPVersion: version,
|
|
||||||
},
|
|
||||||
signer: fakeSigner{},
|
|
||||||
})
|
|
||||||
assert.Contains(ops, putCmd{
|
|
||||||
apiObject: attestationconfigapi.VersionList{Variant: variant.AzureSEVSNP{}, List: []string{"2023-01-01-01-01.json", "2021-01-01-01-01.json", "2019-01-01-01-01.json"}},
|
|
||||||
signer: fakeSigner{},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteAzureSEVSNPVersions(t *testing.T) {
|
func TestDeleteAzureSEVSNPVersions(t *testing.T) {
|
||||||
sut := Client{
|
sut := Client{
|
||||||
bucketID: "bucket",
|
bucketID: "bucket",
|
||||||
}
|
}
|
||||||
versions := attestationconfigapi.VersionList{List: []string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"}}
|
versions := attestationconfigapi.List{List: []string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"}}
|
||||||
|
|
||||||
ops, err := sut.deleteSEVSNPVersion(versions, "2021-01-01")
|
ops, err := sut.deleteVersion(versions, "2021-01-01")
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.Contains(ops, deleteCmd{
|
assert.Contains(ops, deleteCmd{
|
||||||
apiObject: attestationconfigapi.VersionAPIEntry{
|
apiObject: attestationconfigapi.Entry{
|
||||||
Version: "2021-01-01.json",
|
Version: "2021-01-01.json",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Contains(ops, putCmd{
|
assert.Contains(ops, putCmd{
|
||||||
apiObject: attestationconfigapi.VersionList{List: []string{"2023-01-01.json", "2019-01-01.json"}},
|
apiObject: attestationconfigapi.List{List: []string{"2023-01-01.json", "2019-01-01.json"}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeSigner struct{}
|
|
||||||
|
|
||||||
func (fakeSigner) Sign(_ []byte) ([]byte, error) {
|
|
||||||
return []byte("signature"), nil
|
|
||||||
}
|
|
||||||
|
@ -8,6 +8,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
@ -15,9 +16,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/client"
|
"github.com/edgelesssys/constellation/v2/internal/api/client"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
@ -33,153 +31,335 @@ func reportVersionDir(attestation variant.Variant) string {
|
|||||||
return path.Join(attestationconfigapi.AttestationURLPath, attestation.String(), cachedVersionsSubDir)
|
return path.Join(attestationconfigapi.AttestationURLPath, attestation.String(), cachedVersionsSubDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadSEVSNPVersionLatest saves the given version to the cache, determines the smallest
|
// UploadLatestVersion saves the given version to the cache, determines the smallest
|
||||||
// TCB version in the cache among the last cacheWindowSize versions and updates
|
// TCB version in the cache among the last cacheWindowSize versions and updates
|
||||||
// the latest version in the API if there is an update.
|
// the latest version in the API if there is an update.
|
||||||
// force can be used to bypass the validation logic against the cached versions.
|
// force can be used to bypass the validation logic against the cached versions.
|
||||||
func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation variant.Variant, inputVersion,
|
func (c Client) UploadLatestVersion(
|
||||||
latestAPIVersion attestationconfigapi.SEVSNPVersion, now time.Time, force bool,
|
ctx context.Context, attestationVariant variant.Variant,
|
||||||
|
inputVersion, latestVersionInAPI any,
|
||||||
|
now time.Time, force bool,
|
||||||
) error {
|
) error {
|
||||||
if err := c.cacheSEVSNPVersion(ctx, attestation, inputVersion, now); err != nil {
|
// Validate input versions against configured attestation variant
|
||||||
return fmt.Errorf("reporting version: %w", err)
|
// This allows us to skip these checks in the individual variant implementations
|
||||||
}
|
var err error
|
||||||
if force {
|
actionForVariant(attestationVariant,
|
||||||
return c.uploadSEVSNPVersion(ctx, attestation, inputVersion, now)
|
func() {
|
||||||
}
|
if _, ok := inputVersion.(attestationconfigapi.TDXVersion); !ok {
|
||||||
versionDates, err := c.listCachedVersions(ctx, attestation)
|
err = fmt.Errorf("input version %q is not a TDX version", inputVersion)
|
||||||
|
}
|
||||||
|
if _, ok := latestVersionInAPI.(attestationconfigapi.TDXVersion); !ok {
|
||||||
|
err = fmt.Errorf("latest API version %q is not a TDX version", latestVersionInAPI)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func() {
|
||||||
|
if _, ok := inputVersion.(attestationconfigapi.SEVSNPVersion); !ok {
|
||||||
|
err = fmt.Errorf("input version %q is not a SNP version", inputVersion)
|
||||||
|
}
|
||||||
|
if _, ok := latestVersionInAPI.(attestationconfigapi.SEVSNPVersion); !ok {
|
||||||
|
err = fmt.Errorf("latest API version %q is not a SNP version", latestVersionInAPI)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list reported versions: %w", err)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.addVersionToCache(ctx, attestationVariant, inputVersion, now); err != nil {
|
||||||
|
return fmt.Errorf("adding version to cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If force is set, immediately update the latest version to the new version in the API.
|
||||||
|
if force {
|
||||||
|
return c.uploadAsLatestVersion(ctx, attestationVariant, inputVersion, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check the cached versions and update the latest version in the API if necessary.
|
||||||
|
versionDates, err := c.listCachedVersions(ctx, attestationVariant)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listing existing cached versions: %w", err)
|
||||||
}
|
}
|
||||||
if len(versionDates) < c.cacheWindowSize {
|
if len(versionDates) < c.cacheWindowSize {
|
||||||
c.s3Client.Logger.Warn(fmt.Sprintf("Skipping version update, found %d, expected %d reported versions.", len(versionDates), c.cacheWindowSize))
|
c.log.Warn(fmt.Sprintf("Skipping version update, found %d, expected %d reported versions.", len(versionDates), c.cacheWindowSize))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
minVersion, minDate, err := c.findMinVersion(ctx, attestation, versionDates)
|
|
||||||
|
minVersion, minDate, err := c.findMinVersion(ctx, attestationVariant, versionDates)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get minimal version: %w", err)
|
return fmt.Errorf("determining minimal version in cache: %w", err)
|
||||||
}
|
}
|
||||||
c.s3Client.Logger.Info(fmt.Sprintf("Found minimal version: %+v with date: %s", minVersion, minDate))
|
c.log.Info(fmt.Sprintf("Found minimal version: %+v with date: %s", minVersion, minDate))
|
||||||
shouldUpdateAPI, err := isInputNewerThanOtherVersion(minVersion, latestAPIVersion)
|
|
||||||
if err != nil {
|
if !isInputNewerThanOtherVersion(attestationVariant, minVersion, latestVersionInAPI) {
|
||||||
|
c.log.Info(fmt.Sprintf("Input version: %+v is not newer than latest API version: %+v. Skipping list update", minVersion, latestVersionInAPI))
|
||||||
return ErrNoNewerVersion
|
return ErrNoNewerVersion
|
||||||
}
|
}
|
||||||
if !shouldUpdateAPI {
|
|
||||||
c.s3Client.Logger.Info(fmt.Sprintf("Input version: %+v is not newer than latest API version: %+v", minVersion, latestAPIVersion))
|
c.log.Info(fmt.Sprintf("Input version: %+v is newer than latest API version: %+v", minVersion, latestVersionInAPI))
|
||||||
return nil
|
|
||||||
}
|
|
||||||
c.s3Client.Logger.Info(fmt.Sprintf("Input version: %+v is newer than latest API version: %+v", minVersion, latestAPIVersion))
|
|
||||||
t, err := time.Parse(VersionFormat, minDate)
|
t, err := time.Parse(VersionFormat, minDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parsing date: %w", err)
|
return fmt.Errorf("parsing date: %w", err)
|
||||||
}
|
}
|
||||||
if err := c.uploadSEVSNPVersion(ctx, attestation, minVersion, t); err != nil {
|
|
||||||
return fmt.Errorf("uploading version: %w", err)
|
if err := c.uploadAsLatestVersion(ctx, attestationVariant, minVersion, t); err != nil {
|
||||||
|
return fmt.Errorf("uploading as latest version: %w", err)
|
||||||
}
|
}
|
||||||
c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new SEV-SNP version: %+v", minVersion))
|
|
||||||
|
c.log.Info(fmt.Sprintf("Successfully uploaded new %s version: %+v", attestationVariant, minVersion))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cacheSEVSNPVersion uploads the latest observed version numbers of the SEVSNP. This version is used to later report the latest version numbers to the API.
|
// uploadAsLatestVersion uploads the given version and updates the list to set it as the "latest" version.
|
||||||
func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version attestationconfigapi.SEVSNPVersion, date time.Time) error {
|
// The version's name is the UTC timestamp of the date.
|
||||||
|
// The /list entry stores the version name + .json suffix.
|
||||||
|
func (c Client) uploadAsLatestVersion(ctx context.Context, variant variant.Variant, inputVersion any, date time.Time) error {
|
||||||
|
versions, err := c.List(ctx, variant)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetch version list: %w", err)
|
||||||
|
}
|
||||||
|
if !variant.Equal(versions.Variant) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
dateStr := date.Format(VersionFormat) + ".json"
|
dateStr := date.Format(VersionFormat) + ".json"
|
||||||
res := putCmd{
|
var ops []crudCmd
|
||||||
apiObject: reportedSEVSNPVersionAPI{Version: dateStr, variant: attestation, SEVSNPVersion: version},
|
|
||||||
|
obj := apiVersionObject{version: dateStr, variant: variant, cached: false}
|
||||||
|
obj.setVersion(inputVersion)
|
||||||
|
ops = append(ops, putCmd{
|
||||||
|
apiObject: obj,
|
||||||
|
signer: c.signer,
|
||||||
|
})
|
||||||
|
|
||||||
|
versions.AddVersion(dateStr)
|
||||||
|
|
||||||
|
ops = append(ops, putCmd{
|
||||||
|
apiObject: versions,
|
||||||
|
signer: c.signer,
|
||||||
|
})
|
||||||
|
|
||||||
|
return executeAllCmds(ctx, c.s3Client, ops)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addVersionToCache adds the given version to the cache.
|
||||||
|
func (c Client) addVersionToCache(ctx context.Context, variant variant.Variant, inputVersion any, date time.Time) error {
|
||||||
|
dateStr := date.Format(VersionFormat) + ".json"
|
||||||
|
obj := apiVersionObject{version: dateStr, variant: variant, cached: true}
|
||||||
|
obj.setVersion(inputVersion)
|
||||||
|
cmd := putCmd{
|
||||||
|
apiObject: obj,
|
||||||
signer: c.signer,
|
signer: c.signer,
|
||||||
}
|
}
|
||||||
return res.Execute(ctx, c.s3Client)
|
return cmd.Execute(ctx, c.s3Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) listCachedVersions(ctx context.Context, attestation variant.Variant) ([]string, error) {
|
// findMinVersion returns the minimal version in the cache among the last cacheWindowSize versions.
|
||||||
list, err := c.s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
|
func (c Client) findMinVersion(
|
||||||
Bucket: aws.String(c.bucketID),
|
ctx context.Context, attestationVariant variant.Variant, versionDates []string,
|
||||||
Prefix: aws.String(reportVersionDir(attestation)),
|
) (any, string, error) {
|
||||||
})
|
var getMinimalVersion func() (any, string, error)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("list objects: %w", err)
|
actionForVariant(attestationVariant,
|
||||||
}
|
func() {
|
||||||
var dates []string
|
getMinimalVersion = func() (any, string, error) {
|
||||||
for _, obj := range list.Contents {
|
return findMinimalVersion[attestationconfigapi.TDXVersion](ctx, attestationVariant, versionDates, c.s3Client, c.cacheWindowSize)
|
||||||
fileName := path.Base(*obj.Key)
|
}
|
||||||
if strings.HasSuffix(fileName, ".json") {
|
},
|
||||||
dates = append(dates, fileName[:len(fileName)-5])
|
func() {
|
||||||
}
|
getMinimalVersion = func() (any, string, error) {
|
||||||
}
|
return findMinimalVersion[attestationconfigapi.SEVSNPVersion](ctx, attestationVariant, versionDates, c.s3Client, c.cacheWindowSize)
|
||||||
return dates, nil
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return getMinimalVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
// findMinVersion finds the minimal version of the given version dates among the latest values in the version window size.
|
func findMinimalVersion[T attestationconfigapi.TDXVersion | attestationconfigapi.SEVSNPVersion](
|
||||||
func (c Client) findMinVersion(ctx context.Context, attesation variant.Variant, versionDates []string) (attestationconfigapi.SEVSNPVersion, string, error) {
|
ctx context.Context, variant variant.Variant, versionDates []string,
|
||||||
var minimalVersion *attestationconfigapi.SEVSNPVersion
|
s3Client *client.Client, cacheWindowSize int,
|
||||||
|
) (T, string, error) {
|
||||||
|
var minimalVersion *T
|
||||||
var minimalDate string
|
var minimalDate string
|
||||||
sort.Sort(sort.Reverse(sort.StringSlice(versionDates))) // sort in reverse order to slice the latest versions
|
sort.Sort(sort.Reverse(sort.StringSlice(versionDates))) // sort in reverse order to slice the latest versions
|
||||||
versionDates = versionDates[:c.cacheWindowSize]
|
versionDates = versionDates[:cacheWindowSize]
|
||||||
sort.Strings(versionDates) // sort with oldest first to to take the minimal version with the oldest date
|
sort.Strings(versionDates) // sort with oldest first to to take the minimal version with the oldest date
|
||||||
|
|
||||||
for _, date := range versionDates {
|
for _, date := range versionDates {
|
||||||
obj, err := client.Fetch(ctx, c.s3Client, reportedSEVSNPVersionAPI{Version: date + ".json", variant: attesation})
|
obj, err := client.Fetch(ctx, s3Client, apiVersionObject{version: date + ".json", variant: variant, cached: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return attestationconfigapi.SEVSNPVersion{}, "", fmt.Errorf("get object: %w", err)
|
return *new(T), "", fmt.Errorf("get object: %w", err)
|
||||||
}
|
}
|
||||||
// Need to set this explicitly as the variant is not part of the marshalled JSON.
|
obj.variant = variant // variant is not set by Fetch, set it manually
|
||||||
obj.variant = attesation
|
|
||||||
|
|
||||||
if minimalVersion == nil {
|
if minimalVersion == nil {
|
||||||
minimalVersion = &obj.SEVSNPVersion
|
v := obj.getVersion().(T)
|
||||||
|
minimalVersion = &v
|
||||||
|
minimalDate = date
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current minimal version has newer versions than the one we just fetched,
|
||||||
|
// update the minimal version to the older version.
|
||||||
|
if isInputNewerThanOtherVersion(variant, *minimalVersion, obj.getVersion()) {
|
||||||
|
v := obj.getVersion().(T)
|
||||||
|
minimalVersion = &v
|
||||||
minimalDate = date
|
minimalDate = date
|
||||||
} else {
|
|
||||||
shouldUpdateMinimal, err := isInputNewerThanOtherVersion(*minimalVersion, obj.SEVSNPVersion)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if shouldUpdateMinimal {
|
|
||||||
minimalVersion = &obj.SEVSNPVersion
|
|
||||||
minimalDate = date
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return *minimalVersion, minimalDate, nil
|
return *minimalVersion, minimalDate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isInputNewerThanOtherVersion compares all version fields and returns true if any input field is newer.
|
func isInputNewerThanOtherVersion(variant variant.Variant, inputVersion, otherVersion any) bool {
|
||||||
func isInputNewerThanOtherVersion(input, other attestationconfigapi.SEVSNPVersion) (bool, error) {
|
var result bool
|
||||||
if input == other {
|
actionForVariant(variant,
|
||||||
return false, nil
|
func() {
|
||||||
}
|
input := inputVersion.(attestationconfigapi.TDXVersion)
|
||||||
if input.TEE < other.TEE {
|
other := otherVersion.(attestationconfigapi.TDXVersion)
|
||||||
return false, fmt.Errorf("input TEE version: %d is older than latest API version: %d", input.TEE, other.TEE)
|
result = isInputNewerThanOtherTDXVersion(input, other)
|
||||||
}
|
},
|
||||||
if input.SNP < other.SNP {
|
func() {
|
||||||
return false, fmt.Errorf("input SNP version: %d is older than latest API version: %d", input.SNP, other.SNP)
|
input := inputVersion.(attestationconfigapi.SEVSNPVersion)
|
||||||
}
|
other := otherVersion.(attestationconfigapi.SEVSNPVersion)
|
||||||
if input.Microcode < other.Microcode {
|
result = isInputNewerThanOtherSEVSNPVersion(input, other)
|
||||||
return false, fmt.Errorf("input Microcode version: %d is older than latest API version: %d", input.Microcode, other.Microcode)
|
},
|
||||||
}
|
)
|
||||||
if input.Bootloader < other.Bootloader {
|
return result
|
||||||
return false, fmt.Errorf("input Bootloader version: %d is older than latest API version: %d", input.Bootloader, other.Bootloader)
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reportedSEVSNPVersionAPI is the request to get the version information of the specific version in the config api.
|
type apiVersionObject struct {
|
||||||
type reportedSEVSNPVersionAPI struct {
|
version string `json:"-"`
|
||||||
Version string `json:"-"`
|
|
||||||
variant variant.Variant `json:"-"`
|
variant variant.Variant `json:"-"`
|
||||||
attestationconfigapi.SEVSNPVersion
|
cached bool `json:"-"`
|
||||||
|
snp attestationconfigapi.SEVSNPVersion
|
||||||
|
tdx attestationconfigapi.TDXVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a apiVersionObject) MarshalJSON() ([]byte, error) {
|
||||||
|
var res []byte
|
||||||
|
var err error
|
||||||
|
actionForVariant(a.variant,
|
||||||
|
func() {
|
||||||
|
res, err = json.Marshal(a.tdx)
|
||||||
|
},
|
||||||
|
func() {
|
||||||
|
res, err = json.Marshal(a.snp)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiVersionObject) UnmarshalJSON(data []byte) error {
|
||||||
|
errTDX := json.Unmarshal(data, &a.tdx)
|
||||||
|
errSNP := json.Unmarshal(data, &a.snp)
|
||||||
|
if errTDX == nil || errSNP == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("trying to unmarshal data into both TDX and SNP versions: %w", errors.Join(errTDX, errSNP))
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONPath returns the path to the JSON file for the request to the config api.
|
// JSONPath returns the path to the JSON file for the request to the config api.
|
||||||
func (i reportedSEVSNPVersionAPI) JSONPath() string {
|
// This is the path to the cached version in the S3 bucket.
|
||||||
return path.Join(reportVersionDir(i.variant), i.Version)
|
func (a apiVersionObject) JSONPath() string {
|
||||||
|
if a.cached {
|
||||||
|
return path.Join(reportVersionDir(a.variant), a.version)
|
||||||
|
}
|
||||||
|
return path.Join(attestationconfigapi.AttestationURLPath, a.variant.String(), a.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRequest validates the request.
|
// ValidateRequest validates the request.
|
||||||
func (i reportedSEVSNPVersionAPI) ValidateRequest() error {
|
func (a apiVersionObject) ValidateRequest() error {
|
||||||
if !strings.HasSuffix(i.Version, ".json") {
|
if !strings.HasSuffix(a.version, ".json") {
|
||||||
return fmt.Errorf("version has no .json suffix")
|
return fmt.Errorf("version has no .json suffix")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate is a No-Op at the moment.
|
// Validate is a No-Op.
|
||||||
func (i reportedSEVSNPVersionAPI) Validate() error {
|
func (a apiVersionObject) Validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getVersion returns the version.
|
||||||
|
func (a apiVersionObject) getVersion() any {
|
||||||
|
var res any
|
||||||
|
actionForVariant(a.variant,
|
||||||
|
func() {
|
||||||
|
res = a.tdx
|
||||||
|
},
|
||||||
|
func() {
|
||||||
|
res = a.snp
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// setVersion sets the version.
|
||||||
|
func (a *apiVersionObject) setVersion(version any) {
|
||||||
|
actionForVariant(a.variant,
|
||||||
|
func() {
|
||||||
|
a.tdx = version.(attestationconfigapi.TDXVersion)
|
||||||
|
},
|
||||||
|
func() {
|
||||||
|
a.snp = version.(attestationconfigapi.SEVSNPVersion)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// actionForVariant performs the given action based on the whether variant is a TDX or SEV-SNP variant.
|
||||||
|
func actionForVariant(
|
||||||
|
attestationVariant variant.Variant,
|
||||||
|
tdxAction func(), snpAction func(),
|
||||||
|
) {
|
||||||
|
switch attestationVariant {
|
||||||
|
case variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.GCPSEVSNP{}:
|
||||||
|
snpAction()
|
||||||
|
case variant.AzureTDX{}:
|
||||||
|
tdxAction()
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsupported attestation variant: %s", attestationVariant))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isInputNewerThanOtherSEVSNPVersion compares all version fields and returns false if any input field is older, or the versions are equal.
|
||||||
|
func isInputNewerThanOtherSEVSNPVersion(input, other attestationconfigapi.SEVSNPVersion) bool {
|
||||||
|
if input == other {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if input.TEE < other.TEE {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if input.SNP < other.SNP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if input.Microcode < other.Microcode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if input.Bootloader < other.Bootloader {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isInputNewerThanOtherSEVSNPVersion compares all version fields and returns false if any input field is older, or the versions are equal.
|
||||||
|
func isInputNewerThanOtherTDXVersion(input, other attestationconfigapi.TDXVersion) bool {
|
||||||
|
if input == other {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.PCESVN < other.PCESVN {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if input.QESVN < other.QESVN {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate component-wise security version numbers
|
||||||
|
for idx, inputVersion := range input.TEETCBSVN {
|
||||||
|
if inputVersion < other.TEETCBSVN[idx] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
func TestIsInputNewerThanOtherSEVSNPVersion(t *testing.T) {
|
||||||
newTestCfg := func() attestationconfigapi.SEVSNPVersion {
|
newTestCfg := func() attestationconfigapi.SEVSNPVersion {
|
||||||
return attestationconfigapi.SEVSNPVersion{
|
return attestationconfigapi.SEVSNPVersion{
|
||||||
Microcode: 93,
|
Microcode: 93,
|
||||||
@ -25,7 +25,6 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
|||||||
latest attestationconfigapi.SEVSNPVersion
|
latest attestationconfigapi.SEVSNPVersion
|
||||||
input attestationconfigapi.SEVSNPVersion
|
input attestationconfigapi.SEVSNPVersion
|
||||||
expect bool
|
expect bool
|
||||||
errMsg string
|
|
||||||
}{
|
}{
|
||||||
"input is older than latest": {
|
"input is older than latest": {
|
||||||
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
||||||
@ -34,7 +33,6 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
|||||||
}(newTestCfg()),
|
}(newTestCfg()),
|
||||||
latest: newTestCfg(),
|
latest: newTestCfg(),
|
||||||
expect: false,
|
expect: false,
|
||||||
errMsg: "input Microcode version: 92 is older than latest API version: 93",
|
|
||||||
},
|
},
|
||||||
"input has greater and smaller version field than latest": {
|
"input has greater and smaller version field than latest": {
|
||||||
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
||||||
@ -44,7 +42,6 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
|||||||
}(newTestCfg()),
|
}(newTestCfg()),
|
||||||
latest: newTestCfg(),
|
latest: newTestCfg(),
|
||||||
expect: false,
|
expect: false,
|
||||||
errMsg: "input Bootloader version: 1 is older than latest API version: 2",
|
|
||||||
},
|
},
|
||||||
"input is newer than latest": {
|
"input is newer than latest": {
|
||||||
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
||||||
@ -62,14 +59,80 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for name, tc := range testCases {
|
for name, tc := range testCases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
isNewer, err := isInputNewerThanOtherVersion(tc.input, tc.latest)
|
isNewer := isInputNewerThanOtherSEVSNPVersion(tc.input, tc.latest)
|
||||||
assert := assert.New(t)
|
assert.Equal(t, tc.expect, isNewer)
|
||||||
if tc.errMsg != "" {
|
})
|
||||||
assert.EqualError(err, tc.errMsg)
|
}
|
||||||
} else {
|
}
|
||||||
assert.NoError(err)
|
|
||||||
assert.Equal(tc.expect, isNewer)
|
func TestIsInputNewerThanOtherTDXVersion(t *testing.T) {
|
||||||
}
|
newTestVersion := func() attestationconfigapi.TDXVersion {
|
||||||
|
return attestationconfigapi.TDXVersion{
|
||||||
|
QESVN: 1,
|
||||||
|
PCESVN: 2,
|
||||||
|
TEETCBSVN: [16]byte{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
|
||||||
|
QEVendorID: [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
|
||||||
|
XFAM: [8]byte{0, 1, 2, 3, 4, 5, 6, 7},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
latest attestationconfigapi.TDXVersion
|
||||||
|
input attestationconfigapi.TDXVersion
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
"input is older than latest": {
|
||||||
|
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||||
|
c.QESVN--
|
||||||
|
return c
|
||||||
|
}(newTestVersion()),
|
||||||
|
latest: newTestVersion(),
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
"input has greater and smaller version field than latest": {
|
||||||
|
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||||
|
c.QESVN++
|
||||||
|
c.PCESVN--
|
||||||
|
return c
|
||||||
|
}(newTestVersion()),
|
||||||
|
latest: newTestVersion(),
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
"input is newer than latest": {
|
||||||
|
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||||
|
c.QESVN++
|
||||||
|
return c
|
||||||
|
}(newTestVersion()),
|
||||||
|
latest: newTestVersion(),
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
"input is equal to latest": {
|
||||||
|
input: newTestVersion(),
|
||||||
|
latest: newTestVersion(),
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
"tee tcb svn is newer": {
|
||||||
|
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||||
|
c.TEETCBSVN[4]++
|
||||||
|
return c
|
||||||
|
}(newTestVersion()),
|
||||||
|
latest: newTestVersion(),
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
"xfam is different": {
|
||||||
|
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||||
|
c.XFAM[3]++
|
||||||
|
return c
|
||||||
|
}(newTestVersion()),
|
||||||
|
latest: newTestVersion(),
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
isNewer := isInputNewerThanOtherTDXVersion(tc.input, tc.latest)
|
||||||
|
assert.Equal(t, tc.expect, isNewer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli/client"
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli/client"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/staticupload"
|
"github.com/edgelesssys/constellation/v2/internal/staticupload"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -27,21 +26,21 @@ import (
|
|||||||
// newDeleteCmd creates the delete command.
|
// newDeleteCmd creates the delete command.
|
||||||
func newDeleteCmd() *cobra.Command {
|
func newDeleteCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "delete {aws|azure|gcp} {snp-report|guest-firmware} <version>",
|
Use: "delete {aws-sev-snp|azure-sev-snp|azure-tdx|gcp-sev-snp} {attestation-report|guest-firmware} <version>",
|
||||||
Short: "Delete an object from the attestationconfig API",
|
Short: "Delete an object from the attestationconfig API",
|
||||||
Long: "Delete a specific object version from the config api. <version> is the name of the object to delete (without .json suffix)",
|
Long: "Delete a specific object version from the config api. <version> is the name of the object to delete (without .json suffix)",
|
||||||
Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete azure snp-report 1.0.0",
|
Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete azure-sev-snp attestation-report 1.0.0",
|
||||||
Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)),
|
Args: cobra.MatchAll(cobra.ExactArgs(3), isAttestationVariant(0), isValidKind(1)),
|
||||||
PreRunE: envCheck,
|
PreRunE: envCheck,
|
||||||
RunE: runDelete,
|
RunE: runDelete,
|
||||||
}
|
}
|
||||||
|
|
||||||
recursivelyCmd := &cobra.Command{
|
recursivelyCmd := &cobra.Command{
|
||||||
Use: "recursive {aws|azure|gcp}",
|
Use: "recursive {aws-sev-snp|azure-sev-snp|azure-tdx|gcp-sev-snp}",
|
||||||
Short: "delete all objects from the API path constellation/v1/attestation/<csp>",
|
Short: "delete all objects from the API path constellation/v1/attestation/<csp>",
|
||||||
Long: "Delete all objects from the API path constellation/v1/attestation/<csp>",
|
Long: "Delete all objects from the API path constellation/v1/attestation/<csp>",
|
||||||
Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete recursive azure",
|
Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete recursive azure-sev-snp",
|
||||||
Args: cobra.MatchAll(cobra.ExactArgs(1), isCloudProvider(0)),
|
Args: cobra.MatchAll(cobra.ExactArgs(1), isAttestationVariant(0)),
|
||||||
RunE: runRecursiveDelete,
|
RunE: runRecursiveDelete,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,16 +74,7 @@ func runDelete(cmd *cobra.Command, args []string) (retErr error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
switch deleteCfg.provider {
|
return deleteEntry(cmd.Context(), client, deleteCfg)
|
||||||
case cloudprovider.AWS:
|
|
||||||
return deleteEntry(cmd.Context(), variant.AWSSEVSNP{}, client, deleteCfg)
|
|
||||||
case cloudprovider.Azure:
|
|
||||||
return deleteEntry(cmd.Context(), variant.AzureSEVSNP{}, client, deleteCfg)
|
|
||||||
case cloudprovider.GCP:
|
|
||||||
return deleteEntry(cmd.Context(), variant.GCPSEVSNP{}, client, deleteCfg)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) {
|
func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) {
|
||||||
@ -112,23 +102,13 @@ func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var deletePath string
|
deletePath := path.Join(attestationconfigapi.AttestationURLPath, deleteCfg.variant.String())
|
||||||
switch deleteCfg.provider {
|
|
||||||
case cloudprovider.AWS:
|
|
||||||
deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AWSSEVSNP{}.String())
|
|
||||||
case cloudprovider.Azure:
|
|
||||||
deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AzureSEVSNP{}.String())
|
|
||||||
case cloudprovider.GCP:
|
|
||||||
deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.GCPSEVSNP{}.String())
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
return deleteEntryRecursive(cmd.Context(), deletePath, client, deleteCfg)
|
return deleteEntryRecursive(cmd.Context(), deletePath, client, deleteCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
type deleteConfig struct {
|
type deleteConfig struct {
|
||||||
provider cloudprovider.Provider
|
variant variant.Variant
|
||||||
kind objectKind
|
kind objectKind
|
||||||
version string
|
version string
|
||||||
region string
|
region string
|
||||||
@ -155,12 +135,15 @@ func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) {
|
|||||||
}
|
}
|
||||||
apiCfg := getAPIEnvironment(testing)
|
apiCfg := getAPIEnvironment(testing)
|
||||||
|
|
||||||
provider := cloudprovider.FromString(args[0])
|
variant, err := variant.FromString(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return deleteConfig{}, fmt.Errorf("invalid attestation variant: %q: %w", args[0], err)
|
||||||
|
}
|
||||||
kind := kindFromString(args[1])
|
kind := kindFromString(args[1])
|
||||||
version := args[2]
|
version := args[2]
|
||||||
|
|
||||||
return deleteConfig{
|
return deleteConfig{
|
||||||
provider: provider,
|
variant: variant,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
version: version,
|
version: version,
|
||||||
region: region,
|
region: region,
|
||||||
@ -171,12 +154,12 @@ func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteEntry(ctx context.Context, attvar variant.Variant, client *client.Client, cfg deleteConfig) error {
|
func deleteEntry(ctx context.Context, client *client.Client, cfg deleteConfig) error {
|
||||||
if cfg.kind != snpReport {
|
if cfg.kind != attestationReport {
|
||||||
return fmt.Errorf("kind %s not supported", cfg.kind)
|
return fmt.Errorf("kind %s not supported", cfg.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.DeleteSEVSNPVersion(ctx, attvar, cfg.version)
|
return client.DeleteVersion(ctx, cfg.variant, cfg.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteEntryRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error {
|
func deleteEntryRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error {
|
||||||
|
@ -19,39 +19,67 @@ configapi_cli=$(realpath @@CONFIGAPI_CLI@@)
|
|||||||
stat "${configapi_cli}" >> /dev/null
|
stat "${configapi_cli}" >> /dev/null
|
||||||
configapi_cli="${configapi_cli} --testing"
|
configapi_cli="${configapi_cli} --testing"
|
||||||
###### script body ######
|
###### script body ######
|
||||||
function variant() {
|
attestationVariant=$1
|
||||||
if [[ $1 == "aws" ]]; then
|
readonly attestationVariant
|
||||||
echo "aws-sev-snp"
|
|
||||||
return 0
|
|
||||||
elif [[ $1 == "azure" ]]; then
|
|
||||||
echo "azure-sev-snp"
|
|
||||||
return 0
|
|
||||||
elif [[ $1 == "gcp" ]]; then
|
|
||||||
echo "gcp-sev-snp"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "Unknown CSP: $1"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
csp=$1
|
|
||||||
readonly csp
|
|
||||||
attestationType=$(variant "$csp")
|
|
||||||
|
|
||||||
readonly region="eu-west-1"
|
readonly region="eu-west-1"
|
||||||
readonly bucket="resource-api-testing"
|
readonly bucket="resource-api-testing"
|
||||||
|
|
||||||
tmpdir=$(mktemp -d)
|
tmpdir=$(mktemp -d)
|
||||||
readonly tmpdir
|
readonly tmpdir
|
||||||
registerExitHandler "rm -rf $tmpdir"
|
registerExitHandler "rm -rf ${tmpdir}"
|
||||||
|
|
||||||
# empty the bucket version state
|
# empty the bucket version state
|
||||||
${configapi_cli} delete recursive "$csp" --region "$region" --bucket "$bucket"
|
${configapi_cli} delete recursive "${attestationVariant}" --region "${region}" --bucket "${bucket}"
|
||||||
|
|
||||||
# the high version numbers ensure that it's newer than the current latest value
|
readonly current_report_path="${tmpdir}/attestationReportCurrent.json"
|
||||||
readonly current_report_path="$tmpdir/currentSnpReport.json"
|
readonly report_path="${tmpdir}/attestationReport.json"
|
||||||
cat << EOF > "$current_report_path"
|
readonly older_report_path="${tmpdir}/attestationReportOld.json"
|
||||||
|
|
||||||
|
if [[ ${attestationVariant} == *-tdx ]]; then
|
||||||
|
cat << EOF > "${current_report_path}"
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"qe_svn": "AAA=",
|
||||||
|
"pce_svn": "AAA=",
|
||||||
|
"qe_vendor_id": "KioqKioqKioqKioqKioqKg=="
|
||||||
|
},
|
||||||
|
"td_quote_body": {
|
||||||
|
"tee_tcb_svn": "AAAAAAAAAAAAAAAAAAAAAA==",
|
||||||
|
"xfam": "AAAAAAAAAAA="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
# the high version numbers ensure that it's newer than the current latest value
|
||||||
|
cat << EOF > "${report_path}"
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"qe_svn": "//8=",
|
||||||
|
"pce_svn": "//8=",
|
||||||
|
"qe_vendor_id": "KioqKioqKioqKioqKioqKg=="
|
||||||
|
},
|
||||||
|
"td_quote_body": {
|
||||||
|
"tee_tcb_svn": "/////////////////////w==",
|
||||||
|
"xfam": "AQIDBAUGBwg="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
# has an older version
|
||||||
|
cat << EOF > "${older_report_path}"
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"qe_svn": "//8=",
|
||||||
|
"pce_svn": "/v8=",
|
||||||
|
"qe_vendor_id": "KioqKioqKioqKioqKioqKg=="
|
||||||
|
},
|
||||||
|
"td_quote_body": {
|
||||||
|
"tee_tcb_svn": "/////////////////////g==",
|
||||||
|
"xfam": "AQIDBAUGBwg="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
elif [[ ${attestationVariant} == *-sev-snp ]]; then
|
||||||
|
cat << EOF > "${current_report_path}"
|
||||||
{
|
{
|
||||||
"snp_report": {
|
"snp_report": {
|
||||||
"reported_tcb": {
|
"reported_tcb": {
|
||||||
@ -75,90 +103,105 @@ cat << EOF > "$current_report_path"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
# the high version numbers ensure that it's newer than the current latest value
|
||||||
|
cat << EOF > "${report_path}"
|
||||||
|
{
|
||||||
|
"snp_report": {
|
||||||
|
"reported_tcb": {
|
||||||
|
"bootloader": 255,
|
||||||
|
"tee": 255,
|
||||||
|
"snp": 255,
|
||||||
|
"microcode": 255
|
||||||
|
},
|
||||||
|
"committed_tcb": {
|
||||||
|
"bootloader": 255,
|
||||||
|
"tee": 255,
|
||||||
|
"snp": 255,
|
||||||
|
"microcode": 255
|
||||||
|
},
|
||||||
|
"launch_tcb": {
|
||||||
|
"bootloader": 255,
|
||||||
|
"tee": 255,
|
||||||
|
"snp": 255,
|
||||||
|
"microcode": 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
# has an older version
|
||||||
|
cat << EOF > "${older_report_path}"
|
||||||
|
{
|
||||||
|
"snp_report": {
|
||||||
|
"reported_tcb": {
|
||||||
|
"bootloader": 255,
|
||||||
|
"tee": 255,
|
||||||
|
"snp": 255,
|
||||||
|
"microcode": 254
|
||||||
|
},
|
||||||
|
"committed_tcb": {
|
||||||
|
"bootloader": 255,
|
||||||
|
"tee": 255,
|
||||||
|
"snp": 255,
|
||||||
|
"microcode": 254
|
||||||
|
},
|
||||||
|
"launch_tcb": {
|
||||||
|
"bootloader": 255,
|
||||||
|
"tee": 255,
|
||||||
|
"snp": 255,
|
||||||
|
"microcode": 254
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
echo "Unknown attestation variant: ${attestationVariant}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# upload a fake latest version for the fetcher
|
# upload a fake latest version for the fetcher
|
||||||
${configapi_cli} upload "$csp" snp-report "$current_report_path" --force --upload-date "2000-01-01-01-01" --region "$region" --bucket "$bucket"
|
${configapi_cli} upload "${attestationVariant}" attestation-report "${current_report_path}" --force --upload-date "2000-01-01-01-01" --region "${region}" --bucket "${bucket}"
|
||||||
|
|
||||||
# the high version numbers ensure that it's newer than the current latest value
|
|
||||||
readonly report_path="$tmpdir/snpReport.json"
|
|
||||||
cat << EOF > "$report_path"
|
|
||||||
{
|
|
||||||
"snp_report": {
|
|
||||||
"reported_tcb": {
|
|
||||||
"bootloader": 255,
|
|
||||||
"tee": 255,
|
|
||||||
"snp": 255,
|
|
||||||
"microcode": 255
|
|
||||||
},
|
|
||||||
"committed_tcb": {
|
|
||||||
"bootloader": 255,
|
|
||||||
"tee": 255,
|
|
||||||
"snp": 255,
|
|
||||||
"microcode": 255
|
|
||||||
},
|
|
||||||
"launch_tcb": {
|
|
||||||
"bootloader": 255,
|
|
||||||
"tee": 255,
|
|
||||||
"snp": 255,
|
|
||||||
"microcode": 255
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# has an older version
|
|
||||||
readonly older_report_path="$tmpdir/snpReportOld.json"
|
|
||||||
cat << EOF > "$older_report_path"
|
|
||||||
{
|
|
||||||
"snp_report": {
|
|
||||||
"reported_tcb": {
|
|
||||||
"bootloader": 255,
|
|
||||||
"tee": 255,
|
|
||||||
"snp": 255,
|
|
||||||
"microcode": 254
|
|
||||||
},
|
|
||||||
"committed_tcb": {
|
|
||||||
"bootloader": 255,
|
|
||||||
"tee": 255,
|
|
||||||
"snp": 255,
|
|
||||||
"microcode": 254
|
|
||||||
},
|
|
||||||
"launch_tcb": {
|
|
||||||
"bootloader": 255,
|
|
||||||
"tee": 255,
|
|
||||||
"snp": 255,
|
|
||||||
"microcode": 254
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# report 3 versions with different dates to fill the reporter cache
|
# report 3 versions with different dates to fill the reporter cache
|
||||||
readonly date_oldest="2023-02-01-03-04"
|
readonly date_oldest="2023-02-01-03-04"
|
||||||
${configapi_cli} upload "$csp" snp-report "$older_report_path" --upload-date "$date_oldest" --region "$region" --bucket "$bucket" --cache-window-size 3
|
${configapi_cli} upload "${attestationVariant}" attestation-report "${older_report_path}" --upload-date "${date_oldest}" --region "${region}" --bucket "${bucket}" --cache-window-size 3
|
||||||
readonly date_older="2023-02-02-03-04"
|
readonly date_older="2023-02-02-03-04"
|
||||||
${configapi_cli} upload "$csp" snp-report "$older_report_path" --upload-date "$date_older" --region "$region" --bucket "$bucket" --cache-window-size 3
|
${configapi_cli} upload "${attestationVariant}" attestation-report "${older_report_path}" --upload-date "${date_older}" --region "${region}" --bucket "${bucket}" --cache-window-size 3
|
||||||
readonly date="2023-02-03-03-04"
|
readonly date="2023-02-03-03-04"
|
||||||
${configapi_cli} upload "$csp" snp-report "$report_path" --upload-date "$date" --region "$region" --bucket "$bucket" --cache-window-size 3
|
${configapi_cli} upload "${attestationVariant}" attestation-report "${report_path}" --upload-date "${date}" --region "${region}" --bucket "${bucket}" --cache-window-size 3
|
||||||
|
|
||||||
# expect that $date_oldest is served as latest version
|
# expect that $date_oldest is served as latest version
|
||||||
basepath="constellation/v1/attestation/${attestationType}"
|
basepath="constellation/v1/attestation/${attestationVariant}"
|
||||||
baseurl="https://d33dzgxuwsgbpw.cloudfront.net/${basepath}"
|
baseurl="https://d33dzgxuwsgbpw.cloudfront.net/${basepath}"
|
||||||
if ! curl -fsSL "${baseurl}"/${date_oldest}.json > version.json; then
|
if ! curl -fsSL "${baseurl}/${date_oldest}.json" > version.json; then
|
||||||
echo "Checking for uploaded version file ${basepath}/${date_oldest}.json: request returned ${?}"
|
echo "Checking for uploaded version file ${basepath}/${date_oldest}.json: request returned ${?}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# check that version values are equal to expected
|
|
||||||
if ! cmp -s <(echo -n '{"bootloader":255,"tee":255,"snp":255,"microcode":254}') version.json; then
|
if [[ ${attestationVariant} == *-tdx ]]; then
|
||||||
echo "The version content:"
|
# check that version values are equal to expected
|
||||||
cat version.json
|
if ! cmp -s <(echo -n '{"qeSVN":65535,"pceSVN":65534,"teeTCBSVN":[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254],"qeVendorID":[42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42],"xfam":[1,2,3,4,5,6,7,8]}') version.json; then
|
||||||
echo " is not equal to the expected version content:"
|
echo "The version content:"
|
||||||
echo '{"bootloader":255,"tee":255,"snp":255,"microcode":254}'
|
cat version.json
|
||||||
exit 1
|
echo " is not equal to the expected version content:"
|
||||||
|
echo '{"qeSVN":65535,"pceSVN":65534,"teeTCBSVN":[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254],"qeVendorID":[42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42],"xfam":[1,2,3,4,5,6,7,8]}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [[ ${attestationVariant} == *-sev-snp ]]; then
|
||||||
|
# check that version values are equal to expected
|
||||||
|
if ! cmp -s <(echo -n '{"bootloader":255,"tee":255,"snp":255,"microcode":254}') version.json; then
|
||||||
|
echo "The version content:"
|
||||||
|
cat version.json
|
||||||
|
echo " is not equal to the expected version content:"
|
||||||
|
echo '{"bootloader":255,"tee":255,"snp":255,"microcode":254}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if ! curl -fsSL "${baseurl}"/${date_oldest}.json.sig > /dev/null; then
|
|
||||||
|
if ! curl -fsSL "${baseurl}/${date_oldest}.json.sig" > /dev/null; then
|
||||||
echo "Checking for uploaded version signature file ${basepath}/${date_oldest}.json.sig: request returned ${?}"
|
echo "Checking for uploaded version signature file ${basepath}/${date_oldest}.json.sig: request returned ${?}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# check list endpoint
|
# check list endpoint
|
||||||
if ! curl -fsSL "${baseurl}"/list > list.json; then
|
if ! curl -fsSL "${baseurl}"/list > list.json; then
|
||||||
echo "Checking for uploaded list file ${basepath}/list: request returned ${?}"
|
echo "Checking for uploaded list file ${basepath}/list: request returned ${?}"
|
||||||
@ -174,28 +217,28 @@ if ! cmp -s <(echo -n '["2023-02-01-03-04.json","2000-01-01-01-01.json"]') list.
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# check that the other versions are not uploaded
|
# check that the other versions are not uploaded
|
||||||
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}"/${date_older}.json)
|
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}/${date_older}.json")
|
||||||
if [[ $http_code -ne 404 ]]; then
|
if [[ ${http_code} -ne 404 ]]; then
|
||||||
echo "Expected HTTP code 404 for: ${basepath}/${date_older}.json, but got ${http_code}"
|
echo "Expected HTTP code 404 for: ${basepath}/${date_older}.json, but got ${http_code}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}"/${date}.json.sig)
|
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}/${date}.json.sig")
|
||||||
if [[ $http_code -ne 404 ]]; then
|
if [[ ${http_code} -ne 404 ]]; then
|
||||||
echo "Expected HTTP code 404 for: ${basepath}/${date}.json, but got ${http_code}"
|
echo "Expected HTTP code 404 for: ${basepath}/${date}.json, but got ${http_code}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
${configapi_cli} delete "$csp" snp-report "$date_oldest" --region "$region" --bucket "$bucket"
|
${configapi_cli} delete "${attestationVariant}" attestation-report "${date_oldest}" --region "${region}" --bucket "${bucket}"
|
||||||
|
|
||||||
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail.
|
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail.
|
||||||
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}"/${date_oldest}.json)
|
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}/${date_oldest}.json")
|
||||||
if [[ $http_code -ne 404 ]]; then
|
if [[ ${http_code} -ne 404 ]]; then
|
||||||
echo "Expected HTTP code 404 for: ${basepath}/${date_oldest}.json, but got ${http_code}"
|
echo "Expected HTTP code 404 for: ${basepath}/${date_oldest}.json, but got ${http_code}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail.
|
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail.
|
||||||
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}"/${date_oldest}.json.sig)
|
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}/${date_oldest}.json.sig")
|
||||||
if [[ $http_code -ne 404 ]]; then
|
if [[ ${http_code} -ne 404 ]]; then
|
||||||
echo "Expected HTTP code 404 for: ${basepath}/${date_oldest}.json, but got ${http_code}"
|
echo "Expected HTTP code 404 for: ${basepath}/${date_oldest}.json, but got ${http_code}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
@ -7,6 +7,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@ -17,30 +18,30 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli/client"
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli/client"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/fetcher"
|
"github.com/edgelesssys/constellation/v2/internal/api/fetcher"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/staticupload"
|
"github.com/edgelesssys/constellation/v2/internal/staticupload"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/verify"
|
"github.com/edgelesssys/constellation/v2/internal/verify"
|
||||||
|
"github.com/google/go-tdx-guest/proto/tdx"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newUploadCmd() *cobra.Command {
|
func newUploadCmd() *cobra.Command {
|
||||||
uploadCmd := &cobra.Command{
|
uploadCmd := &cobra.Command{
|
||||||
Use: "upload {aws|azure|gcp} {snp-report|guest-firmware} <path>",
|
Use: "upload {aws-sev-snp|azure-sev-snp|azure-tdx|gcp-sev-snp} {attestation-report|guest-firmware} <path>",
|
||||||
Short: "Upload an object to the attestationconfig API",
|
Short: "Upload an object to the attestationconfig API",
|
||||||
|
|
||||||
Long: fmt.Sprintf("Upload a new object to the attestationconfig API. For snp-reports the new object is added to a cache folder first."+
|
Long: fmt.Sprintf("Upload a new object to the attestationconfig API. For snp-reports the new object is added to a cache folder first.\n"+
|
||||||
"The CLI then determines the lowest version within the cache-window present in the cache and writes that value to the config api if necessary. "+
|
"The CLI then determines the lowest version within the cache-window present in the cache and writes that value to the config api if necessary. "+
|
||||||
"For guest-firmware objects the object is added to the API directly. "+
|
"For guest-firmware objects the object is added to the API directly.\n"+
|
||||||
"Please authenticate with AWS through your preferred method (e.g. environment variables, CLI)"+
|
"Please authenticate with AWS through your preferred method (e.g. environment variables, CLI) "+
|
||||||
"to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.",
|
"to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.",
|
||||||
envCosignPrivateKey, envCosignPwd,
|
envCosignPrivateKey, envCosignPwd,
|
||||||
),
|
),
|
||||||
Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli upload azure snp-report /some/path/report.json",
|
Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli upload azure-sev-snp attestation-report /some/path/report.json",
|
||||||
|
|
||||||
Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)),
|
Args: cobra.MatchAll(cobra.ExactArgs(3), isAttestationVariant(0), isValidKind(1)),
|
||||||
PreRunE: envCheck,
|
PreRunE: envCheck,
|
||||||
RunE: runUpload,
|
RunE: runUpload,
|
||||||
}
|
}
|
||||||
@ -91,42 +92,19 @@ func runUpload(cmd *cobra.Command, args []string) (retErr error) {
|
|||||||
return fmt.Errorf("creating client: %w", err)
|
return fmt.Errorf("creating client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var attestation variant.Variant
|
return uploadReport(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
|
||||||
switch uploadCfg.provider {
|
|
||||||
case cloudprovider.AWS:
|
|
||||||
attestation = variant.AWSSEVSNP{}
|
|
||||||
case cloudprovider.Azure:
|
|
||||||
attestation = variant.AzureSEVSNP{}
|
|
||||||
case cloudprovider.GCP:
|
|
||||||
attestation = variant.GCPSEVSNP{}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
return uploadReport(ctx, attestation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadReport(ctx context.Context,
|
func uploadReport(
|
||||||
attestation variant.Variant,
|
ctx context.Context, apiClient *client.Client,
|
||||||
apiClient *client.Client,
|
cfg uploadConfig, fs file.Handler, log *slog.Logger,
|
||||||
cfg uploadConfig,
|
|
||||||
fs file.Handler,
|
|
||||||
log *slog.Logger,
|
|
||||||
) error {
|
) error {
|
||||||
if cfg.kind != snpReport {
|
if cfg.kind != attestationReport {
|
||||||
return fmt.Errorf("kind %s not supported", cfg.kind)
|
return fmt.Errorf("kind %s not supported", cfg.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info(fmt.Sprintf("Reading SNP report from file: %s", cfg.path))
|
apiFetcher := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey)
|
||||||
var report verify.Report
|
latestVersionInAPI, err := apiFetcher.FetchLatestVersion(ctx, cfg.variant)
|
||||||
if err := fs.ReadJSON(cfg.path, &report); err != nil {
|
|
||||||
return fmt.Errorf("reading snp report: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
inputVersion := convertTCBVersionToSNPVersion(report.SNPReport.LaunchTCB)
|
|
||||||
log.Info(fmt.Sprintf("Input report: %+v", inputVersion))
|
|
||||||
|
|
||||||
latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchLatestVersion(ctx, attestation)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var notFoundErr *fetcher.NotFoundError
|
var notFoundErr *fetcher.NotFoundError
|
||||||
if errors.As(err, ¬FoundErr) {
|
if errors.As(err, ¬FoundErr) {
|
||||||
@ -136,12 +114,39 @@ func uploadReport(ctx context.Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
latestAPIVersion := latestAPIVersionAPI.SEVSNPVersion
|
var newVersion, latestVersion any
|
||||||
if err := apiClient.UploadSEVSNPVersionLatest(ctx, attestation, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil {
|
switch cfg.variant {
|
||||||
if errors.Is(err, client.ErrNoNewerVersion) {
|
case variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.GCPSEVSNP{}:
|
||||||
log.Info(fmt.Sprintf("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion))
|
latestVersion = latestVersionInAPI.SEVSNPVersion
|
||||||
return nil
|
|
||||||
|
log.Info(fmt.Sprintf("Reading SNP report from file: %s", cfg.path))
|
||||||
|
var report verify.Report
|
||||||
|
if err := fs.ReadJSON(cfg.path, &report); err != nil {
|
||||||
|
return fmt.Errorf("reading snp report: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newVersion = convertTCBVersionToSNPVersion(report.SNPReport.LaunchTCB)
|
||||||
|
log.Info(fmt.Sprintf("Input SNP report: %+v", newVersion))
|
||||||
|
|
||||||
|
case variant.AzureTDX{}:
|
||||||
|
latestVersion = latestVersionInAPI.TDXVersion
|
||||||
|
|
||||||
|
log.Info(fmt.Sprintf("Reading TDX report from file: %s", cfg.path))
|
||||||
|
var report *tdx.QuoteV4
|
||||||
|
if err := fs.ReadJSON(cfg.path, &report); err != nil {
|
||||||
|
return fmt.Errorf("reading tdx report: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newVersion = convertQuoteToTDXVersion(report)
|
||||||
|
log.Info(fmt.Sprintf("Input TDX report: %+v", newVersion))
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("variant %s not supported", cfg.variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := apiClient.UploadLatestVersion(
|
||||||
|
ctx, cfg.variant, newVersion, latestVersion, cfg.uploadDate, cfg.force,
|
||||||
|
); err != nil && !errors.Is(err, client.ErrNoNewerVersion) {
|
||||||
return fmt.Errorf("updating latest version: %w", err)
|
return fmt.Errorf("updating latest version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,8 +162,18 @@ func convertTCBVersionToSNPVersion(tcb verify.TCBVersion) attestationconfigapi.S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertQuoteToTDXVersion(quote *tdx.QuoteV4) attestationconfigapi.TDXVersion {
|
||||||
|
return attestationconfigapi.TDXVersion{
|
||||||
|
QESVN: binary.LittleEndian.Uint16(quote.Header.QeSvn),
|
||||||
|
PCESVN: binary.LittleEndian.Uint16(quote.Header.PceSvn),
|
||||||
|
QEVendorID: [16]byte(quote.Header.QeVendorId),
|
||||||
|
XFAM: [8]byte(quote.TdQuoteBody.Xfam),
|
||||||
|
TEETCBSVN: [16]byte(quote.TdQuoteBody.TeeTcbSvn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type uploadConfig struct {
|
type uploadConfig struct {
|
||||||
provider cloudprovider.Provider
|
variant variant.Variant
|
||||||
kind objectKind
|
kind objectKind
|
||||||
path string
|
path string
|
||||||
uploadDate time.Time
|
uploadDate time.Time
|
||||||
@ -210,12 +225,16 @@ func newConfig(cmd *cobra.Command, args [3]string) (uploadConfig, error) {
|
|||||||
return uploadConfig{}, fmt.Errorf("getting cache window size: %w", err)
|
return uploadConfig{}, fmt.Errorf("getting cache window size: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := cloudprovider.FromString(args[0])
|
variant, err := variant.FromString(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return uploadConfig{}, fmt.Errorf("invalid attestation variant: %q: %w", args[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
kind := kindFromString(args[1])
|
kind := kindFromString(args[1])
|
||||||
path := args[2]
|
path := args[2]
|
||||||
|
|
||||||
return uploadConfig{
|
return uploadConfig{
|
||||||
provider: provider,
|
variant: variant,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
path: path,
|
path: path,
|
||||||
uploadDate: uploadDate,
|
uploadDate: uploadDate,
|
||||||
|
@ -10,16 +10,22 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isCloudProvider(arg int) cobra.PositionalArgs {
|
func isAttestationVariant(arg int) cobra.PositionalArgs {
|
||||||
return func(_ *cobra.Command, args []string) error {
|
return func(_ *cobra.Command, args []string) error {
|
||||||
if provider := cloudprovider.FromString(args[arg]); provider == cloudprovider.Unknown {
|
attestationVariant, err := variant.FromString(args[arg])
|
||||||
return fmt.Errorf("argument %s isn't a valid cloud provider", args[arg])
|
if err != nil {
|
||||||
|
return fmt.Errorf("argument %s isn't a valid attestation variant", args[arg])
|
||||||
|
}
|
||||||
|
switch attestationVariant {
|
||||||
|
case variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.AzureTDX{}, variant.GCPSEVSNP{}:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("argument %s isn't a supported attestation variant", args[arg])
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,16 +43,15 @@ type objectKind string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// unknown is the default objectKind and does nothing.
|
// unknown is the default objectKind and does nothing.
|
||||||
unknown objectKind = "unknown-kind"
|
unknown objectKind = "unknown-kind"
|
||||||
snpReport objectKind = "snp-report"
|
attestationReport objectKind = "attestation-report"
|
||||||
tdxReport objectKind = "tdx-report"
|
guestFirmware objectKind = "guest-firmware"
|
||||||
guestFirmware objectKind = "guest-firmware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func kindFromString(s string) objectKind {
|
func kindFromString(s string) objectKind {
|
||||||
lower := strings.ToLower(s)
|
lower := strings.ToLower(s)
|
||||||
switch objectKind(lower) {
|
switch objectKind(lower) {
|
||||||
case snpReport, guestFirmware, tdxReport:
|
case attestationReport, guestFirmware:
|
||||||
return objectKind(lower)
|
return objectKind(lower)
|
||||||
default:
|
default:
|
||||||
return unknown
|
return unknown
|
||||||
|
@ -20,7 +20,7 @@ const cosignPublicKey = constants.CosignPublicKeyReleases
|
|||||||
|
|
||||||
// Fetcher fetches config API resources without authentication.
|
// Fetcher fetches config API resources without authentication.
|
||||||
type Fetcher interface {
|
type Fetcher interface {
|
||||||
FetchLatestVersion(ctx context.Context, attestation variant.Variant) (VersionAPIEntry, error)
|
FetchLatestVersion(ctx context.Context, attestation variant.Variant) (Entry, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetcher fetches AttestationCfg API resources without authentication.
|
// fetcher fetches AttestationCfg API resources without authentication.
|
||||||
@ -60,10 +60,10 @@ func newFetcherWithClientAndVerifier(client apifetcher.HTTPClient, cosignVerifie
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FetchLatestVersion returns the latest versions of the given type.
|
// FetchLatestVersion returns the latest versions of the given type.
|
||||||
func (f *fetcher) FetchLatestVersion(ctx context.Context, variant variant.Variant) (VersionAPIEntry, error) {
|
func (f *fetcher) FetchLatestVersion(ctx context.Context, variant variant.Variant) (Entry, error) {
|
||||||
list, err := f.fetchVersionList(ctx, variant)
|
list, err := f.fetchVersionList(ctx, variant)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VersionAPIEntry{}, err
|
return Entry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// latest version is first in list
|
// latest version is first in list
|
||||||
@ -71,11 +71,10 @@ func (f *fetcher) FetchLatestVersion(ctx context.Context, variant variant.Varian
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetchVersionList fetches the version list information from the config API.
|
// fetchVersionList fetches the version list information from the config API.
|
||||||
func (f *fetcher) fetchVersionList(ctx context.Context, variant variant.Variant) (VersionList, error) {
|
func (f *fetcher) fetchVersionList(ctx context.Context, variant variant.Variant) (List, error) {
|
||||||
// TODO(derpsteb): Replace with FetchAndVerify once we move to v2 of the config API and the list is saved as (.json) file.
|
fetchedList, err := apifetcher.FetchAndVerify(ctx, f.HTTPClient, f.cdnURL, List{Variant: variant}, f.verifier)
|
||||||
fetchedList, err := apifetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, VersionList{Variant: variant})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VersionList{}, fmt.Errorf("fetching version list: %w", err)
|
return List{}, fmt.Errorf("fetching version list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the attestation variant of the list as it is not part of the marshalled JSON retrieved by Fetch
|
// Set the attestation variant of the list as it is not part of the marshalled JSON retrieved by Fetch
|
||||||
@ -85,14 +84,14 @@ func (f *fetcher) fetchVersionList(ctx context.Context, variant variant.Variant)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetchVersion fetches the version information from the config API.
|
// fetchVersion fetches the version information from the config API.
|
||||||
func (f *fetcher) fetchVersion(ctx context.Context, version string, variant variant.Variant) (VersionAPIEntry, error) {
|
func (f *fetcher) fetchVersion(ctx context.Context, version string, variant variant.Variant) (Entry, error) {
|
||||||
obj := VersionAPIEntry{
|
obj := Entry{
|
||||||
Version: version,
|
Version: version,
|
||||||
Variant: variant,
|
Variant: variant,
|
||||||
}
|
}
|
||||||
fetchedVersion, err := apifetcher.FetchAndVerify(ctx, f.HTTPClient, f.cdnURL, obj, f.verifier)
|
fetchedVersion, err := apifetcher.FetchAndVerify(ctx, f.HTTPClient, f.cdnURL, obj, f.verifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VersionAPIEntry{}, fmt.Errorf("fetching version %q: %w", version, err)
|
return Entry{}, fmt.Errorf("fetching version %q: %w", version, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the attestation variant of the list as it is not part of the marshalled JSON retrieved by FetchAndVerify
|
// Set the attestation variant of the list as it is not part of the marshalled JSON retrieved by FetchAndVerify
|
||||||
|
@ -22,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFetchLatestSEVSNPVersion(t *testing.T) {
|
func TestFetchLatestSEVSNPVersion(t *testing.T) {
|
||||||
latestVersionSNP := VersionAPIEntry{
|
latestVersionSNP := Entry{
|
||||||
SEVSNPVersion: SEVSNPVersion{
|
SEVSNPVersion: SEVSNPVersion{
|
||||||
Microcode: 93,
|
Microcode: 93,
|
||||||
TEE: 0,
|
TEE: 0,
|
||||||
@ -30,7 +30,7 @@ func TestFetchLatestSEVSNPVersion(t *testing.T) {
|
|||||||
Bootloader: 2,
|
Bootloader: 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
olderVersionSNP := VersionAPIEntry{
|
olderVersionSNP := Entry{
|
||||||
SEVSNPVersion: SEVSNPVersion{
|
SEVSNPVersion: SEVSNPVersion{
|
||||||
Microcode: 1,
|
Microcode: 1,
|
||||||
TEE: 0,
|
TEE: 0,
|
||||||
@ -38,7 +38,7 @@ func TestFetchLatestSEVSNPVersion(t *testing.T) {
|
|||||||
Bootloader: 1,
|
Bootloader: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
latestVersionTDX := VersionAPIEntry{
|
latestVersionTDX := Entry{
|
||||||
TDXVersion: TDXVersion{
|
TDXVersion: TDXVersion{
|
||||||
QESVN: 2,
|
QESVN: 2,
|
||||||
PCESVN: 3,
|
PCESVN: 3,
|
||||||
@ -47,7 +47,7 @@ func TestFetchLatestSEVSNPVersion(t *testing.T) {
|
|||||||
XFAM: [8]byte{6},
|
XFAM: [8]byte{6},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
olderVersionTDX := VersionAPIEntry{
|
olderVersionTDX := Entry{
|
||||||
TDXVersion: TDXVersion{
|
TDXVersion: TDXVersion{
|
||||||
QESVN: 1,
|
QESVN: 1,
|
||||||
PCESVN: 2,
|
PCESVN: 2,
|
||||||
@ -64,30 +64,30 @@ func TestFetchLatestSEVSNPVersion(t *testing.T) {
|
|||||||
timeAtTest time.Time
|
timeAtTest time.Time
|
||||||
wantErr bool
|
wantErr bool
|
||||||
attestation variant.Variant
|
attestation variant.Variant
|
||||||
expectedVersion VersionAPIEntry
|
expectedVersion Entry
|
||||||
olderVersion VersionAPIEntry
|
olderVersion Entry
|
||||||
latestVersion VersionAPIEntry
|
latestVersion Entry
|
||||||
}{
|
}{
|
||||||
"get latest version azure-sev-snp": {
|
"get latest version azure-sev-snp": {
|
||||||
fetcherVersions: []string{latestStr, olderStr},
|
fetcherVersions: []string{latestStr, olderStr},
|
||||||
attestation: variant.AzureSEVSNP{},
|
attestation: variant.AzureSEVSNP{},
|
||||||
expectedVersion: func() VersionAPIEntry { tmp := latestVersionSNP; tmp.Variant = variant.AzureSEVSNP{}; return tmp }(),
|
expectedVersion: func() Entry { tmp := latestVersionSNP; tmp.Variant = variant.AzureSEVSNP{}; return tmp }(),
|
||||||
olderVersion: func() VersionAPIEntry { tmp := olderVersionSNP; tmp.Variant = variant.AzureSEVSNP{}; return tmp }(),
|
olderVersion: func() Entry { tmp := olderVersionSNP; tmp.Variant = variant.AzureSEVSNP{}; return tmp }(),
|
||||||
latestVersion: func() VersionAPIEntry { tmp := latestVersionSNP; tmp.Variant = variant.AzureSEVSNP{}; return tmp }(),
|
latestVersion: func() Entry { tmp := latestVersionSNP; tmp.Variant = variant.AzureSEVSNP{}; return tmp }(),
|
||||||
},
|
},
|
||||||
"get latest version aws-sev-snp": {
|
"get latest version aws-sev-snp": {
|
||||||
fetcherVersions: []string{latestStr, olderStr},
|
fetcherVersions: []string{latestStr, olderStr},
|
||||||
attestation: variant.AWSSEVSNP{},
|
attestation: variant.AWSSEVSNP{},
|
||||||
expectedVersion: func() VersionAPIEntry { tmp := latestVersionSNP; tmp.Variant = variant.AWSSEVSNP{}; return tmp }(),
|
expectedVersion: func() Entry { tmp := latestVersionSNP; tmp.Variant = variant.AWSSEVSNP{}; return tmp }(),
|
||||||
olderVersion: func() VersionAPIEntry { tmp := olderVersionSNP; tmp.Variant = variant.AWSSEVSNP{}; return tmp }(),
|
olderVersion: func() Entry { tmp := olderVersionSNP; tmp.Variant = variant.AWSSEVSNP{}; return tmp }(),
|
||||||
latestVersion: func() VersionAPIEntry { tmp := latestVersionSNP; tmp.Variant = variant.AWSSEVSNP{}; return tmp }(),
|
latestVersion: func() Entry { tmp := latestVersionSNP; tmp.Variant = variant.AWSSEVSNP{}; return tmp }(),
|
||||||
},
|
},
|
||||||
"get latest version azure-tdx": {
|
"get latest version azure-tdx": {
|
||||||
fetcherVersions: []string{latestStr, olderStr},
|
fetcherVersions: []string{latestStr, olderStr},
|
||||||
attestation: variant.AzureTDX{},
|
attestation: variant.AzureTDX{},
|
||||||
expectedVersion: func() VersionAPIEntry { tmp := latestVersionTDX; tmp.Variant = variant.AzureTDX{}; return tmp }(),
|
expectedVersion: func() Entry { tmp := latestVersionTDX; tmp.Variant = variant.AzureTDX{}; return tmp }(),
|
||||||
olderVersion: func() VersionAPIEntry { tmp := olderVersionTDX; tmp.Variant = variant.AzureTDX{}; return tmp }(),
|
olderVersion: func() Entry { tmp := olderVersionTDX; tmp.Variant = variant.AzureTDX{}; return tmp }(),
|
||||||
latestVersion: func() VersionAPIEntry { tmp := latestVersionTDX; tmp.Variant = variant.AzureTDX{}; return tmp }(),
|
latestVersion: func() Entry { tmp := latestVersionTDX; tmp.Variant = variant.AzureTDX{}; return tmp }(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for name, tc := range testCases {
|
for name, tc := range testCases {
|
||||||
@ -119,14 +119,15 @@ type fakeConfigAPIHandler struct {
|
|||||||
attestation variant.Variant
|
attestation variant.Variant
|
||||||
versions []string
|
versions []string
|
||||||
latestDate string
|
latestDate string
|
||||||
latestVersion VersionAPIEntry
|
latestVersion Entry
|
||||||
olderDate string
|
olderDate string
|
||||||
olderVersion VersionAPIEntry
|
olderVersion Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoundTrip resolves the request and returns a dummy response.
|
// RoundTrip resolves the request and returns a dummy response.
|
||||||
func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/list", f.attestation.String()) {
|
switch req.URL.Path {
|
||||||
|
case fmt.Sprintf("/constellation/v1/attestation/%s/list", f.attestation.String()):
|
||||||
res := &http.Response{}
|
res := &http.Response{}
|
||||||
bt, err := json.Marshal(f.versions)
|
bt, err := json.Marshal(f.versions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -137,7 +138,14 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err
|
|||||||
res.Header.Set("Content-Type", "application/json")
|
res.Header.Set("Content-Type", "application/json")
|
||||||
res.StatusCode = http.StatusOK
|
res.StatusCode = http.StatusOK
|
||||||
return res, nil
|
return res, nil
|
||||||
} else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s", f.attestation.String(), f.latestDate) {
|
|
||||||
|
case fmt.Sprintf("/constellation/v1/attestation/%s/list.sig", f.attestation.String()):
|
||||||
|
res := &http.Response{}
|
||||||
|
res.Body = io.NopCloser(bytes.NewReader([]byte("null")))
|
||||||
|
res.StatusCode = http.StatusOK
|
||||||
|
return res, nil
|
||||||
|
|
||||||
|
case fmt.Sprintf("/constellation/v1/attestation/%s/%s", f.attestation.String(), f.latestDate):
|
||||||
res := &http.Response{}
|
res := &http.Response{}
|
||||||
bt, err := json.Marshal(f.latestVersion)
|
bt, err := json.Marshal(f.latestVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -147,7 +155,7 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err
|
|||||||
res.StatusCode = http.StatusOK
|
res.StatusCode = http.StatusOK
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
||||||
} else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s", f.attestation.String(), f.olderDate) {
|
case fmt.Sprintf("/constellation/v1/attestation/%s/%s", f.attestation.String(), f.olderDate):
|
||||||
res := &http.Response{}
|
res := &http.Response{}
|
||||||
bt, err := json.Marshal(f.olderVersion)
|
bt, err := json.Marshal(f.olderVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -156,13 +164,14 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err
|
|||||||
res.Body = io.NopCloser(bytes.NewReader(bt))
|
res.Body = io.NopCloser(bytes.NewReader(bt))
|
||||||
res.StatusCode = http.StatusOK
|
res.StatusCode = http.StatusOK
|
||||||
return res, nil
|
return res, nil
|
||||||
} else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s.sig", f.attestation.String(), f.latestDate) {
|
|
||||||
|
case fmt.Sprintf("/constellation/v1/attestation/%s/%s.sig", f.attestation.String(), f.latestDate):
|
||||||
res := &http.Response{}
|
res := &http.Response{}
|
||||||
res.Body = io.NopCloser(bytes.NewReader([]byte("null")))
|
res.Body = io.NopCloser(bytes.NewReader([]byte("null")))
|
||||||
res.StatusCode = http.StatusOK
|
res.StatusCode = http.StatusOK
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
||||||
} else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s.sig", f.attestation.String(), f.olderDate) {
|
case fmt.Sprintf("/constellation/v1/attestation/%s/%s.sig", f.attestation.String(), f.olderDate):
|
||||||
res := &http.Response{}
|
res := &http.Response{}
|
||||||
res.Body = io.NopCloser(bytes.NewReader([]byte("null")))
|
res.Body = io.NopCloser(bytes.NewReader([]byte("null")))
|
||||||
res.StatusCode = http.StatusOK
|
res.StatusCode = http.StatusOK
|
||||||
|
@ -45,12 +45,12 @@ type TDXVersion struct {
|
|||||||
XFAM [8]byte `json:"xfam"`
|
XFAM [8]byte `json:"xfam"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VersionAPIEntry is the request to get the version information of the specific version in the config api.
|
// Entry is the request to get the version information of the specific version in the config api.
|
||||||
//
|
//
|
||||||
// TODO: Because variant is not part of the marshalled JSON, fetcher and client methods need to fill the variant property.
|
// TODO: Because variant is not part of the marshalled JSON, fetcher and client methods need to fill the variant property.
|
||||||
// In API v2 we should embed the variant in the object and remove some code from fetcher & client.
|
// In API v2 we should embed the variant in the object and remove some code from fetcher & client.
|
||||||
// That would remove the possibility of some fetcher/client code forgetting to set the variant.
|
// That would remove the possibility of some fetcher/client code forgetting to set the variant.
|
||||||
type VersionAPIEntry struct {
|
type Entry struct {
|
||||||
Version string `json:"-"`
|
Version string `json:"-"`
|
||||||
Variant variant.Variant `json:"-"`
|
Variant variant.Variant `json:"-"`
|
||||||
SEVSNPVersion
|
SEVSNPVersion
|
||||||
@ -58,12 +58,12 @@ type VersionAPIEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// JSONPath returns the path to the JSON file for the request to the config api.
|
// JSONPath returns the path to the JSON file for the request to the config api.
|
||||||
func (i VersionAPIEntry) JSONPath() string {
|
func (i Entry) JSONPath() string {
|
||||||
return path.Join(AttestationURLPath, i.Variant.String(), i.Version)
|
return path.Join(AttestationURLPath, i.Variant.String(), i.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRequest validates the request.
|
// ValidateRequest validates the request.
|
||||||
func (i VersionAPIEntry) ValidateRequest() error {
|
func (i Entry) ValidateRequest() error {
|
||||||
if !strings.HasSuffix(i.Version, ".json") {
|
if !strings.HasSuffix(i.Version, ".json") {
|
||||||
return fmt.Errorf("version has no .json suffix")
|
return fmt.Errorf("version has no .json suffix")
|
||||||
}
|
}
|
||||||
@ -71,47 +71,47 @@ func (i VersionAPIEntry) ValidateRequest() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate is a No-Op at the moment.
|
// Validate is a No-Op at the moment.
|
||||||
func (i VersionAPIEntry) Validate() error {
|
func (i Entry) Validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VersionList is the request to retrieve of all versions in the API for one attestation variant.
|
// List is the request to retrieve of all versions in the API for one attestation variant.
|
||||||
//
|
//
|
||||||
// TODO: Because variant is not part of the marshalled JSON, fetcher and client methods need to fill the variant property.
|
// TODO: Because variant is not part of the marshalled JSON, fetcher and client methods need to fill the variant property.
|
||||||
// In API v2 we should embed the variant in the object and remove some code from fetcher & client.
|
// In API v2 we should embed the variant in the object and remove some code from fetcher & client.
|
||||||
// That would remove the possibility of some fetcher/client code forgetting to set the variant.
|
// That would remove the possibility of some fetcher/client code forgetting to set the variant.
|
||||||
type VersionList struct {
|
type List struct {
|
||||||
Variant variant.Variant
|
Variant variant.Variant
|
||||||
List []string
|
List []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON marshals the i's list property to JSON.
|
// MarshalJSON marshals the i's list property to JSON.
|
||||||
func (i VersionList) MarshalJSON() ([]byte, error) {
|
func (i List) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(i.List)
|
return json.Marshal(i.List)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON unmarshals a list of strings into i's list property.
|
// UnmarshalJSON unmarshals a list of strings into i's list property.
|
||||||
func (i *VersionList) UnmarshalJSON(data []byte) error {
|
func (i *List) UnmarshalJSON(data []byte) error {
|
||||||
return json.Unmarshal(data, &i.List)
|
return json.Unmarshal(data, &i.List)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONPath returns the path to the JSON file for the request to the config api.
|
// JSONPath returns the path to the JSON file for the request to the config api.
|
||||||
func (i VersionList) JSONPath() string {
|
func (i List) JSONPath() string {
|
||||||
return path.Join(AttestationURLPath, i.Variant.String(), "list")
|
return path.Join(AttestationURLPath, i.Variant.String(), "list")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRequest is a NoOp as there is no input.
|
// ValidateRequest is a NoOp as there is no input.
|
||||||
func (i VersionList) ValidateRequest() error {
|
func (i List) ValidateRequest() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortReverse sorts the list of versions in reverse order.
|
// SortReverse sorts the list of versions in reverse order.
|
||||||
func (i *VersionList) SortReverse() {
|
func (i *List) SortReverse() {
|
||||||
sort.Sort(sort.Reverse(sort.StringSlice(i.List)))
|
sort.Sort(sort.Reverse(sort.StringSlice(i.List)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddVersion adds new to i's list and sorts the element in descending order.
|
// AddVersion adds new to i's list and sorts the element in descending order.
|
||||||
func (i *VersionList) AddVersion(new string) {
|
func (i *List) AddVersion(new string) {
|
||||||
i.List = append(i.List, new)
|
i.List = append(i.List, new)
|
||||||
i.List = variant.RemoveDuplicate(i.List)
|
i.List = variant.RemoveDuplicate(i.List)
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ func (i *VersionList) AddVersion(new string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the response.
|
// Validate validates the response.
|
||||||
func (i VersionList) Validate() error {
|
func (i List) Validate() error {
|
||||||
if len(i.List) < 1 {
|
if len(i.List) < 1 {
|
||||||
return fmt.Errorf("no versions found in /list")
|
return fmt.Errorf("no versions found in /list")
|
||||||
}
|
}
|
||||||
|
@ -16,21 +16,21 @@ import (
|
|||||||
|
|
||||||
func TestVersionListMarshalUnmarshalJSON(t *testing.T) {
|
func TestVersionListMarshalUnmarshalJSON(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
input VersionList
|
input List
|
||||||
output VersionList
|
output List
|
||||||
wantDiff bool
|
wantDiff bool
|
||||||
}{
|
}{
|
||||||
"success": {
|
"success": {
|
||||||
input: VersionList{List: []string{"v1", "v2"}},
|
input: List{List: []string{"v1", "v2"}},
|
||||||
output: VersionList{List: []string{"v1", "v2"}},
|
output: List{List: []string{"v1", "v2"}},
|
||||||
},
|
},
|
||||||
"variant is lost": {
|
"variant is lost": {
|
||||||
input: VersionList{List: []string{"v1", "v2"}, Variant: variant.AzureSEVSNP{}},
|
input: List{List: []string{"v1", "v2"}, Variant: variant.AzureSEVSNP{}},
|
||||||
output: VersionList{List: []string{"v1", "v2"}},
|
output: List{List: []string{"v1", "v2"}},
|
||||||
},
|
},
|
||||||
"wrong order": {
|
"wrong order": {
|
||||||
input: VersionList{List: []string{"v1", "v2"}},
|
input: List{List: []string{"v1", "v2"}},
|
||||||
output: VersionList{List: []string{"v2", "v1"}},
|
output: List{List: []string{"v2", "v1"}},
|
||||||
wantDiff: true,
|
wantDiff: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -40,7 +40,7 @@ func TestVersionListMarshalUnmarshalJSON(t *testing.T) {
|
|||||||
inputRaw, err := tc.input.MarshalJSON()
|
inputRaw, err := tc.input.MarshalJSON()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var actual VersionList
|
var actual List
|
||||||
err = actual.UnmarshalJSON(inputRaw)
|
err = actual.UnmarshalJSON(inputRaw)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ func TestVersionListAddVersion(t *testing.T) {
|
|||||||
|
|
||||||
for name, tc := range tests {
|
for name, tc := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
v := VersionList{List: tc.versions}
|
v := List{List: tc.versions}
|
||||||
v.AddVersion(tc.new)
|
v.AddVersion(tc.new)
|
||||||
|
|
||||||
assert.Equal(t, tc.expected, v.List)
|
assert.Equal(t, tc.expected, v.List)
|
||||||
|
@ -53,7 +53,7 @@ type Client struct {
|
|||||||
dirtyPaths []string // written paths to be invalidated
|
dirtyPaths []string // written paths to be invalidated
|
||||||
DryRun bool // no write operations are performed
|
DryRun bool // no write operations are performed
|
||||||
|
|
||||||
Logger *slog.Logger
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReadOnlyClient creates a new read-only client.
|
// NewReadOnlyClient creates a new read-only client.
|
||||||
@ -77,7 +77,7 @@ func NewReadOnlyClient(ctx context.Context, region, bucket, distributionID strin
|
|||||||
s3ClientClose: staticUploadClientClose,
|
s3ClientClose: staticUploadClientClose,
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
DryRun: true,
|
DryRun: true,
|
||||||
Logger: log,
|
log: log,
|
||||||
}
|
}
|
||||||
clientClose := func(ctx context.Context) error {
|
clientClose := func(ctx context.Context) error {
|
||||||
return client.Close(ctx)
|
return client.Close(ctx)
|
||||||
@ -106,7 +106,7 @@ func NewClient(ctx context.Context, region, bucket, distributionID string, dryRu
|
|||||||
s3ClientClose: staticUploadClientClose,
|
s3ClientClose: staticUploadClientClose,
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
DryRun: dryRun,
|
DryRun: dryRun,
|
||||||
Logger: log,
|
log: log,
|
||||||
}
|
}
|
||||||
clientClose := func(ctx context.Context) error {
|
clientClose := func(ctx context.Context) error {
|
||||||
return client.Close(ctx)
|
return client.Close(ctx)
|
||||||
@ -119,7 +119,7 @@ func NewClient(ctx context.Context, region, bucket, distributionID string, dryRu
|
|||||||
// It invalidates the CDN cache for all uploaded files.
|
// It invalidates the CDN cache for all uploaded files.
|
||||||
func (c *Client) Close(ctx context.Context) error {
|
func (c *Client) Close(ctx context.Context) error {
|
||||||
if c.s3ClientClose == nil {
|
if c.s3ClientClose == nil {
|
||||||
c.Logger.Debug("Client has no s3ClientClose")
|
c.log.Debug("Client has no s3ClientClose")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return c.s3ClientClose(ctx)
|
return c.s3ClientClose(ctx)
|
||||||
@ -131,7 +131,7 @@ func (c *Client) DeletePath(ctx context.Context, path string) error {
|
|||||||
Bucket: &c.bucket,
|
Bucket: &c.bucket,
|
||||||
Prefix: &path,
|
Prefix: &path,
|
||||||
}
|
}
|
||||||
c.Logger.Debug(fmt.Sprintf("Listing objects in %q", path))
|
c.log.Debug(fmt.Sprintf("Listing objects in %q", path))
|
||||||
objs := []s3types.Object{}
|
objs := []s3types.Object{}
|
||||||
out := &s3.ListObjectsV2Output{IsTruncated: ptr(true)}
|
out := &s3.ListObjectsV2Output{IsTruncated: ptr(true)}
|
||||||
for out.IsTruncated != nil && *out.IsTruncated {
|
for out.IsTruncated != nil && *out.IsTruncated {
|
||||||
@ -142,10 +142,10 @@ func (c *Client) DeletePath(ctx context.Context, path string) error {
|
|||||||
}
|
}
|
||||||
objs = append(objs, out.Contents...)
|
objs = append(objs, out.Contents...)
|
||||||
}
|
}
|
||||||
c.Logger.Debug(fmt.Sprintf("Found %d objects in %q", len(objs), path))
|
c.log.Debug(fmt.Sprintf("Found %d objects in %q", len(objs), path))
|
||||||
|
|
||||||
if len(objs) == 0 {
|
if len(objs) == 0 {
|
||||||
c.Logger.Warn(fmt.Sprintf("Path %s is already empty", path))
|
c.log.Warn(fmt.Sprintf("Path %s is already empty", path))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ func (c *Client) DeletePath(ctx context.Context, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.DryRun {
|
if c.DryRun {
|
||||||
c.Logger.Debug(fmt.Sprintf("DryRun: Deleting %d objects with IDs %v", len(objs), objIDs))
|
c.log.Debug(fmt.Sprintf("DryRun: Deleting %d objects with IDs %v", len(objs), objIDs))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ func (c *Client) DeletePath(ctx context.Context, path string) error {
|
|||||||
Objects: objIDs,
|
Objects: objIDs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
c.Logger.Debug(fmt.Sprintf("Deleting %d objects in %q", len(objs), path))
|
c.log.Debug(fmt.Sprintf("Deleting %d objects in %q", len(objs), path))
|
||||||
if _, err := c.s3Client.DeleteObjects(ctx, deleteIn); err != nil {
|
if _, err := c.s3Client.DeleteObjects(ctx, deleteIn); err != nil {
|
||||||
return fmt.Errorf("deleting objects in %s: %w", path, err)
|
return fmt.Errorf("deleting objects in %s: %w", path, err)
|
||||||
}
|
}
|
||||||
@ -197,7 +197,7 @@ func Fetch[T APIObject](ctx context.Context, c *Client, obj T) (T, error) {
|
|||||||
Key: ptr(obj.JSONPath()),
|
Key: ptr(obj.JSONPath()),
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Logger.Debug(fmt.Sprintf("Fetching %T from s3: %q", obj, obj.JSONPath()))
|
c.log.Debug(fmt.Sprintf("Fetching %T from s3: %q", obj, obj.JSONPath()))
|
||||||
out, err := c.s3Client.GetObject(ctx, in)
|
out, err := c.s3Client.GetObject(ctx, in)
|
||||||
var noSuchkey *s3types.NoSuchKey
|
var noSuchkey *s3types.NoSuchKey
|
||||||
if errors.As(err, &noSuchkey) {
|
if errors.As(err, &noSuchkey) {
|
||||||
@ -231,7 +231,7 @@ func Update(ctx context.Context, c *Client, obj APIObject) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.DryRun {
|
if c.DryRun {
|
||||||
c.Logger.With(slog.String("bucket", c.bucket), slog.String("key", obj.JSONPath()), slog.String("body", string(rawJSON))).Debug("DryRun: s3 put object")
|
c.log.With(slog.String("bucket", c.bucket), slog.String("key", obj.JSONPath()), slog.String("body", string(rawJSON))).Debug("DryRun: s3 put object")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +243,7 @@ func Update(ctx context.Context, c *Client, obj APIObject) error {
|
|||||||
|
|
||||||
c.dirtyPaths = append(c.dirtyPaths, "/"+obj.JSONPath())
|
c.dirtyPaths = append(c.dirtyPaths, "/"+obj.JSONPath())
|
||||||
|
|
||||||
c.Logger.Debug(fmt.Sprintf("Uploading %T to s3: %q", obj, obj.JSONPath()))
|
c.log.Debug(fmt.Sprintf("Uploading %T to s3: %q", obj, obj.JSONPath()))
|
||||||
if _, err := c.Upload(ctx, in); err != nil {
|
if _, err := c.Upload(ctx, in); err != nil {
|
||||||
return fmt.Errorf("uploading %T: %w", obj, err)
|
return fmt.Errorf("uploading %T: %w", obj, err)
|
||||||
}
|
}
|
||||||
@ -306,7 +306,7 @@ func Delete(ctx context.Context, c *Client, obj APIObject) error {
|
|||||||
Key: ptr(obj.JSONPath()),
|
Key: ptr(obj.JSONPath()),
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Logger.Debug(fmt.Sprintf("Deleting %T from s3: %q", obj, obj.JSONPath()))
|
c.log.Debug(fmt.Sprintf("Deleting %T from s3: %q", obj, obj.JSONPath()))
|
||||||
if _, err := c.DeleteObject(ctx, in); err != nil {
|
if _, err := c.DeleteObject(ctx, in); err != nil {
|
||||||
return fmt.Errorf("deleting s3 object at %s: %w", obj.JSONPath(), err)
|
return fmt.Errorf("deleting s3 object at %s: %w", obj.JSONPath(), err)
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,10 @@ package fetcher
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||||
)
|
)
|
||||||
@ -145,7 +143,7 @@ type apiObject interface {
|
|||||||
// signature manages the signature of a object saved at location 'Signed'.
|
// signature manages the signature of a object saved at location 'Signed'.
|
||||||
type signature struct {
|
type signature struct {
|
||||||
// Signed is the object that is signed.
|
// Signed is the object that is signed.
|
||||||
Signed string `json:"-"`
|
Signed string `json:"signed"`
|
||||||
// Signature is the signature of `Signed`.
|
// Signature is the signature of `Signed`.
|
||||||
Signature []byte `json:"signature"`
|
Signature []byte `json:"signature"`
|
||||||
}
|
}
|
||||||
@ -155,12 +153,8 @@ func (s signature) JSONPath() string {
|
|||||||
return s.Signed + ".sig"
|
return s.Signed + ".sig"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRequest validates the request.
|
// ValidateRequest is a no-op.
|
||||||
func (s signature) ValidateRequest() error {
|
func (s signature) ValidateRequest() error {
|
||||||
if !strings.HasSuffix(s.Signed, ".json") {
|
|
||||||
return errors.New("signed object missing .json suffix")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,8 @@ import (
|
|||||||
type Client struct {
|
type Client struct {
|
||||||
*apiclient.Client
|
*apiclient.Client
|
||||||
clientClose func(ctx context.Context) error
|
clientClose func(ctx context.Context) error
|
||||||
|
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new client for the versions API.
|
// NewClient creates a new client for the versions API.
|
||||||
@ -31,8 +33,9 @@ func NewClient(ctx context.Context, region, bucket, distributionID string, dryRu
|
|||||||
) (*Client, CloseFunc, error) {
|
) (*Client, CloseFunc, error) {
|
||||||
genericClient, genericClientClose, err := apiclient.NewClient(ctx, region, bucket, distributionID, dryRun, log)
|
genericClient, genericClientClose, err := apiclient.NewClient(ctx, region, bucket, distributionID, dryRun, log)
|
||||||
versionsClient := &Client{
|
versionsClient := &Client{
|
||||||
genericClient,
|
Client: genericClient,
|
||||||
genericClientClose,
|
clientClose: genericClientClose,
|
||||||
|
log: log,
|
||||||
}
|
}
|
||||||
versionsClientClose := func(ctx context.Context) error {
|
versionsClientClose := func(ctx context.Context) error {
|
||||||
return versionsClient.Close(ctx)
|
return versionsClient.Close(ctx)
|
||||||
@ -50,8 +53,9 @@ func NewReadOnlyClient(ctx context.Context, region, bucket, distributionID strin
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
versionsClient := &Client{
|
versionsClient := &Client{
|
||||||
genericClient,
|
Client: genericClient,
|
||||||
genericClientClose,
|
clientClose: genericClientClose,
|
||||||
|
log: log,
|
||||||
}
|
}
|
||||||
versionsClientClose := func(ctx context.Context) error {
|
versionsClientClose := func(ctx context.Context) error {
|
||||||
return versionsClient.Close(ctx)
|
return versionsClient.Close(ctx)
|
||||||
@ -131,18 +135,18 @@ func (c *Client) DeleteRef(ctx context.Context, ref string) error {
|
|||||||
func (c *Client) DeleteVersion(ctx context.Context, ver Version) error {
|
func (c *Client) DeleteVersion(ctx context.Context, ver Version) error {
|
||||||
var retErr error
|
var retErr error
|
||||||
|
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Deleting version %q from minor version list", ver.version))
|
c.log.Debug(fmt.Sprintf("Deleting version %q from minor version list", ver.version))
|
||||||
possibleNewLatest, err := c.deleteVersionFromMinorVersionList(ctx, ver)
|
possibleNewLatest, err := c.deleteVersionFromMinorVersionList(ctx, ver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
retErr = errors.Join(retErr, fmt.Errorf("removing from minor version list: %w", err))
|
retErr = errors.Join(retErr, fmt.Errorf("removing from minor version list: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Checking latest version for %q", ver.version))
|
c.log.Debug(fmt.Sprintf("Checking latest version for %q", ver.version))
|
||||||
if err := c.deleteVersionFromLatest(ctx, ver, possibleNewLatest); err != nil {
|
if err := c.deleteVersionFromLatest(ctx, ver, possibleNewLatest); err != nil {
|
||||||
retErr = errors.Join(retErr, fmt.Errorf("updating latest version: %w", err))
|
retErr = errors.Join(retErr, fmt.Errorf("updating latest version: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Deleting artifact path %q for %q", ver.ArtifactPath(APIV1), ver.version))
|
c.log.Debug(fmt.Sprintf("Deleting artifact path %q for %q", ver.ArtifactPath(APIV1), ver.version))
|
||||||
if err := c.Client.DeletePath(ctx, ver.ArtifactPath(APIV1)); err != nil {
|
if err := c.Client.DeletePath(ctx, ver.ArtifactPath(APIV1)); err != nil {
|
||||||
retErr = errors.Join(retErr, fmt.Errorf("deleting artifact path: %w", err))
|
retErr = errors.Join(retErr, fmt.Errorf("deleting artifact path: %w", err))
|
||||||
}
|
}
|
||||||
@ -159,20 +163,20 @@ func (c *Client) deleteVersionFromMinorVersionList(ctx context.Context, ver Vers
|
|||||||
Base: ver.WithGranularity(GranularityMinor),
|
Base: ver.WithGranularity(GranularityMinor),
|
||||||
Kind: VersionKindImage,
|
Kind: VersionKindImage,
|
||||||
}
|
}
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Fetching minor version list for version %q", ver.version))
|
c.log.Debug(fmt.Sprintf("Fetching minor version list for version %q", ver.version))
|
||||||
minorList, err := c.FetchVersionList(ctx, minorList)
|
minorList, err := c.FetchVersionList(ctx, minorList)
|
||||||
var notFoundErr *apiclient.NotFoundError
|
var notFoundErr *apiclient.NotFoundError
|
||||||
if errors.As(err, ¬FoundErr) {
|
if errors.As(err, ¬FoundErr) {
|
||||||
c.Client.Logger.Warn(fmt.Sprintf("Minor version list for version %s not found", ver.version))
|
c.log.Warn(fmt.Sprintf("Minor version list for version %s not found", ver.version))
|
||||||
c.Client.Logger.Warn("Skipping update of minor version list")
|
c.log.Warn("Skipping update of minor version list")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, fmt.Errorf("fetching minor version list for version %s: %w", ver.version, err)
|
return nil, fmt.Errorf("fetching minor version list for version %s: %w", ver.version, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !minorList.Contains(ver.version) {
|
if !minorList.Contains(ver.version) {
|
||||||
c.Client.Logger.Warn(fmt.Sprintf("Version %s is not in minor version list %s", ver.version, minorList.JSONPath()))
|
c.log.Warn(fmt.Sprintf("Version %s is not in minor version list %s", ver.version, minorList.JSONPath()))
|
||||||
c.Client.Logger.Warn("Skipping update of minor version list")
|
c.log.Warn("Skipping update of minor version list")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,20 +196,20 @@ func (c *Client) deleteVersionFromMinorVersionList(ctx context.Context, ver Vers
|
|||||||
Kind: VersionKindImage,
|
Kind: VersionKindImage,
|
||||||
Version: minorList.Versions[len(minorList.Versions)-1],
|
Version: minorList.Versions[len(minorList.Versions)-1],
|
||||||
}
|
}
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Possible latest version replacement %q", latest.Version))
|
c.log.Debug(fmt.Sprintf("Possible latest version replacement %q", latest.Version))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Client.DryRun {
|
if c.Client.DryRun {
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("DryRun: Updating minor version list %q to %v", minorList.JSONPath(), minorList))
|
c.log.Debug(fmt.Sprintf("DryRun: Updating minor version list %q to %v", minorList.JSONPath(), minorList))
|
||||||
return latest, nil
|
return latest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Updating minor version list %q", minorList.JSONPath()))
|
c.log.Debug(fmt.Sprintf("Updating minor version list %q", minorList.JSONPath()))
|
||||||
if err := c.UpdateVersionList(ctx, minorList); err != nil {
|
if err := c.UpdateVersionList(ctx, minorList); err != nil {
|
||||||
return latest, fmt.Errorf("updating minor version list %s: %w", minorList.JSONPath(), err)
|
return latest, fmt.Errorf("updating minor version list %s: %w", minorList.JSONPath(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Removed version %q from minor version list %q", ver.version, minorList.JSONPath()))
|
c.log.Debug(fmt.Sprintf("Removed version %q from minor version list %q", ver.version, minorList.JSONPath()))
|
||||||
return latest, nil
|
return latest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,33 +220,33 @@ func (c *Client) deleteVersionFromLatest(ctx context.Context, ver Version, possi
|
|||||||
Stream: ver.stream,
|
Stream: ver.stream,
|
||||||
Kind: VersionKindImage,
|
Kind: VersionKindImage,
|
||||||
}
|
}
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Fetching latest version from %q", latest.JSONPath()))
|
c.log.Debug(fmt.Sprintf("Fetching latest version from %q", latest.JSONPath()))
|
||||||
latest, err := c.FetchVersionLatest(ctx, latest)
|
latest, err := c.FetchVersionLatest(ctx, latest)
|
||||||
var notFoundErr *apiclient.NotFoundError
|
var notFoundErr *apiclient.NotFoundError
|
||||||
if errors.As(err, ¬FoundErr) {
|
if errors.As(err, ¬FoundErr) {
|
||||||
c.Client.Logger.Warn(fmt.Sprintf("Latest version for %s not found", latest.JSONPath()))
|
c.log.Warn(fmt.Sprintf("Latest version for %s not found", latest.JSONPath()))
|
||||||
return nil
|
return nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("fetching latest version: %w", err)
|
return fmt.Errorf("fetching latest version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if latest.Version != ver.version {
|
if latest.Version != ver.version {
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Latest version is %q, not the deleted version %q", latest.Version, ver.version))
|
c.log.Debug(fmt.Sprintf("Latest version is %q, not the deleted version %q", latest.Version, ver.version))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if possibleNewLatest == nil {
|
if possibleNewLatest == nil {
|
||||||
c.Client.Logger.Error(fmt.Sprintf("Latest version is %s, but no new latest version was found", latest.Version))
|
c.log.Error(fmt.Sprintf("Latest version is %s, but no new latest version was found", latest.Version))
|
||||||
c.Client.Logger.Error(fmt.Sprintf("A manual update of latest at %s might be needed", latest.JSONPath()))
|
c.log.Error(fmt.Sprintf("A manual update of latest at %s might be needed", latest.JSONPath()))
|
||||||
return fmt.Errorf("latest version is %s, but no new latest version was found", latest.Version)
|
return fmt.Errorf("latest version is %s, but no new latest version was found", latest.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Client.DryRun {
|
if c.Client.DryRun {
|
||||||
c.Client.Logger.Debug(fmt.Sprintf("Would update latest version from %q to %q", latest.Version, possibleNewLatest.Version))
|
c.log.Debug(fmt.Sprintf("Would update latest version from %q to %q", latest.Version, possibleNewLatest.Version))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Client.Logger.Info(fmt.Sprintf("Updating latest version from %s to %s", latest.Version, possibleNewLatest.Version))
|
c.log.Info(fmt.Sprintf("Updating latest version from %s to %s", latest.Version, possibleNewLatest.Version))
|
||||||
if err := c.UpdateVersionLatest(ctx, *possibleNewLatest); err != nil {
|
if err := c.UpdateVersionLatest(ctx, *possibleNewLatest); err != nil {
|
||||||
return fmt.Errorf("updating latest version: %w", err)
|
return fmt.Errorf("updating latest version: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -1165,7 +1165,7 @@ type AzureTDX struct {
|
|||||||
// Expected 48 byte hex-encoded MR_SEAM value.
|
// Expected 48 byte hex-encoded MR_SEAM value.
|
||||||
MRSeam encoding.HexBytes `json:"mrSeam,omitempty" yaml:"mrSeam,omitempty"`
|
MRSeam encoding.HexBytes `json:"mrSeam,omitempty" yaml:"mrSeam,omitempty"`
|
||||||
// description: |
|
// description: |
|
||||||
// Expected 8 byte hex-encoded XFAM field.
|
// Expected 8 byte hex-encoded eXtended Features Available Mask (XFAM) field. Defaults to the latest available XFAM on Azure VMs. Unset to disable validation.
|
||||||
XFAM AttestationVersion[encoding.HexBytes] `json:"xfam" yaml:"xfam"`
|
XFAM AttestationVersion[encoding.HexBytes] `json:"xfam" yaml:"xfam"`
|
||||||
// description: |
|
// description: |
|
||||||
// Intel Root Key certificate used to verify the TDX certificate chain.
|
// Intel Root Key certificate used to verify the TDX certificate chain.
|
||||||
|
@ -545,22 +545,22 @@ func init() {
|
|||||||
GCPSEVSNPDoc.Fields[0].Description = "Expected TPM measurements."
|
GCPSEVSNPDoc.Fields[0].Description = "Expected TPM measurements."
|
||||||
GCPSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
|
GCPSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
|
||||||
GCPSEVSNPDoc.Fields[1].Name = "bootloaderVersion"
|
GCPSEVSNPDoc.Fields[1].Name = "bootloaderVersion"
|
||||||
GCPSEVSNPDoc.Fields[1].Type = "AttestationVersion"
|
GCPSEVSNPDoc.Fields[1].Type = ""
|
||||||
GCPSEVSNPDoc.Fields[1].Note = ""
|
GCPSEVSNPDoc.Fields[1].Note = ""
|
||||||
GCPSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version."
|
GCPSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version."
|
||||||
GCPSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version."
|
GCPSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version."
|
||||||
GCPSEVSNPDoc.Fields[2].Name = "teeVersion"
|
GCPSEVSNPDoc.Fields[2].Name = "teeVersion"
|
||||||
GCPSEVSNPDoc.Fields[2].Type = "AttestationVersion"
|
GCPSEVSNPDoc.Fields[2].Type = ""
|
||||||
GCPSEVSNPDoc.Fields[2].Note = ""
|
GCPSEVSNPDoc.Fields[2].Note = ""
|
||||||
GCPSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version."
|
GCPSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version."
|
||||||
GCPSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version."
|
GCPSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version."
|
||||||
GCPSEVSNPDoc.Fields[3].Name = "snpVersion"
|
GCPSEVSNPDoc.Fields[3].Name = "snpVersion"
|
||||||
GCPSEVSNPDoc.Fields[3].Type = "AttestationVersion"
|
GCPSEVSNPDoc.Fields[3].Type = ""
|
||||||
GCPSEVSNPDoc.Fields[3].Note = ""
|
GCPSEVSNPDoc.Fields[3].Note = ""
|
||||||
GCPSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version."
|
GCPSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version."
|
||||||
GCPSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version."
|
GCPSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version."
|
||||||
GCPSEVSNPDoc.Fields[4].Name = "microcodeVersion"
|
GCPSEVSNPDoc.Fields[4].Name = "microcodeVersion"
|
||||||
GCPSEVSNPDoc.Fields[4].Type = "AttestationVersion"
|
GCPSEVSNPDoc.Fields[4].Type = ""
|
||||||
GCPSEVSNPDoc.Fields[4].Note = ""
|
GCPSEVSNPDoc.Fields[4].Note = ""
|
||||||
GCPSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version."
|
GCPSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version."
|
||||||
GCPSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version."
|
GCPSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version."
|
||||||
@ -623,22 +623,22 @@ func init() {
|
|||||||
AWSSEVSNPDoc.Fields[0].Description = "Expected TPM measurements."
|
AWSSEVSNPDoc.Fields[0].Description = "Expected TPM measurements."
|
||||||
AWSSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
|
AWSSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
|
||||||
AWSSEVSNPDoc.Fields[1].Name = "bootloaderVersion"
|
AWSSEVSNPDoc.Fields[1].Name = "bootloaderVersion"
|
||||||
AWSSEVSNPDoc.Fields[1].Type = "AttestationVersion"
|
AWSSEVSNPDoc.Fields[1].Type = ""
|
||||||
AWSSEVSNPDoc.Fields[1].Note = ""
|
AWSSEVSNPDoc.Fields[1].Note = ""
|
||||||
AWSSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version."
|
AWSSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version."
|
||||||
AWSSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version."
|
AWSSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version."
|
||||||
AWSSEVSNPDoc.Fields[2].Name = "teeVersion"
|
AWSSEVSNPDoc.Fields[2].Name = "teeVersion"
|
||||||
AWSSEVSNPDoc.Fields[2].Type = "AttestationVersion"
|
AWSSEVSNPDoc.Fields[2].Type = ""
|
||||||
AWSSEVSNPDoc.Fields[2].Note = ""
|
AWSSEVSNPDoc.Fields[2].Note = ""
|
||||||
AWSSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version."
|
AWSSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version."
|
||||||
AWSSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version."
|
AWSSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version."
|
||||||
AWSSEVSNPDoc.Fields[3].Name = "snpVersion"
|
AWSSEVSNPDoc.Fields[3].Name = "snpVersion"
|
||||||
AWSSEVSNPDoc.Fields[3].Type = "AttestationVersion"
|
AWSSEVSNPDoc.Fields[3].Type = ""
|
||||||
AWSSEVSNPDoc.Fields[3].Note = ""
|
AWSSEVSNPDoc.Fields[3].Note = ""
|
||||||
AWSSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version."
|
AWSSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version."
|
||||||
AWSSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version."
|
AWSSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version."
|
||||||
AWSSEVSNPDoc.Fields[4].Name = "microcodeVersion"
|
AWSSEVSNPDoc.Fields[4].Name = "microcodeVersion"
|
||||||
AWSSEVSNPDoc.Fields[4].Type = "AttestationVersion"
|
AWSSEVSNPDoc.Fields[4].Type = ""
|
||||||
AWSSEVSNPDoc.Fields[4].Note = ""
|
AWSSEVSNPDoc.Fields[4].Note = ""
|
||||||
AWSSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version."
|
AWSSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version."
|
||||||
AWSSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version."
|
AWSSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version."
|
||||||
@ -685,22 +685,22 @@ func init() {
|
|||||||
AzureSEVSNPDoc.Fields[0].Description = "Expected TPM measurements."
|
AzureSEVSNPDoc.Fields[0].Description = "Expected TPM measurements."
|
||||||
AzureSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
|
AzureSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
|
||||||
AzureSEVSNPDoc.Fields[1].Name = "bootloaderVersion"
|
AzureSEVSNPDoc.Fields[1].Name = "bootloaderVersion"
|
||||||
AzureSEVSNPDoc.Fields[1].Type = "AttestationVersion"
|
AzureSEVSNPDoc.Fields[1].Type = ""
|
||||||
AzureSEVSNPDoc.Fields[1].Note = ""
|
AzureSEVSNPDoc.Fields[1].Note = ""
|
||||||
AzureSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version."
|
AzureSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version."
|
||||||
AzureSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version."
|
AzureSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version."
|
||||||
AzureSEVSNPDoc.Fields[2].Name = "teeVersion"
|
AzureSEVSNPDoc.Fields[2].Name = "teeVersion"
|
||||||
AzureSEVSNPDoc.Fields[2].Type = "AttestationVersion"
|
AzureSEVSNPDoc.Fields[2].Type = ""
|
||||||
AzureSEVSNPDoc.Fields[2].Note = ""
|
AzureSEVSNPDoc.Fields[2].Note = ""
|
||||||
AzureSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version."
|
AzureSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version."
|
||||||
AzureSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version."
|
AzureSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version."
|
||||||
AzureSEVSNPDoc.Fields[3].Name = "snpVersion"
|
AzureSEVSNPDoc.Fields[3].Name = "snpVersion"
|
||||||
AzureSEVSNPDoc.Fields[3].Type = "AttestationVersion"
|
AzureSEVSNPDoc.Fields[3].Type = ""
|
||||||
AzureSEVSNPDoc.Fields[3].Note = ""
|
AzureSEVSNPDoc.Fields[3].Note = ""
|
||||||
AzureSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version."
|
AzureSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version."
|
||||||
AzureSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version."
|
AzureSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version."
|
||||||
AzureSEVSNPDoc.Fields[4].Name = "microcodeVersion"
|
AzureSEVSNPDoc.Fields[4].Name = "microcodeVersion"
|
||||||
AzureSEVSNPDoc.Fields[4].Type = "AttestationVersion"
|
AzureSEVSNPDoc.Fields[4].Type = ""
|
||||||
AzureSEVSNPDoc.Fields[4].Note = ""
|
AzureSEVSNPDoc.Fields[4].Note = ""
|
||||||
AzureSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version."
|
AzureSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version."
|
||||||
AzureSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version."
|
AzureSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version."
|
||||||
@ -752,22 +752,22 @@ func init() {
|
|||||||
AzureTDXDoc.Fields[0].Description = "Expected TPM measurements."
|
AzureTDXDoc.Fields[0].Description = "Expected TPM measurements."
|
||||||
AzureTDXDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
|
AzureTDXDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
|
||||||
AzureTDXDoc.Fields[1].Name = "qeSVN"
|
AzureTDXDoc.Fields[1].Name = "qeSVN"
|
||||||
AzureTDXDoc.Fields[1].Type = "uint16"
|
AzureTDXDoc.Fields[1].Type = ""
|
||||||
AzureTDXDoc.Fields[1].Note = ""
|
AzureTDXDoc.Fields[1].Note = ""
|
||||||
AzureTDXDoc.Fields[1].Description = "Minimum required QE security version number (SVN)."
|
AzureTDXDoc.Fields[1].Description = "Minimum required QE security version number (SVN)."
|
||||||
AzureTDXDoc.Fields[1].Comments[encoder.LineComment] = "Minimum required QE security version number (SVN)."
|
AzureTDXDoc.Fields[1].Comments[encoder.LineComment] = "Minimum required QE security version number (SVN)."
|
||||||
AzureTDXDoc.Fields[2].Name = "pceSVN"
|
AzureTDXDoc.Fields[2].Name = "pceSVN"
|
||||||
AzureTDXDoc.Fields[2].Type = "uint16"
|
AzureTDXDoc.Fields[2].Type = ""
|
||||||
AzureTDXDoc.Fields[2].Note = ""
|
AzureTDXDoc.Fields[2].Note = ""
|
||||||
AzureTDXDoc.Fields[2].Description = "Minimum required PCE security version number (SVN)."
|
AzureTDXDoc.Fields[2].Description = "Minimum required PCE security version number (SVN)."
|
||||||
AzureTDXDoc.Fields[2].Comments[encoder.LineComment] = "Minimum required PCE security version number (SVN)."
|
AzureTDXDoc.Fields[2].Comments[encoder.LineComment] = "Minimum required PCE security version number (SVN)."
|
||||||
AzureTDXDoc.Fields[3].Name = "teeTCBSVN"
|
AzureTDXDoc.Fields[3].Name = "teeTCBSVN"
|
||||||
AzureTDXDoc.Fields[3].Type = "HexBytes"
|
AzureTDXDoc.Fields[3].Type = ""
|
||||||
AzureTDXDoc.Fields[3].Note = ""
|
AzureTDXDoc.Fields[3].Note = ""
|
||||||
AzureTDXDoc.Fields[3].Description = "Component-wise minimum required 16 byte hex-encoded TEE_TCB security version number (SVN)."
|
AzureTDXDoc.Fields[3].Description = "Component-wise minimum required 16 byte hex-encoded TEE_TCB security version number (SVN)."
|
||||||
AzureTDXDoc.Fields[3].Comments[encoder.LineComment] = "Component-wise minimum required 16 byte hex-encoded TEE_TCB security version number (SVN)."
|
AzureTDXDoc.Fields[3].Comments[encoder.LineComment] = "Component-wise minimum required 16 byte hex-encoded TEE_TCB security version number (SVN)."
|
||||||
AzureTDXDoc.Fields[4].Name = "qeVendorID"
|
AzureTDXDoc.Fields[4].Name = "qeVendorID"
|
||||||
AzureTDXDoc.Fields[4].Type = "HexBytes"
|
AzureTDXDoc.Fields[4].Type = ""
|
||||||
AzureTDXDoc.Fields[4].Note = ""
|
AzureTDXDoc.Fields[4].Note = ""
|
||||||
AzureTDXDoc.Fields[4].Description = "Expected 16 byte hex-encoded QE_VENDOR_ID field."
|
AzureTDXDoc.Fields[4].Description = "Expected 16 byte hex-encoded QE_VENDOR_ID field."
|
||||||
AzureTDXDoc.Fields[4].Comments[encoder.LineComment] = "Expected 16 byte hex-encoded QE_VENDOR_ID field."
|
AzureTDXDoc.Fields[4].Comments[encoder.LineComment] = "Expected 16 byte hex-encoded QE_VENDOR_ID field."
|
||||||
@ -777,10 +777,10 @@ func init() {
|
|||||||
AzureTDXDoc.Fields[5].Description = "Expected 48 byte hex-encoded MR_SEAM value."
|
AzureTDXDoc.Fields[5].Description = "Expected 48 byte hex-encoded MR_SEAM value."
|
||||||
AzureTDXDoc.Fields[5].Comments[encoder.LineComment] = "Expected 48 byte hex-encoded MR_SEAM value."
|
AzureTDXDoc.Fields[5].Comments[encoder.LineComment] = "Expected 48 byte hex-encoded MR_SEAM value."
|
||||||
AzureTDXDoc.Fields[6].Name = "xfam"
|
AzureTDXDoc.Fields[6].Name = "xfam"
|
||||||
AzureTDXDoc.Fields[6].Type = "HexBytes"
|
AzureTDXDoc.Fields[6].Type = ""
|
||||||
AzureTDXDoc.Fields[6].Note = ""
|
AzureTDXDoc.Fields[6].Note = ""
|
||||||
AzureTDXDoc.Fields[6].Description = "Expected 8 byte hex-encoded XFAM field."
|
AzureTDXDoc.Fields[6].Description = "Expected 8 byte hex-encoded eXtended Features Available Mask (XFAM) field. Defaults to the latest available XFAM on Azure VMs. Unset to disable validation."
|
||||||
AzureTDXDoc.Fields[6].Comments[encoder.LineComment] = "Expected 8 byte hex-encoded XFAM field."
|
AzureTDXDoc.Fields[6].Comments[encoder.LineComment] = "Expected 8 byte hex-encoded eXtended Features Available Mask (XFAM) field. Defaults to the latest available XFAM on Azure VMs. Unset to disable validation."
|
||||||
AzureTDXDoc.Fields[7].Name = "intelRootKey"
|
AzureTDXDoc.Fields[7].Name = "intelRootKey"
|
||||||
AzureTDXDoc.Fields[7].Type = "Certificate"
|
AzureTDXDoc.Fields[7].Type = "Certificate"
|
||||||
AzureTDXDoc.Fields[7].Note = ""
|
AzureTDXDoc.Fields[7].Note = ""
|
||||||
|
@ -1051,8 +1051,8 @@ func getConfigAsMap(conf *Config, t *testing.T) (res configMap) {
|
|||||||
|
|
||||||
type stubAttestationFetcher struct{}
|
type stubAttestationFetcher struct{}
|
||||||
|
|
||||||
func (f stubAttestationFetcher) FetchLatestVersion(_ context.Context, _ variant.Variant) (attestationconfigapi.VersionAPIEntry, error) {
|
func (f stubAttestationFetcher) FetchLatestVersion(_ context.Context, _ variant.Variant) (attestationconfigapi.Entry, error) {
|
||||||
return attestationconfigapi.VersionAPIEntry{
|
return attestationconfigapi.Entry{
|
||||||
SEVSNPVersion: testCfg,
|
SEVSNPVersion: testCfg,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -162,7 +162,7 @@ func (d *AttestationDataSource) Read(ctx context.Context, req datasource.ReadReq
|
|||||||
|
|
||||||
insecureFetch := data.Insecure.ValueBool()
|
insecureFetch := data.Insecure.ValueBool()
|
||||||
|
|
||||||
latestVersions := attestationconfigapi.VersionAPIEntry{}
|
latestVersions := attestationconfigapi.Entry{}
|
||||||
if attestationVariant.Equal(variant.AWSSEVSNP{}) ||
|
if attestationVariant.Equal(variant.AWSSEVSNP{}) ||
|
||||||
attestationVariant.Equal(variant.AzureSEVSNP{}) ||
|
attestationVariant.Equal(variant.AzureSEVSNP{}) ||
|
||||||
attestationVariant.Equal(variant.AzureTDX{}) ||
|
attestationVariant.Equal(variant.AzureTDX{}) ||
|
||||||
|
@ -138,7 +138,7 @@ func convertFromTfAttestationCfg(tfAttestation attestationAttribute, attestation
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convertToTfAttestationCfg converts the constellation attestation config to the related terraform structs.
|
// convertToTfAttestationCfg converts the constellation attestation config to the related terraform structs.
|
||||||
func convertToTfAttestation(attVar variant.Variant, latestVersions attestationconfigapi.VersionAPIEntry) (tfAttestation attestationAttribute, err error) {
|
func convertToTfAttestation(attVar variant.Variant, latestVersions attestationconfigapi.Entry) (tfAttestation attestationAttribute, err error) {
|
||||||
tfAttestation = attestationAttribute{
|
tfAttestation = attestationAttribute{
|
||||||
Variant: attVar.String(),
|
Variant: attVar.String(),
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user