Skip to content

Commit 3641f7b

Browse files
committed
Add an ED25519 key pair resource
1 parent 3720988 commit 3641f7b

File tree

11 files changed

+237
-15
lines changed

11 files changed

+237
-15
lines changed

docs/index.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ description: |-
77

88
# Provider: sshkey
99

10-
The `sshkey` Terraform provider generates SSH key pairs. Only RSA-type keys are
11-
currently supported.
10+
The `sshkey` Terraform provider generates SSH key pairs. Only RSA and ED25519
11+
keys are currently supported.
1212

1313
## Example Usage
1414

docs/resources/ed25519_key_pair.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
page_title: "ed25519_key_pair Resource - terraform-provider-sshkey"
3+
subcategory: ""
4+
description: |-
5+
The ed25519_key_pair resource generates an ED25519 key pair.
6+
---
7+
8+
# Resource: sshkey_ed25519_key_pair
9+
10+
The `sshkey_ed25519_key_pair` resource generates an ED25519 key pair.
11+
12+
## Example Usage
13+
14+
```terraform
15+
resource "sshkey_ed25519_key_pair" "example" {}
16+
```
17+
18+
## Attributes Reference
19+
20+
The following attributes are exported.
21+
22+
- `private_key_pem` - (Sensitive) The RSA private key in PEM format.
23+
- `public_key` - The public key value in SSH2/`authorized_keys` format.
24+
- `fingerprint_sha256` - SHA256 fingerprint of the key.
25+
- `fingerprint_md5` - Legacy MD5 fingerprint of the key.
26+
- `id` - The SHA256 fingerprint of the key.
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
terraform {
2+
required_providers {
3+
sshkey = {
4+
source = "daveadams/sshkey"
5+
}
6+
}
7+
}
8+
9+
resource "sshkey_ed25519_key_pair" "example" {
10+
count = 2
11+
}
12+
13+
output "example_public_keys" {
14+
value = sshkey_ed25519_key_pair.example[*].public_key
15+
}
16+
17+
output "example_fingerprints_md5" {
18+
value = sshkey_ed25519_key_pair.example[*].fingerprint_md5
19+
}
20+
21+
output "example_fingerprints_sha256" {
22+
value = sshkey_ed25519_key_pair.example[*].fingerprint_sha256
23+
}

examples/sshkey_rsa_key_pair/main.tf

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
terraform {
22
required_providers {
33
sshkey = {
4-
version = "0.1.1"
5-
source = "daveadams/sshkey"
4+
source = "daveadams/sshkey"
65
}
76
}
87
}

go.mod

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.17
44

55
require (
66
github.com/hashicorp/terraform-plugin-framework v0.5.0
7-
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
7+
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
88
)
99

