From eb74b6ea15a15944554ba630a5fb5bcddddbcd66 Mon Sep 17 00:00:00 2001 From: Jared Allard Date: Sat, 1 Mar 2025 10:09:26 -0800 Subject: [PATCH] support encryption --- .gitignore | 1 + cmd/klefkictl/klefkictl.go | 9 +- cmd/klefkictl/klefkictl_requests.go | 124 ++++++++++++++ docs/DESIGN.md | 9 +- go.mod | 13 +- go.sum | 26 ++- internal/db/ent/migrate/schema.go | 2 +- internal/db/ent/runtime/runtime.go | 4 +- internal/machines/machine.go | 39 ++++- internal/server/auth.go | 50 ++++++ .../generated/go/rgst/klefki/v1/kelfki.pb.go | 160 +++++++++++++++--- .../grpc/proto/rgst/klefki/v1/kelfki.proto | 8 +- internal/server/server.go | 71 +++++++- 13 files changed, 463 insertions(+), 53 deletions(-) create mode 100644 cmd/klefkictl/klefkictl_requests.go create mode 100644 internal/server/auth.go diff --git a/.gitignore b/.gitignore index b2a1397..64d69c4 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,5 @@ CHANGELOG.md ## <> data/ !data/.gitkeep +*.key ## <> diff --git a/cmd/klefkictl/klefkictl.go b/cmd/klefkictl/klefkictl.go index 0384e38..e9fd8d5 100644 --- a/cmd/klefkictl/klefkictl.go +++ b/cmd/klefkictl/klefkictl.go @@ -27,11 +27,7 @@ import ( ) func main() { - exitCode := 0 - defer func() { os.Exit(exitCode) }() - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) - defer cancel() rootCmd := &cobra.Command{ Use: "klefkictl", @@ -41,9 +37,12 @@ func main() { newNewCommand(), newListCommand(), newDeleteCommand(), + newRequestsCommand(), ) if err := rootCmd.ExecuteContext(ctx); err != nil { fmt.Fprintln(os.Stderr, err) - exitCode = 1 + os.Exit(1) } + + cancel() } diff --git a/cmd/klefkictl/klefkictl_requests.go b/cmd/klefkictl/klefkictl_requests.go new file mode 100644 index 0000000..fb7ce41 --- /dev/null +++ b/cmd/klefkictl/klefkictl_requests.go @@ -0,0 +1,124 @@ +// Copyright (C) 2025 klefki contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0 + +package main + +import ( + "bytes" + "crypto/ed25519" + "fmt" + "io" + "os" + + pbgrpcv1 "git.rgst.io/homelab/klefki/internal/server/grpc/generated/go/rgst/klefki/v1" + + "git.rgst.io/homelab/klefki/internal/machines" + "git.rgst.io/homelab/sigtool/v3/sign" + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// newRequestsCommand creates a requests [cobra.Command] +func newRequestsCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "requests", + Short: "Make requests to a klefki server", + } + cmd.AddCommand(newGetKeyRequestCommand()) + return cmd +} + +// nopWriteCloser is a no-op [io.WriteCloser] +type nopWriteCloser struct { + io.Writer +} + +// Close implements [io.Closer] +func (nwc nopWriteCloser) Close() error { + return nil +} + +// newNopWriteCloser creates a new nopWriteCloser +func newNopWriteCloser(w io.Writer) *nopWriteCloser { + return &nopWriteCloser{w} +} + +// newGetKeyRequestCommand creates a getkeyrequest [cobra.Command] +func newGetKeyRequestCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "getkeyrequest", + Short: "Get the passphrase for the given machine", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + privKeyPath := cmd.Flag("priv-key").Value.String() + + privKeyByt, err := os.ReadFile(privKeyPath) + if err != nil { + return err + } + + pk, err := machines.DecodePrivateKey(privKeyByt) + if err != nil { + return err + } + + machineID, err := machines.Fingerprint(pk.Public().(ed25519.PublicKey)) + if err != nil { + return fmt.Errorf("failed to get fingerprint for key: %w", err) + } + + spk, err := sign.PrivateKeyFromBytes(pk) + if err != nil { + return fmt.Errorf("failed to create private key for decryption: %w", err) + } + + fmt.Printf("Sending GetKeyRequest: machine_id=%s\n", machineID) + + // TODO(jaredallard): Make a client + conn, err := grpc.NewClient("127.0.0.1:5300", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return fmt.Errorf("failed to connect to klefki server: %w", err) + } + defer conn.Close() + + client := pbgrpcv1.NewKlefkiServiceClient(conn) + + req := &pbgrpcv1.GetKeyRequest{} + req.SetMachineId(machineID) + req.SetNonce("FIXME") + req.SetSignature(ed25519.Sign(pk, []byte(req.GetNonce()))) + + resp, err := client.GetKey(cmd.Context(), req) + if err != nil { + return fmt.Errorf("failed to get key from server: %w", err) + } + + dec, err := sign.NewDecryptor(bytes.NewReader(resp.GetKey())) + if err != nil { + return fmt.Errorf("failed to create decryptor: %w", err) + } + if err := dec.SetPrivateKey(spk, nil); err != nil { + return fmt.Errorf("failed to set private key on decryptor: %w", err) + } + return dec.Decrypt(os.Stdout) + }, + } + flags := cmd.Flags() + flags.String("priv-key", "", "path to private key") + return cmd +} diff --git a/docs/DESIGN.md b/docs/DESIGN.md index c5ed3db..8ef143a 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -39,11 +39,14 @@ disk. ### Security -- Pass-phrases are encrypted using to public key of the authenticated - machine to prevent the pass-phrase from ever being send unencrypted or - being able to decrypted the key. +- Pass-phrases are encrypted to public key of the authenticated machine + to prevent the pass-phrase from ever being sent unencrypted or being + able to decrypted the key. - Machine IDs are derived from the authenticated machine, through a signature check (public keys are stored on the server side). + - This technically is vulnerable to replay attacks. However, the + returned data is encrypted to the key holder. An attacker replaying + this would get encrypted data only. ### Flow diff --git a/go.mod b/go.mod index ad3607a..3a677f1 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,10 @@ module git.rgst.io/homelab/klefki -go 1.23.0 - -toolchain go1.24.0 +go 1.24 require ( entgo.io/ent v0.14.3 + git.rgst.io/homelab/sigtool/v3 v3.2.3-jaredallard.2 github.com/ncruces/go-sqlite3 v0.24.0 github.com/spf13/cobra v1.9.1 google.golang.org/grpc v1.70.0 @@ -17,6 +16,7 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect github.com/go-openapi/inflect v0.21.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -26,11 +26,17 @@ require ( github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/opencoff/go-fio v0.5.13 // indirect + github.com/opencoff/go-mmap v0.1.5 // indirect + github.com/pkg/xattr v0.4.10 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/zclconf/go-cty v1.16.2 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.35.0 // indirect golang.org/x/mod v0.23.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sync v0.11.0 // indirect @@ -38,6 +44,7 @@ require ( golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.30.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) tool entgo.io/ent/cmd/ent diff --git a/go.sum b/go.sum index 29a6524..24a0b73 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 h1:nX4HXncwIdvQ8/8sIUIf1nyC ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w= entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ= entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= +git.rgst.io/homelab/sigtool/v3 v3.2.3-jaredallard.2 h1:uZBtO7FkSvLoR9lgw36StTihkLKh8Rcc9QiAZW+z6gA= +git.rgst.io/homelab/sigtool/v3 v3.2.3-jaredallard.2/go.mod h1:0krgyBHYyZgmdfc9yzP+QVHlEvw+6bKKrSyt8a7A8oE= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= @@ -13,6 +15,8 @@ github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -44,8 +48,18 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opencoff/go-fio v0.5.13 h1:j0vcntRgk475Lw27FOZ35Vw0U7zEY6UFCDfXKynXisY= +github.com/opencoff/go-fio v0.5.13/go.mod h1:mehrXmBVDrLdmPrzeuihR1Fv9SnAo+P+riSQybhOg3k= +github.com/opencoff/go-mmap v0.1.5 h1:RKPtevC4mOW5bi9skBPPo4nFTIH4lVWAL20Tff+FjLg= +github.com/opencoff/go-mmap v0.1.5/go.mod h1:y/6Jk/tDUc00k3oSQpiJX++20Nw7xFSlc5kLkhGnRXw= +github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= +github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -53,8 +67,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= @@ -73,14 +87,19 @@ go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiy go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= @@ -91,6 +110,9 @@ google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/db/ent/migrate/schema.go b/internal/db/ent/migrate/schema.go index e800f30..22da516 100644 --- a/internal/db/ent/migrate/schema.go +++ b/internal/db/ent/migrate/schema.go @@ -12,7 +12,7 @@ var ( MachinesColumns = []*schema.Column{ {Name: "id", Type: field.TypeString}, {Name: "public_key", Type: field.TypeBytes}, - {Name: "created_at", Type: field.TypeString, Default: "2025-02-23T06:06:02Z"}, + {Name: "created_at", Type: field.TypeString, Default: "2025-03-01T18:10:35Z"}, } // MachinesTable holds the schema information for the "machines" table. MachinesTable = &schema.Table{ diff --git a/internal/db/ent/runtime/runtime.go b/internal/db/ent/runtime/runtime.go index fbf43da..7ccdeef 100644 --- a/internal/db/ent/runtime/runtime.go +++ b/internal/db/ent/runtime/runtime.go @@ -5,6 +5,6 @@ package runtime // The schema-stitching logic is generated in git.rgst.io/homelab/klefki/internal/db/ent/runtime.go const ( - Version = "v0.14.2" // Version of ent codegen. - Sum = "h1:ywld/j2Rx4EmnIKs8eZ29cbFA1zpB+DA9TLL5l3rlq0=" // Sum of ent codegen. + Version = "v0.14.3" // Version of ent codegen. + Sum = "h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=" // Sum of ent codegen. ) diff --git a/internal/machines/machine.go b/internal/machines/machine.go index 4085d26..0b4e12f 100644 --- a/internal/machines/machine.go +++ b/internal/machines/machine.go @@ -29,8 +29,8 @@ import ( "git.rgst.io/homelab/klefki/internal/db/ent" ) -// getFingerprint returns a fingerprint of the key. -func getFingerprint(pub ed25519.PublicKey) (string, error) { +// Fingerprint returns a fingerprint of the provided key. +func Fingerprint(pub ed25519.PublicKey) (string, error) { hasher := sha256.New() if _, err := hasher.Write(pub); err != nil { return "", fmt.Errorf("failed to hash provided public key: %w", err) @@ -73,7 +73,7 @@ func (m *Machine) Fingerprint() (string, error) { if m.fingerprint != "" { return // NOOP if already set. } - m.fingerprint, err = getFingerprint(m.PublicKey) + m.fingerprint, err = Fingerprint(m.PublicKey) }) if err != nil { return "", fmt.Errorf("failed to calculate fingerprint: %w", err) @@ -121,3 +121,36 @@ func NewMachine() (*Machine, error) { return &Machine{PublicKey: pub, PrivateKey: priv}, nil } + +// DecodePrivateKey decodes a private key that was encoded by +// [Machine.EncodePrivateKey]. +func DecodePrivateKey(data []byte) (ed25519.PrivateKey, error) { + b, _ := pem.Decode(data) + if b == nil { + return nil, fmt.Errorf("failed to parse private key as PEM encoded data") + } + if b.Type != "ED25519 PRIVATE KEY" { + return nil, fmt.Errorf("expected type \"ED25519 PRIVATE KEY\", got %s", b.Type) + } + + k, err := x509.ParsePKCS8PrivateKey(b.Bytes) + if err != nil { + return nil, err + } + + pk, ok := k.(ed25519.PrivateKey) + if !ok { + return nil, fmt.Errorf("expected ed25519.PrivateKey, got %T", k) + } + return pk, nil +} + +// Verify takes the provided pubKey and determines if the provided +// signature was made by it, for the nonce. A nil error is success. +func Verify(pubKey ed25519.PublicKey, sig []byte, nonce string) error { + if ed25519.Verify(pubKey, []byte(nonce), sig) { + return nil + } + + return fmt.Errorf("invalid signature") +} diff --git a/internal/server/auth.go b/internal/server/auth.go new file mode 100644 index 0000000..922b57a --- /dev/null +++ b/internal/server/auth.go @@ -0,0 +1,50 @@ +// Copyright (C) 2025 klefki contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0 + +package server + +import ( + "context" + "crypto/ed25519" + + pbgrpcv1 "git.rgst.io/homelab/klefki/internal/server/grpc/generated/go/rgst/klefki/v1" +) + +// AuthChallenge is an authentication challenge. +type AuthChallenge struct { + // MachineID is the ID of the machine that sent this request + // (fingerprint). + MachineID string `json:"machine_id"` + + // Signature is an ED25519 signature of the provided message. + Signature []byte `json:"signature"` + + // Nonce is a randomly generated string that corresponds to the + // provided signature. + Nonce string `json:"nonce"` +} + +// ValidateAuth determines if the auth presented is valid or not. +func (s *Server) ValidateAuth(ctx context.Context, req *pbgrpcv1.GetKeyRequest) bool { + // Get the public key for this node. + m, err := s.db.Machine.Get(ctx, req.GetMachineId()) + if err != nil { + return false + } + + return ed25519.Verify(m.PublicKey, []byte(req.GetNonce()), req.GetSignature()) +} diff --git a/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki.pb.go b/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki.pb.go index 0ba5141..b8112b3 100644 --- a/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki.pb.go +++ b/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki.pb.go @@ -23,9 +23,14 @@ const ( ) type GetKeyRequest struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_MachineId *string `protobuf:"bytes,1,opt,name=machine_id,json=machineId"` + xxx_hidden_Signature []byte `protobuf:"bytes,2,opt,name=signature"` + xxx_hidden_Nonce *string `protobuf:"bytes,3,opt,name=nonce"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetKeyRequest) Reset() { @@ -53,21 +58,117 @@ func (x *GetKeyRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } +func (x *GetKeyRequest) GetMachineId() string { + if x != nil { + if x.xxx_hidden_MachineId != nil { + return *x.xxx_hidden_MachineId + } + return "" + } + return "" +} + +func (x *GetKeyRequest) GetSignature() []byte { + if x != nil { + return x.xxx_hidden_Signature + } + return nil +} + +func (x *GetKeyRequest) GetNonce() string { + if x != nil { + if x.xxx_hidden_Nonce != nil { + return *x.xxx_hidden_Nonce + } + return "" + } + return "" +} + +func (x *GetKeyRequest) SetMachineId(v string) { + x.xxx_hidden_MachineId = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 3) +} + +func (x *GetKeyRequest) SetSignature(v []byte) { + if v == nil { + v = []byte{} + } + x.xxx_hidden_Signature = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 3) +} + +func (x *GetKeyRequest) SetNonce(v string) { + x.xxx_hidden_Nonce = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 3) +} + +func (x *GetKeyRequest) HasMachineId() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *GetKeyRequest) HasSignature() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *GetKeyRequest) HasNonce() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 2) +} + +func (x *GetKeyRequest) ClearMachineId() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_MachineId = nil +} + +func (x *GetKeyRequest) ClearSignature() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_Signature = nil +} + +func (x *GetKeyRequest) ClearNonce() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 2) + x.xxx_hidden_Nonce = nil +} + type GetKeyRequest_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + MachineId *string + Signature []byte + Nonce *string } func (b0 GetKeyRequest_builder) Build() *GetKeyRequest { m0 := &GetKeyRequest{} b, x := &b0, m0 _, _ = b, x + if b.MachineId != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 3) + x.xxx_hidden_MachineId = b.MachineId + } + if b.Signature != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 3) + x.xxx_hidden_Signature = b.Signature + } + if b.Nonce != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 3) + x.xxx_hidden_Nonce = b.Nonce + } return m0 } type GetKeyResponse struct { state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_Key *string `protobuf:"bytes,1,opt,name=key"` + xxx_hidden_Key []byte `protobuf:"bytes,1,opt,name=key"` XXX_raceDetectHookData protoimpl.RaceDetectHookData XXX_presence [1]uint32 unknownFields protoimpl.UnknownFields @@ -99,18 +200,18 @@ func (x *GetKeyResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -func (x *GetKeyResponse) GetKey() string { +func (x *GetKeyResponse) GetKey() []byte { if x != nil { - if x.xxx_hidden_Key != nil { - return *x.xxx_hidden_Key - } - return "" + return x.xxx_hidden_Key } - return "" + return nil } -func (x *GetKeyResponse) SetKey(v string) { - x.xxx_hidden_Key = &v +func (x *GetKeyResponse) SetKey(v []byte) { + if v == nil { + v = []byte{} + } + x.xxx_hidden_Key = v protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 1) } @@ -129,7 +230,7 @@ func (x *GetKeyResponse) ClearKey() { type GetKeyResponse_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - Key *string + Key []byte } func (b0 GetKeyResponse_builder) Build() *GetKeyResponse { @@ -151,20 +252,25 @@ var file_rgst_klefki_v1_kelfki_proto_rawDesc = string([]byte{ 0x67, 0x73, 0x74, 0x2e, 0x6b, 0x6c, 0x65, 0x66, 0x6b, 0x69, 0x2e, 0x76, 0x31, 0x1a, 0x21, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, 0x5f, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0x0f, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0x22, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x32, 0x58, 0x0a, 0x0d, 0x4b, 0x6c, 0x65, 0x66, 0x6b, 0x69, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x47, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, - 0x12, 0x1d, 0x2e, 0x72, 0x67, 0x73, 0x74, 0x2e, 0x6b, 0x6c, 0x65, 0x66, 0x6b, 0x69, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1e, 0x2e, 0x72, 0x67, 0x73, 0x74, 0x2e, 0x6b, 0x6c, 0x65, 0x66, 0x6b, 0x69, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, - 0x3f, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x2e, 0x72, 0x67, 0x73, 0x74, 0x2e, 0x69, 0x6f, 0x2f, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x67, 0x65, 0x6e, - 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x6f, 0x2f, 0x72, 0x67, 0x73, 0x74, 0x2f, 0x6b, - 0x6c, 0x65, 0x66, 0x6b, 0x69, 0x2f, 0x76, 0x31, 0x92, 0x03, 0x05, 0xd2, 0x3e, 0x02, 0x10, 0x03, - 0x62, 0x08, 0x65, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x70, 0xe8, 0x07, + 0x22, 0x62, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, + 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x22, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x32, 0x58, 0x0a, 0x0d, 0x4b, 0x6c, 0x65, 0x66, + 0x6b, 0x69, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x47, 0x0a, 0x06, 0x47, 0x65, 0x74, + 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x2e, 0x72, 0x67, 0x73, 0x74, 0x2e, 0x6b, 0x6c, 0x65, 0x66, 0x6b, + 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x72, 0x67, 0x73, 0x74, 0x2e, 0x6b, 0x6c, 0x65, 0x66, 0x6b, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x2e, 0x72, 0x67, 0x73, 0x74, 0x2e, 0x69, + 0x6f, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, + 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x6f, 0x2f, 0x72, 0x67, 0x73, + 0x74, 0x2f, 0x6b, 0x6c, 0x65, 0x66, 0x6b, 0x69, 0x2f, 0x76, 0x31, 0x92, 0x03, 0x05, 0xd2, 0x3e, + 0x02, 0x10, 0x03, 0x62, 0x08, 0x65, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x70, 0xe8, 0x07, }) var file_rgst_klefki_v1_kelfki_proto_msgTypes = make([]protoimpl.MessageInfo, 2) diff --git a/internal/server/grpc/proto/rgst/klefki/v1/kelfki.proto b/internal/server/grpc/proto/rgst/klefki/v1/kelfki.proto index aa8fb9e..9a996f5 100644 --- a/internal/server/grpc/proto/rgst/klefki/v1/kelfki.proto +++ b/internal/server/grpc/proto/rgst/klefki/v1/kelfki.proto @@ -7,10 +7,14 @@ import "google/protobuf/go_features.proto"; option features.(pb.go).api_level = API_OPAQUE; option go_package = "git.rgst.io/internal/grpc/generated/go/rgst/klefki/v1"; -message GetKeyRequest {} +message GetKeyRequest { + string machine_id = 1; + bytes signature = 2; + string nonce = 3; +} message GetKeyResponse { - string key = 1; + bytes key = 1; } service KlefkiService { diff --git a/internal/server/server.go b/internal/server/server.go index cf1a682..ea3cc52 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -19,23 +19,52 @@ package server import ( + "bytes" "context" "fmt" + "io" "net" + "strings" + "git.rgst.io/homelab/klefki/internal/db" + "git.rgst.io/homelab/klefki/internal/db/ent" + "git.rgst.io/homelab/klefki/internal/machines" pbgrpcv1 "git.rgst.io/homelab/klefki/internal/server/grpc/generated/go/rgst/klefki/v1" + "git.rgst.io/homelab/sigtool/v3/sign" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) +// nopWriteCloser is a no-op [io.WriteCloser] +type nopWriteCloser struct { + io.Writer +} + +// Close implements [io.Closer] +func (nwc nopWriteCloser) Close() error { + return nil +} + +// newNopWriteCloser creates a new nopWriteCloser +func newNopWriteCloser(w io.Writer) *nopWriteCloser { + return &nopWriteCloser{w} +} + // Server is a Klefki gRPC server type Server struct { gs *grpc.Server + db *ent.Client pbgrpcv1.UnimplementedKlefkiServiceServer } // Run starts the server -func (s *Server) Run(_ context.Context) error { +func (s *Server) Run(ctx context.Context) error { + var err error + s.db, err = db.New(ctx) + if err != nil { + return fmt.Errorf("failed to open DB: %w", err) + } + s.gs = grpc.NewServer() pbgrpcv1.RegisterKlefkiServiceServer(s.gs, s) reflection.Register(s.gs) @@ -50,9 +79,42 @@ func (s *Server) Run(_ context.Context) error { } // GetKey implements the GetKey request -func (s *Server) GetKey(_ context.Context, _ *pbgrpcv1.GetKeyRequest) (*pbgrpcv1.GetKeyResponse, error) { +func (s *Server) GetKey(ctx context.Context, req *pbgrpcv1.GetKeyRequest) (*pbgrpcv1.GetKeyResponse, error) { resp := &pbgrpcv1.GetKeyResponse{} - resp.SetKey("hello-world") + + nonce := req.GetNonce() + sig := req.GetSignature() + + machine, err := s.db.Machine.Get(ctx, req.GetMachineId()) + if err != nil { + return nil, err + } + + if err := machines.Verify(machine.PublicKey, sig, nonce); err != nil { + return nil, err + } + + spubk, err := sign.PublicKeyFromBytes(machine.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to create pub key for encryption: %w", err) + } + + enc, err := sign.NewEncryptor(nil, 1024) + if err != nil { + return nil, fmt.Errorf("failed to create encryptor instance: %w", err) + } + + if err := enc.AddRecipient(spubk); err != nil { + return nil, fmt.Errorf("failed to add instance public key to encryptor: %w", err) + } + + // TODO(jaredallard): Wait for input here. + var buf bytes.Buffer + if err := enc.Encrypt(strings.NewReader("hello world"), newNopWriteCloser(&buf)); err != nil { + return nil, fmt.Errorf("failed to encrypt passphrase: %w", err) + } + + resp.SetKey(buf.Bytes()) return resp, nil } @@ -63,7 +125,6 @@ func (s *Server) Close(_ context.Context) error { } fmt.Println("shutting down server") - s.gs.GracefulStop() - return nil + return s.db.Close() }