diff --git a/cmd/discovery/client/client.go b/cmd/discovery/client/client.go index 964e09e..5bed811 100644 --- a/cmd/discovery/client/client.go +++ b/cmd/discovery/client/client.go @@ -16,6 +16,9 @@ import ( "github.com/mudler/yip/pkg/utils" ) +// Because of how go-pluggable works, we can't just print to stdout +const LOGFILE = "/tmp/kcrypt-challenger-client.log" + var errPartNotFound error = fmt.Errorf("pass for partition not found") var errBadCertificate error = fmt.Errorf("unknown certificate") @@ -30,6 +33,10 @@ func NewClient() (*Client, error) { // ❯ echo '{ "data": "{ \\"label\\": \\"LABEL\\" }"}' | sudo -E WSS_SERVER="http://localhost:8082/challenge" ./challenger "discovery.password" func (c *Client) Start() error { + if err := os.RemoveAll(LOGFILE); err != nil { // Start fresh + return fmt.Errorf("removing the logfile: %w", err) + } + factory := pluggable.NewPluginFactory() // Input: bus.EventInstallPayload @@ -59,7 +66,7 @@ func (c *Client) Start() error { return factory.Run(pluggable.EventType(os.Args[1]), os.Stdin, os.Stdout) } -func (c *Client) generatePass(postEndpoint string, p *block.Partition) error { +func (c *Client) generatePass(postEndpoint string, headers map[string]string, p *block.Partition) error { rand := utils.RandomString(32) pass, err := tpm.EncryptBlob([]byte(rand)) @@ -75,6 +82,10 @@ func (c *Client) generatePass(postEndpoint string, p *block.Partition) error { tpm.WithAdditionalHeader("name", p.Name), tpm.WithAdditionalHeader("uuid", p.UUID), } + for k, v := range headers { + opts = append(opts, tpm.WithAdditionalHeader(k, v)) + } + conn, err := tpm.Connection(postEndpoint, opts...) if err != nil { return err @@ -84,20 +95,27 @@ func (c *Client) generatePass(postEndpoint string, p *block.Partition) error { } func (c *Client) waitPass(p *block.Partition, attempts int) (pass string, err error) { - // IF we don't have any server configured, just do local - if c.Config.Kcrypt.Challenger.Server == "" { + additionalHeaders := map[string]string{} + serverURL := c.Config.Kcrypt.Challenger.Server + + // If we don't have any server configured, just do local + if serverURL == "" { return localPass(c.Config) } - challengeEndpoint := fmt.Sprintf("%s/getPass", c.Config.Kcrypt.Challenger.Server) - postEndpoint := fmt.Sprintf("%s/postPass", c.Config.Kcrypt.Challenger.Server) + if c.Config.Kcrypt.Challenger.MDNS { + serverURL, additionalHeaders, err = queryMDNS(serverURL) + } + + getEndpoint := fmt.Sprintf("%s/getPass", serverURL) + postEndpoint := fmt.Sprintf("%s/postPass", serverURL) for tries := 0; tries < attempts; tries++ { var generated bool - pass, generated, err = getPass(challengeEndpoint, c.Config.Kcrypt.Challenger.Certificate, p) + pass, generated, err = getPass(getEndpoint, additionalHeaders, c.Config.Kcrypt.Challenger.Certificate, p) if err == errPartNotFound { // IF server doesn't have a pass for us, then we generate one and we set it - err = c.generatePass(postEndpoint, p) + err = c.generatePass(postEndpoint, additionalHeaders, p) if err != nil { return } @@ -118,7 +136,7 @@ func (c *Client) waitPass(p *block.Partition, attempts int) (pass string, err er return } - fmt.Printf("Failed with error: %s . Will retry.\n", err.Error()) + logToFile("Failed with error: %s . Will retry.\n", err.Error()) time.Sleep(1 * time.Second) // network errors? retry } @@ -145,3 +163,14 @@ func (c *Client) decryptPassphrase(pass string) (string, error) { return string(passBytes), err } + +func logToFile(format string, a ...any) { + s := fmt.Sprintf(format, a...) + file, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + panic(err) + } + defer file.Close() + + file.WriteString(s) +} diff --git a/cmd/discovery/client/config.go b/cmd/discovery/client/config.go index 0e54137..27fc290 100644 --- a/cmd/discovery/client/config.go +++ b/cmd/discovery/client/config.go @@ -13,11 +13,10 @@ type Client struct { type Config struct { Kcrypt struct { Challenger struct { - Server string `yaml:"challenger_server,omitempty"` - // Non-volatile index memory: where we store the encrypted passphrase (offline mode) - NVIndex string `yaml:"nv_index,omitempty"` - // Certificate index: this is where the rsa pair that decrypts the passphrase lives - CIndex string `yaml:"c_index,omitempty"` + MDNS bool `yaml:"mdns,omitempty"` + Server string `yaml:"challenger_server,omitempty"` + NVIndex string `yaml:"nv_index,omitempty"` // Non-volatile index memory: where we store the encrypted passphrase (offline mode) + CIndex string `yaml:"c_index,omitempty"` // Certificate index: this is where the rsa pair that decrypts the passphrase lives TPMDevice string `yaml:"tpm_device,omitempty"` Certificate string `yaml:"certificate,omitempty"` } diff --git a/cmd/discovery/client/enc.go b/cmd/discovery/client/enc.go index e60c68d..5d698e8 100644 --- a/cmd/discovery/client/enc.go +++ b/cmd/discovery/client/enc.go @@ -16,13 +16,19 @@ import ( const DefaultNVIndex = "0x1500000" -func getPass(server, certificate string, partition *block.Partition) (string, bool, error) { - msg, err := tpm.Get(server, +func getPass(server string, headers map[string]string, certificate string, partition *block.Partition) (string, bool, error) { + opts := []tpm.Option{ tpm.WithCAs([]byte(certificate)), tpm.AppendCustomCAToSystemCA, tpm.WithAdditionalHeader("label", partition.FilesystemLabel), tpm.WithAdditionalHeader("name", partition.Name), - tpm.WithAdditionalHeader("uuid", partition.UUID)) + tpm.WithAdditionalHeader("uuid", partition.UUID), + } + for k, v := range headers { + opts = append(opts, tpm.WithAdditionalHeader(k, v)) + } + + msg, err := tpm.Get(server, opts...) if err != nil { return "", false, err } diff --git a/cmd/discovery/client/mdns.go b/cmd/discovery/client/mdns.go new file mode 100644 index 0000000..7a6b329 --- /dev/null +++ b/cmd/discovery/client/mdns.go @@ -0,0 +1,85 @@ +package client + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "github.com/hashicorp/mdns" +) + +const ( + MDNSServiceType = "_kcrypt._tcp" + MDNSTimeout = 15 * time.Second +) + +// queryMDNS will make an mdns query on local network to find a kcrypt challenger server +// instance. If none is found, the original URL is returned and no additional headers. +// If a response is received, the IP address and port from the response will be returned// and an additional "Host" header pointing to the original host. +func queryMDNS(originalURL string) (string, map[string]string, error) { + additionalHeaders := map[string]string{} + var err error + + parsedURL, err := url.Parse(originalURL) + if err != nil { + return originalURL, additionalHeaders, fmt.Errorf("parsing the original host: %w", err) + } + + host := parsedURL.Host + if !strings.HasSuffix(host, ".local") { // sanity check + return "", additionalHeaders, fmt.Errorf("domain should end in \".local\" when using mdns") + } + + mdnsIP, mdnsPort := discoverMDNSServer(host) + if mdnsIP == "" { // no reply + logToFile("no reply from mdns\n") + return originalURL, additionalHeaders, nil + } + + additionalHeaders["Host"] = parsedURL.Host + newURL := strings.ReplaceAll(originalURL, host, mdnsIP) + // Remove any port in the original url + if port := parsedURL.Port(); port != "" { + newURL = strings.ReplaceAll(newURL, port, "") + } + + // Add any possible port from the mdns response + if mdnsPort != "" { + newURL = strings.ReplaceAll(newURL, mdnsIP, fmt.Sprintf("%s:%s", mdnsIP, mdnsPort)) + } + + return newURL, additionalHeaders, nil +} + +// discoverMDNSServer performs an mDNS query to discover any running kcrypt challenger +// servers on the same network that matches the given hostname. +// If a response if received, the IP address and the Port from the response are returned. +func discoverMDNSServer(hostname string) (string, string) { + // Make a channel for results and start listening + entriesCh := make(chan *mdns.ServiceEntry, 4) + defer close(entriesCh) + + logToFile("Will now wait for some mdns server to respond\n") + // Start the lookup. It will block until we read from the chan. + mdns.Lookup(MDNSServiceType, entriesCh) + + expectedHost := hostname + "." // FQDN + // Wait until a matching server is found or we reach a timeout + for { + select { + case entry := <-entriesCh: + logToFile("mdns response received\n") + if entry.Host == expectedHost { + logToFile("%s matches %s\n", entry.Host, expectedHost) + return entry.AddrV4.String(), strconv.Itoa(entry.Port) // TODO: v6? + } else { + logToFile("%s didn't match %s\n", entry.Host, expectedHost) + } + case <-time.After(MDNSTimeout): + logToFile("timed out waiting for mdns\n") + return "", "" + } + } +} diff --git a/go.mod b/go.mod index c468f97..a03ba96 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,11 @@ require ( github.com/go-logr/logr v1.2.4 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 + github.com/hashicorp/mdns v1.0.5 github.com/jaypipes/ghw v0.11.0 github.com/kairos-io/kairos-sdk v0.0.15 github.com/kairos-io/kcrypt v0.7.0 - github.com/kairos-io/tpm-helpers v0.0.0-20231220125202-70a0b64e5111 + github.com/kairos-io/tpm-helpers v0.0.0-20240123063624-f7a3fcc66199 github.com/mudler/go-pluggable v0.0.0-20230126220627-7710299a0ae5 github.com/mudler/go-processmanager v0.0.0-20220724164624-c45b5c61312d github.com/mudler/yip v1.3.0 @@ -85,7 +86,6 @@ require ( github.com/gookit/color v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/mdns v1.0.5 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.15 // indirect github.com/ipfs/go-log v1.0.5 // indirect diff --git a/go.sum b/go.sum index 908b8c0..bb31000 100644 --- a/go.sum +++ b/go.sum @@ -415,8 +415,8 @@ github.com/kairos-io/kairos-sdk v0.0.15 h1:1hcnRfKlBzDWcZ8z7UrUqJ2v6GafCHZknPqm9 github.com/kairos-io/kairos-sdk v0.0.15/go.mod h1:Ew3NKFuXByu3Y3yGu8Q92M3oMqsXrg2VilouubdhYqA= github.com/kairos-io/kcrypt v0.7.0 h1:ESmCBIFbBBv7mJf0/f6ugqwSvz63M5oP9sUIdHiDlLc= github.com/kairos-io/kcrypt v0.7.0/go.mod h1:a9eI+vPVIQHPRtqEV/O/yIfDOdMWd9epVrq1p94gccM= -github.com/kairos-io/tpm-helpers v0.0.0-20231220125202-70a0b64e5111 h1:iEBUZdneuS3YfRunKmRpxfCe4XBxeSiBcIXRmNcUzrw= -github.com/kairos-io/tpm-helpers v0.0.0-20231220125202-70a0b64e5111/go.mod h1:mxKPLc7DQu9bcL8hv8A5PqTSH/rWm0M7SGolwYRYJq4= +github.com/kairos-io/tpm-helpers v0.0.0-20240123063624-f7a3fcc66199 h1:eXiZNiQfZDelYfTF733IxDOKGAKJqn0fF0kFY10QreU= +github.com/kairos-io/tpm-helpers v0.0.0-20240123063624-f7a3fcc66199/go.mod h1:6YGebKVrPoJGBd9QE+x4zyuo3vPw1y33iQkNChjlBo8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= diff --git a/mdns-notes.md b/mdns-notes.md index d99abb2..de1dc6c 100644 --- a/mdns-notes.md +++ b/mdns-notes.md @@ -17,19 +17,23 @@ k3d cluster create kcrypt -p '30000:30000@server:0' ``` helm repo add kairos https://kairos-io.github.io/helm-charts helm install kairos-crd kairos/kairos-crds -helm install kairos-challenger kairos/kairos-challenger ``` -- Edit the challenger service `kairos-challenger-escrow-service` and change it to NodePort (or do it through the helm chart when installing) +Create the following 'kcrypt-challenger-values.yaml` file: -``` - ports: - - name: wss + +```yaml +service: + challenger: + type: "NodePort" port: 8082 - protocol: TCP - targetPort: 8082 nodePort: 30000 - type: NodePort +``` + +and deploy the challenger server with it: + +```bash +helm install -f kcrypt-challenger-values.yaml kairos-challenger kairos/kairos-challenger ``` - Add the sealedvolume and secret for the tpm chip: @@ -52,7 +56,7 @@ metadata: spec: TPMHash: "5640e37f4016da16b841a93880dcc44886904392fa3c86681087b77db5afedbe" partitions: - - label: "persistent" + - label: COS_PERSISTENT secret: name: example-host-tpm-secret path: pass @@ -62,7 +66,7 @@ spec: - Start the [simple-mdns-server](https://github.com/kairos-io/simple-mdns-server) ``` -go run . --port 30000 --interfaceName enp121s0 --serviceType _kcrypt._tcp +go run . --port 30000 --interfaceName enp121s0 --serviceType _kcrypt._tcp --hostName mychallenger.local ``` @@ -86,7 +90,7 @@ install: # Kcrypt configuration block kcrypt: challenger: - challenger_server: "http://doesnt-matter-the-name.local" + challenger_server: "http://mychallenger.local" ``` - Install: