@@ -13,7 +13,10 @@ import (
1313 "crypto/rsa"
1414 "crypto/x509"
1515 "encoding/pem"
16+ "errors"
1617 "fmt"
18+ "math/big"
19+ "strings"
1720
1821 "github.com/hashicorp/terraform-plugin-framework/diag"
1922 "github.com/hashicorp/terraform-plugin-framework/path"
@@ -118,18 +121,22 @@ func parsePrivateKeyPEM(keyPEMBytes []byte) (crypto.PrivateKey, Algorithm, error
118121// parsePrivateKeyOpenSSHPEM takes a slide of bytes containing a private key
119122// encoded in [OpenSSH PEM (RFC 4716)](https://datatracker.ietf.org/doc/html/rfc4716) format,
120123// and returns a crypto.PrivateKey implementation, together with the Algorithm used by the key.
121- func parsePrivateKeyOpenSSHPEM (keyOpenSSHPEMBytes []byte ) (crypto.PrivateKey , Algorithm , error ) {
124+ func parsePrivateKeyOpenSSHPEM (keyOpenSSHPEMBytes []byte ) (crypto.PrivateKey , Algorithm , string , error ) {
122125 prvKey , err := ssh .ParseRawPrivateKey (keyOpenSSHPEMBytes )
123126 if err != nil {
124- return nil , "" , fmt .Errorf ("failed to parse openssh private key: %w" , err )
127+ return nil , "" , "" , fmt .Errorf ("failed to parse openssh private key: %w" , err )
125128 }
126129
127- algorithm , err := privateKeyToAlgorithm ( prvKey )
130+ comment , err := getPrivateKeyComment ( keyOpenSSHPEMBytes )
128131 if err != nil {
129- return nil , "" , fmt .Errorf ("failed to determine key algorithm for private key of type %T : %w" , prvKey , err )
132+ return nil , "" , "" , fmt .Errorf ("failed to get private key comment : %w" , err )
130133 }
131134
132- return prvKey , algorithm , nil
135+ algorithm , err := privateKeyToAlgorithm (prvKey )
136+ if err != nil {
137+ return nil , "" , "" , fmt .Errorf ("failed to determine key algorithm for private key of type %T: %w" , prvKey , err )
138+ }
139+ return prvKey , algorithm , comment , nil
133140}
134141
135142// privateKeyToPublicKey takes a crypto.PrivateKey and extracts the corresponding crypto.PublicKey,
@@ -159,7 +166,7 @@ func privateKeyToAlgorithm(prvKey crypto.PrivateKey) (Algorithm, error) {
159166
160167// setPublicKeyAttributes takes a crypto.PrivateKey, extracts the corresponding crypto.PublicKey and then
161168// encodes related attributes on the given *tfsdk.State.
162- func setPublicKeyAttributes (ctx context.Context , s * tfsdk.State , prvKey crypto.PrivateKey ) diag.Diagnostics {
169+ func setPublicKeyAttributes (ctx context.Context , s * tfsdk.State , prvKey crypto.PrivateKey , openSSHComment string ) diag.Diagnostics {
163170 var diags diag.Diagnostics
164171
165172 pubKey , err := privateKeyToPublicKey (prvKey )
@@ -199,8 +206,13 @@ func setPublicKeyAttributes(ctx context.Context, s *tfsdk.State, prvKey crypto.P
199206 var pubKeySSH , pubKeySSHFingerprintMD5 , pubKeySSHFingerprintSHA256 string
200207 if err == nil {
201208 sshPubKeyBytes := ssh .MarshalAuthorizedKey (sshPubKey )
202-
203209 pubKeySSH = string (sshPubKeyBytes )
210+
211+ // Manually add the comment as MarshalAuthorizedKeys ignores it: https://github.com/golang/go/issues/46870
212+ if openSSHComment != "" {
213+ pubKeySSH = fmt .Sprintf ("%s %s\n " , strings .TrimSuffix (pubKeySSH , "\n " ), openSSHComment )
214+ }
215+
204216 pubKeySSHFingerprintMD5 = ssh .FingerprintLegacyMD5 (sshPubKey )
205217 pubKeySSHFingerprintSHA256 = ssh .FingerprintSHA256 (sshPubKey )
206218 }
@@ -225,7 +237,7 @@ func setPublicKeyAttributes(ctx context.Context, s *tfsdk.State, prvKey crypto.P
225237
226238// setPublicKeyAttributes takes a crypto.PrivateKey, extracts the corresponding crypto.PublicKey and then
227239// encodes related attributes on the given *tfsdk.EphemeralResultData.
228- func setPublicKeyAttributesEphemeral (ctx context.Context , d * tfsdk.EphemeralResultData , prvKey crypto.PrivateKey ) diag.Diagnostics {
240+ func setPublicKeyAttributesEphemeral (ctx context.Context , d * tfsdk.EphemeralResultData , prvKey crypto.PrivateKey , openSSHComment string ) diag.Diagnostics {
229241 var diags diag.Diagnostics
230242
231243 pubKey , err := privateKeyToPublicKey (prvKey )
@@ -265,8 +277,13 @@ func setPublicKeyAttributesEphemeral(ctx context.Context, d *tfsdk.EphemeralResu
265277 var pubKeySSH , pubKeySSHFingerprintMD5 , pubKeySSHFingerprintSHA256 string
266278 if err == nil {
267279 sshPubKeyBytes := ssh .MarshalAuthorizedKey (sshPubKey )
268-
269280 pubKeySSH = string (sshPubKeyBytes )
281+
282+ // Manually add the comment as MarshalAuthorizedKeys ignores it: https://github.com/golang/go/issues/46870
283+ if openSSHComment != "" {
284+ pubKeySSH = fmt .Sprintf ("%s %s\n " , strings .TrimSuffix (pubKeySSH , "\n " ), openSSHComment )
285+ }
286+
270287 pubKeySSHFingerprintMD5 = ssh .FingerprintLegacyMD5 (sshPubKey )
271288 pubKeySSHFingerprintSHA256 = ssh .FingerprintSHA256 (sshPubKey )
272289 }
@@ -288,3 +305,106 @@ func setPublicKeyAttributesEphemeral(ctx context.Context, d *tfsdk.EphemeralResu
288305
289306 return nil
290307}
308+
309+ // Note: The SSH package does not currently expose the comment in the private key, so an adapted version of
310+ // parseOpenSSHPrivateKey from https://github.com/golang/crypto/blob/master/ssh/keys.go#L1532
311+ const privateKeyAuthMagic = "openssh-key-v1\x00 "
312+
313+ type openSSHEncryptedPrivateKey struct {
314+ CipherName string
315+ KdfName string
316+ KdfOpts string
317+ NumKeys uint32
318+ PubKey []byte
319+ PrivKeyBlock []byte
320+ }
321+
322+ type openSSHPrivateKey struct {
323+ Check1 uint32
324+ Check2 uint32
325+ Keytype string
326+ Rest []byte `ssh:"rest"`
327+ }
328+
329+ type openSSHRSAPrivateKey struct {
330+ N * big.Int
331+ E * big.Int
332+ D * big.Int
333+ Iqmp * big.Int
334+ P * big.Int
335+ Q * big.Int
336+ Comment string
337+ Pad []byte `ssh:"rest"`
338+ }
339+
340+ type openSSHEd25519PrivateKey struct {
341+ Pub []byte
342+ Priv []byte
343+ Comment string
344+ Pad []byte `ssh:"rest"`
345+ }
346+
347+ type openSSHECDSAPrivateKey struct {
348+ Curve string
349+ Pub []byte
350+ D * big.Int
351+ Comment string
352+ Pad []byte `ssh:"rest"`
353+ }
354+
355+ func getPrivateKeyComment (pemBytes []byte ) (string , error ) {
356+ block , _ := pem .Decode (pemBytes )
357+
358+ if block == nil {
359+ return "" , errors .New ("ssh: no key found" )
360+ }
361+
362+ key := block .Bytes
363+
364+ if len (key ) < len (privateKeyAuthMagic ) || string (key [:len (privateKeyAuthMagic )]) != privateKeyAuthMagic {
365+ return "" , errors .New ("ssh: invalid openssh private key format" )
366+ }
367+ remaining := key [len (privateKeyAuthMagic ):]
368+
369+ var w openSSHEncryptedPrivateKey
370+ if err := ssh .Unmarshal (remaining , & w ); err != nil {
371+ return "" , err
372+ }
373+ if w .NumKeys != 1 {
374+ // We only support single key files, and so does OpenSSH.
375+ // https://github.com/openssh/openssh-portable/blob/4103a3ec7/sshkey.c#L4171
376+ return "" , errors .New ("ssh: multi-key files are not supported" )
377+ }
378+
379+ var pk1 openSSHPrivateKey
380+ if err := ssh .Unmarshal (w .PrivKeyBlock , & pk1 ); err != nil || pk1 .Check1 != pk1 .Check2 {
381+ if w .CipherName != "none" {
382+ return "" , x509 .IncorrectPasswordError
383+ }
384+ return "" , errors .New ("ssh: malformed OpenSSH key" )
385+ }
386+
387+ switch pk1 .Keytype {
388+ case ssh .KeyAlgoRSA :
389+ var key openSSHRSAPrivateKey
390+ if err := ssh .Unmarshal (pk1 .Rest , & key ); err != nil {
391+ return "" , err
392+ }
393+
394+ return key .Comment , nil
395+ case ssh .KeyAlgoED25519 :
396+ var key openSSHEd25519PrivateKey
397+ if err := ssh .Unmarshal (pk1 .Rest , & key ); err != nil {
398+ return "" , err
399+ }
400+ return key .Comment , nil
401+ case ssh .KeyAlgoECDSA256 , ssh .KeyAlgoECDSA384 , ssh .KeyAlgoECDSA521 :
402+ var key openSSHECDSAPrivateKey
403+ if err := ssh .Unmarshal (pk1 .Rest , & key ); err != nil {
404+ return "" , err
405+ }
406+ return key .Comment , nil
407+ default :
408+ return "" , errors .New ("ssh: unhandled key type" )
409+ }
410+ }
0 commit comments