Move create-nebula-cert into nebula create-cert, move most logic into daemon

This commit is contained in:
Brian Picciano 2024-07-13 16:08:13 +02:00
parent cc121f0752
commit cb8fef38c4
13 changed files with 225 additions and 98 deletions

View File

@ -173,6 +173,7 @@ in rec {
pkgs.yq-go pkgs.yq-go
pkgs.jq pkgs.jq
pkgs.dig pkgs.dig
pkgs.nebula
]} ]}
export SHELL=${pkgs.bash}/bin/bash export SHELL=${pkgs.bash}/bin/bash
exec ${pkgs.bash}/bin/bash ${./tests}/entrypoint.sh "$@" exec ${pkgs.bash}/bin/bash ${./tests}/entrypoint.sh "$@"

View File

@ -3,7 +3,7 @@ package bootstrap
import ( import (
"fmt" "fmt"
"isle/nebula" "isle/nebula"
"net" "net/netip"
) )
// NebulaHost describes the nebula configuration of a Host which is relevant for // 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 // This assumes that the Host and its data has already been verified against the
// CA signing key. // CA signing key.
func (h Host) IP() net.IP { func (h Host) IP() netip.Addr {
cert := h.PublicCredentials.Cert.Unwrap() cert := h.PublicCredentials.Cert.Unwrap()
if len(cert.Details.Ips) == 0 { if len(cert.Details.Ips) == 0 {
panic(fmt.Sprintf("host %q not configured with any ips: %+v", h.Name, h)) 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
} }

View File

@ -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{ var subCmdAdmin = subCmd{
name: "admin", name: "admin",
descr: "Sub-commands which only admins can run", descr: "Sub-commands which only admins can run",
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd( return subCmdCtx.doSubCmd(
subCmdAdminCreateBootstrap, subCmdAdminCreateBootstrap,
subCmdAdminCreateNebulaCert,
) )
}, },
} }

View File

@ -1,12 +1,92 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"isle/daemon"
"isle/jsonutil" "isle/jsonutil"
"isle/nebula" "isle/nebula"
"os" "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{ var subCmdNebulaShow = subCmd{
name: "show", name: "show",
descr: "Writes nebula network information to stdout in JSON format", descr: "Writes nebula network information to stdout in JSON format",
@ -30,20 +110,17 @@ var subCmdNebulaShow = subCmd{
return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err) return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
} }
caCert := caPublicCreds.Cert.Unwrap() caCert := caPublicCreds.Cert
caCertPEM, err := caCert.MarshalToPEM() caCertDetails := caCert.Unwrap().Details
if err != nil {
return fmt.Errorf("marshaling CA cert to PEM: %w", err)
}
if len(caCert.Details.Subnets) != 1 { if len(caCertDetails.Subnets) != 1 {
return fmt.Errorf( return fmt.Errorf(
"malformed ca.crt, contains unexpected subnets %#v", "malformed ca.crt, contains unexpected subnets %#v",
caCert.Details.Subnets, caCertDetails.Subnets,
) )
} }
subnet := caCert.Details.Subnets[0] subnet := caCertDetails.Subnets[0]
type outLighthouse struct { type outLighthouse struct {
PublicAddr string PublicAddr string
@ -51,11 +128,11 @@ var subCmdNebulaShow = subCmd{
} }
out := struct { out := struct {
CACert string CACert nebula.Certificate
SubnetCIDR string SubnetCIDR string
Lighthouses []outLighthouse Lighthouses []outLighthouse
}{ }{
CACert: string(caCertPEM), CACert: caCert,
SubnetCIDR: subnet.String(), SubnetCIDR: subnet.String(),
} }
@ -83,6 +160,7 @@ var subCmdNebula = subCmd{
descr: "Sub-commands related to the nebula VPN", descr: "Sub-commands related to the nebula VPN",
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd( return subCmdCtx.doSubCmd(
subCmdNebulaCreateCert,
subCmdNebulaShow, subCmdNebulaShow,
) )
}, },

View File

