Skip to content

Commit

Permalink
cli: accept both PKCS#1 and PKCS#8 private keys for recovery (#798)
Browse files Browse the repository at this point in the history
* cli: accept both PKCS#1 and PKCS#8 private recovery keys
* docs: add hint about converting key to PKCS#8 format

---------

Signed-off-by: Daniel Weiße <[email protected]>
Co-authored-by: Thomas Tendyck <[email protected]>
  • Loading branch information
daniel-weisse and thomasten authored Feb 5, 2025
1 parent 387eb1c commit fcadb65
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 5 deletions.
14 changes: 9 additions & 5 deletions cli/internal/cmd/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"encoding/pem"
"errors"
"fmt"
"os"

"github.com/edgelesssys/marblerun/api"
"github.com/edgelesssys/marblerun/cli/internal/file"
Expand Down Expand Up @@ -62,7 +61,7 @@ func runRecover(cmd *cobra.Command, args []string) error {
return err
}

keyHandle, cancel, err := getRecoveryKeySigner(cmd)
keyHandle, cancel, err := getRecoveryKeySigner(cmd, afero.Afero{Fs: fs})
if err != nil {
return err
}
Expand Down Expand Up @@ -94,7 +93,7 @@ func runRecover(cmd *cobra.Command, args []string) error {
return nil
}

func getRecoveryKeySigner(cmd *cobra.Command) (pkcs11.SignerDecrypter, func() error, error) {
func getRecoveryKeySigner(cmd *cobra.Command, fs afero.Afero) (pkcs11.SignerDecrypter, func() error, error) {
privKeyFile, err := cmd.Flags().GetString("key")
if err != nil {
return nil, nil, err
Expand All @@ -116,7 +115,7 @@ func getRecoveryKeySigner(cmd *cobra.Command) (pkcs11.SignerDecrypter, func() er
return pkcs11.LoadRSAPrivateKey(pkcs11ConfigFile, pkcs11KeyID, pkcs11KeyLabel)
}

privKeyPEM, err := os.ReadFile(privKeyFile)
privKeyPEM, err := fs.ReadFile(privKeyFile)
if err != nil {
return nil, nil, err
}
Expand All @@ -126,7 +125,12 @@ func getRecoveryKeySigner(cmd *cobra.Command) (pkcs11.SignerDecrypter, func() er
}
privK, err := x509.ParsePKCS8PrivateKey(privateKeyBlock.Bytes)
if err != nil {
return nil, nil, err
// Try to parse as PKCS #1 private key as well
var pkcs1Err error
privK, pkcs1Err = x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes)
if pkcs1Err != nil {
return nil, nil, fmt.Errorf("parsing private key: tried PKCS #1 format: %w, tried PKCS #8 format: %w", pkcs1Err, err)
}
}
signer, ok := privK.(pkcs11.SignerDecrypter)
if !ok {
Expand Down
92 changes: 92 additions & 0 deletions cli/internal/cmd/recover_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: BUSL-1.1
*/

package cmd

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetRecoveryKeySigner(t *testing.T) {
testCases := map[string]struct {
keyFlag string
fs afero.Fs
wantErr bool
}{
"PKCS #8 key": {
keyFlag: "private.pem",
fs: func() afero.Fs {
fs := afero.NewMemMapFs()
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
privRaw, err := x509.MarshalPKCS8PrivateKey(privKey)
require.NoError(t, err)
privPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: privRaw,
})
require.NoError(t, afero.WriteFile(fs, "private.pem", privPEM, 0o644))
return fs
}(),
},
"PKCS #1 key": {
keyFlag: "private.pem",
fs: func() afero.Fs {
fs := afero.NewMemMapFs()
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
privRaw := x509.MarshalPKCS1PrivateKey(privKey)
privPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: privRaw,
})
require.NoError(t, afero.WriteFile(fs, "private.pem", privPEM, 0o644))
return fs
}(),
},
"no key file": {
keyFlag: "private.pem",
fs: afero.NewMemMapFs(),
wantErr: true,
},
"invalid key file": {
keyFlag: "private.pem",
fs: func() afero.Fs {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "private.pem", []byte("invalid"), 0o644))
return fs
}(),
wantErr: true,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)

cmd := NewRecoverCmd()
require.NoError(t, cmd.Flags().Set("key", tc.keyFlag))

signer, cancel, err := getRecoveryKeySigner(cmd, afero.Afero{Fs: tc.fs})
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.NotNil(signer)
assert.NotNil(cancel)
assert.NoError(cancel())
})
}
}
13 changes: 13 additions & 0 deletions docs/versioned_docs/version-1.7/workflows/recover-coordinator.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ Assuming you named your recovery key `recoverKey1` in the manifest, and you save
jq -r '.RecoverySecrets.recoverKey1' recovery_data | openssl base64 -d > recovery_key_encrypted
```

:::caution

If you generated the private recovery key using `openssl` version 1, the key will be in PKCS #1 format.
MarbleRun requires the key to be in PKCS #8 format.

Use the following command to convert the key:

```bash
openssl pkcs8 -topk8 -in private_key.pem -out private_key_pkcs8.pem -nocrypt
```

:::

Then decrypt and upload the extracted secret using the MarbleRun CLI:

```bash
Expand Down

0 comments on commit fcadb65

Please sign in to comment.