Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let Coordinator serve monotonic counters to Marbles #741

Merged
merged 4 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"os"

"github.com/edgelesssys/ego/attestation/tcbstatus"
"github.com/edgelesssys/marblerun/api/attestation"
"github.com/edgelesssys/marblerun/api/rest"
"github.com/edgelesssys/marblerun/coordinator/manifest"
apiv2 "github.com/edgelesssys/marblerun/coordinator/server/v2"
"github.com/edgelesssys/marblerun/internal/constants"
"github.com/edgelesssys/marblerun/util"
"github.com/spf13/afero"
)
Expand Down Expand Up @@ -385,6 +388,41 @@ func SecretSet(ctx context.Context, endpoint string, trustedRoot *x509.Certifica
return nil
}

// SetMonotonicCounter increases a monotonic counter managed by the Coordinator.
//
// This function can only be called by a Marble. The counter is bound to the Marble's type and UUID.
//
// If the passed value is greater than the counter's value, it is set as the new value and the old value is returned.
// Otherwise, the value is not changed and the current value is returned.
func SetMonotonicCounter(ctx context.Context, endpoint string, name string, value uint64) (uint64, error) {
marbleKeyPair, trustedRoot, err := getMarbleCredentialsFromEnv()
if err != nil {
return 0, fmt.Errorf("getting credentials from secure environment: %w", err)
}

client, err := rest.NewClient(endpoint, trustedRoot, &marbleKeyPair)
if err != nil {
return 0, fmt.Errorf("setting up client: %w", err)
}

reqBody, err := json.Marshal(apiv2.MonotonicCounterRequest{Name: name, Value: value})
if err != nil {
return 0, fmt.Errorf("marshalling request: %w", err)
}

resp, err := client.Post(ctx, rest.V2API+rest.MonotonicCounterEndpoint, rest.ContentJSON, bytes.NewReader(reqBody))
if err != nil {
return 0, fmt.Errorf("sending request: %w", err)
}

var response apiv2.MonotonicCounterResponse
if err := json.Unmarshal(resp, &response); err != nil {
return 0, fmt.Errorf("unmarshalling Coordinator response: %w", err)
}

return response.Value, nil
}

// SignQuote sends an SGX quote to a Coordinator for signing.
// If the quote is valid, the Coordinator will sign the quote using its root ECDSA key, and return the signature with the TCB status of the quote.
// The Coordinator does not verify if the quote matches any packages in the configured manifest.
Expand Down Expand Up @@ -500,3 +538,42 @@ func verifyOptionsFromConfig(fs afero.Afero, configPath string) (VerifyOptions,
err = json.Unmarshal(optsRaw, &opts)
return opts, err
}

func getByteEnv(name string) ([]byte, error) {
value := os.Getenv(name)
if len(value) == 0 {
return nil, fmt.Errorf("environment variable not set: %s", name)
}
return []byte(value), nil
}

func getMarbleCredentialsFromEnv() (tls.Certificate, *x509.Certificate, error) {
certChain, err := getByteEnv(constants.MarbleEnvironmentCertificateChain)
if err != nil {
return tls.Certificate{}, nil, err
}
rootCA, err := getByteEnv(constants.MarbleEnvironmentCoordinatorRootCA)
if err != nil {
return tls.Certificate{}, nil, err
}
leafPrivk, err := getByteEnv(constants.MarbleEnvironmentPrivateKey)
if err != nil {
return tls.Certificate{}, nil, err
}

block, _ := pem.Decode(rootCA)
if block == nil {
return tls.Certificate{}, nil, errors.New("decoding Coordinator root certificate failed")
}
coordinatorRoot, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return tls.Certificate{}, nil, fmt.Errorf("parsing Coordinator root certificate: %w", err)
}

tlsCert, err := tls.X509KeyPair(certChain, leafPrivk)
if err != nil {
return tls.Certificate{}, nil, fmt.Errorf("creating TLS key pair: %w", err)
}

