From a4f5e040668291b4fae07117b46b25a2265bcbd8 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Tue, 2 Jun 2020 13:58:57 -0400 Subject: [PATCH] lang/funcs: Add support for OpenSSH RSA key format Previously this function only supported the x509 RSA private key format. More recent versions of OpenSSH default to generating a new PEM key format, which this commit now supports using the x/crypto/ssh package. Also improve the returned error messages for various invalid ciphertext or invalid private key errors. --- lang/funcs/crypto.go | 38 +++++++++-------- lang/funcs/crypto_test.go | 90 +++++++++++++++++++++++++++++++++------ 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/lang/funcs/crypto.go b/lang/funcs/crypto.go index 28074fb13..03b1572c2 100644 --- a/lang/funcs/crypto.go +++ b/lang/funcs/crypto.go @@ -6,12 +6,12 @@ import ( "crypto/sha1" "crypto/sha256" "crypto/sha512" - "crypto/x509" + "encoding/asn1" "encoding/base64" "encoding/hex" - "encoding/pem" "fmt" "hash" + "strings" uuidv5 "github.com/google/uuid" uuid "github.com/hashicorp/go-uuid" @@ -19,6 +19,7 @@ import ( "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/gocty" "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/ssh" ) var UUIDFunc = function.New(&function.Spec{ @@ -152,27 +153,30 @@ var RsaDecryptFunc = function.New(&function.Spec{ b, err := base64.StdEncoding.DecodeString(s) if err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode input %q: cipher text must be base64-encoded", s) + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "failed to decode input %q: cipher text must be base64-encoded", s) } - block, _ := pem.Decode([]byte(key)) - if block == nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to parse key: no key found") - } - if block.Headers["Proc-Type"] == "4,ENCRYPTED" { - return cty.UnknownVal(cty.String), fmt.Errorf( - "failed to parse key: password protected keys are not supported. Please decrypt the key prior to use", - ) - } - - x509Key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + rawKey, err := ssh.ParseRawPrivateKey([]byte(key)) if err != nil { - return cty.UnknownVal(cty.String), err + var errStr string + switch e := err.(type) { + case asn1.SyntaxError: + errStr = strings.ReplaceAll(e.Error(), "asn1: syntax error", "invalid ASN1 data in the given private key") + case asn1.StructuralError: + errStr = strings.ReplaceAll(e.Error(), "asn1: struture error", "invalid ASN1 data in the given private key") + default: + errStr = fmt.Sprintf("invalid private key: %s", e) + } + return cty.UnknownVal(cty.String), function.NewArgErrorf(1, errStr) + } + privateKey, ok := rawKey.(*rsa.PrivateKey) + if !ok { + return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "invalid private key type %t", rawKey) } - out, err := rsa.DecryptPKCS1v15(nil, x509Key, b) + out, err := rsa.DecryptPKCS1v15(nil, privateKey, b) if err != nil { - return cty.UnknownVal(cty.String), err + return cty.UnknownVal(cty.String), fmt.Errorf("failed to decrypt: %s", err) } return cty.StringVal(string(out)), nil diff --git a/lang/funcs/crypto_test.go b/lang/funcs/crypto_test.go index a79b67f17..279777743 100644 --- a/lang/funcs/crypto_test.go +++ b/lang/funcs/crypto_test.go @@ -370,58 +370,67 @@ func TestRsaDecrypt(t *testing.T) { Ciphertext cty.Value Privatekey cty.Value Want cty.Value - Err bool + Err string }{ // Base-64 encoded cipher decrypts correctly { cty.StringVal(CipherBase64), cty.StringVal(PrivateKey), cty.StringVal("message"), - false, + "", + }, + // OpenSSH key format + { + cty.StringVal(CipherBase64), + cty.StringVal(OpenSSHPrivateKey), + cty.StringVal("message"), + "", }, // Wrong key { cty.StringVal(CipherBase64), cty.StringVal(WrongPrivateKey), cty.UnknownVal(cty.String), - true, + "failed to decrypt: crypto/rsa: decryption error", }, // Bad key { cty.StringVal(CipherBase64), - cty.StringVal("bad key"), + cty.StringVal(BadPrivateKey), cty.UnknownVal(cty.String), - true, + "invalid ASN1 data in the given private key: data truncated", }, // Empty key { cty.StringVal(CipherBase64), cty.StringVal(""), cty.UnknownVal(cty.String), - true, + "invalid private key: ssh: no key found", }, - // Bad cipher + // Bad ciphertext { - cty.StringVal("bad cipher"), + cty.StringVal("bad"), cty.StringVal(PrivateKey), cty.UnknownVal(cty.String), - true, + `failed to decode input "bad": cipher text must be base64-encoded`, }, - // Empty cipher + // Empty ciphertext { cty.StringVal(""), cty.StringVal(PrivateKey), cty.UnknownVal(cty.String), - true, + "failed to decrypt: crypto/rsa: decryption error", }, } for _, test := range tests { t.Run(fmt.Sprintf("RsaDecrypt(%#v, %#v)", test.Ciphertext, test.Privatekey), func(t *testing.T) { got, err := RsaDecrypt(test.Ciphertext, test.Privatekey) - if test.Err { + if test.Err != "" { if err == nil { t.Fatal("succeeded; want error") + } else if err.Error() != test.Err { + t.Fatalf("wrong error\ngot: %s\nwant: %s", err.Error(), test.Err) } return } else if err != nil { @@ -699,6 +708,35 @@ OLlRAoGBAIZ5Uv4Z3s8O7WKXXUe/lq6j7vfiVkR1NW/Z/WLKXZpnmvJ7FgxN4e56 RXT7GwNQHIY8eDjDnsHxzrxd+raOxOZeKcMHj3XyjCX3NHfTscnsBPAGYpY/Wxzh T8UYnFu6RzkixElTf2rseEav7rkdKkI3LAeIZy7B0HulKKsmqVQ7 -----END RSA PRIVATE KEY----- +` + OpenSSHPrivateKey = ` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAgUElV5mwqkloIrM8ZNZ72gSCcnSJt7+/Usa5G+D15YQUAdf9c1zE +ekTfHgDP+04nw/uFNFaE5v1RbHaPxhZYVg5ZErNCa/hzn+x10xzcepeS3KPVXcxae4MR0B +EegvqZqJzN9loXsNL/c3H/B+2Gle3hTxjlWFb3F5qLgR+4Mf4ruhER1v6eHQa/nchi03MB +pT4UeJ7MrL92hTJYLdpSyCqmr8yjxkKJDVC2uRrr+sTSxfh7r6v24u/vp/QTmBIAlNPgad +VAZw17iNNb7vjV7Gwl/5gHXonCUKURaV++dBNLrHIZpqcAM8wHRph8mD1EfL9hsz77pHew +xolBATV+7QAAA7jbhEFk24RBZAAAAAdzc2gtcnNhAAABAQCBQSVXmbCqSWgiszxk1nvaBI +JydIm3v79Sxrkb4PXlhBQB1/1zXMR6RN8eAM/7TifD+4U0VoTm/VFsdo/GFlhWDlkSs0Jr ++HOf7HXTHNx6l5Lco9VdzFp7gxHQER6C+pmonM32Whew0v9zcf8H7YaV7eFPGOVYVvcXmo +uBH7gx/iu6ERHW/p4dBr+dyGLTcwGlPhR4nsysv3aFMlgt2lLIKqavzKPGQokNULa5Guv6 +xNLF+Huvq/bi7++n9BOYEgCU0+Bp1UBnDXuI01vu+NXsbCX/mAdeicJQpRFpX750E0usch +mmpwAzzAdGmHyYPUR8v2GzPvukd7DGiUEBNX7tAAAAAwEAAQAAAQAtayvpBVt76wGJt/vP +30J0EMOZ3nOKOvnK54OiVUFy3h99ql0oTX/JCyxvyY9L2mHEzzw2cPSQipEzENJio/V0f+ +Qy2wTLFenjV17rySd8eIiluXg/VpCw+BSpTWqwUcju4/LHz06l1u7mrTcVnRR+2LEkbzYf +/ackBy1gOTorbonTK2G3NxFMfAdRjzcifVvEPM5zWC38GDo1OFr9UixOqhkEB/UNFswNll +H/I5JQmMjGEyMsAIxm/JGwCZSoZo9rdiII5qrcLdT2HKRpam7UAQ1Ill7eUuGF/9ZmiEP+ +PcnjVGo46WyYh9w24SWx8BU8z96WfT/Rhzs5RpGEfsEhAAAAgQCGeVL+Gd7PDu1il11Hv5 +auo+734lZEdTVv2f1iyl2aZ5ryexYMTeHuekV0+xsDUByGPHg4w57B8c68Xfq2jsTmXinD +B4918owl9zR307HJ7ATwBmKWP1sc4U/FGJxbukc5IsRJU39q7HhGr+65HSpCNywHiGcuwd +B7pSirJqlUOwAAAIEAw/Xida8kSv8n86V3qSY/I+fYQ5V+jDtXIE+JhRnS8xzbOzz3v0WS +Oo5H+o4nJx5eL3Ghb3Gcm0Jn46dHrxinHbm+3RjXv/X6tlbxIYjRSQfHOTSMCTvdqcliF5 +vC6RCLXuc7R+IWR1Ky6eDEZGtrvt3DyeYABsp9fRUFR/6NluUAAACBAKjbMNWkpe1iWtux +dA6CXZVPS1nq6V3Gs5SXCHTs/0vUA5kit0Q0E3an08UZq8YmCPSxLJpDpL85Z5zgTKZ2d2 +TlOaiEX6a3nESIt+ygwDh1hp5QtBFhoJeOmC2+/414ln9ABmPg3ySTXfYuk2yA1rvNueP3 +qwEumyjIVv96u39pAAAAAAEC +-----END OPENSSH PRIVATE KEY----- ` WrongPrivateKey = ` -----BEGIN RSA PRIVATE KEY----- @@ -728,5 +766,33 @@ CYhwNQKBgGPcLXmjpGtkZvggl0aZr9LsvCTckllSCFSI861kivL/rijdNoCHGxZv dfDkLTLcz9Gk41rD9Gxn/3sqodnTAc3Z2PxFnzg1Q/u3+x6YAgBwI/g/jE2xutGW H7CurtMwALQ/n/6LUKFmjRZjqbKX9SO2QSaC3grd6sY9Tu+bZjLe -----END RSA PRIVATE KEY----- +` + BadPrivateKey = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAgUElV5mwqkloIrM8ZNZ72gSCcnSJt7+/Usa5G+D15YQUAdf9 +c1zEekTfHgDP+04nw/uFNFaE5v1RbHaPxhZYVg5ZErNCa/hzn+x10xzcepeS3KPV +Xcxae4MR0BEegvqZqJzN9loXsNL/c3H/B+2Gle3hTxjlWFb3F5qLgR+4Mf4ruhER +1v6eHQa/nchi03MBpT4UeJ7MrL92hTJYLdpSyCqmr8yjxkKJDVC2uRrr+sTSxfh7 +r6v24u/vp/QTmBIAlNPgadVAZw17iNNb7vjV7Gwl/5gHXonCUKURaV++dBNLrHIZ +pqcAM8wHRph8mD1EfL9hsz77pHewxolBATV+7QIDAQABAoIBAC1rK+kFW3vrAYm3 ++8/fQnQQw5nec4o6+crng6JVQXLeH32qXShNf8kLLG/Jj0vaYcTPPDZw9JCKkTMQ +0mKj9XR/5DLbBMsV6eNXXuvJJ3x4iKW5eD9WkLD4FKlNarBRyO7j8sfPTqXW7uat +NxWdFH7YsSRvNh/9pyQHLWA5OituidMrYbc3EUx8B1GPNyJ9W8Q8znNYLfwYOjU4 +Wv1SLE6qGQQH9Q0WzA2WUf8jklCYyMYTIywAjGb8kbAJlKhmj2t2Igjmqtwt1PYc +pGlqbtQBDUiWXt5S4YX/1maIQ/49yeNUajjpbJiH3DbhJbHwFTzP3pZ9P9GHOzlG +kYR+wSECgYEAw/Xida8kSv8n86V3qSY/I+fYQ5V+jDtXIE+JhRnS8xzbOzz3v0WS +Oo5H+o4nJx5eL3Ghb3Gcm0Jn46dHrxinHbm+3RjXv/X6tlbxIYjRSQfHOTSMCTvd +qcliF5vC6RCLXuc7R+IWR1Ky6eDEZGtrvt3DyeYABsp9fRUFR/6NluUCgYEAqNsw +1aSl7WJa27F0DoJdlU9LWerpXcazlJcIdOz/S9QDmSK3RDQTdqfTxRmrxiYI9LEs +mkOkvzlnnOBMpnZ3ZOU5qIRfprecRIi37KDAOHWGnlC0EWGgl46YLb7/jXiWf0AG +BhXoKvjI2HjYP21z/EyZ+PFPzur/lNaZhIUlMnUfibbwE9pFggQzzf8scM7c7Sf+ +mLoVSdoQ/Rujz7CqvQzi2nKSsM7t0curUIb3lJWee5/UeEaxZcmIufoNUrzohAWH +BJOIPDM4ssUTLRq7wYM9uQKBgHCBau5OP8gE6mjKuXsZXWUoahpFLKwwwmJUp2vQ +pOFPJ/6WZOlqkTVT6QPAcPUbTohKrF80hsZqZyDdSfT3peFx4ZLocBrS56m6NmHR +UYHMvJ8rQm76T1fryHVidz85g3zRmfBeWg8yqT5oFg4LYgfLsPm1gRjOhs8LfPvI +OLlRAoGBAIZ5Uv4Z3s8O7WKXXUe/lq6j7vfiVkR1NW/Z/WLKXZpnmvJ7FgxN4e56 +RXT7GwNQHIY8eDjDnsHxzrxd+raOxOZeKcMHj3XyjCX3NHfTscnsBPAGYpY/Wxzh +T8UYnFu6RzkixElTf2rseEav7rkdKkI3LAeIZy7B0HulKKsmqVQ7 +-----END RSA PRIVATE KEY----- ` )