Skip to content

Commit

Permalink
atls: try to attest with all validators
Browse files Browse the repository at this point in the history
Our aTLS code needs to be adjusted to allow for multiple attestation variants. While we did allow one to have multiple validators before, we tried to validate the first matching one and instantly errored out if validation for that validator failed. Now, we can have multiple matching validators and try all until one successfully validated the document, or until the list of applicable validators is exhausted, where we can return an error.
  • Loading branch information
msanft committed Jul 19, 2024
1 parent c864e45 commit 272953f
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 13 deletions.
4 changes: 2 additions & 2 deletions coordinator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

"github.com/edgelesssys/contrast/coordinator/history"
"github.com/edgelesssys/contrast/coordinator/internal/authority"
"github.com/edgelesssys/contrast/internal/attestation"
"github.com/edgelesssys/contrast/internal/atls"
"github.com/edgelesssys/contrast/internal/grpc/atlscredentials"
"github.com/edgelesssys/contrast/internal/logger"
"github.com/edgelesssys/contrast/internal/meshapi"
Expand Down Expand Up @@ -135,7 +135,7 @@ func newServerMetrics(reg *prometheus.Registry) *grpcprometheus.ServerMetrics {
}

func newGRPCServer(serverMetrics *grpcprometheus.ServerMetrics, log *slog.Logger) (*grpc.Server, error) {
issuer, err := attestation.PlatformIssuer(log)
issuer, err := atls.PlatformIssuer(log)
if err != nil {
return nil, fmt.Errorf("creating issuer: %w", err)
}
Expand Down
3 changes: 1 addition & 2 deletions initializer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"time"

"github.com/edgelesssys/contrast/internal/atls"
"github.com/edgelesssys/contrast/internal/attestation"
"github.com/edgelesssys/contrast/internal/grpc/dialer"
"github.com/edgelesssys/contrast/internal/logger"
"github.com/edgelesssys/contrast/internal/meshapi"
Expand Down Expand Up @@ -55,7 +54,7 @@ func run() (retErr error) {
return fmt.Errorf("generating key: %w", err)
}

issuer, err := attestation.PlatformIssuer(log)
issuer, err := atls.PlatformIssuer(log)
if err != nil {
return fmt.Errorf("creating issuer: %w", err)
}
Expand Down
55 changes: 49 additions & 6 deletions internal/atls/atls.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"math/big"
"time"

"github.com/edgelesssys/contrast/internal/attestation"
"github.com/edgelesssys/contrast/internal/crypto"
)

Expand All @@ -31,6 +32,11 @@ var (
NoValidator Validator
// NoIssuer skips embedding the client's attestation document.
NoIssuer Issuer

// ErrNoValidAttestationExtensions is returned when no valid attestation document certificate extensions are found.
ErrNoValidAttestationExtensions = errors.New("no valid attestation document certificate extensions found")
// ErrNoMatchingValidators is returned when no validator matches the attestation document.
ErrNoMatchingValidators = errors.New("no matching validators found")
)

// CreateAttestationServerTLSConfig creates a tls.Config object with a self-signed certificate and an embedded attestation document.
Expand Down Expand Up @@ -205,19 +211,56 @@ func processCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) (*x509.Certi
}

