Skip to content

Commit e4864f9

Browse files
Merge commit from fork
* verify recovery key signatures before comitting recovery Signed-off-by: Daniel Weiße <[email protected]> * cli: fix private key loading function Signed-off-by: Daniel Weiße <[email protected]> * coordinator: increase security version number to 2 Signed-off-by: Daniel Weiße <[email protected]> * store: tighten up interface Signed-off-by: Daniel Weiße <[email protected]> * stdstore: revert nil check Signed-off-by: Daniel Weiße <[email protected]> * coordinator: bind sealed key to state Signed-off-by: Daniel Weiße <[email protected]> * coordinator: split up setting and sealing of store key Signed-off-by: Daniel Weiße <[email protected]> * coordinator: add context to BeginReadTransaction Signed-off-by: Daniel Weiße <[email protected]> * coordinator: remove redundant key sealing Signed-off-by: Daniel Weiße <[email protected]> * clientapi: fix incorrect secret when starting read transaction Signed-off-by: Daniel Weiße <[email protected]> * cli: add test for pkcs11 rsa key loading Signed-off-by: Daniel Weiße <[email protected]> * cli: update recover command documentation Signed-off-by: Daniel Weiße <[email protected]> * sealer: fix doc comment Signed-off-by: Daniel Weiße <[email protected]> * server: remove redundant return statement Signed-off-by: Daniel Weiße <[email protected]> * stdstore: replace key backup with atomic file replace Signed-off-by: Daniel Weiße <[email protected]> * add tests for security fix Signed-off-by: Daniel Weiße <[email protected]> * stdstore: fix seal mode not being correctly set after recovery Signed-off-by: Daniel Weiße <[email protected]> * add integration test to verify key binding Signed-off-by: Daniel Weiße <[email protected]> * docs: update recovery workflows Signed-off-by: Daniel Weiße <[email protected]> * fix linting issues Signed-off-by: Daniel Weiße <[email protected]> --------- Signed-off-by: Daniel Weiße <[email protected]>
1 parent 08a4d8b commit e4864f9

31 files changed

+848
-365
lines changed

api/api.go