1010
require (
@@ -26,9 +26,9 @@ require (
2626
github.com/oklog/run v1.0.0 // indirect
2727
github.com/stretchr/testify v1.7.0 // indirect
2828
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
29-
golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect
29+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
3030
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
31-
golang.org/x/text v0.3.5 // indirect
31+
golang.org/x/text v0.3.6 // indirect
3232
google.golang.org/appengine v1.6.6 // indirect
3333
google.golang.org/genproto v0.0.0-20200711021454-869866162049 // indirect
3434
google.golang.org/grpc v1.32.0 // indirect

go.sum

+8-6
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6Ac
9999
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
100100
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
101101
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
102-
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
103102
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
103+
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
104+
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
104105
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
105106
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
106107
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -116,8 +117,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
116117
golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
117118
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
118119
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
119-
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
120-
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
120+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
121+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
121122
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
122123
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
123124
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -129,15 +130,16 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
129130
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
130131
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
131132
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
132-
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
133+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
133134
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
134135
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
136+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
135137
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
136138
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
137139
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
138140
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
139-
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
140-
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
141+
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
142+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
141143
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
142144
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
143145
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

sshkey/ed25519_key_pair.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package sshkey
2+
3+
import (
4+
"crypto/ed25519"
5+
"crypto/rand"
6+
"crypto/x509"
7+
"encoding/pem"
8+
"golang.org/x/crypto/ssh"
9+
)
10+
11+
type ed25519KeyPair struct {
12+
privateKey ed25519.PrivateKey
13+
publicKey ssh.PublicKey
14+
}
15+
16+
func generateED25519KeyPair() (*ed25519KeyPair, error) {
17+
rawPubKey, key, err := ed25519.GenerateKey(rand.Reader)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
pub, err := ssh.NewPublicKey(rawPubKey)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
return &ed25519KeyPair{
28+
privateKey: key,
29+
publicKey: pub,
30+
}, nil
31+
}
32+
33+
func (k *ed25519KeyPair) PrivateKeyPEM() (string, error) {
34+
keyBytes, err := x509.MarshalPKCS8PrivateKey(k.privateKey)
35+
if err != nil {
36+
return "", err
37+
}
38+
39+
return string(pem.EncodeToMemory(
40+
&pem.Block{
41+
Type: "OPENSSH PRIVATE KEY",
42+
Headers: nil,
43+
Bytes: keyBytes,
44+
}),
45+
), nil
46+
}
47+
48+
func (k *ed25519KeyPair) PublicKey() string {
49+
return string(ssh.MarshalAuthorizedKey(k.publicKey))
50+
}
51+
52+
func (k *ed25519KeyPair) FingerprintMD5() string {
53+
return ssh.FingerprintLegacyMD5(k.publicKey)
54+
}
55+
56+
func (k *ed25519KeyPair) FingerprintSHA256() string {
57+
return ssh.FingerprintSHA256(k.publicKey)
58+
}

sshkey/models.go

+8
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ type RSAKeyPair struct {
1212
FingerprintMD5 types.String `tfsdk:"fingerprint_md5"`
1313
FingerprintSHA256 types.String `tfsdk:"fingerprint_sha256"`
1414
}
15+
16+
type ED25519KeyPair struct {
17+
ID types.String `tfsdk:"id"`
18+
PrivateKeyPEM types.String `tfsdk:"private_key_pem"`
19+
PublicKey types.String `tfsdk:"public_key"`
20+
FingerprintMD5 types.String `tfsdk:"fingerprint_md5"`
21+
FingerprintSHA256 types.String `tfsdk:"fingerprint_sha256"`
22+
}

sshkey/provider.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ func (p *sshkeyProvider) Configure(ctx context.Context, req tfsdk.ConfigureProvi
3737

3838
func (p *sshkeyProvider) GetResources(_ context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) {
3939
return map[string]tfsdk.ResourceType{
40-
"sshkey_rsa_key_pair": resourceRSAKeyPairType{},
40+
"sshkey_rsa_key_pair": resourceRSAKeyPairType{},
41+
"sshkey_ed25519_key_pair": resourceED25519KeyPairType{},
4142
}, nil
4243
}
4344

sshkey/resource_ed25519_key_pair.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package sshkey
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/diag"
7+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
)
10+
11+
type resourceED25519KeyPairType struct{}
12+
13+
func (r resourceED25519KeyPairType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
14+
return tfsdk.Schema{
15+
Attributes: map[string]tfsdk.Attribute{
16+
"id": {
17+
Type: types.StringType,
18+
Computed: true,
19+
},
20+
"private_key_pem": {
21+
Type: types.StringType,
22+
Computed: true,
23+
Sensitive: true,
24+
},
25+
"public_key": {
26+
Type: types.StringType,
27+
Computed: true,
28+
},
29+
"fingerprint_md5": {
30+
Type: types.StringType,
31+
Computed: true,
32+
},
33+
"fingerprint_sha256": {
34+
Type: types.StringType,
35+
Computed: true,
36+
},
37+
},
38+
}, nil
39+
}
40+
41+
func (r resourceED25519KeyPairType) NewResource(ctx context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) {
42+
return resourceED25519KeyPair{}, nil
43+
}
44+
45+
type resourceED25519KeyPair struct{}
46+
47+
func (r resourceED25519KeyPair) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
48+
var plan ED25519KeyPair
49+
diags := req.Plan.Get(ctx, &plan)
50+
resp.Diagnostics.Append(diags...)
51+
if resp.Diagnostics.HasError() {
52+
return
53+
}
54+
55+
key, err := generateED25519KeyPair()
56+
if err != nil {
57+
resp.Diagnostics.AddError("Unable to generate a new ED25519 key", err.Error())
58+
return
59+
}
60+
61+
privateKeyPEM, err := key.PrivateKeyPEM()
62+
if err != nil {
63+
resp.Diagnostics.AddError("Unable to render private key to PEM format", err.Error())
64+
return
65+
}
66+
67+
result := ED25519KeyPair{
68+
ID: types.String{Value: key.FingerprintSHA256()},
69+
PrivateKeyPEM: types.String{Value: privateKeyPEM},
70+
PublicKey: types.String{Value: key.PublicKey()},
71+
FingerprintMD5: types.String{Value: key.FingerprintMD5()},
72+
FingerprintSHA256: types.String{Value: key.FingerprintSHA256()},
73+
}
74+
75+
diags = resp.State.Set(ctx, result)
76+
resp.Diagnostics.Append(diags...)
77+
if resp.Diagnostics.HasError() {
78+
return
79+
}
80+
}
81+
82+
func (r resourceED25519KeyPair) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) {
83+
// no need to support Read at the moment since the resource is fully within state
84+
// NOTE: if we support sourcing from files on disk in the future, this will have to be implemented
85+
}
86+
87+
func (r resourceED25519KeyPair) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) {
88+
// no need to support Update at the moment, since the only possible changes require replacement
89+
// NOTE: if we need to support changes later for comments, etc, this will need to be implemented
90+
}
91+
92+
func (r resourceED25519KeyPair) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) {
93+
var state ED25519KeyPair
94+
diags := req.State.Get(ctx, &state)
95+
resp.Diagnostics.Append(diags...)
96+
if resp.Diagnostics.HasError() {
97+
return
98+
}
99+
100+
resp.State.RemoveResource(ctx)
101+
}
102+
103+
func (r resourceED25519KeyPair) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) {
104+
tfsdk.ResourceImportStateNotImplemented(ctx, "", resp)
105+
}

sshkey/rsa_key_pair.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func generateRSAKeyPair(bits int) (*rsaKeyPair, error) {
3838
func (k *rsaKeyPair) PrivateKeyPEM() string {
3939
return string(pem.EncodeToMemory(
4040
&pem.Block{
41-
Type: "RSA PRIVATE KEY",
41+
Type: "OPENSSH PRIVATE KEY",
4242
Headers: nil,
4343
Bytes: x509.MarshalPKCS1PrivateKey(k.privateKey),
4444
}),

0 commit comments

Comments
 (0)