Compare commits

...

2 Commits

Author SHA1 Message Date
Brian Picciano
7dceb659ef Store full nebula cert for each host in garage, rather than just the IP
This allows each host to verify the cert against the CA cert. We also
now have each host sign the yaml file that it posts to garage, to ensure
that a host can't arbitrarily overwrite another host's file.
2022-10-29 21:11:40 +02:00
Brian Picciano
711d568036 Use a real private key for garage instances 2022-10-29 00:09:18 +02:00
20 changed files with 552 additions and 443 deletions

View File

@ -35,7 +35,7 @@ type CreationParams struct {
type Admin struct {
CreationParams CreationParams
NebulaCACert nebula.CACert
NebulaCACredentials nebula.CACredentials
GarageRPCSecret string
GarageGlobalBucketS3APICredentials garage.S3APICredentials
@ -67,8 +67,8 @@ func FromFS(adminFS fs.FS) (Admin, error) {
into *string
path string
}{
{&a.NebulaCACert.CACert, nebulaCertsCACertPath},
{&a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{&a.NebulaCACredentials.CACertPEM, nebulaCertsCACertPath},
{&a.NebulaCACredentials.CAKeyPEM, nebulaCertsCAKeyPath},
{&a.GarageRPCSecret, garageRPCSecretPath},
}
@ -122,8 +122,8 @@ func (a Admin) WriteTo(into io.Writer) error {
value string
path string
}{
{a.NebulaCACert.CACert, nebulaCertsCACertPath},
{a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{a.NebulaCACredentials.CACertPEM, nebulaCertsCACertPath},
{a.NebulaCACredentials.CAKeyPEM, nebulaCertsCAKeyPath},
{a.GarageRPCSecret, garageRPCSecretPath},
}

View File

@ -45,7 +45,7 @@ type Bootstrap struct {
Hosts map[string]Host
HostName string
NebulaHostCert nebula.HostCert
NebulaHostCredentials nebula.HostCredentials
GarageRPCSecret string
GarageAdminToken string
@ -84,9 +84,9 @@ func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
path string
}{
{&b.HostName, hostNamePath},
{&b.NebulaHostCert.CACert, nebulaCertsCACertPath},
{&b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
{&b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
{&b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath},
{&b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath},
{&b.NebulaHostCredentials.HostKeyPEM, nebulaCertsHostKeyPath},
{&b.GarageRPCSecret, garageRPCSecretPath},
{&b.GarageAdminToken, garageAdminTokenPath},
}
@ -165,9 +165,9 @@ func (b Bootstrap) WriteTo(into io.Writer) error {
path string
}{
{b.HostName, hostNamePath},
{b.NebulaHostCert.CACert, nebulaCertsCACertPath},
{b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
{b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
{b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath},
{b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath},
{b.NebulaHostCredentials.HostKeyPEM, nebulaCertsHostKeyPath},
{b.GarageRPCSecret, garageRPCSecretPath},
{b.GarageAdminToken, garageAdminTokenPath},
}
@ -209,19 +209,3 @@ func HostsHash(hostsMap map[string]Host) ([]byte, error) {
return h.Sum(nil), nil
}
// WithHosts returns a copy of the Bootstrap with the given set of Hosts applied
// to it. It will _not_ overwrite the Host for _this_ host, however.
func (b Bootstrap) WithHosts(hosts map[string]Host) Bootstrap {
hostsCopy := make(map[string]Host, len(hosts))
for name, host := range hosts {
hostsCopy[name] = host
}
hostsCopy[b.HostName] = b.ThisHost()
b.Hosts = hostsCopy
return b
}

View File

@ -12,9 +12,9 @@ const (
)
// GaragePeers returns a Peer for each known garage instance in the network.
func (b Bootstrap) GaragePeers() []garage.Peer {
func (b Bootstrap) GaragePeers() []garage.RemotePeer {
var peers []garage.Peer
var peers []garage.RemotePeer
for _, host := range b.Hosts {
@ -24,8 +24,9 @@ func (b Bootstrap) GaragePeers() []garage.Peer {
for _, instance := range host.Garage.Instances {
peer := garage.Peer{
IP: host.Nebula.IP,
peer := garage.RemotePeer{
ID: instance.ID,
IP: host.IP().String(),
RPCPort: instance.RPCPort,
S3APIPort: instance.S3APIPort,
}
@ -50,14 +51,16 @@ func (b Bootstrap) GarageRPCPeerAddrs() []string {
// ChooseGaragePeer returns a Peer for a garage instance from the network. It
// will prefer a garage instance on this particular host, if there is one, but
// will otherwise return a random endpoint.
func (b Bootstrap) ChooseGaragePeer() garage.Peer {
func (b Bootstrap) ChooseGaragePeer() garage.RemotePeer {
thisHost := b.ThisHost()
if thisHost.Garage != nil && len(thisHost.Garage.Instances) > 0 {
inst := thisHost.Garage.Instances[0]
return garage.Peer{
IP: thisHost.Nebula.IP,
return garage.RemotePeer{
ID: inst.ID,
IP: thisHost.IP().String(),
RPCPort: inst.RPCPort,
S3APIPort: inst.S3APIPort,
}

View File

@ -4,8 +4,9 @@ import (
"bytes"
"context"
"cryptic-net/garage"
"cryptic-net/nebula"
"fmt"
"log"
"os"
"path/filepath"
"github.com/minio/minio-go/v7"
@ -17,23 +18,32 @@ const (
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
)
// PutGarageBoostrapHost places the <hostname>.yml file for the given host into
// garage so that other hosts are able to see relevant configuration for it.
//
// The given client should be for the global bucket.
func PutGarageBoostrapHost(
ctx context.Context, client garage.S3APIClient, host Host,
) error {
// PutGarageBoostrapHost places the <hostname>.yml.signed file for this host
// into garage so that other hosts are able to see relevant configuration for
// it.
func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error {
host := b.ThisHost()
client := b.GlobalBucketS3APIClient()
hostB, err := yaml.Marshal(host)
if err != nil {
return fmt.Errorf("yaml encoding host data: %w", err)
}
buf := new(bytes.Buffer)
if err := yaml.NewEncoder(buf).Encode(host); err != nil {
log.Fatalf("yaml encoding host data: %v", err)
err = nebula.SignAndWrap(buf, b.NebulaHostCredentials.HostKeyPEM, hostB)
if err != nil {
return fmt.Errorf("signing encoded host data: %w", err)
}
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, host.Name+".yml")
filePath := filepath.Join(
garageGlobalBucketBootstrapHostsDirPath,
host.Name+".yml.signed",
)
_, err := client.PutObject(
_, err = client.PutObject(
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
minio.PutObjectOptions{},
)
@ -45,15 +55,18 @@ func PutGarageBoostrapHost(
return nil
}
// RemoveGarageBootstrapHost removes the <hostname>.yml for the given host from
// garage.
// RemoveGarageBootstrapHost removes the <hostname>.yml.signed for the given
// host from garage.
//
// The given client should be for the global bucket.
func RemoveGarageBootstrapHost(
ctx context.Context, client garage.S3APIClient, hostName string,
) error {
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, hostName+".yml")
filePath := filepath.Join(
garageGlobalBucketBootstrapHostsDirPath,
hostName+".yml.signed",
)
return client.RemoveObject(
ctx, garage.GlobalBucket, filePath,
@ -61,16 +74,17 @@ func RemoveGarageBootstrapHost(
)
}
// GetGarageBootstrapHosts loads the <hostname>.yml file for all hosts stored in
// garage.
//
// The given client should be for the global bucket.
func GetGarageBootstrapHosts(
ctx context.Context, client garage.S3APIClient,
// GetGarageBootstrapHosts loads the <hostname>.yml.signed file for all hosts
// stored in garage.
func (b Bootstrap) GetGarageBootstrapHosts(
ctx context.Context,
) (
map[string]Host, error,
) {
caCertPEM := b.NebulaHostCredentials.CACertPEM
client := b.GlobalBucketS3APIClient()
hosts := map[string]Host{}
objInfoCh := client.ListObjects(
@ -95,15 +109,30 @@ func GetGarageBootstrapHosts(
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
}
var host Host
err = yaml.NewDecoder(obj).Decode(&host)
hostB, sig, err := nebula.Unwrap(obj)
obj.Close()
if err != nil {
return nil, fmt.Errorf("unwrapping signature from %q: %w", objInfo.Key, err)
}
var host Host
if err = yaml.Unmarshal(hostB, &host); err != nil {
return nil, fmt.Errorf("yaml decoding object %q: %w", objInfo.Key, err)
}
hostCertPEM := host.Nebula.CertPEM
if err := nebula.ValidateSignature(hostCertPEM, hostB, sig); err != nil {
fmt.Fprintf(os.Stderr, "invalid host data for %q: %w\n", objInfo.Key, err)
continue
}
if err := nebula.ValidateHostCertPEM(caCertPEM, hostCertPEM); err != nil {
fmt.Fprintf(os.Stderr, "invalid nebula cert for %q: %w\n", objInfo.Key, err)
continue
}
hosts[host.Name] = host
}

View File

@ -1,8 +1,10 @@
package bootstrap
import (
"cryptic-net/nebula"
"fmt"
"io/fs"
"net"
"path/filepath"
"strings"
@ -16,12 +18,13 @@ const (
// NebulaHost describes the nebula configuration of a Host which is relevant for
// other hosts to know.
type NebulaHost struct {
IP string `yaml:"ip"`
CertPEM string `yaml:"crt"`
PublicAddr string `yaml:"public_addr,omitempty"`
}
// GarageHost describes a single garage instance in the GarageHost.
type GarageHostInstance struct {
ID string `yaml:"id"`
RPCPort int `yaml:"rpc_port"`
S3APIPort int `yaml:"s3_api_port"`
}
@ -40,6 +43,18 @@ type Host struct {
Garage *GarageHost `yaml:"garage,omitempty"`
}
// IP returns the IP address encoded in the Host's nebula certificate, or panics
// if there is an error.
func (h Host) IP() net.IP {
ip, err := nebula.IPFromHostCertPEM(h.Nebula.CertPEM)
if err != nil {
panic(fmt.Errorf("could not parse IP out of cert for host %q: %w", h.Name, err))
}
return ip
}
func loadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
hosts := map[string]Host{}

View File

@ -117,12 +117,12 @@ var subCmdAdminCreateNetwork = subCmd{
return fmt.Errorf("daemon config with at least 3 allocations was not provided")
}
nebulaCACert, err := nebula.NewCACert(*domain, subnet)
nebulaCACreds, err := nebula.NewCACredentials(*domain, subnet)
if err != nil {
return fmt.Errorf("creating nebula CA cert: %w", err)
}
nebulaHostCert, err := nebula.NewHostCert(nebulaCACert, *hostName, ip)
nebulaHostCreds, err := nebula.NewHostCredentials(nebulaCACreds, *hostName, ip)
if err != nil {
return fmt.Errorf("creating nebula cert for host: %w", err)
}
@ -138,12 +138,12 @@ var subCmdAdminCreateNetwork = subCmd{
*hostName: bootstrap.Host{
Name: *hostName,
Nebula: bootstrap.NebulaHost{
IP: ip.String(),
CertPEM: nebulaHostCreds.HostCertPEM,
},
},
},
HostName: *hostName,
NebulaHostCert: nebulaHostCert,
NebulaHostCredentials: nebulaHostCreds,
GarageRPCSecret: randStr(32),
GarageAdminToken: randStr(32),
GarageGlobalBucketS3APICredentials: garage.NewS3APICredentials(),
@ -213,7 +213,7 @@ var subCmdAdminCreateNetwork = subCmd{
err = admin.Admin{
CreationParams: adminCreationParams,
NebulaCACert: nebulaCACert,
NebulaCACredentials: nebulaCACreds,
GarageRPCSecret: hostBootstrap.GarageRPCSecret,
GarageGlobalBucketS3APICredentials: hostBootstrap.GarageGlobalBucketS3APICredentials,
GarageAdminBucketS3APICredentials: garage.NewS3APICredentials(),
@ -240,6 +240,11 @@ var subCmdAdminMakeBootstrap = subCmd{
"Name of the host to generate bootstrap.tgz for",
)
ipStr := flags.StringP(
"ip", "i", "",
"IP of the new host",
)
adminPath := flags.StringP(
"admin-path", "a", "",
`Path to admin.tgz file. If the given path is "-" then stdin is used.`,
@ -249,13 +254,18 @@ var subCmdAdminMakeBootstrap = subCmd{
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" || *adminPath == "" {
return errors.New("--name and --admin-path are required")
if *name == "" || *ipStr == "" || *adminPath == "" {
return errors.New("--name, --ip, and --admin-path are required")
}
hostBootstrap, err := loadHostBootstrap()
if err != nil {
return fmt.Errorf("loading host bootstrap: %w", err)
if err := validateHostName(*name); err != nil {
return fmt.Errorf("invalid hostname %q: %w", *name, err)
}
ip := net.ParseIP(*ipStr)
if ip == nil {
return fmt.Errorf("invalid ip %q", *ipStr)
}
adm, err := readAdmin(*adminPath)
@ -263,28 +273,12 @@ var subCmdAdminMakeBootstrap = subCmd{
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
}
client := hostBootstrap.GlobalBucketS3APIClient()
// NOTE this isn't _technically_ required, but if the `hosts add`
// command for this host has been run recently then it might not have
// made it into the bootstrap file yet, and so won't be in
// `hostBootstrap`.
hosts, err := bootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx, client)
hostBootstrap, err := loadHostBootstrap()
if err != nil {
return fmt.Errorf("retrieving host info from garage: %w", err)
return fmt.Errorf("loading host bootstrap: %w", err)
}
host, ok := hosts[*name]
if !ok {
return fmt.Errorf("couldn't find host into for %q in garage, has `cryptic-net hosts add` been run yet?", *name)
}
ip := net.ParseIP(host.Nebula.IP)
if ip == nil {
return fmt.Errorf("invalid IP stored with host %q: %q", *name, host.Nebula.IP)
}
nebulaHostCert, err := nebula.NewHostCert(adm.NebulaCACert, host.Name, ip)
nebulaHostCreds, err := nebula.NewHostCredentials(adm.NebulaCACredentials, *name, ip)
if err != nil {
return fmt.Errorf("creating new nebula host key/cert: %w", err)
}
@ -292,10 +286,10 @@ var subCmdAdminMakeBootstrap = subCmd{
newHostBootstrap := bootstrap.Bootstrap{
AdminCreationParams: adm.CreationParams,
Hosts: hosts,
Hosts: hostBootstrap.Hosts,
HostName: *name,
NebulaHostCert: nebulaHostCert,
NebulaHostCredentials: nebulaHostCreds,
GarageRPCSecret: adm.GarageRPCSecret,
GarageAdminToken: randStr(32),

View File

@ -12,7 +12,6 @@ import (
"cryptic-net/bootstrap"
"cryptic-net/daemon"
"cryptic-net/garage"
"code.betamike.com/cryptic-io/pmux/pmuxlib"
)
@ -42,16 +41,21 @@ import (
func reloadBootstrap(
ctx context.Context,
hostBootstrap bootstrap.Bootstrap,
s3Client garage.S3APIClient,
) (
bootstrap.Bootstrap, bool, error,
) {
newHosts, err := bootstrap.GetGarageBootstrapHosts(ctx, s3Client)
thisHost := hostBootstrap.ThisHost()
newHosts, err := hostBootstrap.GetGarageBootstrapHosts(ctx)
if err != nil {
return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err)
}
// the daemon's view of this host's bootstrap info takes precedence over
// whatever is in garage
newHosts[thisHost.Name] = thisHost
newHostsHash, err := bootstrap.HostsHash(newHosts)
if err != nil {
return bootstrap.Bootstrap{}, false, fmt.Errorf("calculating hash of new hosts: %w", err)
@ -66,13 +70,8 @@ func reloadBootstrap(
return hostBootstrap, false, nil
}
newHostBootstrap := hostBootstrap.WithHosts(newHosts)
if err := writeBootstrapToDataDir(newHostBootstrap); err != nil {
return bootstrap.Bootstrap{}, false, fmt.Errorf("writing new bootstrap.tgz to data dir: %w", err)
}
return newHostBootstrap, true, nil
hostBootstrap.Hosts = newHosts
return hostBootstrap, true, nil
}
// runs a single pmux process of daemon, returning only once the env.Context has
@ -87,14 +86,6 @@ func runDaemonPmuxOnce(
bootstrap.Bootstrap, error,
) {
thisHost := hostBootstrap.ThisHost()
fmt.Fprintf(os.Stderr, "host name is %q, ip is %q\n", thisHost.Name, thisHost.Nebula.IP)
// create s3Client anew on every loop, in case the topology has
// changed and we should be connecting to a different garage
// endpoint.
s3Client := hostBootstrap.GlobalBucketS3APIClient()
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(hostBootstrap, daemonConfig)
if err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("generating nebula config: %w", err)
@ -143,11 +134,9 @@ func runDaemonPmuxOnce(
return
}
thisHost := hostBootstrap.ThisHost()
err := doOnce(ctx, func(ctx context.Context) error {
fmt.Fprintln(os.Stderr, "updating host info in garage")
return bootstrap.PutGarageBoostrapHost(ctx, s3Client, thisHost)
return hostBootstrap.PutGarageBoostrapHost(ctx)
})
if err != nil {
@ -194,7 +183,7 @@ func runDaemonPmuxOnce(
err error
)
if hostBootstrap, changed, err = reloadBootstrap(ctx, hostBootstrap, s3Client); err != nil {
if hostBootstrap, changed, err = reloadBootstrap(ctx, hostBootstrap); err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("reloading bootstrap: %w", err)
} else if changed {

View File

@ -4,6 +4,7 @@ import (
"context"
"cryptic-net/bootstrap"
"cryptic-net/daemon"
"cryptic-net/garage"
"fmt"
"time"
)
@ -25,7 +26,14 @@ func mergeDaemonConfigIntoBootstrap(
host.Garage = new(bootstrap.GarageHost)
for _, alloc := range allocs {
id, err := garage.InitAlloc(alloc.MetaPath)
if err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("initializing alloc at %q: %w", alloc.MetaPath, err)
}
host.Garage.Instances = append(host.Garage.Instances, bootstrap.GarageHostInstance{
ID: id,
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
})

View File

@ -20,19 +20,22 @@ func dnsmasqPmuxProcConfig(
confPath := filepath.Join(envRuntimeDirPath, "dnsmasq.conf")
hostsSlice := make([]bootstrap.Host, 0, len(hostBootstrap.Hosts))
hostsSlice := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts))
for _, host := range hostBootstrap.Hosts {
hostsSlice = append(hostsSlice, host)
hostsSlice = append(hostsSlice, dnsmasq.ConfDataHost{
Name: host.Name,
IP: host.IP().String(),
})
}
sort.Slice(hostsSlice, func(i, j int) bool {
return hostsSlice[i].Nebula.IP < hostsSlice[j].Nebula.IP
return hostsSlice[i].IP < hostsSlice[j].IP
})
confData := dnsmasq.ConfData{
Resolvers: daemonConfig.DNS.Resolvers,
Domain: hostBootstrap.AdminCreationParams.Domain,
IP: hostBootstrap.ThisHost().Nebula.IP,
IP: hostBootstrap.ThisHost().IP().String(),
Hosts: hostsSlice,
}

View File

@ -7,7 +7,6 @@ import (
"cryptic-net/garage"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
@ -24,7 +23,7 @@ func newGarageAdminClient(
return garage.NewAdminClient(
net.JoinHostPort(
thisHost.Nebula.IP,
thisHost.IP().String(),
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort),
),
hostBootstrap.GarageAdminToken,
@ -48,7 +47,7 @@ func waitForGarageAndNebula(
for _, alloc := range allocs {
adminAddr := net.JoinHostPort(
hostBootstrap.ThisHost().Nebula.IP,
hostBootstrap.ThisHost().IP().String(),
strconv.Itoa(alloc.AdminPort),
)
@ -66,6 +65,25 @@ func waitForGarageAndNebula(
}
// bootstrapGarageHostForAlloc returns the bootstrap.GarageHostInstance which
// corresponds with the given alloc from the daemon config. This will panic if
// no associated instance can be found.
//
// This assumes that mergeDaemonConfigIntoBootstrap has already been called.
func bootstrapGarageHostForAlloc(
host bootstrap.Host,
alloc daemon.ConfigStorageAllocation,
) bootstrap.GarageHostInstance {
for _, inst := range host.Garage.Instances {
if inst.RPCPort == alloc.RPCPort {
return inst
}
}
panic(fmt.Sprintf("could not find alloc %+v in the bootstrap data", alloc))
}
func garageWriteChildConfig(
hostBootstrap bootstrap.Bootstrap,
alloc daemon.ConfigStorageAllocation,
@ -73,28 +91,17 @@ func garageWriteChildConfig(
string, error,
) {
if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil {
return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err)
}
thisHost := hostBootstrap.ThisHost()
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
peer := garage.Peer{
IP: thisHost.Nebula.IP,
peer := garage.LocalPeer{
RemotePeer: garage.RemotePeer{
ID: id,
IP: thisHost.IP().String(),
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
}
pubKey, privKey := peer.RPCPeerKey()
nodeKeyPath := filepath.Join(alloc.MetaPath, "node_key")
nodeKeyPubPath := filepath.Join(alloc.MetaPath, "node_keypub")
if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
return "", fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)
} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
return "", fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
},
AdminPort: alloc.AdminPort,
}
garageTomlPath := filepath.Join(
@ -108,9 +115,9 @@ func garageWriteChildConfig(
RPCSecret: hostBootstrap.GarageRPCSecret,
AdminToken: hostBootstrap.GarageAdminToken,
RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
AdminAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.AdminPort)),
RPCAddr: peer.RPCAddr(),
S3APIAddr: peer.S3APIAddr(),
AdminAddr: peer.AdminAddr(),
BootstrapPeers: hostBootstrap.GarageRPCPeerAddrs(),
})
@ -224,7 +231,6 @@ func garageApplyLayout(
adminClient = newGarageAdminClient(hostBootstrap, daemonConfig)
thisHost = hostBootstrap.ThisHost()
hostName = thisHost.Name
ip = thisHost.Nebula.IP
allocs = daemonConfig.Storage.Allocations
)
@ -239,13 +245,9 @@ func garageApplyLayout(
for _, alloc := range allocs {
peer := garage.Peer{
IP: ip,
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
}
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
clusterLayout[peer.RPCPeerID()] = peerLayout{
clusterLayout[id] = peerLayout{
Capacity: alloc.Capacity / 100,
Zone: hostName,
Tags: []string{},

View File

@ -4,7 +4,6 @@ import (
"cryptic-net/bootstrap"
"errors"
"fmt"
"net"
"os"
"regexp"
"sort"
@ -23,60 +22,6 @@ func validateHostName(name string) error {
return nil
}
var subCmdHostsAdd = subCmd{
name: "add",
descr: "Adds a host to the network",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the new host",
)
ip := flags.StringP(
"ip", "i", "",
"IP of the new host",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" || *ip == "" {
return errors.New("--name and --ip are required")
}
if err := validateHostName(*name); err != nil {
return fmt.Errorf("invalid hostname %q: %w", *name, err)
}
if net.ParseIP(*ip) == nil {
return fmt.Errorf("invalid ip %q", *ip)
}
// TODO validate that the IP is in the correct CIDR
hostBootstrap, err := loadHostBootstrap()
if err != nil {
return fmt.Errorf("loading host bootstrap: %w", err)
}
client := hostBootstrap.GlobalBucketS3APIClient()
host := bootstrap.Host{
Name: *name,
Nebula: bootstrap.NebulaHost{
IP: *ip,
},
}
return bootstrap.PutGarageBoostrapHost(subCmdCtx.ctx, client, host)
},
}
var subCmdHostsList = subCmd{
name: "list",
descr: "Lists all hosts in the network, and their IPs",
@ -88,9 +33,7 @@ var subCmdHostsList = subCmd{
return fmt.Errorf("loading host bootstrap: %w", err)
}
client := hostBootstrap.GlobalBucketS3APIClient()
hostsMap, err := bootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx, client)
hostsMap, err := hostBootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx)
if err != nil {
return fmt.Errorf("retrieving hosts from garage: %w", err)
}
@ -143,7 +86,6 @@ var subCmdHosts = subCmd{
descr: "Sub-commands having to do with configuration of hosts in the network",
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdHostsAdd,
subCmdHostsDelete,
subCmdHostsList,
)

View File

@ -18,8 +18,7 @@ import (
// interface has been initialized.
func waitForNebula(ctx context.Context, hostBootstrap bootstrap.Bootstrap) error {
ipStr := hostBootstrap.ThisHost().Nebula.IP
ip := net.ParseIP(ipStr)
ip := hostBootstrap.ThisHost().IP()
lUdpAddr := &net.UDPAddr{IP: ip, Port: 0}
rUdpAddr := &net.UDPAddr{IP: ip, Port: 45535}
@ -52,15 +51,16 @@ func nebulaPmuxProcConfig(
continue
}
lighthouseHostIPs = append(lighthouseHostIPs, host.Nebula.IP)
staticHostMap[host.Nebula.IP] = []string{host.Nebula.PublicAddr}
ip := host.IP().String()
lighthouseHostIPs = append(lighthouseHostIPs, ip)
staticHostMap[ip] = []string{host.Nebula.PublicAddr}
}
config := map[string]interface{}{
"pki": map[string]string{
"ca": hostBootstrap.NebulaHostCert.CACert,
"cert": hostBootstrap.NebulaHostCert.HostCert,
"key": hostBootstrap.NebulaHostCert.HostKey,
"ca": hostBootstrap.NebulaHostCredentials.CACertPEM,
"cert": hostBootstrap.NebulaHostCredentials.HostCertPEM,
"key": hostBootstrap.NebulaHostCredentials.HostKeyPEM,
},
"static_host_map": staticHostMap,
"punchy": map[string]bool{

View File

@ -1,18 +1,23 @@
package dnsmasq
import (
"cryptic-net/bootstrap"
"fmt"
"os"
"text/template"
)
// ConfDataHost describes a host which can be resolved by dnsmasq.
type ConfDataHost struct {
Name string
IP string
}
// ConfData describes all the data needed to populate a dnsmasq.conf file.
type ConfData struct {
Resolvers []string
Domain string
IP string
Hosts []bootstrap.Host
Hosts []ConfDataHost
}
var confTpl = template.Must(template.New("").Parse(`

View File

@ -2,6 +2,15 @@
// setting up garage configs, processes, and deployments.
package garage
import (
"encoding/hex"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
)
const (
// Region is the region which garage is configured with.
@ -15,3 +24,81 @@ const (
// cluster. We currently only support a factor of 3.
ReplicationFactor = 3
)
func nodeKeyPath(metaDirPath string) string {
return filepath.Join(metaDirPath, "node_key")
}
func nodeKeyPubPath(metaDirPath string) string {
return filepath.Join(metaDirPath, "node_key.pub")
}
// LoadAllocID returns the peer ID (ie the public key) of the node at the given
// meta directory.
func LoadAllocID(metaDirPath string) (string, error) {
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
pubKey, err := os.ReadFile(nodeKeyPubPath)
if err != nil {
return "", fmt.Errorf("reading %q: %w", nodeKeyPubPath, err)
}
return hex.EncodeToString(pubKey), nil
}
// InitAlloc initializes the meta directory and keys for a particular
// allocation, if it hasn't been done so already. It returns the peer ID (ie the
// public key) in any case.
func InitAlloc(metaDirPath string) (string, error) {
var err error
exists := func(path string) bool {
if err != nil {
return false
} else if _, err = os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return false
} else if err != nil {
err = fmt.Errorf("checking if %q exists: %w", path, err)
return false
}
return true
}
nodeKeyPath := nodeKeyPath(metaDirPath)
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
nodeKeyPathExists := exists(nodeKeyPath)
nodeKeyPubPathExists := exists(nodeKeyPubPath)
if err != nil {
return "", err
} else if nodeKeyPubPathExists != nodeKeyPathExists {
return "", fmt.Errorf("%q or %q exist without the other existing", nodeKeyPath, nodeKeyPubPath)
} else if nodeKeyPathExists {
return LoadAllocID(metaDirPath)
}
// node key hasn't been written, write it
if err := os.MkdirAll(metaDirPath, 0750); err != nil {
return "", fmt.Errorf("making directory %q: %w", metaDirPath, err)
}
pubKey, privKey := GeneratePeerKey()
if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
return "", fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)
} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
return "", fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
}
return "", nil
}

View File

@ -1,41 +0,0 @@
package garage
import "io"
type infiniteReader struct {
b []byte
i int
}
// NewInfiniteReader returns a reader which will produce the given bytes in
// repetition. len(b) must be greater than 0.
func NewInfiniteReader(b []byte) io.Reader {
if len(b) == 0 {
panic("len(b) must be greater than 0")
}
return &infiniteReader{b: b}
}
func (r *infiniteReader) Read(b []byte) (int, error) {
// here, have a puzzle
var n int
for {
n += copy(b[n:], r.b[r.i:])
if r.i > 0 {
n += copy(b[n:], r.b[:r.i])
}
r.i = (r.i + n) % len(r.b)
if n >= len(b) {
return n, nil
}
}
}

View File

@ -1,101 +0,0 @@
package garage
import (
"bytes"
"strconv"
"testing"
)
func TestInfiniteReader(t *testing.T) {
tests := []struct {
in []byte
size int
exp []string
}{
{
in: []byte("a"),
size: 1,
exp: []string{"a"},
},
{
in: []byte("ab"),
size: 1,
exp: []string{"a", "b"},
},
{
in: []byte("ab"),
size: 2,
exp: []string{"ab"},
},
{
in: []byte("ab"),
size: 3,
exp: []string{"aba", "bab"},
},
{
in: []byte("ab"),
size: 4,
exp: []string{"abab"},
},
{
in: []byte("ab"),
size: 5,
exp: []string{"ababa", "babab"},
},
{
in: []byte("abc"),
size: 1,
exp: []string{"a", "b", "c"},
},
{
in: []byte("abc"),
size: 2,
exp: []string{"ab", "ca", "bc"},
},
{
in: []byte("abc"),
size: 3,
exp: []string{"abc"},
},
{
in: []byte("abc"),
size: 4,
exp: []string{"abca", "bcab", "cabc"},
},
{
in: []byte("abc"),
size: 5,
exp: []string{"abcab", "cabca", "bcabc"},
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
r := NewInfiniteReader(test.in)
buf := make([]byte, test.size)
assertRead := func(expBuf []byte) {
n, err := r.Read(buf)
if !bytes.Equal(buf, expBuf) {
t.Fatalf("expected bytes %q, got %q", expBuf, buf)
} else if n != len(buf) {
t.Fatalf("expected n %d, got %d", len(buf), n)
} else if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
for i := 0; i < 3; i++ {
for _, expStr := range test.exp {
assertRead([]byte(expStr))
}
}
})
}
}

View File

@ -2,35 +2,32 @@ package garage
import (
"crypto/ed25519"
"encoding/hex"
"crypto/rand"
"fmt"
"net"
"strconv"
)
// Peer describes all information necessary to connect to a given garage node.
type Peer struct {
// RemotePeer describes all information necessary to connect to a given garage
// node.
type RemotePeer struct {
ID string
IP string
RPCPort int
S3APIPort int
}
// RPCPeerKey deterministically generates a public/private keys which can
// be used as a garage node key.
//
// DANGER: This function will deterministically produce public/private keys
// given some arbitrary input. This is NEVER what you want. It's only being used
// in cryptic-net for a very specific purpose for which I think it's ok and is
// very necessary, and people are probably _still_ going to yell at me.
//
func (p Peer) RPCPeerKey() (pubKey, privKey []byte) {
input := []byte(net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort)))
// LocalPeer describes the configuration of a local garage instance.
type LocalPeer struct {
RemotePeer
// Append the length of the input to the input, so that the input "foo"
// doesn't generate the same key as the input "foofoo".
input = strconv.AppendInt(input, int64(len(input)), 10)
AdminPort int
}
pubKey, privKey, err := ed25519.GenerateKey(NewInfiniteReader(input))
// GeneratePeerKey generates and returns a public/private key pair for a garage
// instance.
func GeneratePeerKey() (pubKey, privKey []byte) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
@ -38,29 +35,23 @@ func (p Peer) RPCPeerKey() (pubKey, privKey []byte) {
return pubKey, privKey
}
// RPCPeerID returns the peer ID of the garage node for use in communicating
// over RPC.
//
// DANGER: See warning on RPCPeerKey.
func (p Peer) RPCPeerID() string {
pubKey, _ := p.RPCPeerKey()
return hex.EncodeToString(pubKey)
}
// RPCAddr returns the address of the peer's RPC port.
func (p Peer) RPCAddr() string {
func (p RemotePeer) RPCAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort))
}
// RPCPeerAddr returns the full peer address (e.g. "id@ip:port") of the garage
// node for use in communicating over RPC.
//
// DANGER: See warning on RPCPeerKey.
func (p Peer) RPCPeerAddr() string {
return fmt.Sprintf("%s@%s", p.RPCPeerID(), p.RPCAddr())
func (p RemotePeer) RPCPeerAddr() string {
return fmt.Sprintf("%s@%s", p.ID, p.RPCAddr())
}
// S3APIAddr returns the address of the peer's S3 API port.
func (p Peer) S3APIAddr() string {
func (p RemotePeer) S3APIAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.S3APIPort))
}
// AdminAddr returns the address of the peer's S3 API port.
func (p LocalPeer) AdminAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.AdminPort))
}

View File

@ -17,7 +17,7 @@ type GarageTomlData struct {
AdminToken string
RPCAddr string
APIAddr string
S3APIAddr string
AdminAddr string
BootstrapPeers []string
@ -39,7 +39,7 @@ bootstrap_peers = [{{- range .BootstrapPeers }}
{{ end -}}]
[s3_api]
api_bind_addr = "{{ .APIAddr }}"
api_bind_addr = "{{ .S3APIAddr }}"
s3_region = "garage"
[admin]

View File

@ -3,8 +3,11 @@
package nebula
import (
"crypto"
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
@ -14,65 +17,69 @@ import (
"golang.org/x/crypto/curve25519"
)
// HostCert contains the certificate and private key files which will need to
// be present on a particular host. Each file is PEM encoded.
type HostCert struct {
CACert string
HostKey string
HostCert string
// ErrInvalidSignature is returned from functions when a signature validation
// fails.
var ErrInvalidSignature = errors.New("invalid signature")
// HostCredentials contains the certificate and private key files which will
// need to be present on a particular host. Each file is PEM encoded.
type HostCredentials struct {
CACertPEM string
HostKeyPEM string
HostCertPEM string
}
// CACert contains the certificate and private files which can be used to create
// HostCerts. Each file is PEM encoded.
type CACert struct {
CACert string
CAKey string
// CACredentials contains the certificate and private files which can be used to
// create and validate HostCredentials. Each file is PEM encoded.
type CACredentials struct {
CACertPEM string
CAKeyPEM string
}
// NewHostCert generates a new key/cert for a nebula host using the CA key
// which will be found in the adminFS.
func NewHostCert(
caCert CACert, hostName string, ip net.IP,
// NewHostCredentials generates a new key/cert for a nebula host using the CA
// key which will be found in the adminFS.
func NewHostCredentials(
caCreds CACredentials, hostName string, ip net.IP,
) (
HostCert, error,
HostCredentials, error,
) {
// The logic here is largely based on
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCert.CAKey))
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.CAKeyPEM))
if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err)
return HostCredentials{}, fmt.Errorf("unmarshaling ca.key: %w", err)
}
caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCert.CACert))
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.CACertPEM))
if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
return HostCredentials{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
}
issuer, err := caCrt.Sha256Sum()
issuer, err := caCert.Sha256Sum()
if err != nil {
return HostCert{}, fmt.Errorf("getting ca.crt issuer: %w", err)
return HostCredentials{}, fmt.Errorf("getting ca.crt issuer: %w", err)
}
expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
expireAt := caCert.Details.NotAfter.Add(-1 * time.Second)
subnet := caCrt.Details.Subnets[0]
subnet := caCert.Details.Subnets[0]
if !subnet.Contains(ip) {
return HostCert{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
return HostCredentials{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
}
var hostPub, hostKey []byte
{
var pubkey, privkey [32]byte
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
return HostCert{}, fmt.Errorf("reading random bytes to form private key: %w", err)
return HostCredentials{}, fmt.Errorf("reading random bytes to form private key: %w", err)
}
curve25519.ScalarBaseMult(&pubkey, &privkey)
hostPub, hostKey = pubkey[:], privkey[:]
}
hostCrt := cert.NebulaCertificate{
hostCert := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: hostName,
Ips: []*net.IPNet{{
@ -87,31 +94,31 @@ func NewHostCert(
},
}
if err := hostCrt.CheckRootConstrains(caCrt); err != nil {
return HostCert{}, fmt.Errorf("validating certificate constraints: %w", err)
if err := hostCert.CheckRootConstrains(caCert); err != nil {
return HostCredentials{}, fmt.Errorf("validating certificate constraints: %w", err)
}
if err := hostCrt.Sign(caKey); err != nil {
return HostCert{}, fmt.Errorf("signing host cert with ca.key: %w", err)
if err := hostCert.Sign(caKey); err != nil {
return HostCredentials{}, fmt.Errorf("signing host cert with ca.key: %w", err)
}
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
hostCrtPEM, err := hostCrt.MarshalToPEM()
hostCertPEM, err := hostCert.MarshalToPEM()
if err != nil {
return HostCert{}, fmt.Errorf("marshalling host.crt: %w", err)
return HostCredentials{}, fmt.Errorf("marshalling host.crt: %w", err)
}
return HostCert{
CACert: caCert.CACert,
HostKey: string(hostKeyPEM),
HostCert: string(hostCrtPEM),
return HostCredentials{
CACertPEM: caCreds.CACertPEM,
HostKeyPEM: string(hostKeyPEM),
HostCertPEM: string(hostCertPEM),
}, nil
}
// NewCACert generates a CACert. The domain should be the network's root domain,
// NewCACredentials generates a CACredentials. The domain should be the network's root domain,
// and is included in the signing certificate's Name field.
func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
@ -121,7 +128,7 @@ func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
now := time.Now()
expireAt := now.Add(2 * 365 * 24 * time.Hour)
caCrt := cert.NebulaCertificate{
caCert := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: fmt.Sprintf("%s cryptic-net root cert", domain),
Subnets: []*net.IPNet{subnet},
@ -132,19 +139,134 @@ func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
},
}
if err := caCrt.Sign(privKey); err != nil {
return CACert{}, fmt.Errorf("signing caCrt: %w", err)
if err := caCert.Sign(privKey); err != nil {
return CACredentials{}, fmt.Errorf("signing caCert: %w", err)
}
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
caCrtPem, err := caCrt.MarshalToPEM()
caCertPEM, err := caCert.MarshalToPEM()
if err != nil {
return CACert{}, fmt.Errorf("marshaling caCrt: %w", err)
return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err)
}
return CACert{
CACert: string(caCrtPem),
CAKey: string(caKeyPEM),
return CACredentials{
CACertPEM: string(caCertPEM),
CAKeyPEM: string(caKeyPEM),
}, nil
}
// ValidateHostCertPEM checks if the given host certificate was signed by the
// given CA certificate, and returns ErrInvalidSignature if validation fails.
func ValidateHostCertPEM(caCertPEM, hostCertPEM string) error {
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCertPEM))
if err != nil {
return fmt.Errorf("unmarshaling CA certificate as PEM: %w", err)
}
hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(hostCertPEM))
if err != nil {
return fmt.Errorf("unmarshaling host certificate as PEM: %w", err)
}
caPubKey := ed25519.PublicKey(caCert.Details.PublicKey)
if !hostCert.CheckSignature(caPubKey) {
return ErrInvalidSignature
}
return nil
}
// IPFromHostCertPEM is a convenience function for parsing the IP of a host out
// of its nebula cert.
func IPFromHostCertPEM(hostCertPEM string) (net.IP, error) {
hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(hostCertPEM))
if err != nil {
return nil, fmt.Errorf("unmarshaling host certificate as PEM: %w", err)
}
ips := hostCert.Details.Ips
if len(ips) == 0 {
return nil, fmt.Errorf("malformed nebula host cert: no IPs")
}
return ips[0].IP, nil
}
// SignAndWrap signs the given bytes using the keyPEM, and writes an
// encoded, versioned structure containing the signature and the given bytes.
func SignAndWrap(into io.Writer, keyPEM string, b []byte) error {
key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(keyPEM))
if err != nil {
return fmt.Errorf("unmarshaling private key: %w", err)
}
sig, err := key.Sign(rand.Reader, b, crypto.Hash(0))
if err != nil {
return fmt.Errorf("generating signature: %w", err)
}
if _, err := into.Write([]byte("0")); err != nil {
return fmt.Errorf("writing version byte: %w", err)
}
err = pem.Encode(into, &pem.Block{
Type: "SIGNATURE",
Bytes: sig,
})
if err != nil {
return fmt.Errorf("writing PEM encoding of signature: %w", err)
}
if _, err := into.Write(b); err != nil {
return fmt.Errorf("writing input bytes: %w", err)
}
return nil
}
// Unwrap reads a stream of bytes which was produced by SignAndWrap, and returns
// the original inpute to SignAndWrap as well as the signature which was
// created. ValidateSignature can be used to validate the signature.
func Unwrap(from io.Reader) (b, sig []byte, err error) {
full, err := io.ReadAll(from)
if err != nil {
return nil, nil, fmt.Errorf("reading full input: %w", err)
} else if len(full) < 3 {
return nil, nil, fmt.Errorf("input too small")
} else if full[0] != '0' {
return nil, nil, fmt.Errorf("unexpected version byte: %d", full[0])
}
full = full[1:]
pemBlock, rest := pem.Decode(full)
if pemBlock == nil {
return nil, nil, fmt.Errorf("PEM-encoded signature could not be decoded")
}
return rest, pemBlock.Bytes, nil
}
// ValidateSignature can be used to validate a signature produced by Unwrap.
func ValidateSignature(certPEM string, b, sig []byte) error {
cert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(certPEM))
if err != nil {
return fmt.Errorf("unmarshaling certificate as PEM: %w", err)
}
pubKey := ed25519.PublicKey(cert.Details.PublicKey)
if !ed25519.Verify(pubKey, b, sig) {
return ErrInvalidSignature
}
return nil
}

View File

@ -0,0 +1,77 @@
package nebula
import (
"bytes"
"errors"
"net"
"testing"
)
var (
ip net.IP
ipNet *net.IPNet
caCredsA, caCredsB CACredentials
)
func init() {
var err error
ip, ipNet, err = net.ParseCIDR("192.168.0.1/24")
if err != nil {
panic(err)
}
caCredsA, err = NewCACredentials("a.example.com", ipNet)
if err != nil {
panic(err)
}
caCredsB, err = NewCACredentials("b.example.com", ipNet)
if err != nil {
panic(err)
}
}
func TestValidateHostCredentials(t *testing.T) {
hostCreds, err := NewHostCredentials(caCredsA, "foo", ip)
if err != nil {
t.Fatal(err)
}
err = ValidateHostCertPEM(hostCreds.CACertPEM, hostCreds.HostCertPEM)
if err != nil {
t.Fatal(err)
}
err = ValidateHostCertPEM(caCredsB.CACertPEM, hostCreds.HostCertPEM)
if !errors.Is(err, ErrInvalidSignature) {
t.Fatalf("expected ErrInvalidSignature, got %v", err)
}
}
func TestSignAndWrap(t *testing.T) {
b := []byte("foo bar baz")
buf := new(bytes.Buffer)
if err := SignAndWrap(buf, caCredsA.CAKeyPEM, b); err != nil {
t.Fatal(err)
}
gotB, gotSig, err := Unwrap(buf)
if err != nil {
t.Fatal(err)
} else if !bytes.Equal(b, gotB) {
t.Fatalf("got %q but expected %q", gotB, b)
}
if err := ValidateSignature(caCredsA.CACertPEM, b, gotSig); err != nil {
t.Fatal(err)
}
if err := ValidateSignature(caCredsB.CACertPEM, b, gotSig); !errors.Is(err, ErrInvalidSignature) {
t.Fatalf("expected ErrInvalidSignature but got %v", err)
}
}