return tlsCert, coordinatorRoot, nil
}
25 changes: 13 additions & 12 deletions api/rest/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,19 @@ import (

// Endpoints of the MarbleRun Coordinator REST API.
const (
ManifestEndpoint = "manifest"
UpdateEndpoint = "update"
UpdateCancelEndpoint = "update-cancel"
UpdateStatusEndpoint = "update-manifest"
QuoteEndpoint = "quote"
RecoverEndpoint = "recover"
SecretEndpoint = "secrets"
StatusEndpoint = "status"
SignQuoteEndpoint = "sign-quote"
V2API = "/api/v2/"
ContentJSON = "application/json"
ContentPlain = "text/plain"
ManifestEndpoint = "manifest"
UpdateEndpoint = "update"
UpdateCancelEndpoint = "update-cancel"
UpdateStatusEndpoint = "update-manifest"
QuoteEndpoint = "quote"
RecoverEndpoint = "recover"
SecretEndpoint = "secrets"
StatusEndpoint = "status"
SignQuoteEndpoint = "sign-quote"
MonotonicCounterEndpoint = "monotonic-counter"
V2API = "/api/v2/"
ContentJSON = "application/json"
ContentPlain = "text/plain"
)

const (
Expand Down
42 changes: 39 additions & 3 deletions cmd/marble-test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,27 @@ import (
"net/http"
"net/url"
"os"
"time"

"github.com/edgelesssys/ego/marble"
"github.com/edgelesssys/marblerun/api"
"github.com/edgelesssys/marblerun/util"
)

func main() {
addr := util.MustGetenv("EDG_TEST_ADDR")

if len(os.Args) > 1 && os.Args[1] == "serve" {
runServer(addr)
return
if len(os.Args) > 1 {
switch os.Args[1] {
case "serve":
runServer(addr)
return
case "monotonic-counter":
if err := testMonotonicCounter(); err != nil {
log.Fatal(err)
}
return
}
}

if err := runClient(addr); err != nil {
Expand Down Expand Up @@ -81,3 +91,29 @@ func runClient(addr string) error {
log.Printf("Successful connection to Server: %v", resp.Status)
return nil
}

func testMonotonicCounter() error {
const counterName = "foo"
endpoint := os.Getenv("EDG_COORDINATOR_CLIENT_ADDR")

ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()

value, err := api.SetMonotonicCounter(ctx, endpoint, counterName, 2)
if err != nil {
return fmt.Errorf("first call to SetMonotonicCounter: %w", err)
}
if value != 0 {
return fmt.Errorf("expected initial value 0, got %v", value)
}

value, err = api.SetMonotonicCounter(ctx, endpoint, counterName, 3)
if err != nil {
return fmt.Errorf("second call to SetMonotonicCounter: %w", err)
}
if value != 2 {
return fmt.Errorf("expected previous value 2, got %v", value)
}

return nil
}
93 changes: 93 additions & 0 deletions coordinator/clientapi/clientapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/edgelesssys/marblerun/coordinator/constants"
"github.com/edgelesssys/marblerun/coordinator/crypto"
"github.com/edgelesssys/marblerun/coordinator/manifest"
"github.com/edgelesssys/marblerun/coordinator/oid"
"github.com/edgelesssys/marblerun/coordinator/quote"
"github.com/edgelesssys/marblerun/coordinator/recovery"
"github.com/edgelesssys/marblerun/coordinator/seal"
Expand Down Expand Up @@ -506,6 +507,43 @@ func (a *ClientAPI) SetManifest(ctx context.Context, rawManifest []byte) (recove
return recoverySecretMap, nil
}

// SetMonotonicCounter sets the new value (if greater) of the counter identified by marbleType, marbleUUID, and name.
func (a *ClientAPI) SetMonotonicCounter(ctx context.Context, marbleType string, marbleUUID uuid.UUID, name string, value uint64) (prevValue uint64, err error) {
friendlyID := zap.String("id", fmt.Sprintf("%s:%s:%s", marbleType, marbleUUID, name))

a.log.Info("SetMonotonicCounter called", friendlyID, zap.Uint64("value", value))
defer a.core.Unlock()
if err := a.core.RequireState(ctx, state.AcceptingMarbles); err != nil {
a.log.Error("SetMonotonicCounter: Coordinator not in correct state", zap.Error(err))
return 0, err
}
defer func() {
if err != nil {
a.log.Error("SetMonotonicCounter failed", zap.Error(err), friendlyID)
}
}()

counterID := encodeMonotonicCounterID(marbleType, marbleUUID, name)

txdata, rollback, commit, err := wrapper.WrapTransaction(ctx, a.txHandle)
if err != nil {
return 0, err
}
defer rollback()

prevValue, err = txdata.SetMonotonicCounter(counterID, value)
if err != nil {
return 0, err
}

if err := commit(ctx); err != nil {
return 0, fmt.Errorf("committing store transaction: %w", err)
}

a.log.Info("SetMonotonicCounter successful", friendlyID)
return prevValue, nil
}

// UpdateManifest allows to update certain package parameters of the original manifest, supplied via a JSON manifest.
func (a *ClientAPI) UpdateManifest(ctx context.Context, rawUpdateManifest []byte, updater *user.User) (err error) {
a.log.Info("UpdateManifest called")
Expand Down Expand Up @@ -650,6 +688,55 @@ func (a *ClientAPI) UpdateManifest(ctx context.Context, rawUpdateManifest []byte
return nil
}

// VerifyMarble checks if a given client certificate is a Marble certificate signed by this Coordinator.
func (a *ClientAPI) VerifyMarble(ctx context.Context, clientCerts []*x509.Certificate) (string, uuid.UUID, error) {
defer a.core.Unlock()
if err := a.core.RequireState(ctx, state.AcceptingMarbles); err != nil {
a.log.Error("VerifyMarble: Coordinator not in correct state", zap.Error(err))
return "", uuid.UUID{}, err
}

txdata, rollback, _, err := wrapper.WrapTransaction(ctx, a.txHandle)
if err != nil {
return "", uuid.UUID{}, err
}
defer rollback()

marbleRootCert, err := txdata.GetCertificate(constants.SKMarbleRootCert)
if err != nil {
return "", uuid.UUID{}, fmt.Errorf("getting Marble root certificate: %w", err)
}
verifyOpts := x509.VerifyOptions{Roots: x509.NewCertPool()}
verifyOpts.Roots.AddCert(marbleRootCert)

// Check if a supplied client cert was signed by the Marble root cert and get the type and UUID
var errs error
for _, suppliedCert := range clientCerts {
if _, err := suppliedCert.Verify(verifyOpts); err != nil {
errs = errors.Join(errs, err)
continue
}

marbleUUID, err := uuid.Parse(suppliedCert.Subject.CommonName)
if err != nil {
errs = errors.Join(errs, err)
continue
}

for _, ex := range suppliedCert.Extensions {
if ex.Id.Equal(oid.MarbleType) {
return string(ex.Value), marbleUUID, nil
}
}
errs = errors.Join(errs, errors.New("MarbleType extension not found in certificate"))
}

if errs == nil {
errs = errors.New("client certificate did not match any Marble")
}
return "", uuid.UUID{}, errs
}

// VerifyUser checks if a given client certificate matches the admin certificates specified in the manifest.
func (a *ClientAPI) VerifyUser(ctx context.Context, clientCerts []*x509.Certificate) (*user.User, error) {
txdata, rollback, _, err := wrapper.WrapTransaction(ctx, a.txHandle)
Expand Down Expand Up @@ -822,3 +909,9 @@ func (a *ClientAPI) FeatureEnabled(ctx context.Context, feature string) bool {
return strings.EqualFold(s, feature)
})
}

func encodeMonotonicCounterID(marbleType string, marbleUUID uuid.UUID, name string) string {
// use unambiguous concatenation as counter ID
b64 := base64.StdEncoding.EncodeToString
return fmt.Sprintf("%s:%s:%s", b64([]byte(marbleType)), b64(marbleUUID[:]), b64([]byte(name)))
}
Loading
Loading