2022-02-17 21:28:14 +00:00
|
|
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
2022-02-24 20:27:42 +00:00
|
|
|
//go:build linux || (darwin && !ios)
|
|
|
|
// +build linux darwin,!ios
|
2022-02-17 21:28:14 +00:00
|
|
|
|
|
|
|
package ipnlocal
|
|
|
|
|
|
|
|
import (
|
2022-07-22 17:41:21 +00:00
|
|
|
"bytes"
|
2022-02-24 19:07:16 +00:00
|
|
|
"crypto/ecdsa"
|
|
|
|
"crypto/ed25519"
|
|
|
|
"crypto/elliptic"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/pem"
|
2022-02-17 21:28:14 +00:00
|
|
|
"errors"
|
2022-02-24 19:07:16 +00:00
|
|
|
"fmt"
|
2022-02-17 21:28:14 +00:00
|
|
|
"os"
|
2022-02-24 19:07:16 +00:00
|
|
|
"path/filepath"
|
2022-02-17 23:00:41 +00:00
|
|
|
"strings"
|
2022-02-24 19:07:16 +00:00
|
|
|
"sync"
|
2022-02-17 21:28:14 +00:00
|
|
|
|
2022-03-25 22:35:36 +00:00
|
|
|
"github.com/tailscale/golang-x-crypto/ssh"
|
2022-07-22 20:32:16 +00:00
|
|
|
"tailscale.com/util/mak"
|
2022-02-17 21:28:14 +00:00
|
|
|
)
|
|
|
|
|
2022-02-24 19:07:16 +00:00
|
|
|
// keyTypes are the SSH key types that we either try to read from the
|
|
|
|
// system's OpenSSH keys or try to generate for ourselves when not
|
|
|
|
// running as root.
|
|
|
|
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
|
|
|
|
|
|
|
|
func (b *LocalBackend) GetSSH_HostKeys() (keys []ssh.Signer, err error) {
|
2022-07-22 20:32:16 +00:00
|
|
|
var existing map[string]ssh.Signer
|
2022-02-24 19:07:16 +00:00
|
|
|
if os.Geteuid() == 0 {
|
2022-07-22 20:32:16 +00:00
|
|
|
existing = b.getSystemSSH_HostKeys()
|
2022-02-24 19:07:16 +00:00
|
|
|
}
|
2022-07-22 20:32:16 +00:00
|
|
|
return b.getTailscaleSSH_HostKeys(existing)
|
2022-02-24 19:07:16 +00:00
|
|
|
}
|
|
|
|
|
2022-07-22 20:32:16 +00:00
|
|
|
// getTailscaleSSH_HostKeys returns the three (rsa, ecdsa, ed25519) SSH host
|
|
|
|
// keys, reusing the provided ones in existing if present in the map.
|
|
|
|
func (b *LocalBackend) getTailscaleSSH_HostKeys(existing map[string]ssh.Signer) (keys []ssh.Signer, err error) {
|
|
|
|
var keyDir string // lazily initialized $TAILSCALE_VAR/ssh dir.
|
2022-02-24 19:07:16 +00:00
|
|
|
for _, typ := range keyTypes {
|
2022-07-22 20:32:16 +00:00
|
|
|
if s, ok := existing[typ]; ok {
|
|
|
|
keys = append(keys, s)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if keyDir == "" {
|
|
|
|
root := b.TailscaleVarRoot()
|
|
|
|
if root == "" {
|
|
|
|
return nil, errors.New("no var root for ssh keys")
|
|
|
|
}
|
|
|
|
keyDir = filepath.Join(root, "ssh")
|
|
|
|
if err := os.MkdirAll(keyDir, 0700); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2022-02-24 19:07:16 +00:00
|
|
|
hostKey, err := b.hostKeyFileOrCreate(keyDir, typ)
|
|
|
|
if err != nil {
|
2022-07-22 20:32:16 +00:00
|
|
|
return nil, fmt.Errorf("error creating SSH host key type %q in %q: %w", typ, keyDir, err)
|
2022-02-24 19:07:16 +00:00
|
|
|
}
|
|
|
|
signer, err := ssh.ParsePrivateKey(hostKey)
|
|
|
|
if err != nil {
|
2022-07-22 20:32:16 +00:00
|
|
|
return nil, fmt.Errorf("error parsing SSH host key type %q from %q: %w", typ, keyDir, err)
|
2022-02-24 19:07:16 +00:00
|
|
|
}
|
|
|
|
keys = append(keys, signer)
|
|
|
|
}
|
|
|
|
return keys, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var keyGenMu sync.Mutex
|
|
|
|
|
|
|
|
func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
|
|
|
|
keyGenMu.Lock()
|
|
|
|
defer keyGenMu.Unlock()
|
|
|
|
|
|
|
|
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
|
2022-09-15 12:06:59 +00:00
|
|
|
v, err := os.ReadFile(path)
|
2022-02-24 19:07:16 +00:00
|
|
|
if err == nil {
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-03-16 23:27:57 +00:00
|
|
|
var priv any
|
2022-02-24 19:07:16 +00:00
|
|
|
switch typ {
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unsupported key type %q", typ)
|
|
|
|
case "ed25519":
|
|
|
|
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
|
|
|
case "ecdsa":
|
|
|
|
// curve is arbitrary. We pick whatever will at
|
|
|
|
// least pacify clients as the actual encryption
|
|
|
|
// doesn't matter: it's all over WireGuard anyway.
|
|
|
|
curve := elliptic.P256()
|
|
|
|
priv, err = ecdsa.GenerateKey(curve, rand.Reader)
|
|
|
|
case "rsa":
|
|
|
|
// keySize is arbitrary. We pick whatever will at
|
|
|
|
// least pacify clients as the actual encryption
|
|
|
|
// doesn't matter: it's all over WireGuard anyway.
|
|
|
|
const keySize = 2048
|
|
|
|
priv, err = rsa.GenerateKey(rand.Reader, keySize)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
mk, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
|
|
|
|
err = os.WriteFile(path, pemGen, 0700)
|
|
|
|
return pemGen, err
|
2022-02-17 21:28:14 +00:00
|
|
|
}
|
|
|
|
|
2022-07-22 20:32:16 +00:00
|
|
|
func (b *LocalBackend) getSystemSSH_HostKeys() (ret map[string]ssh.Signer) {
|
2022-02-24 19:07:16 +00:00
|
|
|
for _, typ := range keyTypes {
|
2022-07-22 15:01:47 +00:00
|
|
|
filename := "/etc/ssh/ssh_host_" + typ + "_key"
|
2022-09-15 12:06:59 +00:00
|
|
|
hostKey, err := os.ReadFile(filename)
|
2022-07-22 20:32:16 +00:00
|
|
|
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
|
2022-02-17 21:28:14 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
signer, err := ssh.ParsePrivateKey(hostKey)
|
|
|
|
if err != nil {
|
2022-07-22 20:32:16 +00:00
|
|
|
b.logf("warning: error reading host key %s: %v (generating one instead)", filename, err)
|
|
|
|
continue
|
2022-02-17 21:28:14 +00:00
|
|
|
}
|
2022-07-22 20:32:16 +00:00
|
|
|
mak.Set(&ret, typ, signer)
|
2022-02-17 21:28:14 +00:00
|
|
|
}
|
2022-07-22 20:32:16 +00:00
|
|
|
return ret
|
2022-02-17 21:28:14 +00:00
|
|
|
}
|
2022-02-17 23:00:41 +00:00
|
|
|
|
|
|
|
func (b *LocalBackend) getSSHHostKeyPublicStrings() (ret []string) {
|
|
|
|
signers, _ := b.GetSSH_HostKeys()
|
|
|
|
for _, signer := range signers {
|
|
|
|
ret = append(ret, strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey()))))
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|