diff --git a/.github/goreleaser.yml b/.github/goreleaser.yml index 64cb3025..d0cece00 100644 --- a/.github/goreleaser.yml +++ b/.github/goreleaser.yml @@ -25,6 +25,7 @@ builds: - mips64le - s390x goarm: + - 5 - 6 - 7 gomips: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cfea837..b79e1621 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: name: Test strategy: matrix: - go-version: [1.19.x, 1.20.x] + go-version: [1.21.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/README.md b/README.md index ee3da3ad..3af60c8e 100644 --- a/README.md +++ b/README.md @@ -119,12 +119,23 @@ $ chisel server --help --port, -p, Defines the HTTP listening port (defaults to the environment variable PORT and fallsback to port 8080). - --key, An optional string to seed the generation of a ECDSA public + --key, (deprecated use --keygen and --keyfile instead) + An optional string to seed the generation of a ECDSA public and private key pair. All communications will be secured using this key pair. Share the subsequent fingerprint with clients to enable detection of man-in-the-middle attacks (defaults to the CHISEL_KEY environment variable, otherwise a new key is generate each run). + --keygen, A path to write a newly generated PEM-encoded SSH private key file. + If users depend on your --key fingerprint, you may also include your --key to + output your existing key. Use - (dash) to output the generated key to stdout. + + --keyfile, An optional path to a PEM-encoded SSH private key. When + this flag is set, the --key option is ignored, and the provided private key + is used to secure all communications. (defaults to the CHISEL_KEY_FILE + environment variable). Since ECDSA keys are short, you may also set keyfile + to an inline base64 private key (e.g. chisel server --keygen - | base64). + --authfile, An optional path to a users.json file. This file should be an object with users defined like: { @@ -300,6 +311,9 @@ $ chisel client --help --hostname, Optionally set the 'Host' header (defaults to the host found in the server url). + --sni, Override the ServerName when using TLS (defaults to the + hostname). + --tls-ca, An optional root certificate bundle used to verify the chisel server. Only valid when connecting to the server with "https" or "wss". By default, the operating system CAs will be used. @@ -341,7 +355,7 @@ $ chisel client --help ### Security -Encryption is always enabled. When you start up a chisel server, it will generate an in-memory ECDSA public/private key pair. The public key fingerprint (base64 encoded SHA256) will be displayed as the server starts. Instead of generating a random key, the server may optionally specify a key seed, using the `--key` option, which will be used to seed the key generation. When clients connect, they will also display the server's public key fingerprint. The client can force a particular fingerprint using the `--fingerprint` option. See the `--help` above for more information. +Encryption is always enabled. When you start up a chisel server, it will generate an in-memory ECDSA public/private key pair. The public key fingerprint (base64 encoded SHA256) will be displayed as the server starts. Instead of generating a random key, the server may optionally specify a key file, using the `--keyfile` option. When clients connect, they will also display the server's public key fingerprint. The client can force a particular fingerprint using the `--fingerprint` option. See the `--help` above for more information. ### Authentication @@ -349,30 +363,34 @@ Using the `--authfile` option, the server may optionally provide a `user.json` c Internally, this is done using the _Password_ authentication method provided by SSH. Learn more about `crypto/ssh` here http://blog.gopheracademy.com/go-and-ssh/. -### SOCKS5 Guide +### SOCKS5 Guide with Docker + +1. Print a new private key to the terminal + + ```sh + chisel server --keygen - + # or save it to disk --keygen /path/to/mykey + ``` 1. Start your chisel server -```sh -docker run \ - --name chisel -p 9312:9312 \ - -d --restart always \ - jpillora/chisel server -p 9312 --socks5 --key supersecret -``` + ```sh + jpillora/chisel server --keyfile '' -p 9312 --socks5 + ``` -2. Connect your chisel client (using server's fingerprint) +1. Connect your chisel client (using server's fingerprint) -```sh -chisel client --fingerprint 'rHb55mcxf6vSckL2AezFV09rLs7pfPpavVu++MF7AhQ=' :9312 socks -``` + ```sh + chisel client --fingerprint '' :9312 socks + ``` -3. Point your SOCKS5 clients (e.g. OS/Browser) to: +1. Point your SOCKS5 clients (e.g. OS/Browser) to: -``` -:1080 -``` + ``` + :1080 + ``` -4. Now you have an encrypted, authenticated SOCKS5 connection over HTTP +1. Now you have an encrypted, authenticated SOCKS5 connection over HTTP #### Caveats @@ -403,6 +421,8 @@ Since WebSockets support is required: - `1.5` - Added reverse SOCKS support (by @aus) - `1.6` - Added client stdio support (by @BoleynSu) - `1.7` - Added UDP support +- `1.8` - Move to a `scratch`Docker image +- `1.9` - Switch from `--key` seed to P256 key strings with `--key{gen,file}` + bump to Go 1.21 (by @cmenginnz) ## License diff --git a/client/client.go b/client/client.go index 793e4304..7964670b 100644 --- a/client/client.go +++ b/client/client.go @@ -29,7 +29,7 @@ import ( "golang.org/x/sync/errgroup" ) -//Config represents a client configuration +// Config represents a client configuration type Config struct { Fingerprint string Auth string @@ -45,7 +45,7 @@ type Config struct { Verbose bool } -//TLSConfig for a Client +// TLSConfig for a Client type TLSConfig struct { SkipVerify bool CA string @@ -54,7 +54,7 @@ type TLSConfig struct { ServerName string } -//Client represents a client instance +// Client represents a client instance type Client struct { *cio.Logger config *Config @@ -69,7 +69,7 @@ type Client struct { tunnel *tunnel.Tunnel } -//NewClient creates a new client instance +// NewClient creates a new client instance func NewClient(c *Config) (*Client, error) { //apply default scheme if !strings.HasPrefix(c.Server, "http") { @@ -190,7 +190,7 @@ func NewClient(c *Config) (*Client, error) { return client, nil } -//Run starts client and blocks while connected +// Run starts client and blocks while connected func (c *Client) Run() error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -221,7 +221,7 @@ func (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKe return nil } -//verifyLegacyFingerprint calculates and compares legacy MD5 fingerprints +// verifyLegacyFingerprint calculates and compares legacy MD5 fingerprints func (c *Client) verifyLegacyFingerprint(key ssh.PublicKey) error { bytes := md5.Sum(key.Marshal()) strbytes := make([]string, len(bytes)) @@ -236,7 +236,7 @@ func (c *Client) verifyLegacyFingerprint(key ssh.PublicKey) error { return nil } -//Start client and does not block +// Start client and does not block func (c *Client) Start(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) c.stop = cancel @@ -293,12 +293,12 @@ func (c *Client) setProxy(u *url.URL, d *websocket.Dialer) error { return nil } -//Wait blocks while the client is running. +// Wait blocks while the client is running. func (c *Client) Wait() error { return c.eg.Wait() } -//Close manually stops the client +// Close manually stops the client func (c *Client) Close() error { if c.stop != nil { c.stop() diff --git a/client/client_test.go b/client/client_test.go index 3ef50c29..f947171a 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,7 +1,6 @@ package chclient import ( - "crypto/ecdsa" "crypto/elliptic" "log" "net/http" @@ -45,87 +44,62 @@ func TestCustomHeaders(t *testing.T) { c.Close() } -// with the update Go to 1.20, these Unit Tests start failing, -// since this test is related to client side, and the "fingerprint" flag is not available in cloud-connector -// we can remove/comment these 3 Unit Tests, until fixed in upstream - -// func TestFallbackLegacyFingerprint(t *testing.T) { -// config := Config{ -// Fingerprint: "a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44", -// } -// c, err := NewClient(&config) -// if err != nil { -// t.Fatal(err) -// } -// r := ccrypto.NewDetermRand([]byte("test123")) -// priv, err := ecdsa.GenerateKey(elliptic.P256(), r) -// if err != nil { -// t.Fatal(err) -// } -// pub, err := ssh.NewPublicKey(&priv.PublicKey) -// if err != nil { -// t.Fatal(err) -// } -// err = c.verifyServer("", nil, pub) -// if err != nil { -// t.Fatal(err) -// } -// } - -// func TestVerifyLegacyFingerprint(t *testing.T) { -// config := Config{ -// Fingerprint: "a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44", -// } -// c, err := NewClient(&config) -// if err != nil { -// t.Fatal(err) -// } -// r := ccrypto.NewDetermRand([]byte("test123")) -// priv, err := ecdsa.GenerateKey(elliptic.P256(), r) -// if err != nil { -// t.Fatal(err) -// } -// pub, err := ssh.NewPublicKey(&priv.PublicKey) -// if err != nil { -// t.Fatal(err) -// } -// err = c.verifyLegacyFingerprint(pub) -// if err != nil { -// t.Fatal(err) -// } -// } +func TestFallbackLegacyFingerprint(t *testing.T) { + config := Config{ + Fingerprint: "a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44", + } + c, err := NewClient(&config) + if err != nil { + t.Fatal(err) + } + r := ccrypto.NewDetermRand([]byte("test123")) + priv, err := ccrypto.GenerateKeyGo119(elliptic.P256(), r) + if err != nil { + t.Fatal(err) + } + pub, err := ssh.NewPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + err = c.verifyServer("", nil, pub) + if err != nil { + t.Fatal(err) + } +} -// func TestVerifyFingerprint(t *testing.T) { -// config := Config{ -// Fingerprint: "qmrRoo8MIqePv3jC8+wv49gU6uaFgD3FASQx9V8KdmY=", -// } -// c, err := NewClient(&config) -// if err != nil { -// t.Fatal(err) -// } -// r := ccrypto.NewDetermRand([]byte("test123")) -// priv, err := ecdsa.GenerateKey(elliptic.P256(), r) -// if err != nil { -// t.Fatal(err) -// } -// pub, err := ssh.NewPublicKey(&priv.PublicKey) -// if err != nil { -// t.Fatal(err) -// } -// err = c.verifyServer("", nil, pub) -// if err != nil { -// t.Fatal(err) -// } -// } +func TestVerifyLegacyFingerprint(t *testing.T) { + config := Config{ + Fingerprint: "a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44", + } + c, err := NewClient(&config) + if err != nil { + t.Fatal(err) + } + r := ccrypto.NewDetermRand([]byte("test123")) + priv, err := ccrypto.GenerateKeyGo119(elliptic.P256(), r) + if err != nil { + t.Fatal(err) + } + pub, err := ssh.NewPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + err = c.verifyLegacyFingerprint(pub) + if err != nil { + t.Fatal(err) + } +} -func TestVerifyEmptyFingerprint(t *testing.T) { - config := Config{} +func TestVerifyFingerprint(t *testing.T) { + config := Config{ + Fingerprint: "qmrRoo8MIqePv3jC8+wv49gU6uaFgD3FASQx9V8KdmY=", + } c, err := NewClient(&config) if err != nil { t.Fatal(err) } r := ccrypto.NewDetermRand([]byte("test123")) - priv, err := ecdsa.GenerateKey(elliptic.P256(), r) + priv, err := ccrypto.GenerateKeyGo119(elliptic.P256(), r) if err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index 953778e5..dbb4a0e7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/jpillora/chisel -go 1.20 +go 1.21 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 @@ -9,8 +9,8 @@ require ( github.com/jpillora/backoff v1.0.0 github.com/jpillora/requestlog v1.0.0 github.com/jpillora/sizestr v1.0.0 - golang.org/x/crypto v0.10.0 - golang.org/x/net v0.11.0 + golang.org/x/crypto v0.12.0 + golang.org/x/net v0.14.0 golang.org/x/sync v0.3.0 ) @@ -18,6 +18,6 @@ require ( github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect github.com/jpillora/ansi v1.0.3 // indirect github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect ) diff --git a/go.sum b/go.sum index 86b4dd89..6c6389f3 100644 --- a/go.sum +++ b/go.sum @@ -16,25 +16,16 @@ github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2yg github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/main.go b/main.go index eb50e6ca..f78ee156 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,9 @@ import ( chclient "github.com/jpillora/chisel/client" chserver "github.com/jpillora/chisel/server" chshare "github.com/jpillora/chisel/share" + "github.com/jpillora/chisel/share/ccrypto" "github.com/jpillora/chisel/share/cos" + "github.com/jpillora/chisel/share/settings" ) var help = ` @@ -103,12 +105,23 @@ var serverHelp = ` --port, -p, Defines the HTTP listening port (defaults to the environment variable PORT and fallsback to port 8080). - --key, An optional string to seed the generation of a ECDSA public + --key, (deprecated use --keygen and --keyfile instead) + An optional string to seed the generation of a ECDSA public and private key pair. All communications will be secured using this key pair. Share the subsequent fingerprint with clients to enable detection of man-in-the-middle attacks (defaults to the CHISEL_KEY environment variable, otherwise a new key is generate each run). + --keygen, A path to write a newly generated PEM-encoded SSH private key file. + If users depend on your --key fingerprint, you may also include your --key to + output your existing key. Use - (dash) to output the generated key to stdout. + + --keyfile, An optional path to a PEM-encoded SSH private key. When + this flag is set, the --key option is ignored, and the provided private key + is used to secure all communications. (defaults to the CHISEL_KEY_FILE + environment variable). Since ECDSA keys are short, you may also set keyfile + to an inline base64 private key (e.g. chisel server --keygen - | base64). + --authfile, An optional path to a users.json file. This file should be an object with users defined like: { @@ -170,6 +183,7 @@ func server(args []string) { config := &chserver.Config{} flags.StringVar(&config.KeySeed, "key", "", "") + flags.StringVar(&config.KeyFile, "keyfile", "", "") flags.StringVar(&config.AuthFile, "authfile", "", "") flags.StringVar(&config.Auth, "auth", "", "") flags.DurationVar(&config.KeepAlive, "keepalive", 25*time.Second, "") @@ -187,6 +201,7 @@ func server(args []string) { port := flags.String("port", "", "") pid := flags.Bool("pid", false, "") verbose := flags.Bool("v", false, "") + keyGen := flags.String("keygen", "", "") flags.Usage = func() { fmt.Print(serverHelp) @@ -194,6 +209,18 @@ func server(args []string) { } flags.Parse(args) + if *keyGen != "" { + if err := ccrypto.GenerateKeyFile(*keyGen, config.KeySeed); err != nil { + log.Fatal(err) + } + return + } + + if config.KeySeed != "" { + log.Print("Option `--key` is deprecated and will be removed in a future version of chisel.") + log.Print("Please use `chisel server --keygen /file/path`, followed by `chisel server --keyfile /file/path` to specify the SSH private key") + } + if *host == "" { *host = os.Getenv("HOST") } @@ -209,8 +236,10 @@ func server(args []string) { if *port == "" { *port = "8080" } - if config.KeySeed == "" { - config.KeySeed = os.Getenv("CHISEL_KEY") + if config.KeyFile == "" { + config.KeyFile = settings.Env("KEY_FILE") + } else if config.KeySeed == "" { + config.KeySeed = settings.Env("KEY") } s, err := chserver.NewServer(config) if err != nil { diff --git a/server/server.go b/server/server.go index b7df1282..8a702fce 100644 --- a/server/server.go +++ b/server/server.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "os" "regexp" "time" @@ -23,6 +24,7 @@ import ( // Config is the configuration for the chisel service type Config struct { KeySeed string + KeyFile string AuthFile string Auth string Proxy string @@ -73,13 +75,38 @@ func NewServer(c *Config) (*Server, error) { server.users.AddUser(u) } } - //generate private key (optionally using seed) - key, err := ccrypto.GenerateKey(c.KeySeed) - if err != nil { - log.Fatal("Failed to generate key") + + var pemBytes []byte + var err error + if c.KeyFile != "" { + var key []byte + + if ccrypto.IsChiselKey([]byte(c.KeyFile)) { + key = []byte(c.KeyFile) + } else { + key, err = os.ReadFile(c.KeyFile) + if err != nil { + log.Fatalf("Failed to read key file %s", c.KeyFile) + } + } + + pemBytes = key + if ccrypto.IsChiselKey(key) { + pemBytes, err = ccrypto.ChiselKey2PEM(key) + if err != nil { + log.Fatalf("Invalid key %s", string(key)) + } + } + } else { + //generate private key (optionally using seed) + pemBytes, err = ccrypto.Seed2PEM(c.KeySeed) + if err != nil { + log.Fatal("Failed to generate key") + } } + //convert into ssh.PrivateKey - private, err := ssh.ParsePrivateKey(key) + private, err := ssh.ParsePrivateKey(pemBytes) if err != nil { log.Fatal("Failed to parse key") } diff --git a/share/ccrypto/generate_key_go119.go b/share/ccrypto/generate_key_go119.go new file mode 100644 index 00000000..68e59e3f --- /dev/null +++ b/share/ccrypto/generate_key_go119.go @@ -0,0 +1,42 @@ +package ccrypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "io" + "math/big" +) + +var one = new(big.Int).SetInt64(1) + +// This function is copied from ecdsa.GenerateKey() of Go 1.19 +func GenerateKeyGo119(c elliptic.Curve, rand io.Reader) (*ecdsa.PrivateKey, error) { + k, err := randFieldElement(c, rand) + if err != nil { + return nil, err + } + + priv := new(ecdsa.PrivateKey) + priv.PublicKey.Curve = c + priv.D = k + priv.PublicKey.X, priv.PublicKey.Y = c.ScalarBaseMult(k.Bytes()) + return priv, nil +} + +// This function is copied from Go 1.19 +func randFieldElement(c elliptic.Curve, rand io.Reader) (k *big.Int, err error) { + params := c.Params() + // Note that for P-521 this will actually be 63 bits more than the order, as + // division rounds down, but the extra bit is inconsequential. + b := make([]byte, params.N.BitLen()/8+8) + _, err = io.ReadFull(rand, b) + if err != nil { + return + } + + k = new(big.Int).SetBytes(b) + n := new(big.Int).Sub(params.N, one) + k.Mod(k, n) + k.Add(k, one) + return +} diff --git a/share/ccrypto/keys.go b/share/ccrypto/keys.go index 91d875aa..d6d71305 100644 --- a/share/ccrypto/keys.go +++ b/share/ccrypto/keys.go @@ -1,36 +1,34 @@ package ccrypto import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" "crypto/sha256" - "crypto/x509" "encoding/base64" - "encoding/pem" "fmt" + "os" "golang.org/x/crypto/ssh" ) -//GenerateKey for use as an SSH private key +// GenerateKey generates a PEM key func GenerateKey(seed string) ([]byte, error) { - r := rand.Reader - if seed != "" { - r = NewDetermRand([]byte(seed)) - } - priv, err := ecdsa.GenerateKey(elliptic.P256(), r) + return Seed2PEM(seed) +} + +// GenerateKeyFile generates an ChiselKey +func GenerateKeyFile(keyFilePath, seed string) error { + chiselKey, err := seed2ChiselKey(seed) if err != nil { - return nil, err + return err } - b, err := x509.MarshalECPrivateKey(priv) - if err != nil { - return nil, fmt.Errorf("Unable to marshal ECDSA private key: %v", err) + + if keyFilePath == "-" { + fmt.Print(string(chiselKey)) + return nil } - return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}), nil + return os.WriteFile(keyFilePath, chiselKey, 0600) } -//FingerprintKey calculates the SHA256 hash of an SSH public key +// FingerprintKey calculates the SHA256 hash of an SSH public key func FingerprintKey(k ssh.PublicKey) string { bytes := sha256.Sum256(k.Marshal()) return base64.StdEncoding.EncodeToString(bytes[:]) diff --git a/share/ccrypto/keys_helpers.go b/share/ccrypto/keys_helpers.go new file mode 100644 index 00000000..5e21c8bd --- /dev/null +++ b/share/ccrypto/keys_helpers.go @@ -0,0 +1,97 @@ +package ccrypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "strings" +) + +const ChiselKeyPrefix = "ck-" + +// Relations between entities: +// +// .............> PEM <........... +// . ^ . +// . | . +// . | . +// Seed -------> PrivateKey . +// . ^ . +// . | . +// . V . +// ..........> ChiselKey ......... + +func Seed2PEM(seed string) ([]byte, error) { + privateKey, err := seed2PrivateKey(seed) + if err != nil { + return nil, err + } + + return privateKey2PEM(privateKey) +} + +func seed2ChiselKey(seed string) ([]byte, error) { + privateKey, err := seed2PrivateKey(seed) + if err != nil { + return nil, err + } + + return privateKey2ChiselKey(privateKey) +} + +func seed2PrivateKey(seed string) (*ecdsa.PrivateKey, error) { + if seed == "" { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + } else { + return GenerateKeyGo119(elliptic.P256(), NewDetermRand([]byte(seed))) + } +} + +func privateKey2ChiselKey(privateKey *ecdsa.PrivateKey) ([]byte, error) { + b, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return nil, err + } + + encodedPrivateKey := make([]byte, base64.RawStdEncoding.EncodedLen(len(b))) + base64.RawStdEncoding.Encode(encodedPrivateKey, b) + + return append([]byte(ChiselKeyPrefix), encodedPrivateKey...), nil +} + +func privateKey2PEM(privateKey *ecdsa.PrivateKey) ([]byte, error) { + b, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}), nil +} + +func chiselKey2PrivateKey(chiselKey []byte) (*ecdsa.PrivateKey, error) { + rawChiselKey := chiselKey[len(ChiselKeyPrefix):] + + decodedPrivateKey := make([]byte, base64.RawStdEncoding.DecodedLen(len(rawChiselKey))) + _, err := base64.RawStdEncoding.Decode(decodedPrivateKey, rawChiselKey) + if err != nil { + return nil, err + } + + return x509.ParseECPrivateKey(decodedPrivateKey) +} + +func ChiselKey2PEM(chiselKey []byte) ([]byte, error) { + privateKey, err := chiselKey2PrivateKey(chiselKey) + if err == nil { + return privateKey2PEM(privateKey) + } + + return nil, err +} + +func IsChiselKey(chiselKey []byte) bool { + return strings.HasPrefix(string(chiselKey), ChiselKeyPrefix) +}