diff --git a/go-ssh-keysign/go.mod b/go-ssh-keysign/go.mod index 4600e8a..84d2921 100644 --- a/go-ssh-keysign/go.mod +++ b/go-ssh-keysign/go.mod @@ -1,39 +1,35 @@ module binarycodes/ssh-keysign -go 1.24 +go 1.24.0 require ( github.com/mdp/qrterminal/v3 v3.2.1 github.com/mitchellh/mapstructure v1.5.0 - github.com/spf13/cobra v1.8.1 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.19.0 - github.com/stretchr/testify v1.9.0 - go.uber.org/zap v1.21.0 - golang.org/x/crypto v0.21.0 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.11.1 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.43.0 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go-ssh-keysign/go.sum b/go-ssh-keysign/go.sum index af70f5b..583d1f4 100644 --- a/go-ssh-keysign/go.sum +++ b/go-ssh-keysign/go.sum @@ -1,129 +1,67 @@ -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= -go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -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= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= diff --git a/go-ssh-keysign/host_config.sample.yml b/go-ssh-keysign/host_config.sample.yml index 4fdea6e..406d77e 100644 --- a/go-ssh-keysign/host_config.sample.yml +++ b/go-ssh-keysign/host_config.sample.yml @@ -16,7 +16,6 @@ user: host: key: "testdata/id.pub" principal: - - ip-10-0-1-23.ec2.internal - - web + - my-test-client duration: 3600 diff --git a/go-ssh-keysign/internal/apperror/errors.go b/go-ssh-keysign/internal/apperror/errors.go index f83fb67..f35e69e 100644 --- a/go-ssh-keysign/internal/apperror/errors.go +++ b/go-ssh-keysign/internal/apperror/errors.go @@ -21,6 +21,7 @@ const ( KFileSystem // read/write perms KCanceled // context canceled/deadline KHttp // http errors + KCert // cert/keys error ) type appError struct { @@ -43,6 +44,8 @@ func (kind Kind) ExitCode() int { return 13 case KHttp: return 14 + case KCert: + return 15 default: return 1 } @@ -80,6 +83,10 @@ func ErrFileSystem(err error) error { return &appError{Type: KFileSystem, OpError: err} } +func ErrCert(err error) error { + return &appError{Type: KCert, OpError: err} +} + func ErrHTTP(resp *http.Response) error { body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/go-ssh-keysign/internal/cli/testutil/helper.go b/go-ssh-keysign/internal/cli/testutil/helper.go index 0dfc105..dde95b8 100644 --- a/go-ssh-keysign/internal/cli/testutil/helper.go +++ b/go-ssh-keysign/internal/cli/testutil/helper.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "net" "os" "path/filepath" "testing" @@ -13,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" + "golang.org/x/crypto/ssh/agent" "binarycodes/ssh-keysign/internal/ctxkeys" "binarycodes/ssh-keysign/internal/logging" @@ -40,6 +42,51 @@ func ExecuteCommand(t *testing.T, cmd *cobra.Command, args ...string) (stoutStr, cmd.SetErr(&stderr) cmd.SetArgs(args) + sockPath := t.TempDir() + "/agent.sock" + l, err := net.Listen("unix", sockPath) + if err != nil { + t.Fatalf("listen unix: %v", err) + } + + defer func() { + if err := l.Close(); err != nil { + t.Fatalf("close listener: %v", err) + } + }() + + // create in-memory agent keyring + keyring := agent.NewKeyring() + + // serve connections + go func() { + for { + c, err := l.Accept() + if err != nil { + if err := l.Close(); err != nil { + return + } + return + } + go func() { + // handles a single connection until EOF. + if serveErr := agent.ServeAgent(keyring, c); serveErr != nil { + _ = serveErr + return + } + }() + } + }() + + oldSock := os.Getenv("SSH_AUTH_SOCK") + if err := os.Setenv("SSH_AUTH_SOCK", sockPath); err != nil { + t.Fatalf("setenv: %v", err) + } + defer func() { + if err := os.Setenv("SSH_AUTH_SOCK", oldSock); err != nil { + t.Logf("restore SSH_AUTH_SOCK: %v", err) + } + }() + err = cmd.Execute() logs = observed.All() return stdout.String(), stderr.String(), logs, err diff --git a/go-ssh-keysign/internal/config/model.go b/go-ssh-keysign/internal/config/model.go index 3555238..44337eb 100644 --- a/go-ssh-keysign/internal/config/model.go +++ b/go-ssh-keysign/internal/config/model.go @@ -1,12 +1,14 @@ package config import ( + "errors" "fmt" "os" "path/filepath" "strings" "binarycodes/ssh-keysign/internal/apperror" + "binarycodes/ssh-keysign/internal/service/utilities" ) type OAuth struct { @@ -66,14 +68,11 @@ func (c *Config) ValidateHost() error { return err } - return ValidateKeyFile(c.Host.Key) + return ValidateKeyFile(c.Host.Key, false) } func (c *Config) ValidateUser() error { var missing []string - if c.User.Key == "" { - missing = append(missing, "--key") - } if len(c.User.Principals) == 0 { missing = append(missing, "--principal") @@ -98,7 +97,11 @@ func (c *Config) ValidateUser() error { } } - return ValidateKeyFile(c.User.Key) + if c.User.Key != "" { + return ValidateSSHAgent() + } + + return ValidateKeyFile(c.User.Key, true) } func ValidateDeviceFlow(o OAuth) error { @@ -149,15 +152,32 @@ func ValidateClientCredential(o OAuth, required bool) (bool, error) { return len(missing) == 0, nil } -func ValidateKeyFile(keyfilePath string) error { - extension := filepath.Ext(keyfilePath) +func ValidateKeyFile(keyfilePath string, opt bool) error { + if opt && keyfilePath == "" { + return nil + } + + expandedKeyFilePath, err := utilities.NormalizePath(keyfilePath) + if err != nil { + return apperror.ErrFileSystem(err) + } + + extension := filepath.Ext(expandedKeyFilePath) if extension != ".pub" { return apperror.ErrUsage("only public key files are expected here. [Hint: name ending in .pub]") } - if _, err := os.Stat(keyfilePath); err != nil { + if _, err := os.Stat(expandedKeyFilePath); err != nil { return apperror.ErrFileSystem(err) } return nil } + +func ValidateSSHAgent() error { + sock := os.Getenv("SSH_AUTH_SOCK") + if sock == "" { + return apperror.ErrCert(errors.New("SSH_AUTH_SOCK not set; is ssh-agent running ?")) + } + return nil +} diff --git a/go-ssh-keysign/internal/constants/constants.go b/go-ssh-keysign/internal/constants/constants.go index c561a7a..a52e91f 100644 --- a/go-ssh-keysign/internal/constants/constants.go +++ b/go-ssh-keysign/internal/constants/constants.go @@ -9,9 +9,11 @@ const ( defaultDurationForHostKeyInDays time.Duration = 365 defaultDurationForUserKeyInMinutes time.Duration = 30 - AppName string = "ssh-keysign" - ConfigFileName string = "config.yml" - EtcDir string = "/etc" + AppName string = "ssh-keysign" + ConfigFileName string = "config.yml" + EtcDir string = "/etc" + UserSSHDir string = "~/.ssh" + ConfirmCertBeforeUse bool = false ) func DefaultDurationForHostKey() uint64 { diff --git a/go-ssh-keysign/internal/service/cacert/cacertclient.go b/go-ssh-keysign/internal/service/cacert/cacertclient.go index 2558d44..8420a7f 100644 --- a/go-ssh-keysign/internal/service/cacert/cacertclient.go +++ b/go-ssh-keysign/internal/service/cacert/cacertclient.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "net/http" - "path/filepath" "binarycodes/ssh-keysign/internal/apperror" "binarycodes/ssh-keysign/internal/config" @@ -26,9 +25,8 @@ func (CACertClient) userSignURL(cfg config.OAuth) string { func (c CACertClient) IssueUserCert(ctx context.Context, u *service.UserCertRequestConfig) (*service.SignedResponse, error) { signRequest := service.SignRequest{ - Filename: filepath.Base(u.UserConfig.Key), PublicKey: u.PubKey, - Hostname: u.UserConfig.Principals[0], + Principal: u.UserConfig.Principals[0], } postBody := new(bytes.Buffer) @@ -72,9 +70,8 @@ func (c CACertClient) IssueUserCert(ctx context.Context, u *service.UserCertRequ func (c CACertClient) IssueHostCert(ctx context.Context, h *service.HostCertRequestConfig) (*service.SignedResponse, error) { signRequest := service.SignRequest{ - Filename: filepath.Base(h.HostConfig.Key), PublicKey: h.PubKey, - Hostname: h.HostConfig.Principals[0], + Principal: h.HostConfig.Principals[0], } postBody := new(bytes.Buffer) diff --git a/go-ssh-keysign/internal/service/cacert/cacerthandler.go b/go-ssh-keysign/internal/service/cacert/cacerthandler.go index ae4a67b..42fef0c 100644 --- a/go-ssh-keysign/internal/service/cacert/cacerthandler.go +++ b/go-ssh-keysign/internal/service/cacert/cacerthandler.go @@ -2,39 +2,129 @@ package cacert import ( "context" + "errors" "fmt" + "net" "os" - "path/filepath" + "time" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" "binarycodes/ssh-keysign/internal/apperror" + "binarycodes/ssh-keysign/internal/constants" + "binarycodes/ssh-keysign/internal/ctxkeys" + "binarycodes/ssh-keysign/internal/logging" "binarycodes/ssh-keysign/internal/service" ) const ( - defaultCertFileMode os.FileMode = 0o400 + defaultCertFileMode os.FileMode = 0o600 + defaultPrivateFileMode os.FileMode = 0o600 + defaultPublicFileMode os.FileMode = 0o600 ) type CACertHandler struct{} -func (c CACertHandler) StoreUserCert(ctx context.Context, u *service.UserCertHandlerConfig) (path string, err error) { - return c.writeCertForKey(u.UserConfig.Key, u.SignedResponse) +func (c CACertHandler) StoreUserCertFile(ctx context.Context, u *service.UserCertHandlerConfig) (agentmode bool, path string, err error) { + p := ctxkeys.PrinterFrom(ctx) + + if u.Keys.KeyPair != nil { + p.V(logging.Verbose).Println("writing certificate to ssh agent") + + if err := c.StoreUserCertAgent(ctx, u); err != nil { + return true, "", err + } + + return true, "", nil + } + + certSaveFilePath, err := u.Keys.FetchCertFileName() + if err != nil { + return false, "", err + } + + p.V(logging.Verbose).Printf("writing certificate to %s\n", certSaveFilePath) + + path, err = c.writeCertForKey(certSaveFilePath, u.SignedResponse) + return false, path, err } -func (c CACertHandler) StoreHostCert(ctx context.Context, h *service.HostCertHandlerConfig) (path string, err error) { - return c.writeCertForKey(h.HostConfig.Key, h.SignedResponse) +func (c CACertHandler) StoreHostCertFile(ctx context.Context, h *service.HostCertHandlerConfig) (path string, err error) { + return c.writeCertForKey(h.CertSaveFilePath, h.SignedResponse) } func (CACertHandler) writeCertForKey(keyfilePath string, s service.SignedResponse) (path string, err error) { - dir := filepath.Dir(keyfilePath) + if err := os.WriteFile(keyfilePath, []byte(s.SignedPublicKey), defaultCertFileMode); err != nil { + return "", apperror.ErrFileSystem(fmt.Errorf("writing cert file %q: %w", path, err)) + } + + return keyfilePath, nil +} + +func (CACertHandler) StoreKeyPair(privateFilePath string, k service.ED25519KeyPair) (err error) { + publicFilePath := fmt.Sprintf("%s.pub", privateFilePath) + + if err := os.WriteFile(privateFilePath, k.PrivateKeyBytes, defaultPrivateFileMode); err != nil { + return apperror.ErrFileSystem(fmt.Errorf("writing cert file %q: %w", privateFilePath, err)) + } + + if err := os.WriteFile(publicFilePath, []byte(k.PublicKeyString), defaultPublicFileMode); err != nil { + return apperror.ErrFileSystem(fmt.Errorf("writing cert file %q: %w", publicFilePath, err)) + } + + return nil +} - path, err = filepath.Abs(filepath.Join(dir, s.Filename)) +func (CACertHandler) StoreUserCertAgent(ctx context.Context, u *service.UserCertHandlerConfig) error { + log := ctxkeys.LoggerFrom(ctx) + + sock := os.Getenv("SSH_AUTH_SOCK") + if sock == "" { + return errors.New("SSH_AUTH_SOCK not set; is ssh-agent running?") + } + + conn, err := net.Dial("unix", sock) if err != nil { - return "", apperror.ErrFileSystem(fmt.Errorf("resolving absolute path: %w", err)) + return fmt.Errorf("connect to ssh-agent: %w", err) } - if err := os.WriteFile(path, []byte(s.SignedPublicKey), defaultCertFileMode); err != nil { - return "", apperror.ErrFileSystem(fmt.Errorf("writing cert file %q: %w", path, err)) + defer func() { + if err := conn.Close(); err != nil { + log.Error(err.Error()) + } + }() + + pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(u.SignedResponse.SignedPublicKey)) + if err != nil { + return apperror.ErrCert(fmt.Errorf("parse user certificate: %w", err)) + } + + cert, ok := pub.(*ssh.Certificate) + if !ok { + return errors.New("provided blob is not an ssh Certificate") + } + + lifetime := constants.DefaultDurationForUserKey() + + now := uint64(time.Now().Unix()) + if cert.ValidBefore != ssh.CertTimeInfinity && cert.ValidBefore > now { + certDuration := uint64(time.Duration(cert.ValidBefore-now) * time.Second) + lifetime = min(lifetime, certDuration) + } + + add := agent.AddedKey{ + PrivateKey: u.Keys.KeyPair.PrivateKey, + Certificate: cert, + Comment: time.Now().String(), + LifetimeSecs: uint32(lifetime), + ConfirmBeforeUse: constants.ConfirmCertBeforeUse, + } + + ag := agent.NewClient(conn) + if err := ag.Add(add); err != nil { + return fmt.Errorf("add key+cert to ssh-agent: %w", err) } - return path, nil + return nil } diff --git a/go-ssh-keysign/internal/service/hostsvc/service.go b/go-ssh-keysign/internal/service/hostsvc/service.go index d25192a..31d74a7 100644 --- a/go-ssh-keysign/internal/service/hostsvc/service.go +++ b/go-ssh-keysign/internal/service/hostsvc/service.go @@ -38,6 +38,11 @@ func (HostService) SignHostKey(ctx context.Context, r *service.Runner) error { return apperror.ErrFileSystem(err) } + keys := &service.Keys{ + Filename: cfg.Host.Key, + PublicKey: key, + } + p.V(logging.VeryVerbose).Printf("found key type: %v | public key: %v\n", kType, key) log.Info("public key details", zap.String("type", kType), @@ -70,15 +75,18 @@ func (HostService) SignHostKey(ctx context.Context, r *service.Runner) error { } p.V(logging.VeryVerbose).Println("received signed certificate") - log.Info("signed certificate received", - zap.String("filename", signedResponse.Filename), - ) + log.Info("signed certificate received") p.V(logging.VeryVerbose).Println("storing the certificate") - path, err := r.CertHandler.StoreHostCert(ctx, &service.HostCertHandlerConfig{ - HostConfig: cfg.Host, - SignedResponse: *signedResponse, + certSaveFilePath, err := keys.FetchCertFileName() + if err != nil { + return err + } + + path, err := r.CertHandler.StoreHostCertFile(ctx, &service.HostCertHandlerConfig{ + CertSaveFilePath: certSaveFilePath, + SignedResponse: *signedResponse, }) if err != nil { return err diff --git a/go-ssh-keysign/internal/service/keys/keyshandler.go b/go-ssh-keysign/internal/service/keys/keyshandler.go index 401c065..20d0e37 100644 --- a/go-ssh-keysign/internal/service/keys/keyshandler.go +++ b/go-ssh-keysign/internal/service/keys/keyshandler.go @@ -2,37 +2,22 @@ package keys import ( "context" + "crypto/ed25519" + "crypto/rand" + "encoding/pem" "os" - "path/filepath" "strings" "golang.org/x/crypto/ssh" + + "binarycodes/ssh-keysign/internal/service" + "binarycodes/ssh-keysign/internal/service/utilities" ) type CAKeyHandler struct{} -func expandPath(p string) (string, error) { - path := p - - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - - if trimmed, found := strings.CutPrefix(p, "~"); found { - return filepath.Join(home, trimmed), nil - } - - if trimmed, found := strings.CutPrefix(p, "$HOME"); found { - return filepath.Join(home, trimmed), nil - } - - /* allow expansion of other environment variables */ - return os.ExpandEnv(path), nil -} - -func (CAKeyHandler) ReadPublicKey(ctx context.Context, path string) (keyType, pubKey string, err error) { - p, err := expandPath(path) +func (c CAKeyHandler) ReadPublicKey(ctx context.Context, path string) (keyType, pubKey string, err error) { + p, err := utilities.NormalizePath(path) if err != nil { return "", "", err } @@ -41,10 +26,10 @@ func (CAKeyHandler) ReadPublicKey(ctx context.Context, path string) (keyType, pu if err != nil { return "", "", err } - return parsePublicKey(b) + return c.parsePublicKey(b) } -func parsePublicKey(pub []byte) (keyType, pubKey string, err error) { +func (CAKeyHandler) parsePublicKey(pub []byte) (keyType, pubKey string, err error) { pk, _, _, _, err := ssh.ParseAuthorizedKey(pub) if err != nil { return "", "", err @@ -54,5 +39,34 @@ func parsePublicKey(pub []byte) (keyType, pubKey string, err error) { return pk.Type(), pubKeyStr, nil } -func (CAKeyHandler) WriteAtomic(path string, data []byte, perm uint32) error { return nil } -func (CAKeyHandler) BackupIfExists(path string) error { return nil } +func (c CAKeyHandler) NewEd25519(ctx context.Context) (*service.ED25519KeyPair, error) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + + sshPubKey, err := ssh.NewPublicKey(pub) + if err != nil { + return nil, err + } + + pubKeyMarshalled := string(ssh.MarshalAuthorizedKey(sshPubKey)) + kType, pubKeyString, err := c.parsePublicKey([]byte(pubKeyMarshalled)) + if err != nil { + return nil, err + } + + privBlock, err := ssh.MarshalPrivateKey(priv, "") + if err != nil { + return nil, err + } + + privateKeyBytes := pem.EncodeToMemory(privBlock) + + return &service.ED25519KeyPair{ + PrivateKey: &priv, + PrivateKeyBytes: privateKeyBytes, + PublicKeyString: pubKeyString, + Type: kType, + }, err +} diff --git a/go-ssh-keysign/internal/service/types.go b/go-ssh-keysign/internal/service/types.go index 5ba9e49..a0764f5 100644 --- a/go-ssh-keysign/internal/service/types.go +++ b/go-ssh-keysign/internal/service/types.go @@ -2,16 +2,17 @@ package service import ( "context" + "crypto/ed25519" "binarycodes/ssh-keysign/internal/config" "binarycodes/ssh-keysign/internal/ctxkeys" "binarycodes/ssh-keysign/internal/logging" + "binarycodes/ssh-keysign/internal/service/utilities" ) type KeyHandler interface { ReadPublicKey(ctx context.Context, path string) (keyType, pubKey string, err error) - WriteAtomic(path string, data []byte, perm uint32) error - BackupIfExists(path string) error + NewEd25519(ctx context.Context) (*ED25519KeyPair, error) } type CertClient interface { @@ -25,8 +26,9 @@ type OAuthClient interface { } type CertHandler interface { - StoreUserCert(ctx context.Context, u *UserCertHandlerConfig) (path string, err error) - StoreHostCert(ctx context.Context, h *HostCertHandlerConfig) (path string, err error) + StoreUserCertFile(ctx context.Context, u *UserCertHandlerConfig) (agent bool, path string, err error) + StoreUserCertAgent(ctx context.Context, u *UserCertHandlerConfig) error + StoreHostCertFile(ctx context.Context, h *HostCertHandlerConfig) (path string, err error) } type UserCertRequestConfig struct { @@ -44,13 +46,13 @@ type HostCertRequestConfig struct { } type UserCertHandlerConfig struct { - UserConfig config.User + Keys Keys SignedResponse SignedResponse } type HostCertHandlerConfig struct { - HostConfig config.Host - SignedResponse SignedResponse + CertSaveFilePath string + SignedResponse SignedResponse } type Runner struct { @@ -86,13 +88,11 @@ func (a AccessToken) OK(ctx context.Context) bool { } type SignRequest struct { - Filename string `json:"filename"` PublicKey string `json:"publicKey"` - Hostname string `json:"principal"` + Principal string `json:"principal"` } type SignedResponse struct { - Filename string `json:"filename"` SignedPublicKey string `json:"signedKey"` } @@ -104,3 +104,32 @@ type DeviceFlowStartResponse struct { ExpiresIn uint64 `json:"expires_in"` Interval uint64 `json:"interval"` } + +type ED25519KeyPair struct { + PrivateKey *ed25519.PrivateKey + PrivateKeyBytes []byte + PublicKeyString string + Type string +} + +type Keys struct { + Filename string + PublicKey string + KeyPair *ED25519KeyPair +} + +func (k Keys) FetchPublicKey() string { + if k.Filename != "" { + return k.PublicKey + } + + return k.KeyPair.PublicKeyString +} + +func (k Keys) FetchCertFileName() (string, error) { + if k.Filename != "" { + return utilities.GetCertificateFilePath(k.Filename) + } + + return "", nil +} diff --git a/go-ssh-keysign/internal/service/usersvc/service.go b/go-ssh-keysign/internal/service/usersvc/service.go index dadb5e4..95fd15a 100644 --- a/go-ssh-keysign/internal/service/usersvc/service.go +++ b/go-ssh-keysign/internal/service/usersvc/service.go @@ -18,11 +18,11 @@ type Service interface { SignUserKey(ctx context.Context, r *service.Runner) error } -func (UserService) SignUserKey(ctx context.Context, r *service.Runner) error { +func (u UserService) SignUserKey(ctx context.Context, r *service.Runner) error { log := ctxkeys.LoggerFrom(ctx) p := ctxkeys.PrinterFrom(ctx) - cfg := r.Config + log.Info("user run", zap.String("key", cfg.User.Key), zap.Strings("principal", cfg.User.Principals), @@ -32,19 +32,75 @@ func (UserService) SignUserKey(ctx context.Context, r *service.Runner) error { zap.String("token-url", cfg.OAuth.TokenURL), ) - p.V(logging.Verbose).Println("fetching key details") + keys, err := u.fetchKeys(ctx, r) + if err != nil { + return err + } + + accessToken, err := u.fetchAccessToken(ctx, r) + if err != nil { + return err + } - kType, key, err := r.KeyHandler.ReadPublicKey(ctx, cfg.User.Key) + signedResponse, err := u.certSignRequest(ctx, r, keys, accessToken) if err != nil { return err } - p.V(logging.VeryVerbose).Printf("found key type: %v | public key: %v\n", kType, key) + if err := u.storeCertificate(ctx, r, keys, signedResponse); err != nil { + return err + } + + p.V(logging.VeryVerbose).Println("done") + return nil +} + +func (UserService) fetchKeys(ctx context.Context, r *service.Runner) (*service.Keys, error) { + log := ctxkeys.LoggerFrom(ctx) + p := ctxkeys.PrinterFrom(ctx) + cfg := r.Config + + p.V(logging.Verbose).Println("fetching key details") + + if cfg.User.Key != "" { + kType, key, err := r.KeyHandler.ReadPublicKey(ctx, cfg.User.Key) + if err != nil { + return nil, err + } + + p.V(logging.VeryVerbose).Printf("found key type: %v | public key: %v\n", kType, key) + log.Info("public key details", + zap.String("type", kType), + zap.String("key", key), + ) + + return &service.Keys{ + Filename: cfg.User.Key, + PublicKey: key, + }, nil + } + + keyPair, err := r.KeyHandler.NewEd25519(ctx) + if err != nil { + return nil, err + } + + p.V(logging.VeryVerbose).Printf("created key type: %v | public key: %v\n", keyPair.Type, keyPair.PublicKeyString) log.Info("public key details", - zap.String("type", kType), - zap.String("key", key), + zap.String("type", keyPair.Type), + zap.String("key", keyPair.PublicKeyString), ) + return &service.Keys{ + KeyPair: keyPair, + }, nil +} + +func (UserService) fetchAccessToken(ctx context.Context, r *service.Runner) (token *service.AccessToken, err error) { + log := ctxkeys.LoggerFrom(ctx) + p := ctxkeys.PrinterFrom(ctx) + cfg := r.Config + p.V(logging.Verbose).Println("initiating connection to OAuth") var accessToken *service.AccessToken @@ -53,18 +109,18 @@ func (UserService) SignUserKey(ctx context.Context, r *service.Runner) error { p.V(logging.Verbose).Println("using client credential") accessToken, err = r.OAuthClient.ClientCredentialLogin(ctx, cfg.OAuth) if err != nil { - return apperror.ErrAuth(err) + return nil, apperror.ErrAuth(err) } } else { p.V(logging.Verbose).Println("using device flow") accessToken, err = r.OAuthClient.DeviceFlowLogin(ctx, cfg.OAuth) if err != nil { - return apperror.ErrAuth(err) + return nil, apperror.ErrAuth(err) } } if accessToken == nil || !accessToken.OK(ctx) { - return apperror.ErrAuth(errors.New("failed to retrieve access token")) + return nil, apperror.ErrAuth(errors.New("failed to retrieve access token")) } p.V(logging.VeryVerbose).Println("received access token") @@ -73,38 +129,56 @@ func (UserService) SignUserKey(ctx context.Context, r *service.Runner) error { zap.Uint64("expires_in", accessToken.ExpiresIn), ) + return accessToken, nil +} + +func (UserService) certSignRequest(ctx context.Context, r *service.Runner, k *service.Keys, token *service.AccessToken) (certResp *service.SignedResponse, err error) { + log := ctxkeys.LoggerFrom(ctx) + p := ctxkeys.PrinterFrom(ctx) + cfg := r.Config + p.V(logging.Verbose).Println("initiating connection to CA server to sign public key") signedResponse, err := r.CertClient.IssueUserCert(ctx, &service.UserCertRequestConfig{ - UserConfig: r.Config.User, - OAuthConfig: r.Config.OAuth, - PubKey: key, - Token: *accessToken, + UserConfig: cfg.User, + OAuthConfig: cfg.OAuth, + PubKey: k.FetchPublicKey(), + Token: *token, }) if err != nil { - return apperror.ErrNet(err) + return nil, apperror.ErrNet(err) } p.V(logging.VeryVerbose).Println("received signed certificate") - log.Info("signed certificate received", - zap.String("filename", signedResponse.Filename), - ) + log.Info("signed certificate received") + + return signedResponse, nil +} + +func (UserService) storeCertificate(ctx context.Context, r *service.Runner, k *service.Keys, s *service.SignedResponse) (err error) { + log := ctxkeys.LoggerFrom(ctx) + p := ctxkeys.PrinterFrom(ctx) p.V(logging.VeryVerbose).Println("storing the certificate") - path, err := r.CertHandler.StoreUserCert(ctx, &service.UserCertHandlerConfig{ - UserConfig: cfg.User, - SignedResponse: *signedResponse, + agent, path, err := r.CertHandler.StoreUserCertFile(ctx, &service.UserCertHandlerConfig{ + Keys: *k, + SignedResponse: *s, }) if err != nil { return err } - p.V(logging.Normal).Printf("certificate stored at %s", path) + if agent { + p.V(logging.Normal).Println("certificate stored in ssh-agent") + } else { + p.V(logging.Normal).Printf("certificate stored at %s\n", path) + } + log.Info("certificate stored", + zap.Bool("agent", agent), zap.String("filename", path), ) - p.V(logging.VeryVerbose).Println("done") return nil } diff --git a/go-ssh-keysign/internal/service/utilities/util.go b/go-ssh-keysign/internal/service/utilities/util.go new file mode 100644 index 0000000..30f0af2 --- /dev/null +++ b/go-ssh-keysign/internal/service/utilities/util.go @@ -0,0 +1,52 @@ +package utilities + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "binarycodes/ssh-keysign/internal/apperror" +) + +func NormalizePath(p string) (string, error) { + path := p + + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + + if trimmed, found := strings.CutPrefix(p, "~"); found { + return filepath.Join(home, trimmed), nil + } + + if trimmed, found := strings.CutPrefix(p, "$HOME"); found { + return filepath.Join(home, trimmed), nil + } + + /* allow expansion of other environment variables */ + return os.ExpandEnv(path), nil +} + +// The parameter is expected to be an abolute file path +func GetCertificateFilePath(fp string) (string, error) { + normalized, err := NormalizePath(fp) + if err != nil { + return "", apperror.ErrFileSystem(err) + } + + dir := filepath.Dir(normalized) + basename := filepath.Base(normalized) + extension := filepath.Ext(normalized) + + nameWithoutExtension := strings.TrimSuffix(basename, extension) + + certFileName := fmt.Sprintf("%s-cert%s", nameWithoutExtension, extension) + path, err := filepath.Abs(filepath.Join(dir, certFileName)) + if err != nil { + return "", apperror.ErrFileSystem(err) + } + + return path, nil +} diff --git a/go-ssh-keysign/user_config.sample.yml b/go-ssh-keysign/user_config.sample.yml index bff939f..7a6767a 100644 --- a/go-ssh-keysign/user_config.sample.yml +++ b/go-ssh-keysign/user_config.sample.yml @@ -8,10 +8,9 @@ device-flow-url: "http://10.88.0.100:8090/realms/my-test-realm/protocol/openid-c token-poll-url: "http://10.88.0.100:8090/realms/my-test-realm/protocol/openid-connect/token" user: - key: "testdata/id.pub" + #key: "testdata/id.pub" principal: - - alice - - devops + - binarycodes duration: 3600 host: diff --git a/server/pom.xml b/server/pom.xml index 8b12e64..41553d4 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -7,7 +7,7 @@ io.binarycodes.homelab ssh-signer-mono SSH KeySigner - 0.0.5 + 0.0.6 pom diff --git a/server/ssh-key-signer-server/pom.xml b/server/ssh-key-signer-server/pom.xml index 90482c1..7c356d6 100644 --- a/server/ssh-key-signer-server/pom.xml +++ b/server/ssh-key-signer-server/pom.xml @@ -5,7 +5,7 @@ io.binarycodes.homelab ssh-key-signer-server - 0.0.5 + 0.0.6 jar Server - SSH Key Signer @@ -14,7 +14,7 @@ 21 24.9.2 - 0.0.5 + 0.0.6 diff --git a/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyController.java b/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyController.java index 3c159ea..5db38bd 100644 --- a/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyController.java +++ b/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyController.java @@ -38,7 +38,6 @@ public ResponseEntity signUserKey(final JwtAuthenticati } final var signed = keyService.signUserKey( - signPublicKeyRequest.filename(), signPublicKeyRequest.publicKey(), principal.getToken().getId(), signPublicKeyRequest.principal() @@ -58,7 +57,6 @@ public ResponseEntity signHostKey(final JwtAuthenticati } final var signed = keyService.signHostKey( - signPublicKeyRequest.filename(), signPublicKeyRequest.publicKey(), principal.getToken().getId(), signPublicKeyRequest.principal() diff --git a/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyService.java b/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyService.java index dc041b5..315775e 100644 --- a/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyService.java +++ b/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyService.java @@ -11,7 +11,6 @@ import io.binarycodes.homelab.lib.SignedPublicKeyDownload; import io.binarycodes.homelab.sshkeysigner.config.ApplicationProperties; import lombok.extern.log4j.Log4j2; -import org.apache.commons.io.FilenameUtils; import org.springframework.stereotype.Service; import java.io.IOException; @@ -26,7 +25,6 @@ public class KeyService { /* https://jadaptive.com/app/manpage/en/article/2895616 */ - private static final String CERTIFICATE_FILE_NAME_SUFFIX = "cert"; private final ApplicationProperties applicationProperties; public KeyService(final ApplicationProperties applicationProperties) { @@ -113,48 +111,48 @@ private SshKeyPair keyInfoToKeyPair(final KeyInfo keyInfo, final String passphra /** * Signs the given key for user */ - public Optional signUserKey(final String filename, final String pubKey, final String keyId, final String principal) { - return signUserKey(filename, pubKey, keyId, List.of(principal)); + public Optional signUserKey(final String pubKey, final String keyId, final String principal) { + return signUserKey(pubKey, keyId, List.of(principal)); } /** * Signs the given key for user */ - public Optional signUserKey(final String filename, final String pubKey, final String keyId, final List principals) { + public Optional signUserKey(final String pubKey, final String keyId, final List principals) { final var bytes = pubKey.getBytes(StandardCharsets.UTF_8); - return signUserKey(filename, bytes, keyId, principals); + return signUserKey(bytes, keyId, principals); } /** * Signs the given key for user */ - public Optional signUserKey(final String filename, final byte[] bytes, final String keyId, final List principals) { - return signKey(SshCertificateType.USER, filename, bytes, keyId, principals, applicationProperties.caUserValidity()); + public Optional signUserKey(final byte[] bytes, final String keyId, final List principals) { + return signKey(SshCertificateType.USER, bytes, keyId, principals, applicationProperties.caUserValidity()); } /** * Signs the given key for host */ - public Optional signHostKey(final String filename, final String pubKey, final String keyId, final String principal) { - return signHostKey(filename, pubKey, keyId, List.of(principal)); + public Optional signHostKey(final String pubKey, final String keyId, final String principal) { + return signHostKey(pubKey, keyId, List.of(principal)); } /** * Signs the given key for host */ - public Optional signHostKey(final String filename, final String pubKey, final String keyId, final List principals) { + public Optional signHostKey(final String pubKey, final String keyId, final List principals) { final var bytes = pubKey.getBytes(StandardCharsets.UTF_8); - return signHostKey(filename, bytes, keyId, principals); + return signHostKey(bytes, keyId, principals); } /** * Signs the given key for host */ - public Optional signHostKey(final String filename, final byte[] bytes, final String keyId, final List principals) { - return signKey(SshCertificateType.HOST, filename, bytes, keyId, principals, applicationProperties.caHostValidity()); + public Optional signHostKey(final byte[] bytes, final String keyId, final List principals) { + return signKey(SshCertificateType.HOST, bytes, keyId, principals, applicationProperties.caHostValidity()); } - private Optional signKey(final SshCertificateType certType, final String filename, final byte[] bytes, final String keyId, final List principals, final Duration validitySeconds) { + private Optional signKey(final SshCertificateType certType, final byte[] bytes, final String keyId, final List principals, final Duration validitySeconds) { try { final var publicKeyFileToSign = SshPublicKeyFileFactory.parse(bytes); final var keyPairToSign = SshKeyPair.getKeyPair(null, publicKeyFileToSign.toPublicKey()); @@ -168,9 +166,8 @@ private Optional signKey(final SshCertificateType certT final var signedKey = SshPublicKeyFileFactory.create(signed.getCertificate(), publicKeyFileToSign.getComment(), SshPublicKeyFileFactory.OPENSSH_FORMAT); final var signedKeyString = new String(signedKey.getFormattedKey(), StandardCharsets.UTF_8); - final var downloadFilename = "%s-%s.%s".formatted(FilenameUtils.getBaseName(filename), CERTIFICATE_FILE_NAME_SUFFIX, FilenameUtils.getExtension(filename)); - return Optional.of(new SignedPublicKeyDownload(downloadFilename, signedKeyString)); + return Optional.of(new SignedPublicKeyDownload(signedKeyString)); } catch (final IOException e) { log.error(e.getMessage(), e); } catch (final InvalidPassphraseException | SshException e) { diff --git a/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/SshCertManager.java b/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/SshCertManager.java index 4f4d8b6..9f5d063 100644 --- a/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/SshCertManager.java +++ b/server/ssh-key-signer-server/src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/SshCertManager.java @@ -66,7 +66,9 @@ private static List buildCertificateExtensions(SshCertific } final var extensionsBuilder = new CertificateExtension.Builder(); - knownExtensions.forEach(extension -> extensionsBuilder.knownExtension(new NamedCertificateExtension(extension, true))); + knownExtensions.stream() + .sorted() + .forEach(extension -> extensionsBuilder.knownExtension(new NamedCertificateExtension(extension, true))); return extensionsBuilder.build(); } diff --git a/server/ssh-signer-common-lib/pom.xml b/server/ssh-signer-common-lib/pom.xml index 11c292f..6605598 100644 --- a/server/ssh-signer-common-lib/pom.xml +++ b/server/ssh-signer-common-lib/pom.xml @@ -6,7 +6,7 @@ io.binarycodes.homelab ssh-signer-common-lib - 0.0.5 + 0.0.6 Common Library - SSH Key Signer diff --git a/server/ssh-signer-common-lib/src/main/java/io/binarycodes/homelab/lib/SignPublicKeyRequest.java b/server/ssh-signer-common-lib/src/main/java/io/binarycodes/homelab/lib/SignPublicKeyRequest.java index 2dc44ec..0df4263 100644 --- a/server/ssh-signer-common-lib/src/main/java/io/binarycodes/homelab/lib/SignPublicKeyRequest.java +++ b/server/ssh-signer-common-lib/src/main/java/io/binarycodes/homelab/lib/SignPublicKeyRequest.java @@ -1,4 +1,4 @@ package io.binarycodes.homelab.lib; -public record SignPublicKeyRequest(String filename, String publicKey, String principal) { +public record SignPublicKeyRequest(String publicKey, String principal) { } diff --git a/server/ssh-signer-common-lib/src/main/java/io/binarycodes/homelab/lib/SignedPublicKeyDownload.java b/server/ssh-signer-common-lib/src/main/java/io/binarycodes/homelab/lib/SignedPublicKeyDownload.java index 15e3045..5235073 100644 --- a/server/ssh-signer-common-lib/src/main/java/io/binarycodes/homelab/lib/SignedPublicKeyDownload.java +++ b/server/ssh-signer-common-lib/src/main/java/io/binarycodes/homelab/lib/SignedPublicKeyDownload.java @@ -1,4 +1,4 @@ package io.binarycodes.homelab.lib; -public record SignedPublicKeyDownload(String filename, String signedKey) { +public record SignedPublicKeyDownload(String signedKey) { }