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

Encrypt/Decrypt/Sign/Verify using RSA in Transit backend #3489

Merged
merged 19 commits into from
Nov 3, 2017
Merged
Show file tree
Hide file tree
Changes from 17 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
185 changes: 185 additions & 0 deletions builtin/logical/transit/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,191 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
return b, config.StorageView
}

func TestTransit_RSA(t *testing.T) {
testTransit_RSA(t, "rsa-2048")
testTransit_RSA(t, "rsa-4096")
}

func testTransit_RSA(t *testing.T, keyType string) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)

keyReq := &logical.Request{
Path: "keys/rsa",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"type": keyType,
},
Storage: storage,
}

resp, err = b.HandleRequest(keyReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}

plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox"

encryptReq := &logical.Request{
Path: "encrypt/rsa",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"plaintext": plaintext,
},
}

resp, err = b.HandleRequest(encryptReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}

ciphertext1 := resp.Data["ciphertext"].(string)

decryptReq := &logical.Request{
Path: "decrypt/rsa",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"ciphertext": ciphertext1,
},
}

resp, err = b.HandleRequest(decryptReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}

decryptedPlaintext := resp.Data["plaintext"]

if plaintext != decryptedPlaintext {
t.Fatalf("bad: plaintext; expected: %q\nactual: %q", plaintext, decryptedPlaintext)
}

// Rotate the key
rotateReq := &logical.Request{
Path: "keys/rsa/rotate",
Operation: logical.UpdateOperation,
Storage: storage,
}
resp, err = b.HandleRequest(rotateReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}