@ -22,7 +22,7 @@ func waitForNebula(
ctx context.Context, logger *mlog.Logger, hostBootstrap bootstrap.Bootstrap, ctx context.Context, logger *mlog.Logger, hostBootstrap bootstrap.Bootstrap,
) error { ) error {
var ( var (
ip = hostBootstrap.ThisHost().IP() ip = net.IP(hostBootstrap.ThisHost().IP().AsSlice())
lUDPAddr = &net.UDPAddr{IP: ip, Port: 0} lUDPAddr = &net.UDPAddr{IP: ip, Port: 0}
rUDPAddr = &net.UDPAddr{IP: ip, Port: 45535} rUDPAddr = &net.UDPAddr{IP: ip, Port: 45535}
) )

View File

@ -55,6 +55,21 @@ type Daemon interface {
// RemoveHost removes the host of the given name from the network. // RemoveHost removes the host of the given name from the network.
RemoveHost(context.Context, nebula.HostName) error 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, // Shutdown blocks until all resources held or created by the daemon,
// including child processes it has started, have been cleaned up. // 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 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 { func (d *daemon) Shutdown() error {
d.l.Lock() d.l.Lock()
defer d.l.Unlock() defer d.l.Unlock()

View File

@ -24,4 +24,8 @@ var (
// //
// The Data field will be a string containing further details. // The Data field will be a string containing further details.
ErrInvalidConfig = jsonrpc2.NewError(5, "Invalid daemon config") 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")
) )

View File

@ -119,7 +119,7 @@ func (r *RPC) GetNebulaCAPublicCredentials(
return b.CAPublicCredentials, nil 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. // All fields are required.
type RemoveHostRequest struct { type RemoveHostRequest struct {
@ -130,3 +130,38 @@ type RemoveHostRequest struct {
func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{}, error) { func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{}, error) {
return struct{}{}, r.daemon.RemoveHost(ctx, req.HostName) 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
}

View File

@ -46,7 +46,7 @@ func (pk *EncryptingPublicKey) UnmarshalText(b []byte) error {
// UnmarshalNebulaPEM unmarshals the EncryptingPublicKey as a nebula host public // UnmarshalNebulaPEM unmarshals the EncryptingPublicKey as a nebula host public
// key PEM. // key PEM.
func (pk *EncryptingPublicKey) UnmarshalNebulaPEM(b []byte) error { func (pk *EncryptingPublicKey) UnmarshalNebulaPEM(b []byte) error {
b, _, err := cert.UnmarshalEd25519PublicKey(b) b, _, err := cert.UnmarshalX25519PublicKey(b)
if err != nil { if err != nil {
return fmt.Errorf("unmarshaling: %w", err) return fmt.Errorf("unmarshaling: %w", err)
} }

View File

@ -9,8 +9,6 @@ source "$UTILS"/with-1-data-1-empty-node-network.sh
[ "$(jq -r <admin.json '.CreationParams.Name')" = "testing" ] [ "$(jq -r <admin.json '.CreationParams.Name')" = "testing" ]
[ "$(jq -r <admin.json '.CreationParams.Domain')" = "shared.test" ] [ "$(jq -r <admin.json '.CreationParams.Domain')" = "shared.test" ]
bootstrap_file="$XDG_STATE_HOME/isle/bootstrap.json" [ "$(jq -rc <"$BOOTSTRAP_FILE" '.AdminCreationParams')" = "$(jq -rc <admin.json '.CreationParams')" ]
[ "$(jq -rc <"$BOOTSTRAP_FILE" '.CAPublicCredentials')" = "$(jq -rc <admin.json '.Nebula.CACredentials.Public')" ]
[ "$(jq -rc <"$bootstrap_file" '.AdminCreationParams')" = "$(jq -rc <admin.json '.CreationParams')" ] [ "$(jq -r <"$BOOTSTRAP_FILE" '.SignedHostAssigned.Body.Name')" = "primus" ]
[ "$(jq -rc <"$bootstrap_file" '.CAPublicCredentials')" = "$(jq -rc <admin.json '.Nebula.CACredentials.Public')" ]
[ "$(jq -r <"$bootstrap_file" '.SignedHostAssigned.Body.Name')" = "primus" ]

View File

@ -0,0 +1,12 @@
# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh
source "$UTILS"/with-1-data-1-empty-node-network.sh
info="$(isle nebula show)"
[ "$(echo "$info" | jq -r '.CACert')" \
= "$(jq -r <"$BOOTSTRAP_FILE" '.CAPublicCredentials.Cert')" ]
[ "$(echo "$info" | jq -r '.SubnetCIDR')" = "10.6.9.0/24" ]
[ "$(echo "$info" | jq -r '.Lighthouses|length')" = "1" ]
[ "$(echo "$info" | jq -r '.Lighthouses[0].PublicAddr')" = "127.0.0.1:60000" ]
[ "$(echo "$info" | jq -r '.Lighthouses[0].IP')" = "10.6.9.1" ]

View File

@ -0,0 +1,19 @@
# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh
source "$UTILS"/with-1-data-1-empty-node-network.sh
nebula-cert keygen -out-key /dev/null -out-pub pubkey
cat pubkey
(
isle nebula create-cert \
--admin-path admin.json \
--hostname non-esiste \
--public-key-path pubkey \
2>&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-----'

View File

@ -13,5 +13,6 @@ export TMPDIR="$TMPDIR"
export XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" export XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR"
export XDG_STATE_HOME="$XDG_STATE_HOME" export XDG_STATE_HOME="$XDG_STATE_HOME"
export ISLE_DAEMON_HTTP_SOCKET_PATH="$ROOT_TMPDIR/$base-daemon.sock" export ISLE_DAEMON_HTTP_SOCKET_PATH="$ROOT_TMPDIR/$base-daemon.sock"
BOOTSTRAP_FILE="$XDG_STATE_HOME/isle/bootstrap.json"
cd "$TMPDIR" cd "$TMPDIR"
EOF EOF