Skip to content

Commit

Permalink
Retire usage of era in CLI functions (#627)
Browse files Browse the repository at this point in the history
* Adapt and move era code to MarbleRun directly
* Fix info message when using latest era version from GitHub
* Better error message if not manifest signature received

---------

Signed-off-by: Daniel Weiße <[email protected]>
  • Loading branch information
daniel-weisse authored Apr 23, 2024
1 parent faf2625 commit a152112
Show file tree
Hide file tree
Showing 7 changed files with 548 additions and 9 deletions.
5 changes: 5 additions & 0 deletions cli/internal/cmd/manifestVerify.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ func cliManifestVerify(cmd *cobra.Command, localSignature string, client getter)
return err
}
remoteSignature := gjson.GetBytes(resp, "ManifestSignature").String()

if remoteSignature == "" {
return errors.New("Coordinator returned no manifest signature. Is the Coordinator in the correct state?")
}

if remoteSignature != localSignature {
return fmt.Errorf("remote signature differs from local signature: %s != %s", remoteSignature, localSignature)
}
Expand Down
24 changes: 20 additions & 4 deletions cli/internal/rest/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
Expand All @@ -21,8 +22,8 @@ import (
"net/url"
"os"

"github.com/edgelesssys/era/era"
"github.com/edgelesssys/marblerun/cli/internal/kube"
"github.com/edgelesssys/marblerun/internal/attestation"
"github.com/edgelesssys/marblerun/internal/tcb"
"github.com/tidwall/gjson"
"k8s.io/client-go/tools/clientcmd"
Expand Down Expand Up @@ -161,7 +162,8 @@ func VerifyCoordinator(
// skip verification if specified
if insecure {
fmt.Fprintln(out, "Warning: skipping quote verification")
return era.InsecureGetCertificate(host)
certs, _, err := attestation.InsecureGetCertificate(ctx, host)
return certs, err
}

if configFilename == "" {
Expand All @@ -176,7 +178,17 @@ func VerifyCoordinator(
}
}

pemBlock, tcbStatus, err := era.GetCertificate(host, configFilename)
eraCfgRaw, err := os.ReadFile(configFilename)
if err != nil {
return nil, fmt.Errorf("reading era config file: %w", err)
}

var eraCfg attestation.Config
if err := json.Unmarshal(eraCfgRaw, &eraCfg); err != nil {
return nil, fmt.Errorf("unmarshalling era config: %w", err)
}

pemBlock, tcbStatus, _, err := attestation.GetCertificate(ctx, host, nil, eraCfg)
validity, err := tcb.CheckStatus(tcbStatus, err, acceptedTCBStatuses)
if err != nil {
return nil, err
Expand Down Expand Up @@ -227,6 +239,10 @@ func fetchLatestCoordinatorConfiguration(ctx context.Context, out io.Writer, k8s
return fmt.Errorf("writing era config file: %w", err)
}

fmt.Fprintf(out, "Got era config for version %s\n", coordinatorVersion)
if coordinatorVersion != "" {
fmt.Fprintf(out, "Got era config for version %s\n", coordinatorVersion)
} else {
fmt.Fprintln(out, "Got latest era config")
}
return nil
}
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ go 1.21
require (
github.com/cert-manager/cert-manager v1.14.4
github.com/edgelesssys/ego v1.5.0
github.com/edgelesssys/era v0.3.3
github.com/gofrs/flock v0.8.1
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@ github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arX
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/edgelesssys/ego v1.5.0 h1:euwXc69GRGlxpklIaVZtyh0v27YXzf9ow3iODE7CrPc=
github.com/edgelesssys/ego v1.5.0/go.mod h1:N58b0J+s3U4sxXeNUT5uiQV9Q9M/U2KsILC44Ku5dnw=
github.com/edgelesssys/era v0.3.3 h1:EyZvkR/WJceMaV+15lt+CYI0h6mC8pUOFklbbKT8KwQ=
github.com/edgelesssys/era v0.3.3/go.mod h1:yGYrnt8vxxFUsbMQCMwEBItNK9vNqS0f9YONvr78lBY=
github.com/emicklei/go-restful/v3 v3.11.3 h1:yagOQz/38xJmcNeZJtrUcKjkHRltIaIFXKWeG1SkWGE=
github.com/emicklei/go-restful/v3 v3.11.3/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
Expand Down
203 changes: 203 additions & 0 deletions internal/attestation/attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright (c) Edgeless Systems GmbH.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package attestation

import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/url"

"github.com/edgelesssys/ego/attestation"
"github.com/edgelesssys/ego/attestation/tcbstatus"
"github.com/edgelesssys/ego/eclient"
"github.com/tidwall/gjson"
)

// Config is the expected attestation metadata of a MarbleRun Coordinator enclave.
// It is used to verify the Coordinator's remote attestation report.
// At minimum, either UniqueID or the tuple of SignerID, ProductID, and SecurityVersion must be provided.
type Config struct {
SecurityVersion uint `json:"SecurityVersion"`
UniqueID string `json:"UniqueID"`
SignerID string `json:"SignerID"`
ProductID uint16 `json:"ProductID"`
Debug bool `json:"Debug"`
}

// ErrEmptyQuote defines an error type when no quote was received. This likely occurs when the host is running in OE Simulation mode.
var ErrEmptyQuote = errors.New("no quote received")

// GetCertificate gets the Coordinator's TLS certificate using remote attestation.
// A config with the expected attestation metadata must be provided.
// An optional nonce may be provided to force the Coordinator to generate a new quote for this request.
// It returns the verified certificate chain in PEM format, the TCB status of the enclave, the quote, and an error, if any.
func GetCertificate(ctx context.Context, host string, nonce []byte, config Config) ([]*pem.Block, tcbstatus.Status, []byte, error) {
return getCertificate(ctx, host, nonce, config, eclient.VerifyRemoteReport)
}

// InsecureGetCertificate gets the Coordinator's TLS certificate, but does not perform remote attestation.
func InsecureGetCertificate(ctx context.Context, host string) ([]*pem.Block, []byte, error) {
certs, _, quote, err := getCertificate(ctx, host, nil, Config{}, nil)
return certs, quote, err
}

type verifyFunc func([]byte) (attestation.Report, error)

func getCertificate(ctx context.Context, host string, nonce []byte, config Config, verifyRemoteReport verifyFunc) ([]*pem.Block, tcbstatus.Status, []byte, error) {
cert, quote, err := httpGetCertQuote(ctx, host, nonce)
if err != nil {
return nil, tcbstatus.Unknown, nil, err
}

var certs []*pem.Block
block, rest := pem.Decode([]byte(cert))
if block == nil {
return nil, tcbstatus.Unknown, nil, errors.New("could not parse certificate")
}
certs = append(certs, block)

// If we get more than one certificate, append it to the slice
for len(rest) > 0 {
block, rest = pem.Decode(rest)
if block == nil {
return nil, tcbstatus.Unknown, nil, errors.New("could not parse certificate chain")
}
certs = append(certs, block)
}

if verifyRemoteReport == nil {
return certs, tcbstatus.Unknown, quote, nil
}

if len(quote) == 0 {
return nil, tcbstatus.Unknown, quote, ErrEmptyQuote
}

report, verifyErr := verifyRemoteReport(quote)
if verifyErr != nil && verifyErr != attestation.ErrTCBLevelInvalid {
return nil, tcbstatus.Unknown, quote, verifyErr
}

// Use Root CA (last entry in certs) for attestation
certRaw := certs[len(certs)-1].Bytes

if err := verifyReport(report, certRaw, nonce, config); err != nil {
return nil, tcbstatus.Unknown, quote, err
}

return certs, report.TCBStatus, quote, verifyErr
}

// verifyReport checks the attestation report against the provided configuration.
// The reports quote must match the hash of the certificate and (optional) nonce.
func verifyReport(report attestation.Report, cert, nonce []byte, cfg Config) error {
hash := sha256.Sum256(append(cert, nonce...))
if !bytes.Equal(report.Data[:len(hash)], hash[:]) {
return errors.New("report data does not match the certificate's hash")
}

if cfg.UniqueID == "" {
if cfg.SecurityVersion == 0 {
return errors.New("missing SecurityVersion in config")
}
if cfg.ProductID == 0 {
return errors.New("missing ProductID in config")
}
}

if cfg.SecurityVersion != 0 && report.SecurityVersion < cfg.SecurityVersion {
return errors.New("invalid SecurityVersion")
}
if cfg.ProductID != 0 && binary.LittleEndian.Uint16(report.ProductID) != cfg.ProductID {
return errors.New("invalid ProductID")
}
if report.Debug && !cfg.Debug {
return errors.New("debug enclave not allowed")
}
if err := verifyID(cfg.UniqueID, report.UniqueID, "UniqueID"); err != nil {
return err
}
if err := verifyID(cfg.SignerID, report.SignerID, "SignerID"); err != nil {
return err
}
if cfg.UniqueID == "" && cfg.SignerID == "" {
fmt.Println("Warning: Configuration contains neither UniqueID nor SignerID!")
}

return nil
}

func verifyID(expected string, actual []byte, name string) error {
if expected == "" {
return nil
}
expectedBytes, err := hex.DecodeString(expected)
if err != nil {
return err
}
if !bytes.Equal(expectedBytes, actual) {
return errors.New("invalid " + name)
}
return nil
}

// httpGetCertQuote requests the Coordinator's quote and certificate chain.
func httpGetCertQuote(ctx context.Context, host string, nonce []byte) (string, []byte, error) {
client := http.Client{
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
}

url := url.URL{Scheme: "https", Host: host, Path: "quote"}
if len(nonce) > 0 {
url.Query().Add("nonce", base64.URLEncoding.EncodeToString(nonce))
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
return "", nil, err
}

resp, err := client.Do(req)
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}

if resp.StatusCode != http.StatusOK {
errorMessage := gjson.GetBytes(body, "message")
if errorMessage.Exists() {
return "", nil, errors.New(resp.Status + ": " + errorMessage.String())
}
return "", nil, errors.New(resp.Status + ": " + string(body))
}

var certQuote certQuoteResp
if err := json.Unmarshal([]byte(gjson.GetBytes(body, "data").String()), &certQuote); err != nil {
return "", nil, err
}
return certQuote.Cert, certQuote.Quote, nil
}

type certQuoteResp struct {
Cert string
Quote []byte
}
Loading

0 comments on commit a152112

Please sign in to comment.