1package transit
2
3import (
4	"context"
5	"crypto/ecdsa"
6	"crypto/elliptic"
7	"crypto/rsa"
8	"crypto/x509"
9	"encoding/base64"
10	"encoding/pem"
11	"errors"
12	"fmt"
13	"strconv"
14	"strings"
15
16	"github.com/hashicorp/vault/helper/keysutil"
17	"github.com/hashicorp/vault/logical"
18	"github.com/hashicorp/vault/logical/framework"
19)
20
21const (
22	exportTypeEncryptionKey = "encryption-key"
23	exportTypeSigningKey    = "signing-key"
24	exportTypeHMACKey       = "hmac-key"
25)
26
27func (b *backend) pathExportKeys() *framework.Path {
28	return &framework.Path{
29		Pattern: "export/" + framework.GenericNameRegex("type") + "/" + framework.GenericNameRegex("name") + framework.OptionalParamRegex("version"),
30		Fields: map[string]*framework.FieldSchema{
31			"type": &framework.FieldSchema{
32				Type:        framework.TypeString,
33				Description: "Type of key to export (encryption-key, signing-key, hmac-key)",
34			},
35			"name": &framework.FieldSchema{
36				Type:        framework.TypeString,
37				Description: "Name of the key",
38			},
39			"version": &framework.FieldSchema{
40				Type:        framework.TypeString,
41				Description: "Version of the key",
42			},
43		},
44
45		Callbacks: map[logical.Operation]framework.OperationFunc{
46			logical.ReadOperation: b.pathPolicyExportRead,
47		},
48
49		HelpSynopsis:    pathExportHelpSyn,
50		HelpDescription: pathExportHelpDesc,
51	}
52}
53
54func (b *backend) pathPolicyExportRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
55	exportType := d.Get("type").(string)
56	name := d.Get("name").(string)
57	version := d.Get("version").(string)
58
59	switch exportType {
60	case exportTypeEncryptionKey:
61	case exportTypeSigningKey:
62	case exportTypeHMACKey:
63	default:
64		return logical.ErrorResponse(fmt.Sprintf("invalid export type: %s", exportType)), logical.ErrInvalidRequest
65	}
66
67	p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{
68		Storage: req.Storage,
69		Name:    name,
70	})
71	if err != nil {
72		return nil, err
73	}
74	if p == nil {
75		return nil, nil
76	}
77	if !b.System().CachingDisabled() {
78		p.Lock(false)
79	}
80	defer p.Unlock()
81
82	if !p.Exportable {
83		return logical.ErrorResponse("key is not exportable"), nil
84	}
85
86	switch exportType {
87	case exportTypeEncryptionKey:
88		if !p.Type.EncryptionSupported() {
89			return logical.ErrorResponse("encryption not supported for the key"), logical.ErrInvalidRequest
90		}
91	case exportTypeSigningKey:
92		if !p.Type.SigningSupported() {
93			return logical.ErrorResponse("signing not supported for the key"), logical.ErrInvalidRequest
94		}
95	}
96
97	retKeys := map[string]string{}
98	switch version {
99	case "":
100		for k, v := range p.Keys {
101			exportKey, err := getExportKey(p, &v, exportType)
102			if err != nil {
103				return nil, err
104			}
105			retKeys[k] = exportKey
106		}
107
108	default:
109		var versionValue int
110		if version == "latest" {
111			versionValue = p.LatestVersion
112		} else {
113			version = strings.TrimPrefix(version, "v")
114			versionValue, err = strconv.Atoi(version)
115			if err != nil {
116				return logical.ErrorResponse("invalid key version"), logical.ErrInvalidRequest
117			}
118		}
119
120		if versionValue < p.MinDecryptionVersion {
121			return logical.ErrorResponse("version for export is below minimum decryption version"), logical.ErrInvalidRequest
122		}
123		key, ok := p.Keys[strconv.Itoa(versionValue)]
124		if !ok {
125			return logical.ErrorResponse("version does not exist or cannot be found"), logical.ErrInvalidRequest
126		}
127
128		exportKey, err := getExportKey(p, &key, exportType)
129		if err != nil {
130			return nil, err
131		}
132
133		retKeys[strconv.Itoa(versionValue)] = exportKey
134	}
135
136	resp := &logical.Response{
137		Data: map[string]interface{}{
138			"name": p.Name,
139			"type": p.Type.String(),
140			"keys": retKeys,
141		},
142	}
143
144	return resp, nil
145}
146
147func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType string) (string, error) {
148	if policy == nil {
149		return "", errors.New("nil policy provided")
150	}
151
152	switch exportType {
153	case exportTypeHMACKey:
154		return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.HMACKey)), nil
155
156	case exportTypeEncryptionKey:
157		switch policy.Type {
158		case keysutil.KeyType_AES256_GCM96, keysutil.KeyType_ChaCha20_Poly1305:
159			return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil
160
161		case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA4096:
162			return encodeRSAPrivateKey(key.RSAKey), nil
163		}
164
165	case exportTypeSigningKey:
166		switch policy.Type {
167		case keysutil.KeyType_ECDSA_P256:
168			ecKey, err := keyEntryToECPrivateKey(key, elliptic.P256())
169			if err != nil {
170				return "", err
171			}
172			return ecKey, nil
173
174		case keysutil.KeyType_ED25519:
175			return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil
176
177		case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA4096:
178			return encodeRSAPrivateKey(key.RSAKey), nil
179		}
180	}
181
182	return "", fmt.Errorf("unknown key type %v", policy.Type)
183}
184
185func encodeRSAPrivateKey(key *rsa.PrivateKey) string {
186	// When encoding PKCS1, the PEM header should be `RSA PRIVATE KEY`. When Go
187	// has PKCS8 encoding support, we may want to change this.
188	derBytes := x509.MarshalPKCS1PrivateKey(key)
189	pemBlock := &pem.Block{
190		Type:  "RSA PRIVATE KEY",
191		Bytes: derBytes,
192	}
193	pemBytes := pem.EncodeToMemory(pemBlock)
194	return string(pemBytes)
195}
196
197func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, error) {
198	if k == nil {
199		return "", errors.New("nil KeyEntry provided")
200	}
201
202	privKey := &ecdsa.PrivateKey{
203		PublicKey: ecdsa.PublicKey{
204			Curve: curve,
205			X:     k.EC_X,
206			Y:     k.EC_Y,
207		},
208		D: k.EC_D,
209	}
210	ecder, err := x509.MarshalECPrivateKey(privKey)
211	if err != nil {
212		return "", err
213	}
214	if ecder == nil {
215		return "", errors.New("no data returned when marshalling to private key")
216	}
217
218	block := pem.Block{
219		Type:  "EC PRIVATE KEY",
220		Bytes: ecder,
221	}
222	return strings.TrimSpace(string(pem.EncodeToMemory(&block))), nil
223}
224
225const pathExportHelpSyn = `Export named encryption or signing key`
226
227const pathExportHelpDesc = `
228This path is used to export the named keys that are configured as
229exportable.
230`
231