Skip to content
Open
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
51 changes: 51 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,63 @@ func Recover(ctx context.Context, endpoint string, opts VerifyOptions, recoveryS
// This is the same as [Recover], but allows passing in the recoverySecretSignature directly,
// instead of generating it using a [crypto.Signer].
// The recoveryKeySignature must be a PKCS#1 v1.5 signature over the SHA-256 hash of recoverySecret.
// recoverySecret may be encrypted using the Coordinator's ephemeral recovery key retrieved using [RecoveryPublicKey] and [EncryptRecoverySecretWithEphemeralKey],
// but the signature must always be generated over the plain recoverySecret.
//
// If this function is called from inside an EGo enclave, the "marblerun_ego_enclave" build tag must be set when building the binary.
func RecoverWithSignature(ctx context.Context, endpoint string, opts VerifyOptions, recoverySecret, recoverySecretSignature []byte) (remaining int, sgxQuote []byte, err error) {
return recoverCoordinator(ctx, endpoint, opts, recoverySecret, recoverySecretSignature)
}

// RecoveryPublicKey retrieves the Coordinator's ephemeral recovery public key.
// The key can be used to encrypt recovery secrets before passing them to [RecoverWithSignature].
//
// If this function is called from inside an EGo enclave, the "marblerun_ego_enclave" build tag must be set when building the binary.
func RecoveryPublicKey(ctx context.Context, endpoint string, opts VerifyOptions) (pub crypto.PublicKey, sgxQuote []byte, err error) {
opts.setDefaults()

rootCert, _, sgxQuote, err := VerifyCoordinator(ctx, endpoint, opts)
if err != nil {
return nil, nil, err
}

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

body, err := client.Get(ctx, "/api/v2/recover/public-key", http.NoBody)
if err != nil {
return nil, nil, fmt.Errorf("retrieving recovery public key: %w", err)
}

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

pubBlock, _ := pem.Decode(response.EphemeralPublicKey)
if pubBlock == nil {
return nil, nil, fmt.Errorf("decoding PEM block: %w", err)
}
pub, err = x509.ParsePKIXPublicKey(pubBlock.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("parsing public key: %w", err)
}
return pub, sgxQuote, nil
}

// EncryptRecoverySecretWithEphemeralKey encrypts a recovery secret using the ephemeral public key retrieved from [RecoveryPublicKey].
// The encrypted secret can be passed to [RecoverWithSignature].
func EncryptRecoverySecretWithEphemeralKey(recoverySecret []byte, recoveryPublicKey crypto.PublicKey) ([]byte, error) {
switch pub := recoveryPublicKey.(type) {
case *rsa.PublicKey:
return rsa.EncryptOAEP(sha256.New(), rand.Reader, pub, recoverySecret, nil)
default:
return nil, fmt.Errorf("unsupported public key type: %T", pub)
}
}

// DecryptRecoveryData decrypts recovery data returned by a Coordinator during [ManifestSet] using a parties private recovery key.
func DecryptRecoveryData(recoveryData []byte, recoveryPrivateKey crypto.Decrypter) ([]byte, error) {
return recoveryPrivateKey.Decrypt(rand.Reader, recoveryData, &rsa.OAEPOptions{Hash: crypto.SHA256})
Expand Down
2 changes: 2 additions & 0 deletions cli/internal/cmd/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func NewRecoverCmd() *cobra.Command {
cmd.MarkFlagsOneRequired("pkcs11-key-id", "pkcs11-key-label", "key")
cmd.MarkFlagsMutuallyExclusive("pkcs11-config", "key")

cmd.AddCommand(newRecoverPublicKeyCmd())

return cmd
}

Expand Down
73 changes: 73 additions & 0 deletions cli/internal/cmd/recoverPublickey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: BUSL-1.1
*/

package cmd

import (
"crypto/x509"
"encoding/pem"
"fmt"

"github.com/edgelesssys/marblerun/api"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)

func newRecoverPublicKeyCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "public-key <IP:PORT>",
Short: "Retrieve the Coordinator's ephemeral public key for encrypting recovery secrets",
Long: "Retrieve the Coordinator's ephemeral public key for encrypting recovery secrets.",
RunE: recoverPublicKeyCmdHandler,
Args: cobra.ExactArgs(1),
}

cmd.Flags().StringP("output", "o", "", "File to save the public key to")

return cmd
}

func recoverPublicKeyCmdHandler(cmd *cobra.Command, args []string) error {
hostname := args[0]
fs := afero.NewOsFs()

output, err := cmd.Flags().GetString("output")
if err != nil {
return err
}

verifyOpts, sgxQuotePath, err := parseRestFlags(cmd)
if err != nil {
return err
}

pubKey, sgxQuote, err := api.RecoveryPublicKey(cmd.Context(), hostname, verifyOpts)
if err != nil {
return fmt.Errorf("retrieving recovery public key: %w", err)
}

pubKeyDER, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return fmt.Errorf("marshalling public key: %w", err)
}
pubKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: pubKeyDER,
})

if err := saveSgxQuote(fs, sgxQuote, sgxQuotePath); err != nil {
return err
}

if output != "" {
if err := afero.WriteFile(fs, output, pubKeyPEM, 0o644); err != nil {
return fmt.Errorf("writing public key to file: %w", err)
}
} else {
fmt.Printf("%s", pubKeyPEM)
}
return nil
}
120 changes: 0 additions & 120 deletions coordinator/clientapi/clientapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,126 +286,6 @@ func (a *ClientAPI) GetUpdateLog(ctx context.Context) ([]string, error) {
return strings.Split(strings.TrimSpace(updateLog), "\n"), nil
}

// Recover sets an encryption key (ideally decrypted from the recovery data) and tries to unseal and load a saved state of the Coordinator.
func (a *ClientAPI) Recover(ctx context.Context, encryptionKey, encryptionKeySignature []byte) (keysLeft int, err error) {
left, err := a.recover(ctx, encryptionKey, encryptionKeySignature)
if err != nil || left > 0 {
return left, err
}

// Since a recovery was required, no valid key encryption keys are available.
// Seal a new key encryption key by committing a transaction and start sharing the key.
_, rollback, commit, err := dwrapper.WrapTransaction(ctx, a.txHandle)
if err != nil {
return 0, err
}
defer rollback()
if err := commit(ctx); err != nil {
return 0, err
}

err = a.keyServer.StartSharing(ctx)
return 0, err
}

func (a *ClientAPI) recover(ctx context.Context, encryptionKey, encryptionKeySignature []byte) (keysLeft int, retErr error) {
a.log.Info("Recover called")
defer a.core.Unlock()
if err := a.core.RequireState(ctx, state.Recovery); err != nil {
a.log.Error("Recover: Coordinator not in correct state", zap.Error(err))
return -1, err
}
defer func() {
if retErr != nil {
a.log.Error("Recover failed", zap.Error(retErr))
}
}()
if a.recoverySignatureCache == nil {
a.recoverySignatureCache = make(map[string][]byte)
}

remaining, secret, err := a.recovery.RecoverKey(encryptionKey)
if err != nil {
return -1, fmt.Errorf("setting recovery key: %w", err)
}
a.recoverySignatureCache[string(encryptionKey)] = encryptionKeySignature

// another key is needed to finish the recovery
if remaining != 0 {
a.log.Info("Recover: recovery incomplete, more keys needed", zap.Int("remaining", remaining))
return remaining, nil
}

// reset signature cache on return after this point
// the recovery module was already cleaned up if no more keys are missing
defer func() {
a.recoverySignatureCache = nil
}()

// verify the recovery keys before properly loading the state and releasing recovery mode
sealedStore, err := a.txHandle.BeginReadTransaction(ctx, secret)
if err != nil {
return -1, fmt.Errorf("loading sealed state: %w", err)
}
readTx := wrapper.New(sealedStore)
mnf, err := readTx.GetManifest()
if err != nil {
return -1, fmt.Errorf("loading manifest from store: %w", err)
}
if len(mnf.RecoveryKeys) != len(a.recoverySignatureCache) {
return -1, fmt.Errorf("recovery keys in manifest do not match the keys used for recovery: expected %d, got %d", len(mnf.RecoveryKeys), len(a.recoverySignatureCache))
}
for keyName, keyPEM := range mnf.RecoveryKeys {
pubKey, err := crypto.ParseRSAPublicKeyFromPEM(keyPEM)
if err != nil {
return -1, fmt.Errorf("parsing recovery public key %q: %w", keyName, err)
}

found := false
for key, signature := range a.recoverySignatureCache {
if err := util.VerifyPKCS1v15(pubKey, []byte(key), signature); err == nil {
found = true
delete(a.recoverySignatureCache, key)
break
}
}
if !found {
return -1, fmt.Errorf("no matching recovery key found for recovery public key %q", keyName)
}
}

// cache SGX quote over the root certificate
rootCert, err := readTx.GetCertificate(constants.SKCoordinatorRootCert)
if err != nil {
return -1, fmt.Errorf("loading root certificate from store: %w", err)
}
if err := a.core.GenerateQuote(rootCert.Raw); err != nil {
return -1, fmt.Errorf("generating quote failed: %w", err)
}

// load state and set seal mode defined in manifest
a.txHandle.SetEncryptionKey(secret, seal.ModeFromString(mnf.Config.SealMode))
defer func() {
if retErr != nil {
a.txHandle.SetEncryptionKey(nil, seal.ModeDisabled) // reset encryption key in case of failure
}
}()
recoveryData, sealedState, err := a.txHandle.LoadState()
if err != nil {
return -1, fmt.Errorf("loading state: %w", err)
}
a.txHandle.SetRecoveryData(recoveryData)
if err := a.recovery.SetRecoveryData(recoveryData); err != nil {
a.log.Error("Could not retrieve recovery data from state. Recovery will be unavailable", zap.Error(err))
}
if err := a.txHandle.SealEncryptionKey(sealedState); err != nil {
a.log.Error("Could not seal encryption key after recovery. Restart will require another recovery", zap.Error(err))
}

a.log.Info("Recover successful")
return 0, nil
}

// SetManifest sets the manifest of the Coordinator.
//
// rawManifest is the manifest of type Manifest in JSON format.
Expand Down
Loading