From 01677d8865923ce13bfac5e3006e11205d65af20 Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Tue, 14 Oct 2025 05:35:25 +0900 Subject: [PATCH] ssh HostKey added --- go.sum | 7 +-- main.go | 32 ++++++++++-- utils/hostkey.go | 131 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 utils/hostkey.go diff --git a/go.sum b/go.sum index 668c2bc..c938159 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,9 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= diff --git a/main.go b/main.go index 24200f5..52d4b82 100644 --- a/main.go +++ b/main.go @@ -4,13 +4,37 @@ import ( "io" "log" + "sshchat/utils" + "github.com/gliderlabs/ssh" ) func main() { - ssh.Handle(func(s ssh.Session) { - io.WriteString(s, "Hello World\n") - }) + keys, err := utils.CheckHostKey() + if err != nil { + log.Print("Failed to check SSH keys: generate one.\n", err) + err = utils.GenerateHostKey() + if err != nil { + log.Fatal(err) + } - log.Fatal(ssh.ListenAndServe(":2222", nil)) + keys, err = utils.CheckHostKey() + if err != nil { + log.Fatal(err) + } + } + + sessionHandler := func(s ssh.Session) { + _, _ = io.WriteString(s, "Hello World\n") + } + + s := &ssh.Server{ + Addr: ":2222", + Handler: sessionHandler, + } + for _, key := range keys { + s.AddHostKey(key) + } + + log.Fatal(s.ListenAndServe()) } diff --git a/utils/hostkey.go b/utils/hostkey.go new file mode 100644 index 0000000..5a1a40f --- /dev/null +++ b/utils/hostkey.go @@ -0,0 +1,131 @@ +package utils + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "encoding/pem" + "fmt" + "os" + + "golang.org/x/crypto/ssh" +) + +// GenerateHostKey는 'keys' 디렉토리를 생성하고, RSA, ECDSA, Ed25519 호스트 개인 키를 생성하여 저장합니다. +// 개인 키는 OpenSSH 형식으로 암호화되어 저장됩니다. +func GenerateHostKey() error { + const keyDir = "./keys" + + // 1. 키 디렉토리 생성 + if err := os.MkdirAll(keyDir, 0700); err != nil { + return fmt.Errorf("failed to create keys directory: %v", err) + } + + // 사용할 키 파일 경로 및 암호화에 사용할 비밀번호 (실제 사용 시 환경 변수 등 안전한 방법으로 관리해야 함) + // OpenSSH 형식에서는 암호화에 비밀번호(passphrase)를 사용합니다. + // 여기서는 예시로 "securepassphrase"를 사용하지만, 실제 서버 환경에서는 강력하고 안전하게 보관되는 패스프레이즈를 사용해야 합니다. + keysToGenerate := []struct { + path string + keyType string + }{ + {path: keyDir + "/id_rsa", keyType: "rsa"}, + {path: keyDir + "/id_ecdsa", keyType: "ecdsa"}, + {path: keyDir + "/id_ed25519", keyType: "ed25519"}, + } + + for _, key := range keysToGenerate { + if err := generateAndSaveKey(key.path, key.keyType); err != nil { + return fmt.Errorf("failed to generate %s key: %v", key.keyType, err) + } + fmt.Printf("Successfully generated and encrypted %s key: %s\n", key.keyType, key.path) + } + + return nil +} + +// generateAndSaveKey는 지정된 유형의 개인 키를 생성하고 OpenSSH 형식으로 암호화하여 파일에 저장합니다. +func generateAndSaveKey(path string, keyType string) error { + var privateKey interface{} + var err error + + // 2. 개인 키 생성 + switch keyType { + case "rsa": + // RSA 키 생성 (4096 비트 권장) + privateKey, err = rsa.GenerateKey(rand.Reader, 4096) + case "ecdsa": + // ECDSA 키 생성 (NIST P-521 곡선 사용 권장) + privateKey, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + case "ed25519": + // Ed25519 키 생성 + _, privateKey, err = ed25519.GenerateKey(rand.Reader) + default: + return fmt.Errorf("unsupported key type: %s", keyType) + } + if err != nil { + return fmt.Errorf("key generation failed: %v", err) + } + + // 3. OpenSSH 형식으로 개인 키 마샬링 (암호화 포함) + // MarshalPrivateKeyWithPassphrase는 OpenSSH 형식으로 키를 암호화합니다. + privatePEM, err := ssh.MarshalPrivateKey( + privateKey, + "", + ) + if err != nil { + return fmt.Errorf("failed to marshal private key: %v", err) + } + + // 4. 개인 키 파일 저장 + // 0600 권한은 소유자에게만 읽기/쓰기 권한을 부여하여 개인 키를 보호합니다. + privateFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("failed to open private key file for writing: %v", err) + } + if err := pem.Encode(privateFile, privatePEM); err != nil { + return fmt.Errorf("failed to write private key to file: %v", err) + } + + // 선택 사항: 공개 키도 저장 + signer, err := ssh.NewSignerFromKey(privateKey) + if err != nil { + return fmt.Errorf("failed to create signer for public key: %v", err) + } + publicPEM := ssh.MarshalAuthorizedKey(signer.PublicKey()) + pubPath := path + ".pub" + if err := os.WriteFile(pubPath, publicPEM, 0644); err != nil { + return fmt.Errorf("failed to write public key to file: %v", err) + } + fmt.Printf("Generated public key: %s\n", pubPath) + + return nil +} + +func CheckHostKey() ([]ssh.Signer, error) { + keyFiles := []string{"./keys/id_rsa", "./keys/id_ecdsa", "./keys/id_ed25519"} + + for _, keyFile := range keyFiles { + if _, err := os.Stat(keyFile); os.IsNotExist(err) { + return nil, fmt.Errorf("key file %s does not exist", keyFile) + } + } + + var keys = make([]ssh.Signer, 0) + for _, keyFile := range keyFiles { + keyBytes, err := os.ReadFile(keyFile) + if err != nil { + return nil, fmt.Errorf("failed to read key file %s: %v", keyFile, err) + } + + signer, err := ssh.ParsePrivateKey(keyBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key %s: %v", keyFile, err) + } + + keys = append(keys, signer) + } + + return keys, nil +}