diff --git a/internal/attestation/insecure/insecure_test.go b/internal/attestation/insecure/insecure_test.go new file mode 100644 index 00000000000..ce321f53b70 --- /dev/null +++ b/internal/attestation/insecure/insecure_test.go @@ -0,0 +1,60 @@ +// Copyright 2026 Edgeless Systems GmbH +// SPDX-License-Identifier: BUSL-1.1 + +package insecure + +import ( + "context" + "log/slog" + "net/http" + "net/http/httptest" + "testing" + + "github.com/edgelesssys/contrast/internal/attestation" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIssueAndValidate(t *testing.T) { + hostData := []byte("hostdata") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/hostdata", r.URL.Path) + _, err := w.Write(hostData) + assert.NoError(t, err) + })) + defer server.Close() + + issuer := &Issuer{hostdataURL: server.URL + "/hostdata", client: server.Client()} + var reportData [64]byte + copy(reportData[:], []byte("report-data")) + + attDoc, err := issuer.Issue(context.Background(), reportData) + require.NoError(t, err) + + setter := &stubReportSetter{} + validator := NewValidatorWithReportSetter(slog.Default(), setter, "insecure") + require.NoError(t, validator.Validate(context.Background(), attDoc, reportData[:])) + require.NotNil(t, setter.report) + assert.Equal(t, hostData, setter.report.HostData()) +} + +func TestValidateMismatchingReportData(t *testing.T) { + validator := NewValidator(slog.Default(), "insecure") + attDoc := []byte(`{"reportData":"AQ==","hostData":"Ag=="}`) + + err := validator.Validate(context.Background(), attDoc, []byte{0x02}) + require.Error(t, err) + assert.Contains(t, err.Error(), "reportData mismatch") +} + +func TestAttestationDocumentOID(t *testing.T) { + assert.True(t, attestation.IsAttestationDocumentExtension(NewIssuer().OID())) +} + +type stubReportSetter struct { + report attestation.Report +} + +func (s *stubReportSetter) SetReport(report attestation.Report) { + s.report = report +} diff --git a/internal/attestation/insecure/issuer.go b/internal/attestation/insecure/issuer.go new file mode 100644 index 00000000000..364342b06c4 --- /dev/null +++ b/internal/attestation/insecure/issuer.go @@ -0,0 +1,74 @@ +// Copyright 2025 Edgeless Systems GmbH +// SPDX-License-Identifier: BUSL-1.1 + +// Package insecure provides a fake aTLS issuer and validator for development +// platforms without confidential computing hardware. +package insecure + +import ( + "context" + "encoding/asn1" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/edgelesssys/contrast/internal/oid" +) + +// HostdataAddr is the address where the initdata-processor serves the +// hostdata digest on insecure platforms. +const HostdataAddr = "127.0.0.1:19629" + +// HostdataURL is the full URL for fetching the hostdata digest. +const HostdataURL = "http://" + HostdataAddr + "/hostdata" + +// Issuer issues fake attestation documents for insecure (non-CC) platforms. +// +// It fetches the initdata digest from the local initdata-processor HTTP server +// and packages it with the report data into a JSON attestation document. +type Issuer struct { + hostdataURL string + client *http.Client +} + +// NewIssuer creates a new insecure issuer. +func NewIssuer() *Issuer { + return &Issuer{hostdataURL: HostdataURL, client: http.DefaultClient} +} + +// OID returns the OID for the insecure attestation. +func (i *Issuer) OID() asn1.ObjectIdentifier { + return oid.RawInsecureReport +} + +// Issue creates a fake attestation document containing the report data and +// the initdata digest fetched from the local hostdata server. +func (i *Issuer) Issue(ctx context.Context, reportData [64]byte) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, i.hostdataURL, nil) + if err != nil { + return nil, fmt.Errorf("creating hostdata request: %w", err) + } + resp, err := i.client.Do(req) + if err != nil { + return nil, fmt.Errorf("fetching hostdata from %q: %w", i.hostdataURL, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("fetching hostdata: status %s", resp.Status) + } + hostData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading hostdata response: %w", err) + } + return json.Marshal(attestationDoc{ + ReportData: reportData[:], + HostData: hostData, + }) +} + +// attestationDoc is the fake attestation document exchanged between issuer and validator. +type attestationDoc struct { + ReportData []byte `json:"reportData"` + HostData []byte `json:"hostData"` +} diff --git a/internal/attestation/insecure/validator.go b/internal/attestation/insecure/validator.go new file mode 100644 index 00000000000..2d06a203953 --- /dev/null +++ b/internal/attestation/insecure/validator.go @@ -0,0 +1,74 @@ +// Copyright 2025 Edgeless Systems GmbH +// SPDX-License-Identifier: BUSL-1.1 + +package insecure + +import ( + "bytes" + "context" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/json" + "fmt" + "log/slog" + + "github.com/edgelesssys/contrast/internal/attestation" + "github.com/edgelesssys/contrast/internal/oid" +) + +// Validator validates fake attestation documents from insecure (non-CC) platforms. +type Validator struct { + reportSetter attestation.ReportSetter + logger *slog.Logger + name string +} + +// NewValidator creates a new insecure validator. +func NewValidator(log *slog.Logger, name string) *Validator { + return &Validator{logger: log, name: name} +} + +// NewValidatorWithReportSetter creates a new insecure validator with a report setter callback. +func NewValidatorWithReportSetter(log *slog.Logger, reportSetter attestation.ReportSetter, name string) *Validator { + return &Validator{reportSetter: reportSetter, logger: log, name: name} +} + +// OID returns the OID for the insecure attestation. +func (v *Validator) OID() asn1.ObjectIdentifier { + return oid.RawInsecureReport +} + +// Validate verifies the fake attestation document and extracts the host data. +func (v *Validator) Validate(_ context.Context, attDocRaw []byte, reportData []byte) error { + var doc attestationDoc + if err := json.Unmarshal(attDocRaw, &doc); err != nil { + return fmt.Errorf("unmarshaling insecure attestation: %w", err) + } + if !bytes.Equal(doc.ReportData, reportData) { + return fmt.Errorf("reportData mismatch: expected %x, got %x", reportData, doc.ReportData) + } + if v.reportSetter != nil { + v.reportSetter.SetReport(report{hostData: doc.HostData}) + } + return nil +} + +// String returns the validator's name. +func (v *Validator) String() string { + return v.name +} + +// report implements the [attestation.Report] interface for insecure platforms. +type report struct { + hostData []byte +} + +// HostData returns the initdata digest. +func (r report) HostData() []byte { + return r.hostData +} + +// ClaimsToCertExtension returns no extensions for insecure platforms. +func (r report) ClaimsToCertExtension() ([]pkix.Extension, error) { + return nil, nil +} diff --git a/internal/attestation/oid.go b/internal/attestation/oid.go index 76151f88e5f..2e58fa830c0 100644 --- a/internal/attestation/oid.go +++ b/internal/attestation/oid.go @@ -10,7 +10,7 @@ import ( ) // IsAttestationDocumentExtension checks whether the given OID corresponds to an attestation document extension -// supported by Contrast (i.e. TDX or SNP). +// supported by Contrast (i.e. TDX, SNP, or insecure). func IsAttestationDocumentExtension(oid asn1.ObjectIdentifier) bool { - return oid.Equal(oids.RawTDXReport) || oid.Equal(oids.RawSNPReport) + return oid.Equal(oids.RawTDXReport) || oid.Equal(oids.RawSNPReport) || oid.Equal(oids.RawInsecureReport) } diff --git a/internal/oid/oid.go b/internal/oid/oid.go index 60598d2f15b..69ce1d8ec81 100644 --- a/internal/oid/oid.go +++ b/internal/oid/oid.go @@ -13,6 +13,10 @@ var RawSNPReport = asn1.ObjectIdentifier{1, 3, 9901, 2, 1} // used by the aTLS issuer and validator. var RawTDXReport = asn1.ObjectIdentifier{1, 3, 9901, 2, 2} +// RawInsecureReport is the OID for the insecure (non-CC) attestation, +// used on development platforms without CC hardware. +var RawInsecureReport = asn1.ObjectIdentifier{1, 3, 9901, 2, 99} + // WorkloadSecretOID is the root OID for the workloadSecretID report // extension, added to the mesh certificates to allow verification // and authorization based on the workloadSecretID.