diff --git a/authority/authority.go b/authority/authority.go index 5b6f7761..98037cc0 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -2,6 +2,7 @@ package authority import ( "context" + "crypto" "crypto/sha256" "crypto/x509" "encoding/hex" @@ -17,6 +18,7 @@ import ( "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/kms" kmsapi "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/sshagentkms" "github.com/smallstep/certificates/templates" "go.step.sm/crypto/pemutil" "golang.org/x/crypto/ssh" @@ -237,7 +239,17 @@ func (a *Authority) init() error { if err != nil { return err } - a.sshCAHostCertSignKey, err = ssh.NewSignerFromSigner(signer) + // If our signer is from sshagentkms, just unwrap it instead of + // wrapping it in another layer, and this prevents crypto from + // erroring out with: ssh: unsupported key type *agent.Key + switch s := signer.(type) { + case *sshagentkms.WrappedSSHSigner: + a.sshCAHostCertSignKey = s.Sshsigner + case crypto.Signer: + a.sshCAHostCertSignKey, err = ssh.NewSignerFromSigner(s) + default: + return errors.Errorf("unsupported signer type %T", signer) + } if err != nil { return errors.Wrap(err, "error creating ssh signer") } @@ -253,7 +265,17 @@ func (a *Authority) init() error { if err != nil { return err } - a.sshCAUserCertSignKey, err = ssh.NewSignerFromSigner(signer) + // If our signer is from sshagentkms, just unwrap it instead of + // wrapping it in another layer, and this prevents crypto from + // erroring out with: ssh: unsupported key type *agent.Key + switch s := signer.(type) { + case *sshagentkms.WrappedSSHSigner: + a.sshCAUserCertSignKey = s.Sshsigner + case crypto.Signer: + a.sshCAUserCertSignKey, err = ssh.NewSignerFromSigner(s) + default: + return errors.Errorf("unsupported signer type %T", signer) + } if err != nil { return errors.Wrap(err, "error creating ssh signer") } diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index db2f03d8..096935af 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -28,6 +28,7 @@ import ( _ "github.com/smallstep/certificates/kms/awskms" _ "github.com/smallstep/certificates/kms/cloudkms" _ "github.com/smallstep/certificates/kms/softkms" + _ "github.com/smallstep/certificates/kms/sshagentkms" // Experimental kms interfaces. _ "github.com/smallstep/certificates/kms/yubikey" diff --git a/docs/kms.md b/docs/kms.md index 53d63bed..976963c0 100644 --- a/docs/kms.md +++ b/docs/kms.md @@ -213,3 +213,25 @@ and configure the `kms` property with the `type` and your `pin` in it. ... } ``` + +## SSHAgentKMS + +SSHAgentKMS is a KMS that wrapps a ssh-agent which has access to the keys to +sign ssh certificates. This was primarly written to be able to use gpg-agent +to provide the keys stored in a YubiKeys openpgp interface. + +```json +{ + "kms": { + "type": "sshagentkms" + }, + "ssh": { + "hostKey": "sshagentkms:cardno:000123456789", + "userKey": "sshagentkms:cardno:000123456789", + }, + ... +} +``` + +This KMS requires that "root", "crt" and "key" are stored in plain files as for +SoftKMS. diff --git a/go.sum b/go.sum index d5aac68d..cc71da4b 100644 --- a/go.sum +++ b/go.sum @@ -328,7 +328,6 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -337,7 +336,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -353,7 +351,6 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -365,7 +362,6 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= @@ -373,9 +369,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= @@ -418,7 +412,6 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200828194041-157a740278f4 h1:kCCpuwSAoYJPkNc6x0xT9yTtV4oKtARo4RGBQWOfg9E= golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= @@ -426,7 +419,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -472,10 +464,8 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 h1:ZB1XYzdDo7c/O48jzjMkvIjnC120Z9/CwgDWhePjQdQ= golang.org/x/tools v0.0.0-20201017001424-6003fad69a88/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -488,7 +478,6 @@ google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEn google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= @@ -498,7 +487,6 @@ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.33.0 h1:+gL0XvACeMIvpwLZ5rQZzLn5cwOsgg8dIcfJ2SYfBVw= google.golang.org/api v0.33.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= @@ -506,7 +494,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -521,7 +508,6 @@ google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBr google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb h1:ADPHZzpzM4tk4V4S5cnCrr5SwzvlrPRmqqCuJDB8UTs= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= @@ -539,9 +525,7 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154 h1:bFFRpT+e8JJVY7lMMfvezL1ZIwqiwmPl2bsE2yx4HqM= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -550,16 +534,13 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index 76a79563..b7845109 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -52,6 +52,8 @@ const ( PKCS11 Type = "pkcs11" // YubiKey is a KMS implementation using a YubiKey PIV. YubiKey Type = "yubikey" + // SSHAgentKMS is a KMS implementation using ssh-agent to access keys. + SSHAgentKMS Type = "sshagentkms" ) // Options are the KMS options. They represent the kms object in the ca.json. @@ -91,7 +93,7 @@ func (o *Options) Validate() error { } switch Type(strings.ToLower(o.Type)) { - case DefaultKMS, SoftKMS, CloudKMS, AmazonKMS: + case DefaultKMS, SoftKMS, CloudKMS, AmazonKMS, SSHAgentKMS: case YubiKey: case PKCS11: return ErrNotImplemented{"support for PKCS11 is not yet implemented"} diff --git a/kms/apiv1/options_test.go b/kms/apiv1/options_test.go index 47ba3ec0..150dd17b 100644 --- a/kms/apiv1/options_test.go +++ b/kms/apiv1/options_test.go @@ -14,6 +14,7 @@ func TestOptions_Validate(t *testing.T) { {"softkms", &Options{Type: "softkms"}, false}, {"cloudkms", &Options{Type: "cloudkms"}, false}, {"awskms", &Options{Type: "awskms"}, false}, + {"sshagentkms", &Options{Type: "sshagentkms"}, false}, {"pkcs11", &Options{Type: "pkcs11"}, true}, {"unsupported", &Options{Type: "unsupported"}, true}, } diff --git a/kms/sshagentkms/sshagentkms.go b/kms/sshagentkms/sshagentkms.go new file mode 100644 index 00000000..b3627a08 --- /dev/null +++ b/kms/sshagentkms/sshagentkms.go @@ -0,0 +1,206 @@ +package sshagentkms + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "io" + "net" + "os" + "strings" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + + "go.step.sm/crypto/pemutil" +) + +// SSHAgentKMS is a key manager that uses keys provided by ssh-agent +type SSHAgentKMS struct { + agentClient agent.Agent +} + +// New returns a new SSHAgentKMS. +func New(ctx context.Context, opts apiv1.Options) (*SSHAgentKMS, error) { + socket := os.Getenv("SSH_AUTH_SOCK") + conn, err := net.Dial("unix", socket) + if err != nil { + return nil, errors.Wrap(err, "failed to open SSH_AUTH_SOCK") + } + + agentClient := agent.NewClient(conn) + + return &SSHAgentKMS{ + agentClient: agentClient, + }, nil +} + +// NewFromAgent initializes an SSHAgentKMS from a given agent, this method is +// used for testing purposes. +func NewFromAgent(ctx context.Context, opts apiv1.Options, agentClient agent.Agent) (*SSHAgentKMS, error) { + return &SSHAgentKMS{ + agentClient: agentClient, + }, nil +} + +func init() { + apiv1.Register(apiv1.SSHAgentKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + return New(ctx, opts) + }) +} + +// Close closes the agent. This is a noop for the SSHAgentKMS. +func (k *SSHAgentKMS) Close() error { + return nil +} + +// WrappedSSHSigner is a utility type to wrap a ssh.Signer as a crypto.Signer +type WrappedSSHSigner struct { + Sshsigner ssh.Signer +} + +// Public returns the agent public key. The type of this public key is +// *agent.Key. +func (s *WrappedSSHSigner) Public() crypto.PublicKey { + return s.Sshsigner.PublicKey() +} + +// Sign signs the given digest using the ssh agent and returns the signature. +func (s *WrappedSSHSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + sig, err := s.Sshsigner.Sign(rand, digest) + if err != nil { + return nil, err + } + return sig.Blob, nil +} + +// NewWrappedSignerFromSSHSigner returns a new crypto signer wrapping the given +// one. +func NewWrappedSignerFromSSHSigner(signer ssh.Signer) crypto.Signer { + return &WrappedSSHSigner{signer} +} + +func (k *SSHAgentKMS) findKey(signingKey string) (target int, err error) { + if strings.HasPrefix(signingKey, "sshagentkms:") { + var key = strings.TrimPrefix(signingKey, "sshagentkms:") + + l, err := k.agentClient.List() + if err != nil { + return -1, err + } + for i, s := range l { + if s.Comment == key { + return i, nil + } + } + } + + return -1, errors.Errorf("SSHAgentKMS couldn't find %s", signingKey) +} + +// CreateSigner returns a new signer configured with the given signing key. +func (k *SSHAgentKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + if req.Signer != nil { + return req.Signer, nil + } + if strings.HasPrefix(req.SigningKey, "sshagentkms:") { + target, err := k.findKey(req.SigningKey) + + if err != nil { + return nil, err + } + s, err := k.agentClient.Signers() + if err != nil { + return nil, err + } + return NewWrappedSignerFromSSHSigner(s[target]), nil + } + // OK: We don't actually care about non-ssh certificates, + // but we can't disable it in step-ca so this code is copy-pasted from + // softkms just to keep step-ca happy. + var opts []pemutil.Options + if req.Password != nil { + opts = append(opts, pemutil.WithPassword(req.Password)) + } + switch { + case len(req.SigningKeyPEM) != 0: + v, err := pemutil.ParseKey(req.SigningKeyPEM, opts...) + if err != nil { + return nil, err + } + sig, ok := v.(crypto.Signer) + if !ok { + return nil, errors.New("signingKeyPEM is not a crypto.Signer") + } + return sig, nil + case req.SigningKey != "": + v, err := pemutil.Read(req.SigningKey, opts...) + if err != nil { + return nil, err + } + sig, ok := v.(crypto.Signer) + if !ok { + return nil, errors.New("signingKey is not a crypto.Signer") + } + return sig, nil + default: + return nil, errors.New("failed to load softKMS: please define signingKeyPEM or signingKey") + } +} + +// CreateKey generates a new key and returns both public and private key. +func (k *SSHAgentKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + return nil, errors.Errorf("SSHAgentKMS doesn't support generating keys") +} + +// GetPublicKey returns the public key from the file passed in the request name. +func (k *SSHAgentKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + var v crypto.PublicKey + if strings.HasPrefix(req.Name, "sshagentkms:") { + target, err := k.findKey(req.Name) + + if err != nil { + return nil, err + } + + s, err := k.agentClient.Signers() + if err != nil { + return nil, err + } + + sshPub := s[target].PublicKey() + + sshPubBytes := sshPub.Marshal() + + parsed, err := ssh.ParsePublicKey(sshPubBytes) + if err != nil { + return nil, err + } + + parsedCryptoKey := parsed.(ssh.CryptoPublicKey) + + // Then, we can call CryptoPublicKey() to get the actual crypto.PublicKey + v = parsedCryptoKey.CryptoPublicKey() + } else { + var err error + v, err = pemutil.Read(req.Name) + if err != nil { + return nil, err + } + } + + switch vv := v.(type) { + case *x509.Certificate: + return vv.PublicKey, nil + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + return vv, nil + default: + return nil, errors.Errorf("unsupported public key type %T", v) + } +} diff --git a/kms/sshagentkms/sshagentkms_test.go b/kms/sshagentkms/sshagentkms_test.go new file mode 100644 index 00000000..4c572530 --- /dev/null +++ b/kms/sshagentkms/sshagentkms_test.go @@ -0,0 +1,608 @@ +package sshagentkms + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "io/ioutil" + "net" + "os" + "os/exec" + "path/filepath" + "reflect" + "strconv" + "strings" + "testing" + + "github.com/smallstep/certificates/kms/apiv1" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + + "go.step.sm/crypto/pemutil" +) + +// Some helpers with inspiration from crypto/ssh/agent/client_test.go + +// startOpenSSHAgent executes ssh-agent, and returns an Agent interface to it. +func startOpenSSHAgent(t *testing.T) (client agent.Agent, socket string, cleanup func()) { + /* Always test with OpenSSHAgent + if testing.Short() { + // ssh-agent is not always available, and the key + // types supported vary by platform. + t.Skip("skipping test due to -short") + } + */ + + bin, err := exec.LookPath("ssh-agent") + if err != nil { + t.Skip("could not find ssh-agent") + } + + cmd := exec.Command(bin, "-s") + cmd.Env = []string{} // Do not let the user's environment influence ssh-agent behavior. + cmd.Stderr = new(bytes.Buffer) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%s failed: %v\n%s", strings.Join(cmd.Args, " "), err, cmd.Stderr) + } + + // Output looks like: + // + // SSH_AUTH_SOCK=/tmp/ssh-P65gpcqArqvH/agent.15541; export SSH_AUTH_SOCK; + // SSH_AGENT_PID=15542; export SSH_AGENT_PID; + // echo Agent pid 15542; + + fields := bytes.Split(out, []byte(";")) + line := bytes.SplitN(fields[0], []byte("="), 2) + line[0] = bytes.TrimLeft(line[0], "\n") + if string(line[0]) != "SSH_AUTH_SOCK" { + t.Fatalf("could not find key SSH_AUTH_SOCK in %q", fields[0]) + } + socket = string(line[1]) + + line = bytes.SplitN(fields[2], []byte("="), 2) + line[0] = bytes.TrimLeft(line[0], "\n") + if string(line[0]) != "SSH_AGENT_PID" { + t.Fatalf("could not find key SSH_AGENT_PID in %q", fields[2]) + } + pidStr := line[1] + pid, err := strconv.Atoi(string(pidStr)) + if err != nil { + t.Fatalf("Atoi(%q): %v", pidStr, err) + } + + conn, err := net.Dial("unix", string(socket)) + if err != nil { + t.Fatalf("net.Dial: %v", err) + } + + ac := agent.NewClient(conn) + return ac, socket, func() { + proc, _ := os.FindProcess(pid) + if proc != nil { + proc.Kill() + } + conn.Close() + os.RemoveAll(filepath.Dir(socket)) + } +} + +func startAgent(t *testing.T, sshagent agent.Agent) (client agent.Agent, cleanup func()) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + go agent.ServeAgent(sshagent, c2) + + return agent.NewClient(c1), func() { + c1.Close() + c2.Close() + } +} + +// startKeyringAgent uses Keyring to simulate a ssh-agent Server and returns a client. +func startKeyringAgent(t *testing.T) (client agent.Agent, cleanup func()) { + return startAgent(t, agent.NewKeyring()) +} + +type startTestAgentFunc func(t *testing.T, keysToAdd ...agent.AddedKey) (sshagent agent.Agent) + +func startTestOpenSSHAgent(t *testing.T, keysToAdd ...agent.AddedKey) (sshagent agent.Agent) { + sshagent, _, cleanup := startOpenSSHAgent(t) + for _, keyToAdd := range keysToAdd { + err := sshagent.Add(keyToAdd) + if err != nil { + t.Fatalf("sshagent.add: %v", err) + } + } + t.Cleanup(cleanup) + + //testAgentInterface(t, sshagent, key, cert, lifetimeSecs) + return sshagent +} + +func startTestKeyringAgent(t *testing.T, keysToAdd ...agent.AddedKey) (sshagent agent.Agent) { + sshagent, cleanup := startKeyringAgent(t) + for _, keyToAdd := range keysToAdd { + err := sshagent.Add(keyToAdd) + if err != nil { + t.Fatalf("sshagent.add: %v", err) + } + } + t.Cleanup(cleanup) + + //testAgentInterface(t, agent, key, cert, lifetimeSecs) + return sshagent +} + +// netPipe is analogous to net.Pipe, but it uses a real net.Conn, and +// therefore is buffered (net.Pipe deadlocks if both sides start with +// a write.) +func netPipe() (net.Conn, net.Conn, error) { + listener, err := netListener() + if err != nil { + return nil, nil, err + } + defer listener.Close() + c1, err := net.Dial("tcp", listener.Addr().String()) + if err != nil { + return nil, nil, err + } + + c2, err := listener.Accept() + if err != nil { + c1.Close() + return nil, nil, err + } + + return c1, c2, nil +} + +// netListener creates a localhost network listener. +func netListener() (net.Listener, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + listener, err = net.Listen("tcp", "[::1]:0") + if err != nil { + return nil, err + } + } + return listener, nil +} + +func TestNew(t *testing.T) { + comment := "Key from OpenSSHAgent" + // Ensure we don't "inherit" any SSH_AUTH_SOCK + os.Unsetenv("SSH_AUTH_SOCK") + + sshagent, socket, cleanup := startOpenSSHAgent(t) + + os.Setenv("SSH_AUTH_SOCK", socket) + t.Cleanup(func() { + os.Unsetenv("SSH_AUTH_SOCK") + cleanup() + }) + + // Test that we can't find any signers in the agent before we have loaded them + t.Run("No keys with OpenSSHAgent", func(t *testing.T) { + kms, err := New(context.Background(), apiv1.Options{}) + if kms == nil || err != nil { + t.Errorf("New() = %v, %v", kms, err) + } + signer, err := kms.CreateSigner(&apiv1.CreateSignerRequest{SigningKey: "sshagentkms:" + comment}) + if err == nil || signer != nil { + t.Errorf("SSHAgentKMS.CreateSigner() error = \"%v\", signer = \"%v\"", err, signer) + } + }) + + // Load ssh test fixtures + b, err := ioutil.ReadFile("testdata/ssh") + if err != nil { + t.Fatal(err) + } + privateKey, err := ssh.ParseRawPrivateKey(b) + if err != nil { + t.Fatal(err) + } + + // And add that key to the agent + err = sshagent.Add(agent.AddedKey{PrivateKey: privateKey, Comment: comment}) + if err != nil { + t.Fatalf("sshagent.add: %v", err) + } + + // And test that we can find it when it's loaded + t.Run("Keys with OpenSSHAgent", func(t *testing.T) { + kms, err := New(context.Background(), apiv1.Options{}) + if kms == nil || err != nil { + t.Errorf("New() = %v, %v", kms, err) + } + signer, err := kms.CreateSigner(&apiv1.CreateSignerRequest{SigningKey: "sshagentkms:" + comment}) + if err != nil || signer == nil { + t.Errorf("SSHAgentKMS.CreateSigner() error = \"%v\", signer = \"%v\"", err, signer) + } + }) +} + +func TestNewFromAgent(t *testing.T) { + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + args args + sshagentstarter startTestAgentFunc + wantErr bool + }{ + {"ok OpenSSHAgent", args{context.Background(), apiv1.Options{}}, startTestOpenSSHAgent, false}, + {"ok KeyringAgent", args{context.Background(), apiv1.Options{}}, startTestKeyringAgent, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewFromAgent(tt.args.ctx, tt.args.opts, tt.sshagentstarter(t)) + if (err != nil) != tt.wantErr { + t.Errorf("NewFromAgent() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got == nil { + t.Errorf("NewFromAgent() = %v", got) + } + }) + } +} + +func TestSSHAgentKMS_Close(t *testing.T) { + tests := []struct { + name string + wantErr bool + }{ + {"ok", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SSHAgentKMS{} + if err := k.Close(); (err != nil) != tt.wantErr { + t.Errorf("SSHAgentKMS.Close() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSSHAgentKMS_CreateSigner(t *testing.T) { + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pemBlock, err := pemutil.Serialize(pk) + if err != nil { + t.Fatal(err) + } + pemBlockPassword, err := pemutil.Serialize(pk, pemutil.WithPassword([]byte("pass"))) + if err != nil { + t.Fatal(err) + } + + // Read and decode file using standard packages + b, err := ioutil.ReadFile("testdata/priv.pem") + if err != nil { + t.Fatal(err) + } + block, _ := pem.Decode(b) + block.Bytes, err = x509.DecryptPEMBlock(block, []byte("pass")) + if err != nil { + t.Fatal(err) + } + pk2, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + t.Fatal(err) + } + + // Create a public PEM + b, err = x509.MarshalPKIXPublicKey(pk.Public()) + if err != nil { + t.Fatal(err) + } + pub := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: b, + }) + + // Load ssh test fixtures + sshPubKeyStr, err := ioutil.ReadFile("testdata/ssh.pub") + if err != nil { + t.Fatal(err) + } + _, comment, _, _, err := ssh.ParseAuthorizedKey(sshPubKeyStr) + if err != nil { + t.Fatal(err) + } + b, err = ioutil.ReadFile("testdata/ssh") + if err != nil { + t.Fatal(err) + } + privateKey, err := ssh.ParseRawPrivateKey(b) + if err != nil { + t.Fatal(err) + } + sshPrivateKey, err := ssh.NewSignerFromKey(privateKey) + if err != nil { + t.Fatal(err) + } + wrappedSSHPrivateKey := NewWrappedSignerFromSSHSigner(sshPrivateKey) + + type args struct { + req *apiv1.CreateSignerRequest + } + tests := []struct { + name string + args args + want crypto.Signer + wantErr bool + }{ + {"signer", args{&apiv1.CreateSignerRequest{Signer: pk}}, pk, false}, + {"pem", args{&apiv1.CreateSignerRequest{SigningKeyPEM: pem.EncodeToMemory(pemBlock)}}, pk, false}, + {"pem password", args{&apiv1.CreateSignerRequest{SigningKeyPEM: pem.EncodeToMemory(pemBlockPassword), Password: []byte("pass")}}, pk, false}, + {"file", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/priv.pem", Password: []byte("pass")}}, pk2, false}, + {"sshagent", args{&apiv1.CreateSignerRequest{SigningKey: "sshagentkms:" + comment}}, wrappedSSHPrivateKey, false}, + {"sshagent Nonexistant", args{&apiv1.CreateSignerRequest{SigningKey: "sshagentkms:Nonexistant"}}, nil, true}, + {"fail", args{&apiv1.CreateSignerRequest{}}, nil, true}, + {"fail bad pem", args{&apiv1.CreateSignerRequest{SigningKeyPEM: []byte("bad pem")}}, nil, true}, + {"fail bad password", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/priv.pem", Password: []byte("bad-pass")}}, nil, true}, + {"fail not a signer", args{&apiv1.CreateSignerRequest{SigningKeyPEM: pub}}, nil, true}, + {"fail not a signer from file", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/pub.pem"}}, nil, true}, + {"fail missing", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/missing"}}, nil, true}, + } + starters := []struct { + name string + starter startTestAgentFunc + }{ + {"startTestOpenSSHAgent", startTestOpenSSHAgent}, + {"startTestKeyringAgent", startTestKeyringAgent}, + } + for _, starter := range starters { + k, err := NewFromAgent(context.Background(), apiv1.Options{}, starter.starter(t, agent.AddedKey{PrivateKey: privateKey, Comment: comment})) + if err != nil { + t.Fatal(err) + } + for _, tt := range tests { + t.Run(starter.name+"/"+tt.name, func(t *testing.T) { + got, err := k.CreateSigner(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("SSHAgentKMS.CreateSigner() error = %v, wantErr %v", err, tt.wantErr) + return + } + switch s := got.(type) { + case *WrappedSSHSigner: + gotPkS := s.Sshsigner.PublicKey().(*agent.Key).String() + "\n" + wantPkS := string(sshPubKeyStr) + if !reflect.DeepEqual(gotPkS, wantPkS) { + t.Errorf("SSHAgentKMS.CreateSigner() = %T, want %T", gotPkS, wantPkS) + t.Errorf("SSHAgentKMS.CreateSigner() = %v, want %v", gotPkS, wantPkS) + } + default: + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SSHAgentKMS.CreateSigner() = %T, want %T", got, tt.want) + t.Errorf("SSHAgentKMS.CreateSigner() = %v, want %v", got, tt.want) + } + } + }) + } + } +} + +/* +func restoreGenerateKey() func() { + oldGenerateKey := generateKey + return func() { + generateKey = oldGenerateKey + } +} +*/ + +/* +func TestSSHAgentKMS_CreateKey(t *testing.T) { + fn := restoreGenerateKey() + defer fn() + + p256, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + rsa2048, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + edpub, edpriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + type args struct { + req *apiv1.CreateKeyRequest + } + type params struct { + kty string + crv string + size int + } + tests := []struct { + name string + args args + generateKey func() (interface{}, interface{}, error) + want *apiv1.CreateKeyResponse + wantParams params + wantErr bool + }{ + {"p256", args{&apiv1.CreateKeyRequest{Name: "p256", SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, func() (interface{}, interface{}, error) { + return p256.Public(), p256, nil + }, &apiv1.CreateKeyResponse{Name: "p256", PublicKey: p256.Public(), PrivateKey: p256, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: p256}}, params{"EC", "P-256", 0}, false}, + {"rsa", args{&apiv1.CreateKeyRequest{Name: "rsa3072", SignatureAlgorithm: apiv1.SHA256WithRSA}}, func() (interface{}, interface{}, error) { + return rsa2048.Public(), rsa2048, nil + }, &apiv1.CreateKeyResponse{Name: "rsa3072", PublicKey: rsa2048.Public(), PrivateKey: rsa2048, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: rsa2048}}, params{"RSA", "", 0}, false}, + {"rsa2048", args{&apiv1.CreateKeyRequest{Name: "rsa2048", SignatureAlgorithm: apiv1.SHA256WithRSA, Bits: 2048}}, func() (interface{}, interface{}, error) { + return rsa2048.Public(), rsa2048, nil + }, &apiv1.CreateKeyResponse{Name: "rsa2048", PublicKey: rsa2048.Public(), PrivateKey: rsa2048, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: rsa2048}}, params{"RSA", "", 2048}, false}, + {"rsaPSS2048", args{&apiv1.CreateKeyRequest{Name: "rsa2048", SignatureAlgorithm: apiv1.SHA256WithRSAPSS, Bits: 2048}}, func() (interface{}, interface{}, error) { + return rsa2048.Public(), rsa2048, nil + }, &apiv1.CreateKeyResponse{Name: "rsa2048", PublicKey: rsa2048.Public(), PrivateKey: rsa2048, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: rsa2048}}, params{"RSA", "", 2048}, false}, + {"ed25519", args{&apiv1.CreateKeyRequest{Name: "ed25519", SignatureAlgorithm: apiv1.PureEd25519}}, func() (interface{}, interface{}, error) { + return edpub, edpriv, nil + }, &apiv1.CreateKeyResponse{Name: "ed25519", PublicKey: edpub, PrivateKey: edpriv, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: edpriv}}, params{"OKP", "Ed25519", 0}, false}, + {"default", args{&apiv1.CreateKeyRequest{Name: "default"}}, func() (interface{}, interface{}, error) { + return p256.Public(), p256, nil + }, &apiv1.CreateKeyResponse{Name: "default", PublicKey: p256.Public(), PrivateKey: p256, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: p256}}, params{"EC", "P-256", 0}, false}, + {"fail algorithm", args{&apiv1.CreateKeyRequest{Name: "fail", SignatureAlgorithm: apiv1.SignatureAlgorithm(100)}}, func() (interface{}, interface{}, error) { + return p256.Public(), p256, nil + }, nil, params{}, true}, + {"fail generate key", args{&apiv1.CreateKeyRequest{Name: "fail", SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, func() (interface{}, interface{}, error) { + return nil, nil, fmt.Errorf("an error") + }, nil, params{"EC", "P-256", 0}, true}, + {"fail no signer", args{&apiv1.CreateKeyRequest{Name: "fail", SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, func() (interface{}, interface{}, error) { + return 1, 2, nil + }, nil, params{"EC", "P-256", 0}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SSHAgentKMS{} + generateKey = func(kty, crv string, size int) (interface{}, interface{}, error) { + if tt.wantParams.kty != kty { + t.Errorf("GenerateKey() kty = %s, want %s", kty, tt.wantParams.kty) + } + if tt.wantParams.crv != crv { + t.Errorf("GenerateKey() crv = %s, want %s", crv, tt.wantParams.crv) + } + if tt.wantParams.size != size { + t.Errorf("GenerateKey() size = %d, want %d", size, tt.wantParams.size) + } + return tt.generateKey() + } + + got, err := k.CreateKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("SSHAgentKMS.CreateKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SSHAgentKMS.CreateKey() = %v, want %v", got, tt.want) + } + }) + } +} +*/ + +func TestSSHAgentKMS_GetPublicKey(t *testing.T) { + b, err := ioutil.ReadFile("testdata/pub.pem") + if err != nil { + t.Fatal(err) + } + block, _ := pem.Decode(b) + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + t.Fatal(err) + } + + // Load ssh test fixtures + b, err = ioutil.ReadFile("testdata/ssh.pub") + if err != nil { + t.Fatal(err) + } + sshPubKey, comment, _, _, err := ssh.ParseAuthorizedKey(b) + if err != nil { + t.Fatal(err) + } + b, err = ioutil.ReadFile("testdata/ssh") + if err != nil { + t.Fatal(err) + } + // crypto.PrivateKey + sshPrivateKey, err := ssh.ParseRawPrivateKey(b) + if err != nil { + t.Fatal(err) + } + + type args struct { + req *apiv1.GetPublicKeyRequest + } + tests := []struct { + name string + args args + want crypto.PublicKey + wantErr bool + }{ + {"key", args{&apiv1.GetPublicKeyRequest{Name: "testdata/pub.pem"}}, pub, false}, + {"cert", args{&apiv1.GetPublicKeyRequest{Name: "testdata/cert.crt"}}, pub, false}, + {"sshagent", args{&apiv1.GetPublicKeyRequest{Name: "sshagentkms:" + comment}}, sshPubKey, false}, + {"sshagent Nonexistant", args{&apiv1.GetPublicKeyRequest{Name: "sshagentkms:Nonexistant"}}, nil, true}, + {"fail not exists", args{&apiv1.GetPublicKeyRequest{Name: "testdata/missing"}}, nil, true}, + {"fail type", args{&apiv1.GetPublicKeyRequest{Name: "testdata/cert.key"}}, nil, true}, + } + starters := []struct { + name string + starter startTestAgentFunc + }{ + {"startTestOpenSSHAgent", startTestOpenSSHAgent}, + {"startTestKeyringAgent", startTestKeyringAgent}, + } + for _, starter := range starters { + k, err := NewFromAgent(context.Background(), apiv1.Options{}, starter.starter(t, agent.AddedKey{PrivateKey: sshPrivateKey, Comment: comment})) + if err != nil { + t.Fatal(err) + } + for _, tt := range tests { + t.Run(starter.name+"/"+tt.name, func(t *testing.T) { + got, err := k.GetPublicKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("SSHAgentKMS.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + switch tt.want.(type) { + case ssh.PublicKey: + // If we want a ssh.PublicKey, protote got to a + got, err = ssh.NewPublicKey(got) + if err != nil { + t.Fatal(err) + } + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SSHAgentKMS.GetPublicKey() = %T, want %T", got, tt.want) + t.Errorf("SSHAgentKMS.GetPublicKey() = %v, want %v", got, tt.want) + } + }) + } + } +} + +func TestSSHAgentKMS_CreateKey(t *testing.T) { + starters := []struct { + name string + starter startTestAgentFunc + }{ + {"startTestOpenSSHAgent", startTestOpenSSHAgent}, + {"startTestKeyringAgent", startTestKeyringAgent}, + } + for _, starter := range starters { + k, err := NewFromAgent(context.Background(), apiv1.Options{}, starter.starter(t)) + if err != nil { + t.Fatal(err) + } + t.Run(starter.name+"/CreateKey", func(t *testing.T) { + got, err := k.CreateKey(&apiv1.CreateKeyRequest{ + Name: "sshagentkms:0", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if got != nil { + t.Error("SSHAgentKMS.CreateKey() shoudn't return a value") + } + if err == nil { + t.Error("SSHAgentKMS.CreateKey() didn't return a value") + } + }) + } +} diff --git a/kms/sshagentkms/testdata/cert.crt b/kms/sshagentkms/testdata/cert.crt new file mode 100644 index 00000000..d6f02b21 --- /dev/null +++ b/kms/sshagentkms/testdata/cert.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBpzCCAU2gAwIBAgIQWaY8KIDAfak8aYljelf8eTAKBggqhkjOPQQDAjAdMRsw +GQYDVQQDExJ0ZXN0LnNtYWxsc3RlcC5jb20wHhcNMjAwMTE2MDAwNDU4WhcNMjAw +MTE3MDAwNDU4WjAdMRswGQYDVQQDExJ0ZXN0LnNtYWxsc3RlcC5jb20wWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAATlU8P9blFefSWuzYx2g215NJn6yHW95PXeFqQ9 +kX1jNo1VmC6Oord3We37iM8QJT4QP9ZDUaAVmJUZSjd+W8H/o28wbTAOBgNVHQ8B +Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW +BBTn0wonKkm2lLRNYZrKhUukiynvqzAdBgNVHREEFjAUghJ0ZXN0LnNtYWxsc3Rl +cC5jb20wCgYIKoZIzj0EAwIDSAAwRQIhAJ5XqryBIY1X4fl/9l0isV69eQfA0Qo5 +1mjervUcEnOWAiBsmN4frz5YVw7i4UXChVBeZLZfJOKvn5eyh2gEzoq1+w== +-----END CERTIFICATE----- diff --git a/kms/sshagentkms/testdata/cert.key b/kms/sshagentkms/testdata/cert.key new file mode 100644 index 00000000..187713cd --- /dev/null +++ b/kms/sshagentkms/testdata/cert.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICB6lIrMa9fVQJtdAYS4qmdYQ1BHJsEQDx8zxL38gA8toAoGCCqGSM49 +AwEHoUQDQgAE5VPD/W5RXn0lrs2MdoNteTSZ+sh1veT13hakPZF9YzaNVZgujqK3 +d1nt+4jPECU+ED/WQ1GgFZiVGUo3flvB/w== +-----END EC PRIVATE KEY----- diff --git a/kms/sshagentkms/testdata/priv.pem b/kms/sshagentkms/testdata/priv.pem new file mode 100644 index 00000000..81116ce7 --- /dev/null +++ b/kms/sshagentkms/testdata/priv.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,1fcec5dfbf3327f61bfe5ab6ae8a0626 + +V39b/pNHMbP80TXSHLsUY6UOTCzf3KwIxvj1e7S9brNMJJc9b3UiloMBJIYBkl00 +NKI8JU4jSlcerR58DqsTHIELiX6a+RJLe3/iR2/5Gru+CmmWJ68jQu872WCgh6Ms +o8TzhyGx74ETmdKn5CdtylsnKMa9heW3tBLFAbNCgKc= +-----END EC PRIVATE KEY----- diff --git a/kms/sshagentkms/testdata/pub.pem b/kms/sshagentkms/testdata/pub.pem new file mode 100644 index 00000000..e31e583e --- /dev/null +++ b/kms/sshagentkms/testdata/pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5VPD/W5RXn0lrs2MdoNteTSZ+sh1 +veT13hakPZF9YzaNVZgujqK3d1nt+4jPECU+ED/WQ1GgFZiVGUo3flvB/w== +-----END PUBLIC KEY----- diff --git a/kms/sshagentkms/testdata/ssh b/kms/sshagentkms/testdata/ssh new file mode 100644 index 00000000..3a2ac73d --- /dev/null +++ b/kms/sshagentkms/testdata/ssh @@ -0,0 +1,49 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/kms/sshagentkms/testdata/ssh.pub b/kms/sshagentkms/testdata/ssh.pub new file mode 100644 index 00000000..35673a99 --- /dev/null +++ b/kms/sshagentkms/testdata/ssh.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC2H93vNENu/nrujjopPphioL8RV0OlnybSvMh2ugkv5EdUNc1DWlTeWsL0hZ+TKNf5jA72McZuton4KCicCSXfqa0vM0LjrjlAZLb9Z0WpFHqZALPu/gbqc5oU1IJ4SsyGDR+fh/uN2pel3SFijMimS1FU1hymLZEyavx78pp2vkRVB9thmB0oiXU7paz07dDUGEsaL0aKkSy1RO+OrFnMXuhSBYofniPl4c1d1sWoqqcrw01CbKk0tgYOxMFAcb3NGz6jTsLZ+g7110g7Y4JqtwFZl8aVjTmT9mESasQ442HatIuEnCrD4QmBB5TQ+h78iqMXLuOdNGSL6157Z6UFhW8dg9Kh9loGZfuLbWzx4FGX9mIXHeKnoTczniZe8pntK4Daqr9hbQh4rInkNK4vT6xEDKJJDYhyzJvdgq/oteiO4lOYn0eD2/j5o4/E5CG81+HdvuwiH9ybPV2r9tpsvI45GtscoPEgkwTtU8yymZ11X3AmAHga7V5Y6JH8K6/qOphZN+JYkMiiplEPmcXW0Woq1nmICqR+W/gpujXS2/qdOVdHpNa4+7Zn+lmnH4037tDFVRKVGDFvtFNVwdG+mB3RM0RcZ25HxqtZUOU17A6Z/koWuhbUhf/JHU68TlGwOLHFxfs1765KWr2GijL1X8inR99wX8qDQ0mg/Cucgw== ssh.test.smallstep.com