// verifyEmbeddedReport verifies an aTLS certificate by validating the attestation document embedded in the TLS certificate.
func verifyEmbeddedReport(validators []Validator, cert *x509.Certificate, peerPublicKey, nonce []byte) error {
//
// It will check against all applicable validator for the type of attestation document, and return success on the first match.
func verifyEmbeddedReport(validators []Validator, cert *x509.Certificate, peerPublicKey, nonce []byte) (retErr error) {
// For better error reporting, let's keep track of whether we've found a valid extension at all..
var foundExtension bool
// .. and whether we've found a matching validator.
var foundMatchingValidator bool

// We'll need to have a look at all extensions in the certificate to find the attestation document.
for _, ex := range cert.Extensions {
// Optimization: Skip the extension early before heading into the m*n complexity of the validator check
// if the extension is not an attestation document.
if !attestation.IsAttestationDocumentExtension(ex.Id) {
continue
}

// We have a valid attestation document. Let's check it against all applicable validators.
foundExtension = true
for _, validator := range validators {
if ex.Id.Equal(validator.OID()) {
ctx, cancel := context.WithTimeout(context.Background(), attestationTimeout)
defer cancel()
// Optimization: Skip the validator if it doesn't match the attestation type of the document.
if !ex.Id.Equal(validator.OID()) {
continue
}

// We've found a matching validator. Let's validate the document.
foundMatchingValidator = true

return validator.Validate(ctx, ex.Value, nonce, peerPublicKey)
ctx, cancel := context.WithTimeout(context.Background(), attestationTimeout)
defer cancel()

validationErr := validator.Validate(ctx, ex.Value, nonce, peerPublicKey)
if validationErr == nil {
// The validator has successfully verified the document. We can exit.
return nil
}
// Otherwise, we'll keep track of the error and continue with the next validator.
retErr = errors.Join(retErr, fmt.Errorf("validator %s failed: %w", validator.OID(), validationErr))
}
}

return errors.New("certificate does not contain attestation document")
if !foundExtension {
return ErrNoValidAttestationExtensions
}

if !foundMatchingValidator {
return ErrNoMatchingValidators
}

// If we're here, an error must've happened during validation.
return retErr
}

// encodeNonceToCertPool returns a cert pool that contains a certificate whose CN is the base64-encoded nonce.
Expand Down
131 changes: 131 additions & 0 deletions internal/atls/atls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2024 Edgeless Systems GmbH
// SPDX-License-Identifier: AGPL-3.0-only

package atls

import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/json"
"testing"

"github.com/edgelesssys/contrast/internal/oid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestVerifyEmbeddedReport(t *testing.T) {
fakeAttDoc := FakeAttestationDoc{}
attDocBytes, err := json.Marshal(fakeAttDoc)
assert.NoError(t, err)

testCases := map[string]struct {
cert *x509.Certificate
validators []Validator
wantErr bool
targetErr error
}{
"success": {
cert: &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: oid.RawTDXReport,
},
{
Id: oid.RawSNPReport,
Value: attDocBytes,
},
},
},
validators: NewFakeValidators(stubSNPValidator{}),
},
"multiple matches": {
cert: &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: oid.RawSNPReport,
Value: []byte("foo"),
},
{
Id: oid.RawSNPReport,
Value: attDocBytes,
},
},
},
validators: NewFakeValidators(stubSNPValidator{}),
},
"skip non-matching validator": {
cert: &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: []int{4, 5, 6},
},
{
Id: oid.RawSNPReport,
Value: attDocBytes,
},
},
},
validators: append(NewFakeValidators(stubSNPValidator{}), NewFakeValidator(stubFooValidator{})),
},
"match, error": {
cert: &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: oid.RawSNPReport,
Value: []byte("foo"),
},
},
},
validators: NewFakeValidators(stubSNPValidator{}),
wantErr: true,
},
"no extensions": {
cert: &x509.Certificate{},
validators: nil,
targetErr: ErrNoValidAttestationExtensions,
wantErr: true,
},
"no matching validator": {
cert: &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: oid.RawSNPReport,
},
},
},
validators: nil,
targetErr: ErrNoMatchingValidators,
wantErr: true,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
err := verifyEmbeddedReport(tc.validators, tc.cert, nil, nil)
if tc.wantErr {
require.Error(err)
if tc.targetErr != nil {
assert.ErrorIs(err, tc.targetErr)
}
} else {
require.NoError(err)
}
})
}
}

type stubSNPValidator struct{}

func (v stubSNPValidator) OID() asn1.ObjectIdentifier {
return oid.RawSNPReport
}

type stubFooValidator struct{}

func (v stubFooValidator) OID() asn1.ObjectIdentifier {
return []int{1, 2, 3}
}
5 changes: 2 additions & 3 deletions internal/attestation/issuer.go → internal/atls/issuer.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
// Copyright 2024 Edgeless Systems GmbH
// SPDX-License-Identifier: AGPL-3.0-only

package attestation
package atls

import (
"fmt"
"log/slog"

"github.com/edgelesssys/contrast/internal/atls"
"github.com/edgelesssys/contrast/internal/attestation/snp"
"github.com/edgelesssys/contrast/internal/attestation/tdx"
"github.com/edgelesssys/contrast/internal/logger"
"github.com/klauspost/cpuid/v2"
)

// PlatformIssuer creates an attestation issuer for the current platform.
func PlatformIssuer(log *slog.Logger) (atls.Issuer, error) {
func PlatformIssuer(log *slog.Logger) (Issuer, error) {
cpuid.Detect()
switch {
case cpuid.CPU.Supports(cpuid.SEV_SNP):
Expand Down
16 changes: 16 additions & 0 deletions internal/attestation/oid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2024 Edgeless Systems GmbH
// SPDX-License-Identifier: AGPL-3.0-only

package attestation

import (
"encoding/asn1"

oids "github.com/edgelesssys/contrast/internal/oid"
)

// IsAttestationDocumentExtension checks whether the given OID corresponds to an attestation document extension
// supported by Contrast (i.e. TDX or SNP).
func IsAttestationDocumentExtension(oid asn1.ObjectIdentifier) bool {
return oid.Equal(oids.RawTDXReport) || oid.Equal(oids.RawSNPReport)
}

0 comments on commit 272953f

Please sign in to comment.