// Encrypt again
resp, err = b.HandleRequest(encryptReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
ciphertext2 := resp.Data["ciphertext"].(string)

if ciphertext1 == ciphertext2 {
t.Fatalf("expected different ciphertexts")
}

// See if the older ciphertext can still be decrypted
resp, err = b.HandleRequest(decryptReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if resp.Data["plaintext"].(string) != plaintext {
t.Fatal("failed to decrypt old ciphertext after rotating the key")
}

// Decrypt the new ciphertext
decryptReq.Data = map[string]interface{}{
"ciphertext": ciphertext2,
}
resp, err = b.HandleRequest(decryptReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if resp.Data["plaintext"].(string) != plaintext {
t.Fatal("failed to decrypt ciphertext after rotating the key")
}

signReq := &logical.Request{
Path: "sign/rsa",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"input": plaintext,
},
}
resp, err = b.HandleRequest(signReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
signature := resp.Data["signature"].(string)

verifyReq := &logical.Request{
Path: "verify/rsa",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"input": plaintext,
"signature": signature,
},
}

resp, err = b.HandleRequest(verifyReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if !resp.Data["valid"].(bool) {
t.Fatalf("failed to verify the RSA signature")
}

signReq.Data = map[string]interface{}{
"input": plaintext,
"algorithm": "invalid",
}
resp, err = b.HandleRequest(signReq)
if err != nil {
t.Fatal(err)
}
if resp == nil || !resp.IsError() {
t.Fatal("expected an error response")
}

signReq.Data = map[string]interface{}{
"input": plaintext,
"algorithm": "sha2-512",
}
resp, err = b.HandleRequest(signReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
signature = resp.Data["signature"].(string)

verifyReq.Data = map[string]interface{}{
"input": plaintext,
"signature": signature,
}
resp, err = b.HandleRequest(verifyReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if resp.Data["valid"].(bool) {
t.Fatalf("expected validation to fail")
}

verifyReq.Data = map[string]interface{}{
"input": plaintext,
"signature": signature,
"algorithm": "sha2-512",
}
resp, err = b.HandleRequest(verifyReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if !resp.Data["valid"].(bool) {
t.Fatalf("failed to verify the RSA signature")
}
}

func TestBackend_basic(t *testing.T) {
decryptData := make(map[string]interface{})
logicaltest.Test(t, logicaltest.TestCase{
Expand Down
2 changes: 1 addition & 1 deletion builtin/logical/transit/path_encrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestTransit_BatchEncryptionCase1(t *testing.T) {
t.Fatalf("err:%v resp:%#v", err, resp)
}

plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA=="
plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox"

encData := map[string]interface{}{
"plaintext": plaintext,
Expand Down
19 changes: 19 additions & 0 deletions builtin/logical/transit/path_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package transit
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
Expand Down Expand Up @@ -152,6 +153,9 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st
switch policy.Type {
case keysutil.KeyType_AES256_GCM96:
return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil

case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA4096:
return encodeRSAPrivateKey(key.RSAKey), nil
}

case exportTypeSigningKey:
Expand All @@ -165,12 +169,27 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st

case keysutil.KeyType_ED25519:
return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil

case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA4096:
return encodeRSAPrivateKey(key.RSAKey), nil
}
}

return "", fmt.Errorf("unknown key type %v", policy.Type)
}

func encodeRSAPrivateKey(key *rsa.PrivateKey) string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC this actually depends on the format. For PKCS1 yes, but PKCS8 I believe should just be PRIVATE KEY because the type is encoded into the binary block. Might be worth mentioning that in the comment for the future as Go 1.10 will have PKCS8 encoding support and we will be adding it to PKI in this release.

// When encoding PKCS1, the PEM header should be `RSA PRIVATE KEY`. When Go
// has PKCS8 encoding support, we may want to change this.
derBytes := x509.MarshalPKCS1PrivateKey(key)
pemBlock := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: derBytes,
}
pemBytes := pem.EncodeToMemory(pemBlock)
return string(pemBytes)
}

func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, error) {
if k == nil {
return "", errors.New("nil KeyEntry provided")
Expand Down
37 changes: 33 additions & 4 deletions builtin/logical/transit/path_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package transit

import (
"crypto/elliptic"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"strconv"
"time"
Expand Down Expand Up @@ -40,9 +42,11 @@ func (b *backend) pathKeys() *framework.Path {
"type": &framework.FieldSchema{
Type: framework.TypeString,
Default: "aes256-gcm96",
Description: `The type of key to create. Currently,
"aes256-gcm96" (symmetric) and "ecdsa-p256" (asymmetric), and
'ed25519' (asymmetric) are supported. Defaults to "aes256-gcm96".`,
Description: `
The type of key to create. Currently, "aes256-gcm96" (symmetric), "ecdsa-p256"
(asymmetric), 'ed25519' (asymmetric), 'rsa-2048' (asymmetric), 'rsa-4096'
(asymmetric) are supported. Defaults to "aes256-gcm96".
`,
},

"derived": &framework.FieldSchema{
Expand Down Expand Up @@ -131,6 +135,10 @@ func (b *backend) pathPolicyWrite(
polReq.KeyType = keysutil.KeyType_ECDSA_P256
case "ed25519":
polReq.KeyType = keysutil.KeyType_ED25519
case "rsa-2048":
polReq.KeyType = keysutil.KeyType_RSA2048
case "rsa-4096":
polReq.KeyType = keysutil.KeyType_RSA4096
default:
return logical.ErrorResponse(fmt.Sprintf("unknown key type %v", keyType)), logical.ErrInvalidRequest
}
Expand Down Expand Up @@ -225,7 +233,7 @@ func (b *backend) pathPolicyRead(
}
resp.Data["keys"] = retKeys

case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ED25519:
case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ED25519, keysutil.KeyType_RSA2048, keysutil.KeyType_RSA4096:
retKeys := map[string]map[string]interface{}{}
for k, v := range p.Keys {
key := asymKey{
Expand Down Expand Up @@ -253,6 +261,27 @@ func (b *backend) pathPolicyRead(
}
}
key.Name = "ed25519"
case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA4096:
key.Name = "rsa-2048"
if p.Type == keysutil.KeyType_RSA4096 {
key.Name = "rsa-4096"
}

// Encode the RSA public key in PEM format to return over the
// API
derBytes, err := x509.MarshalPKIXPublicKey(v.RSAKey.Public())
if err != nil {
return nil, fmt.Errorf("error marshaling RSA public key: %v", err)
}
pemBlock := &pem.Block{
Type: "PUBLIC KEY",
Bytes: derBytes,
}
pemBytes := pem.EncodeToMemory(pemBlock)
if pemBytes == nil || len(pemBytes) == 0 {
return nil, fmt.Errorf("failed to PEM-encode RSA public key")
}
key.PublicKey = string(pemBytes)
}

retKeys[strconv.Itoa(k)] = structs.New(key).Map()
Expand Down
Loading