diff --git a/default.nix b/default.nix index 5eb7a76..5646cad 100644 --- a/default.nix +++ b/default.nix @@ -173,6 +173,7 @@ in rec { pkgs.yq-go pkgs.jq pkgs.dig + pkgs.nebula ]} export SHELL=${pkgs.bash}/bin/bash exec ${pkgs.bash}/bin/bash ${./tests}/entrypoint.sh "$@" diff --git a/go/bootstrap/hosts.go b/go/bootstrap/hosts.go index f50435f..1f18721 100644 --- a/go/bootstrap/hosts.go +++ b/go/bootstrap/hosts.go @@ -3,7 +3,7 @@ package bootstrap import ( "fmt" "isle/nebula" - "net" + "net/netip" ) // NebulaHost describes the nebula configuration of a Host which is relevant for @@ -77,10 +77,17 @@ type Host struct { // // This assumes that the Host and its data has already been verified against the // CA signing key. -func (h Host) IP() net.IP { +func (h Host) IP() netip.Addr { cert := h.PublicCredentials.Cert.Unwrap() if len(cert.Details.Ips) == 0 { panic(fmt.Sprintf("host %q not configured with any ips: %+v", h.Name, h)) } - return cert.Details.Ips[0].IP + + ip := cert.Details.Ips[0].IP + addr, ok := netip.AddrFromSlice(ip) + if !ok { + panic(fmt.Sprintf("ip %q (%#v) is not valid, somehow", ip, ip)) + } + + return addr } diff --git a/go/cmd/entrypoint/admin.go b/go/cmd/entrypoint/admin.go index a1112b0..3e05040 100644 --- a/go/cmd/entrypoint/admin.go +++ b/go/cmd/entrypoint/admin.go @@ -111,89 +111,12 @@ var subCmdAdminCreateBootstrap = subCmd{ }, } -var subCmdAdminCreateNebulaCert = subCmd{ - name: "create-nebula-cert", - descr: "Creates a signed nebula certificate file and writes it to stdout", - do: func(subCmdCtx subCmdCtx) error { - var ( - flags = subCmdCtx.flagSet(false) - hostName nebula.HostName - ip netip.Addr - ) - - hostNameF := flags.VarPF( - textUnmarshalerFlag{&hostName}, - "hostname", "h", - "Name of the host to generate a certificate for", - ) - - ipF := flags.VarPF( - textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host", - ) - - adminPath := flags.StringP( - "admin-path", "a", "", - `Path to admin.json file. If the given path is "-" then stdin is used.`, - ) - - pubKeyPath := flags.StringP( - "public-key-path", "p", "", - `Path to PEM file containing public key which will be embedded in the cert.`, - ) - - if err := flags.Parse(subCmdCtx.args); err != nil { - return fmt.Errorf("parsing flags: %w", err) - } - - if !hostNameF.Changed || - !ipF.Changed || - *adminPath == "" || - *pubKeyPath == "" { - return errors.New("--hostname, --ip, --admin-path, and --pub-key-path are required") - } - - adm, err := readAdmin(*adminPath) - if err != nil { - return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err) - } - - hostPubPEM, err := os.ReadFile(*pubKeyPath) - if err != nil { - return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err) - } - - var hostPub nebula.EncryptingPublicKey - if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil { - return fmt.Errorf("unmarshaling public key as PEM: %w", err) - } - - nebulaHostCert, err := nebula.NewHostCert( - adm.Nebula.CACredentials, hostPub, hostName, ip, - ) - if err != nil { - return fmt.Errorf("creating cert: %w", err) - } - - nebulaHostCertPEM, err := nebulaHostCert.Unwrap().MarshalToPEM() - if err != nil { - return fmt.Errorf("marshaling cert to PEM: %w", err) - } - - if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil { - return fmt.Errorf("writing to stdout: %w", err) - } - - return nil - }, -} - var subCmdAdmin = subCmd{ name: "admin", descr: "Sub-commands which only admins can run", do: func(subCmdCtx subCmdCtx) error { return subCmdCtx.doSubCmd( subCmdAdminCreateBootstrap, - subCmdAdminCreateNebulaCert, ) }, } diff --git a/go/cmd/entrypoint/nebula.go b/go/cmd/entrypoint/nebula.go index 88e412c..94f035e 100644 --- a/go/cmd/entrypoint/nebula.go +++ b/go/cmd/entrypoint/nebula.go @@ -1,12 +1,92 @@ package main import ( + "errors" "fmt" + "isle/daemon" "isle/jsonutil" "isle/nebula" "os" ) +var subCmdNebulaCreateCert = subCmd{ + name: "create-cert", + descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout", + do: func(subCmdCtx subCmdCtx) error { + var ( + flags = subCmdCtx.flagSet(false) + hostName nebula.HostName + ) + + hostNameF := flags.VarPF( + textUnmarshalerFlag{&hostName}, + "hostname", "h", + "Name of the host to generate a certificate for", + ) + + adminPath := flags.StringP( + "admin-path", "a", "", + `Path to admin.json file. If the given path is "-" then stdin is used.`, + ) + + pubKeyPath := flags.StringP( + "public-key-path", "p", "", + `Path to PEM file containing public key which will be embedded in the cert.`, + ) + + if err := flags.Parse(subCmdCtx.args); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + + if !hostNameF.Changed || + *adminPath == "" || + *pubKeyPath == "" { + return errors.New("--hostname, --admin-path, and --pub-key-path are required") + } + + adm, err := readAdmin(*adminPath) + if err != nil { + return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err) + } + + hostPubPEM, err := os.ReadFile(*pubKeyPath) + if err != nil { + return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err) + } + + var hostPub nebula.EncryptingPublicKey + if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil { + return fmt.Errorf("unmarshaling public key as PEM: %w", err) + } + + var res daemon.CreateNebulaCertificateResult + err = subCmdCtx.daemonRCPClient.Call( + subCmdCtx.ctx, + &res, + "CreateNebulaCertificate", + daemon.CreateNebulaCertificateRequest{ + CASigningPrivateKey: adm.Nebula.CACredentials.SigningPrivateKey, + HostName: hostName, + HostEncryptingPublicKey: hostPub, + }, + ) + if err != nil { + return fmt.Errorf("calling CreateNebulaCertificate: %w", err) + } + + nebulaHostCertPEM, err := res.HostNebulaCertifcate.Unwrap().MarshalToPEM() + if err != nil { + return fmt.Errorf("marshaling cert to PEM: %w", err) + } + + if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil { + return fmt.Errorf("writing to stdout: %w", err) + } + + return nil + }, +} + var subCmdNebulaShow = subCmd{ name: "show", descr: "Writes nebula network information to stdout in JSON format", @@ -30,20 +110,17 @@ var subCmdNebulaShow = subCmd{ return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err) } - caCert := caPublicCreds.Cert.Unwrap() - caCertPEM, err := caCert.MarshalToPEM() - if err != nil { - return fmt.Errorf("marshaling CA cert to PEM: %w", err) - } + caCert := caPublicCreds.Cert + caCertDetails := caCert.Unwrap().Details - if len(caCert.Details.Subnets) != 1 { + if len(caCertDetails.Subnets) != 1 { return fmt.Errorf( "malformed ca.crt, contains unexpected subnets %#v", - caCert.Details.Subnets, + caCertDetails.Subnets, ) } - subnet := caCert.Details.Subnets[0] + subnet := caCertDetails.Subnets[0] type outLighthouse struct { PublicAddr string @@ -51,11 +128,11 @@ var subCmdNebulaShow = subCmd{ } out := struct { - CACert string + CACert nebula.Certificate SubnetCIDR string Lighthouses []outLighthouse }{ - CACert: string(caCertPEM), + CACert: caCert, SubnetCIDR: subnet.String(), } @@ -83,6 +160,7 @@ var subCmdNebula = subCmd{ descr: "Sub-commands related to the nebula VPN", do: func(subCmdCtx subCmdCtx) error { return subCmdCtx.doSubCmd( + subCmdNebulaCreateCert, subCmdNebulaShow, ) }, diff --git a/go/daemon/child_nebula.go b/go/daemon/child_nebula.go index bcc58d4..6acc88d 100644 --- a/go/daemon/child_nebula.go +++ b/go/daemon/child_nebula.go @@ -22,7 +22,7 @@ func waitForNebula( ctx context.Context, logger *mlog.Logger, hostBootstrap bootstrap.Bootstrap, ) error { var ( - ip = hostBootstrap.ThisHost().IP() + ip = net.IP(hostBootstrap.ThisHost().IP().AsSlice()) lUDPAddr = &net.UDPAddr{IP: ip, Port: 0} rUDPAddr = &net.UDPAddr{IP: ip, Port: 45535} ) diff --git a/go/daemon/daemon.go b/go/daemon/daemon.go index 674fc2a..03ddef0 100644 --- a/go/daemon/daemon.go +++ b/go/daemon/daemon.go @@ -55,6 +55,21 @@ type Daemon interface { // RemoveHost removes the host of the given name from the network. RemoveHost(context.Context, nebula.HostName) error + // CreateNebulaCertificate creates and signs a new nebula certficate for an + // existing host, given the public key for that host. This is currently + // mostly useful for creating certs for mobile devices. + // + // Errors: + // - ErrHostNotFound + CreateNebulaCertificate( + ctx context.Context, + caSigningPrivateKey nebula.SigningPrivateKey, // TODO load from secrets storage + hostName nebula.HostName, + hostPubKey nebula.EncryptingPublicKey, + ) ( + nebula.Certificate, error, + ) + // Shutdown blocks until all resources held or created by the daemon, // including child processes it has started, have been cleaned up. // @@ -592,6 +607,40 @@ func (d *daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error return err } +func makeCACreds( + currBootstrap bootstrap.Bootstrap, + caSigningPrivateKey nebula.SigningPrivateKey, +) nebula.CACredentials { + return nebula.CACredentials{ + Public: currBootstrap.CAPublicCredentials, + SigningPrivateKey: caSigningPrivateKey, + } +} + +func (d *daemon) CreateNebulaCertificate( + ctx context.Context, + caSigningPrivateKey nebula.SigningPrivateKey, + hostName nebula.HostName, + hostPubKey nebula.EncryptingPublicKey, +) ( + nebula.Certificate, error, +) { + return withCurrBootstrap(d, func( + currBootstrap bootstrap.Bootstrap, + ) ( + nebula.Certificate, error, + ) { + host, ok := currBootstrap.Hosts[hostName] + if !ok { + return nebula.Certificate{}, ErrHostNotFound + } + + caCreds := makeCACreds(currBootstrap, caSigningPrivateKey) + + return nebula.NewHostCert(caCreds, hostPubKey, hostName, host.IP()) + }) +} + func (d *daemon) Shutdown() error { d.l.Lock() defer d.l.Unlock() diff --git a/go/daemon/errors.go b/go/daemon/errors.go index 2378e67..d3ac064 100644 --- a/go/daemon/errors.go +++ b/go/daemon/errors.go @@ -24,4 +24,8 @@ var ( // // The Data field will be a string containing further details. ErrInvalidConfig = jsonrpc2.NewError(5, "Invalid daemon config") + + // ErrHostNotFound is returned when performing an operation which expected a + // host to exist in the network, but that host wasn't found. + ErrHostNotFound = jsonrpc2.NewError(6, "Host not found") ) diff --git a/go/daemon/rpc.go b/go/daemon/rpc.go index 9968faf..c0c8959 100644 --- a/go/daemon/rpc.go +++ b/go/daemon/rpc.go @@ -119,7 +119,7 @@ func (r *RPC) GetNebulaCAPublicCredentials( return b.CAPublicCredentials, nil } -// RemoveHostRequest contains the arguments to the RemoveHost method. +// RemoveHostRequest contains the arguments to the RemoveHost RPC method. // // All fields are required. type RemoveHostRequest struct { @@ -130,3 +130,38 @@ type RemoveHostRequest struct { func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{}, error) { return struct{}{}, r.daemon.RemoveHost(ctx, req.HostName) } + +// CreateNebulaCertificateRequest contains the arguments to the +// CreateNebulaCertificate RPC method. +// +// All fields are required. +type CreateNebulaCertificateRequest struct { + CASigningPrivateKey nebula.SigningPrivateKey // TODO load from secrets storage + HostName nebula.HostName + HostEncryptingPublicKey nebula.EncryptingPublicKey +} + +// CreateNebulaCertificateResult wraps the results from the +// CreateNebulaCertificate RPC method. +type CreateNebulaCertificateResult struct { + HostNebulaCertifcate nebula.Certificate +} + +// CreateNebulaCertificate passes the call through to the Daemon method of the +// same name. +func (r *RPC) CreateNebulaCertificate( + ctx context.Context, req CreateNebulaCertificateRequest, +) ( + CreateNebulaCertificateResult, error, +) { + cert, err := r.daemon.CreateNebulaCertificate( + ctx, req.CASigningPrivateKey, req.HostName, req.HostEncryptingPublicKey, + ) + if err != nil { + return CreateNebulaCertificateResult{}, err + } + + return CreateNebulaCertificateResult{ + HostNebulaCertifcate: cert, + }, nil +} diff --git a/go/nebula/encrypting_key.go b/go/nebula/encrypting_key.go index 0a72e5a..7e74912 100644 --- a/go/nebula/encrypting_key.go +++ b/go/nebula/encrypting_key.go @@ -46,7 +46,7 @@ func (pk *EncryptingPublicKey) UnmarshalText(b []byte) error { // UnmarshalNebulaPEM unmarshals the EncryptingPublicKey as a nebula host public // key PEM. func (pk *EncryptingPublicKey) UnmarshalNebulaPEM(b []byte) error { - b, _, err := cert.UnmarshalEd25519PublicKey(b) + b, _, err := cert.UnmarshalX25519PublicKey(b) if err != nil { return fmt.Errorf("unmarshaling: %w", err) } diff --git a/tests/cases/admin/01-create-network.sh b/tests/cases/admin/01-create-network.sh index 1f5d3b6..8bced65 100644 --- a/tests/cases/admin/01-create-network.sh +++ b/tests/cases/admin/01-create-network.sh @@ -9,8 +9,6 @@ source "$UTILS"/with-1-data-1-empty-node-network.sh [ "$(jq -r &1 || true \ +) | grep '\[6\] Host not found' + +isle nebula create-cert \ + --admin-path admin.json \ + --hostname primus \ + --public-key-path pubkey \ + | grep -- '-----BEGIN NEBULA CERTIFICATE-----' diff --git a/tests/utils/shared-daemon-env.sh b/tests/utils/shared-daemon-env.sh index ab5b3e6..926b906 100644 --- a/tests/utils/shared-daemon-env.sh +++ b/tests/utils/shared-daemon-env.sh @@ -13,5 +13,6 @@ export TMPDIR="$TMPDIR" export XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" export XDG_STATE_HOME="$XDG_STATE_HOME" export ISLE_DAEMON_HTTP_SOCKET_PATH="$ROOT_TMPDIR/$base-daemon.sock" +BOOTSTRAP_FILE="$XDG_STATE_HOME/isle/bootstrap.json" cd "$TMPDIR" EOF