+45-21
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ package api
99
import (
1010
"bytes"
1111
"context"
12+
"crypto"
1213
"crypto/ecdsa"
14+
"crypto/rand"
1315
"crypto/rsa"
1416
"crypto/sha256"
1517
"crypto/tls"
@@ -159,38 +161,33 @@ func VerifyMarbleRunDeployment(ctx context.Context, endpoint string, opts Verify
159161
}
160162

161163
// Recover performs recovery on a Coordinator instance by setting the decrypted recoverySecret.
164+
// The signer is used to generate a signature over the recoverySecret.
165+
// The Coordinator will verify this signature matches one of the recovery public keys set in the manifest.
162166
// On success, it returns the number of remaining recovery secrets to be set,
163167
// as well as the verified SGX quote.
164168
//
165169
// If this function is called from inside an EGo enclave, the "marblerun_ego_enclave" build tag must be set when building the binary.
166-
func Recover(ctx context.Context, endpoint string, opts VerifyOptions, recoverySecret []byte) (remaining int, sgxQuote []byte, err error) {
167-
opts.setDefaults()
168-
169-
rootCert, _, sgxQuote, err := VerifyCoordinator(ctx, endpoint, opts)
170+
func Recover(ctx context.Context, endpoint string, opts VerifyOptions, recoverySecret []byte, signer crypto.Signer) (remaining int, sgxQuote []byte, err error) {
171+
signature, err := util.SignPKCS1v15(signer, recoverySecret)
170172
if err != nil {
171173
return -1, nil, err
172174
}
175+
return recoverCoordinator(ctx, endpoint, opts, recoverySecret, signature)
176+
}
173177

174-
client, err := rest.NewClient(endpoint, rootCert, nil)
175-
if err != nil {
176-
return -1, nil, fmt.Errorf("setting up client: %w", err)
177-
}
178-
179-
// Attempt recovery using the v2 API first
180-
remaining, err = recoverV2(ctx, client, recoverySecret)
181-
if rest.IsNotAllowedErr(err) {
182-
remaining, err = recoverV1(ctx, client, recoverySecret)
183-
}
184-
if err != nil {
185-
return -1, nil, fmt.Errorf("sending recovery request: %w", err)
186-
}
187-
188-
return remaining, sgxQuote, err
178+
// RecoverWithSignature performs recovery on a Coordinator instance by setting the decrypted recoverySecret.
179+
// This is the same as [Recover], but allows passing in the recoverySecretSignature directly,
180+
// instead of generating it using a [crypto.Signer].
181+
// The recoveryKeySignature must be a PKCS#1 v1.5 signature over the SHA-256 hash of recoverySecret.
182+
//
183+
// If this function is called from inside an EGo enclave, the "marblerun_ego_enclave" build tag must be set when building the binary.
184+
func RecoverWithSignature(ctx context.Context, endpoint string, opts VerifyOptions, recoverySecret, recoverySecretSignature []byte) (remaining int, sgxQuote []byte, err error) {
185+
return recoverCoordinator(ctx, endpoint, opts, recoverySecret, recoverySecretSignature)
189186
}
190187

191188
// DecryptRecoveryData decrypts recovery data returned by a Coordinator during [ManifestSet] using a parties private recovery key.
192-
func DecryptRecoveryData(recoveryData []byte, recoveryPrivateKey *rsa.PrivateKey) ([]byte, error) {
193-
return util.DecryptOAEP(recoveryPrivateKey, recoveryData)
189+
func DecryptRecoveryData(recoveryData []byte, recoveryPrivateKey crypto.Decrypter) ([]byte, error) {
190+
return recoveryPrivateKey.Decrypt(rand.Reader, recoveryData, &rsa.OAEPOptions{Hash: crypto.SHA256})
194191
}
195192

196193
// GetStatus retrieves the status of a MarbleRun Coordinator instance.
@@ -577,3 +574,30 @@ func getMarbleCredentialsFromEnv() (tls.Certificate, *x509.Certificate, error) {
577574

578575
return tlsCert, coordinatorRoot, nil
579576
}
577+
578+
// recoverCoordinator performs recovery on a Coordinator instance by setting the decrypted recoverySecret.
579+
// The signer is used to generate a signature over the recoverySecret.
580+
// The Coordinator will verify this signature matches one of the recovery public keys set in the manifest.
581+
// On success, it returns the number of remaining recovery secrets to be set,
582+
// as well as the verified SGX quote.
583+
func recoverCoordinator(ctx context.Context, endpoint string, opts VerifyOptions, recoverySecret, recoverySecretSignature []byte) (remaining int, sgxQuote []byte, err error) {
584+
opts.setDefaults()
585+
586+
rootCert, _, sgxQuote, err := VerifyCoordinator(ctx, endpoint, opts)
587+
if err != nil {
588+
return -1, nil, err
589+
}
590+
591+
client, err := rest.NewClient(endpoint, rootCert, nil)
592+
if err != nil {
593+
return -1, nil, fmt.Errorf("setting up client: %w", err)
594+
}
595+
596+
// The v1 API does not support recovery, therefore only attempt the v2 API
597+
remaining, err = recoverV2(ctx, client, recoverySecret, recoverySecretSignature)
598+
if err != nil {
599+
return -1, nil, fmt.Errorf("sending recovery request: %w", err)
600+
}
601+
602+
return remaining, sgxQuote, err
603+
}

api/v1.go

-25
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,6 @@ import (
2020
apiv1 "github.com/edgelesssys/marblerun/coordinator/server/v1"
2121
)
2222

23-
// recoverV1 performs recovery of the Coordinator using the legacy v1 API.
24-
func recoverV1(ctx context.Context, client *rest.Client, recoverySecret []byte) (remaining int, err error) {
25-
resp, err := client.Post(ctx, rest.RecoverEndpoint, rest.ContentPlain, bytes.NewReader(recoverySecret))
26-
if err != nil {
27-
return -1, err
28-
}
29-
30-
var response apiv1.RecoveryStatusResponse
31-
if err := json.Unmarshal(resp, &response); err != nil {
32-
return -1, fmt.Errorf("unmarshalling Coordinator response: %w", err)
33-
}
34-
35-
if response.StatusMessage == "Recovery successful." {
36-
return 0, nil
37-
}
38-
39-
remainingStr, _, _ := strings.Cut(response.StatusMessage, ": ")
40-
remaining, err = strconv.Atoi(remainingStr)
41-
if err != nil {
42-
return -1, fmt.Errorf("parsing remaining recovery secrets: %w", err)
43-
}
44-
45-
return remaining, nil
46-
}
47-
4823
// getStatusV1 retrieves the status of the Coordinator using the legacy v1 API.
4924
func getStatusV1(ctx context.Context, client *rest.Client) (int, string, error) {
5025
resp, err := client.Get(ctx, rest.StatusEndpoint, http.NoBody)

api/v2.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import (
1919
)
2020

2121
// recoverV2 performs recovery of the Coordinator using the v2 API.
22-
func recoverV2(ctx context.Context, client *rest.Client, recoverySecret []byte) (remaining int, err error) {
23-
recoverySecretJSON, err := json.Marshal(apiv2.RecoveryRequest{RecoverySecret: recoverySecret})
22+
func recoverV2(ctx context.Context, client *rest.Client, recoverySecret, recoverySecretSignature []byte) (remaining int, err error) {
23+
recoverySecretJSON, err := json.Marshal(apiv2.RecoveryRequest{
24+
RecoverySecret: recoverySecret,
25+
RecoverySecretSignature: recoverySecretSignature,
26+
})
2427
if err != nil {
2528
return -1, fmt.Errorf("marshalling request: %w", err)
2629
}

cli/internal/cmd/recover.go

+89-4
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,43 @@ SPDX-License-Identifier: BUSL-1.1
77
package cmd
88

99
import (
10+
"crypto"
11+
"crypto/rand"
12+
"crypto/rsa"
13+
"crypto/x509"
14+
"encoding/pem"
15+
"errors"
16+
"fmt"
17+
"os"
18+
1019
"github.com/edgelesssys/marblerun/api"
1120
"github.com/edgelesssys/marblerun/cli/internal/file"
21+
"github.com/edgelesssys/marblerun/cli/internal/pkcs11"
1222
"github.com/spf13/afero"
1323
"github.com/spf13/cobra"
1424
)
1525

1626
// NewRecoverCmd returns the recover command.
1727
func NewRecoverCmd() *cobra.Command {
1828
cmd := &cobra.Command{
19-
Use: "recover <recovery_key_decrypted> <IP:PORT>",
20-
Short: "Recover the MarbleRun Coordinator from a sealed state",
21-
Long: "Recover the MarbleRun Coordinator from a sealed state",
29+
Use: "recover <recovery_key_file> <IP:PORT>",
30+
Short: "Recover the MarbleRun Coordinator from a sealed state",
31+
Long: "Recover the MarbleRun Coordinator from a sealed state.\n" +
32+
"<recovery_key_file> may be either a decrypted recovery secret, or an encrypted recovery secret,\n" +
33+
"in which case the private key is used to decrypt the secret.",
2234
Example: "marblerun recover recovery_key_decrypted $MARBLERUN",
2335
Args: cobra.ExactArgs(2),
2436
RunE: runRecover,
2537
}
2638

39+
cmd.Flags().StringP("key", "k", "", "Path to a the recovery private key to decrypt and/or sign the recovery key")
40+
cmd.Flags().String("pkcs11-config", "", "Path to a PKCS#11 configuration file to load the recovery private key with")
41+
cmd.Flags().String("pkcs11-key-id", "", "ID of the private key in the PKCS#11 token")
42+
cmd.Flags().String("pkcs11-key-label", "", "Label of the private key in the PKCS#11 token")
43+
must(cobra.MarkFlagFilename(cmd.Flags(), "pkcs11-config", "json"))
44+
cmd.MarkFlagsOneRequired("pkcs11-key-id", "pkcs11-key-label", "key")
45+
cmd.MarkFlagsMutuallyExclusive("pkcs11-config", "key")
46+
2747
return cmd
2848
}
2949

@@ -42,7 +62,22 @@ func runRecover(cmd *cobra.Command, args []string) error {
4262
return err
4363
}
4464

45-
remaining, sgxQuote, err := api.Recover(cmd.Context(), hostname, verifyOpts, recoveryKey)
65+
keyHandle, cancel, err := getRecoveryKeySigner(cmd)
66+
if err != nil {
67+
return err
68+
}
69+
defer func() {
70+
if err := cancel(); err != nil {
71+
cmd.PrintErrf("Failed to close PKCS #11 session: %s\n", err)
72+
}
73+
}()
74+
75+
recoveryKey, err = maybeDecryptRecoveryKey(recoveryKey, keyHandle)
76+
if err != nil {
77+
return err
78+
}
79+
80+
remaining, sgxQuote, err := api.Recover(cmd.Context(), hostname, verifyOpts, recoveryKey, keyHandle)
4681
if err != nil {
4782
return err
4883
}
@@ -58,3 +93,53 @@ func runRecover(cmd *cobra.Command, args []string) error {
5893
}
5994
return nil
6095
}
96+
97+
func getRecoveryKeySigner(cmd *cobra.Command) (pkcs11.SignerDecrypter, func() error, error) {
98+
privKeyFile, err := cmd.Flags().GetString("key")
99+
if err != nil {
100+
return nil, nil, err
101+
}
102+
103+
if privKeyFile == "" {
104+
pkcs11ConfigFile, err := cmd.Flags().GetString("pkcs11-config")
105+
if err != nil {
106+
return nil, nil, err
107+
}
108+
pkcs11KeyID, err := cmd.Flags().GetString("pkcs11-key-id")
109+
if err != nil {
110+
return nil, nil, err
111+
}
112+
pkcs11KeyLabel, err := cmd.Flags().GetString("pkcs11-key-label")
113+
if err != nil {
114+
return nil, nil, err
115+
}
116+
return pkcs11.LoadRSAPrivateKey(pkcs11ConfigFile, pkcs11KeyID, pkcs11KeyLabel)
117+
}
118+
119+
privKeyPEM, err := os.ReadFile(privKeyFile)
120+
if err != nil {
121+
return nil, nil, err
122+
}
123+
privateKeyBlock, _ := pem.Decode(privKeyPEM)
124+
if privateKeyBlock == nil {
125+
return nil, nil, fmt.Errorf("%q did not contain a valid PEM block", privKeyFile)
126+
}
127+
privK, err := x509.ParsePKCS8PrivateKey(privateKeyBlock.Bytes)
128+
if err != nil {
129+
return nil, nil, err
130+
}
131+
signer, ok := privK.(pkcs11.SignerDecrypter)
132+
if !ok {
133+
return nil, nil, errors.New("loaded private key does not fulfill required interface")
134+
}
135+
return signer, func() error { return nil }, nil
136+
}
137+
138+
// maybeDecryptRecoveryKey tries to decrypt the given recoveryKey using OAEP.
139+
// If the recoveryKey is already 16 bytes long, it is returned as is.
140+
func maybeDecryptRecoveryKey(recoveryKey []byte, decrypter crypto.Decrypter) ([]byte, error) {
141+
if len(recoveryKey) != 16 {
142+
return decrypter.Decrypt(rand.Reader, recoveryKey, &rsa.OAEPOptions{Hash: crypto.SHA256})
143+
}
144+
return recoveryKey, nil
145+
}

cli/internal/pkcs11/pkcs11.go

+42-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ import (
1616
"github.com/ThalesGroup/crypto11"
1717
)
1818

19-
// LoadX509KeyPair loads a [tls.Certificate] using the provided PKCS#11 configuration file.
20-
// The returned cancel function must be called to release the PKCS#11 resources only after the certificate is no longer needed.
19+
// SignerDecrypter is a combined interface for [crypto.Signer] and [crypto.Decrypter].
20+
// An RSA private key in a PKCS #11 token implements this interface.
21+
type SignerDecrypter interface {
22+
crypto.Signer
23+
crypto.Decrypter
24+
}
25+
26+
// LoadX509KeyPair loads a [tls.Certificate] using the provided PKCS #11 configuration file.
27+
// The returned cancel function must be called to release the PKCS #11 resources only after the certificate is no longer needed.
2128
func LoadX509KeyPair(pkcs11ConfigPath string, keyID, keyLabel, certID, certLabel string) (crt tls.Certificate, cancel func() error, err error) {
2229
pkcs11, err := crypto11.ConfigureFromFile(pkcs11ConfigPath)
2330
if err != nil {
@@ -59,6 +66,39 @@ func LoadX509KeyPair(pkcs11ConfigPath string, keyID, keyLabel, certID, certLabel
5966
}, pkcs11.Close, nil
6067
}
6168

69+
// LoadRSAPrivateKey loads a [SignerDecrypter] using the provided PKCS #11 configuration file.
70+
// The returned cancel function must be called to release the PKCS #11 resources only after the key is no longer needed.
71+
func LoadRSAPrivateKey(pkcs11ConfigPath string, keyID, keyLabel string) (signer SignerDecrypter, cancel func() error, err error) {
72+
pkcs11, err := crypto11.ConfigureFromFile(pkcs11ConfigPath)
73+
if err != nil {
74+
return nil, nil, err
75+
}
76+
defer func() {
77+
if err != nil {
78+
err = errors.Join(err, pkcs11.Close())
79+
}
80+
}()
81+
82+
var keyIDBytes, keyLabelBytes []byte
83+
if keyID != "" {
84+
keyIDBytes = []byte(keyID)
85+
}
86+
if keyLabel != "" {
87+
keyLabelBytes = []byte(keyLabel)
88+
}
89+
90+
privK, err := loadPrivateKey(pkcs11, keyIDBytes, keyLabelBytes)
91+
if err != nil {
92+
return nil, nil, err
93+
}
94+
95+
signer, ok := privK.(crypto11.SignerDecrypter)
96+
if !ok {
97+
return nil, nil, errors.New("loaded private key does not support decryption")
98+
}
99+
return signer, pkcs11.Close, err
100+
}
101+
62102
func loadPrivateKey(pkcs11 *crypto11.Context, id, label []byte) (crypto.Signer, error) {
63103
priv, err := pkcs11.FindKeyPair(id, label)
64104
if err != nil {

0 commit comments

Comments
 (0)