Refactor how host data is signed, now it's simpler and probably more secure
This commit is contained in:
parent
f13a08abfb
commit
78890d1f77
@ -10,6 +10,7 @@ import (
|
|||||||
"isle/admin"
|
"isle/admin"
|
||||||
"isle/garage"
|
"isle/garage"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@ -27,31 +28,81 @@ func AppDirPath(appDirPath string) string {
|
|||||||
return filepath.Join(appDirPath, "share/bootstrap.json")
|
return filepath.Join(appDirPath, "share/bootstrap.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Garage contains parameters needed to connect to and use the garage cluster.
|
||||||
|
type Garage struct {
|
||||||
|
// TODO RPCSecret and GlobalBucketS3APICredentials are duplicated here and
|
||||||
|
// in AdminCreationParams, might as well just use them from there
|
||||||
|
RPCSecret string
|
||||||
|
AdminToken string
|
||||||
|
GlobalBucketS3APICredentials garage.S3APICredentials
|
||||||
|
}
|
||||||
|
|
||||||
// Bootstrap is used for accessing all information contained within a
|
// Bootstrap is used for accessing all information contained within a
|
||||||
// bootstrap.json file.
|
// bootstrap.json file.
|
||||||
type Bootstrap struct {
|
type Bootstrap struct {
|
||||||
AdminCreationParams admin.CreationParams
|
AdminCreationParams admin.CreationParams
|
||||||
|
CAPublicCredentials nebula.CAPublicCredentials
|
||||||
|
Garage Garage
|
||||||
|
|
||||||
Hosts map[string]Host
|
PrivateCredentials nebula.HostPrivateCredentials
|
||||||
HostName string
|
HostAssigned `json:"-"`
|
||||||
|
SignedHostAssigned nebula.Signed[HostAssigned] // signed by CA
|
||||||
|
|
||||||
Nebula struct {
|
Hosts map[string]Host
|
||||||
CAPublicCredentials nebula.CAPublicCredentials
|
}
|
||||||
HostCredentials nebula.HostCredentials
|
|
||||||
SignedPublicCredentials string
|
// New initializes and returns a new Bootstrap file for a new host. This
|
||||||
|
// function assigns Hosts an empty map.
|
||||||
|
func New(
|
||||||
|
caCreds nebula.CACredentials,
|
||||||
|
adminCreationParams admin.CreationParams,
|
||||||
|
garage Garage,
|
||||||
|
name string,
|
||||||
|
ip net.IP,
|
||||||
|
) (
|
||||||
|
Bootstrap, error,
|
||||||
|
) {
|
||||||
|
hostPubCreds, hostPrivCreds, err := nebula.NewHostCredentials(
|
||||||
|
caCreds, name, ip,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("generating host credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
Garage struct {
|
assigned := HostAssigned{
|
||||||
RPCSecret string
|
Name: name,
|
||||||
AdminToken string
|
PublicCredentials: hostPubCreds,
|
||||||
GlobalBucketS3APICredentials garage.S3APICredentials
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signedAssigned, err := nebula.Sign(assigned, caCreds.SigningPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("signing assigned fields: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bootstrap{
|
||||||
|
AdminCreationParams: adminCreationParams,
|
||||||
|
CAPublicCredentials: caCreds.Public,
|
||||||
|
Garage: garage,
|
||||||
|
PrivateCredentials: hostPrivCreds,
|
||||||
|
HostAssigned: assigned,
|
||||||
|
SignedHostAssigned: signedAssigned,
|
||||||
|
Hosts: map[string]Host{},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromReader reads a bootstrap file from the given io.Reader.
|
// FromReader reads a bootstrap file from the given io.Reader.
|
||||||
func FromReader(r io.Reader) (Bootstrap, error) {
|
func FromReader(r io.Reader) (Bootstrap, error) {
|
||||||
var b Bootstrap
|
var b Bootstrap
|
||||||
|
|
||||||
err := json.NewDecoder(r).Decode(&b)
|
err := json.NewDecoder(r).Decode(&b)
|
||||||
|
if err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("decoding json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.HostAssigned, err = b.SignedHostAssigned.UnwrapUnsafe(); err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("unwrapping host assigned: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,9 +127,9 @@ func (b Bootstrap) WriteTo(into io.Writer) error {
|
|||||||
// HostName isn't found in the Hosts map.
|
// HostName isn't found in the Hosts map.
|
||||||
func (b Bootstrap) ThisHost() Host {
|
func (b Bootstrap) ThisHost() Host {
|
||||||
|
|
||||||
host, ok := b.Hosts[b.HostName]
|
host, ok := b.Hosts[b.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.HostName))
|
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
return host
|
return host
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"isle/garage"
|
"isle/garage"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
||||||
@ -24,26 +23,24 @@ const (
|
|||||||
// into garage so that other hosts are able to see relevant configuration for
|
// into garage so that other hosts are able to see relevant configuration for
|
||||||
// it.
|
// it.
|
||||||
func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error {
|
func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error {
|
||||||
|
var (
|
||||||
|
host = b.ThisHost()
|
||||||
|
client = b.GlobalBucketS3APIClient()
|
||||||
|
)
|
||||||
|
|
||||||
host := b.ThisHost()
|
configured, err := nebula.Sign(
|
||||||
client := b.GlobalBucketS3APIClient()
|
host.HostConfigured, b.PrivateCredentials.SigningPrivateKey,
|
||||||
|
)
|
||||||
// the base Bootstrap has the public credentials signed by the CA, but we
|
|
||||||
// need this to be presented in the data stored into garage, so other hosts
|
|
||||||
// can verify that the stored host object is signed by the host public key,
|
|
||||||
// and that the host public key is signed by the CA.
|
|
||||||
host.Nebula.SignedPublicCredentials = b.Nebula.SignedPublicCredentials
|
|
||||||
|
|
||||||
hostB, err := json.Marshal(host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encoding host data: %w", err)
|
return fmt.Errorf("signing host configured data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
hostB, err := json.Marshal(AuthenticatedHost{
|
||||||
|
Assigned: b.SignedHostAssigned,
|
||||||
err = nebula.SignAndWrap(buf, b.Nebula.HostCredentials.SigningPrivateKeyPEM, hostB)
|
Configured: configured,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("signing encoded host data: %w", err)
|
return fmt.Errorf("encoding host data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath := filepath.Join(
|
filePath := filepath.Join(
|
||||||
@ -52,7 +49,11 @@ func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error {
|
|||||||
)
|
)
|
||||||
|
|
||||||
_, err = client.PutObject(
|
_, err = client.PutObject(
|
||||||
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
|
ctx,
|
||||||
|
garage.GlobalBucket,
|
||||||
|
filePath,
|
||||||
|
bytes.NewReader(hostB),
|
||||||
|
int64(len(hostB)),
|
||||||
minio.PutObjectOptions{},
|
minio.PutObjectOptions{},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -119,49 +120,19 @@ func (b Bootstrap) GetGarageBootstrapHosts(
|
|||||||
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
|
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hostB, hostSig, err := nebula.Unwrap(obj)
|
var authedHost AuthenticatedHost
|
||||||
|
|
||||||
|
err = json.NewDecoder(obj).Decode(&authedHost)
|
||||||
obj.Close()
|
obj.Close()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unwrapping signature from %q: %w", objInfo.Key, err)
|
logger.Warn(ctx, "object contains invalid json", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var host Host
|
host, err := authedHost.Unwrap(b.CAPublicCredentials)
|
||||||
if err = json.Unmarshal(hostB, &host); err != nil {
|
|
||||||
return nil, fmt.Errorf("decoding object %q: %w", objInfo.Key, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostPublicCredsB, hostPublicCredsSig, err := nebula.Unwrap(
|
|
||||||
strings.NewReader(host.Nebula.SignedPublicCredentials),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(ctx, "unwrapping signed public creds", err)
|
logger.Warn(ctx, "host could not be authenticated", err)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = nebula.ValidateSignature(
|
|
||||||
b.Nebula.CAPublicCredentials.SigningKeyPEM,
|
|
||||||
hostPublicCredsB,
|
|
||||||
hostPublicCredsSig,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn(ctx, "invalid signed public creds", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostPublicCreds nebula.HostPublicCredentials
|
|
||||||
if err := json.Unmarshal(hostPublicCredsB, &hostPublicCreds); err != nil {
|
|
||||||
logger.Warn(ctx, "unmarshaling signed public creds", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = nebula.ValidateSignature(hostPublicCreds.SigningKeyPEM, hostB, hostSig)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn(ctx, "invalid host data", err)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts[host.Name] = host
|
hosts[host.Name] = host
|
||||||
|
@ -1,44 +1,15 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NebulaHost describes the nebula configuration of a Host which is relevant for
|
// NebulaHost describes the nebula configuration of a Host which is relevant for
|
||||||
// other hosts to know.
|
// other hosts to know.
|
||||||
type NebulaHost struct {
|
type NebulaHost struct {
|
||||||
SignedPublicCredentials string
|
PublicAddr string
|
||||||
PublicAddr string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNebulaHostSignedPublicCredentials constructs the SignedPublicCredentials
|
|
||||||
// field of the NebulaHost struct, using the CACredentials to sign the
|
|
||||||
// HostPublicCredentials.
|
|
||||||
func NewNebulaHostSignedPublicCredentials(
|
|
||||||
caCreds nebula.CACredentials,
|
|
||||||
hostPublicCreds nebula.HostPublicCredentials,
|
|
||||||
) (
|
|
||||||
string, error,
|
|
||||||
) {
|
|
||||||
|
|
||||||
hostPublicCredsB, err := json.Marshal(hostPublicCreds)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("marshaling host's public credentials: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
|
|
||||||
err = nebula.SignAndWrap(buf, caCreds.SigningPrivateKeyPEM, hostPublicCredsB)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("signing host's public credentials: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GarageHost describes a single garage instance in the GarageHost.
|
// GarageHost describes a single garage instance in the GarageHost.
|
||||||
@ -54,12 +25,51 @@ type GarageHost struct {
|
|||||||
Instances []GarageHostInstance
|
Instances []GarageHostInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host consolidates all information about a single host from the bootstrap
|
// HostAssigned are all fields related to a host which were assigned to it by an
|
||||||
// file.
|
// admin.
|
||||||
|
type HostAssigned struct {
|
||||||
|
Name string
|
||||||
|
PublicCredentials nebula.HostPublicCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostConfigured are all the fields a host can configure for itself.
|
||||||
|
type HostConfigured struct {
|
||||||
|
Nebula NebulaHost `json:",omitempty"`
|
||||||
|
Garage GarageHost `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticatedHost wraps all the data about a host which other hosts may know
|
||||||
|
// about it, such that those hosts can authenticate that the data is valid and
|
||||||
|
// approved by an admin.
|
||||||
|
type AuthenticatedHost struct {
|
||||||
|
Assigned nebula.Signed[HostAssigned] // signed by CA
|
||||||
|
Configured nebula.Signed[HostConfigured] // signed by host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap attempts to authenticate and unwrap the Host embedded in this
|
||||||
|
// instance. nebula.ErrInvalidSignature is returned if any signatures are
|
||||||
|
// invalid.
|
||||||
|
func (ah AuthenticatedHost) Unwrap(caCreds nebula.CAPublicCredentials) (Host, error) {
|
||||||
|
assigned, err := ah.Assigned.Unwrap(caCreds.SigningKey)
|
||||||
|
if err != nil {
|
||||||
|
return Host{}, fmt.Errorf("unwrapping assigned fields using CA public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configured, err := ah.Configured.Unwrap(assigned.PublicCredentials.SigningKey)
|
||||||
|
if err != nil {
|
||||||
|
return Host{}, fmt.Errorf("unwrapping configured fields using host public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Host{assigned, configured}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host contains all data bout a host which other hosts may know about it.
|
||||||
|
//
|
||||||
|
// A Host should only be obtained over the network as an AuthenticatedHost, and
|
||||||
|
// subsequently Unwrapped.
|
||||||
type Host struct {
|
type Host struct {
|
||||||
Name string
|
HostAssigned
|
||||||
Nebula NebulaHost
|
HostConfigured
|
||||||
Garage GarageHost
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IP returns the IP address encoded in the Host's nebula certificate, or panics
|
// IP returns the IP address encoded in the Host's nebula certificate, or panics
|
||||||
@ -68,21 +78,7 @@ 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() net.IP {
|
||||||
|
ip, err := nebula.IPFromHostCertPEM(h.PublicCredentials.CertPEM)
|
||||||
hostPublicCredsB, _, err := nebula.Unwrap(
|
|
||||||
strings.NewReader(h.Nebula.SignedPublicCredentials),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("unwrapping host's signed public credentials: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostPublicCreds nebula.HostPublicCredentials
|
|
||||||
if err := json.Unmarshal(hostPublicCredsB, &hostPublicCreds); err != nil {
|
|
||||||
panic(fmt.Errorf("unmarshaling host's public credentials: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ip, err := nebula.IPFromHostCertPEM(hostPublicCreds.CertPEM)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("could not parse IP out of cert for host %q: %w", h.Name, err))
|
panic(fmt.Errorf("could not parse IP out of cert for host %q: %w", h.Name, err))
|
||||||
}
|
}
|
||||||
|
@ -142,46 +142,28 @@ var subCmdAdminCreateNetwork = subCmd{
|
|||||||
return fmt.Errorf("creating nebula CA cert: %w", err)
|
return fmt.Errorf("creating nebula CA cert: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
nebulaHostCreds, err := nebula.NewHostCredentials(nebulaCACreds, *hostName, ip)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating nebula cert for host: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nebulaHostSignedPublicCreds, err := bootstrap.NewNebulaHostSignedPublicCredentials(
|
|
||||||
nebulaCACreds,
|
|
||||||
nebulaHostCreds.Public,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating signed public credentials for host: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
adminCreationParams := admin.CreationParams{
|
adminCreationParams := admin.CreationParams{
|
||||||
ID: randStr(32),
|
ID: randStr(32),
|
||||||
Name: *name,
|
Name: *name,
|
||||||
Domain: *domain,
|
Domain: *domain,
|
||||||
}
|
}
|
||||||
|
|
||||||
hostBootstrap := bootstrap.Bootstrap{
|
garageBootstrap := bootstrap.Garage{
|
||||||
AdminCreationParams: adminCreationParams,
|
RPCSecret: randStr(32),
|
||||||
Hosts: map[string]bootstrap.Host{
|
AdminToken: randStr(32),
|
||||||
*hostName: bootstrap.Host{
|
GlobalBucketS3APICredentials: garage.NewS3APICredentials(),
|
||||||
Name: *hostName,
|
|
||||||
Nebula: bootstrap.NebulaHost{
|
|
||||||
SignedPublicCredentials: nebulaHostSignedPublicCreds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
HostName: *hostName,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hostBootstrap.Nebula.CAPublicCredentials = nebulaCACreds.Public
|
hostBootstrap, err := bootstrap.New(
|
||||||
hostBootstrap.Nebula.HostCredentials = nebulaHostCreds
|
nebulaCACreds,
|
||||||
hostBootstrap.Nebula.SignedPublicCredentials = nebulaHostSignedPublicCreds
|
adminCreationParams,
|
||||||
|
garageBootstrap,
|
||||||
hostBootstrap.Garage.RPCSecret = randStr(32)
|
*hostName,
|
||||||
hostBootstrap.Garage.AdminToken = randStr(32)
|
ip,
|
||||||
hostBootstrap.Garage.GlobalBucketS3APICredentials = garage.NewS3APICredentials()
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initializing bootstrap data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if hostBootstrap, daemonConfig, err = coalesceDaemonConfigAndBootstrap(hostBootstrap, daemonConfig); err != nil {
|
if hostBootstrap, daemonConfig, err = coalesceDaemonConfigAndBootstrap(hostBootstrap, daemonConfig); err != nil {
|
||||||
return fmt.Errorf("merging daemon config into bootstrap data: %w", err)
|
return fmt.Errorf("merging daemon config into bootstrap data: %w", err)
|
||||||
@ -263,7 +245,7 @@ var subCmdAdminCreateNetwork = subCmd{
|
|||||||
var subCmdAdminCreateBootstrap = subCmd{
|
var subCmdAdminCreateBootstrap = subCmd{
|
||||||
name: "create-bootstrap",
|
name: "create-bootstrap",
|
||||||
descr: "Creates a new bootstrap.json file for a particular host and writes it to stdout",
|
descr: "Creates a new bootstrap.json file for a particular host and writes it to stdout",
|
||||||
checkLock: true,
|
checkLock: false,
|
||||||
do: func(subCmdCtx subCmdCtx) error {
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
flags := subCmdCtx.flagSet(false)
|
flags := subCmdCtx.flagSet(false)
|
||||||
@ -306,39 +288,29 @@ var subCmdAdminCreateBootstrap = subCmd{
|
|||||||
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
|
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
garageBootstrap := bootstrap.Garage{
|
||||||
|
RPCSecret: adm.Garage.RPCSecret,
|
||||||
|
AdminToken: randStr(32),
|
||||||
|
GlobalBucketS3APICredentials: adm.Garage.GlobalBucketS3APICredentials,
|
||||||
|
}
|
||||||
|
|
||||||
|
newHostBootstrap, err := bootstrap.New(
|
||||||
|
adm.Nebula.CACredentials,
|
||||||
|
adm.CreationParams,
|
||||||
|
garageBootstrap,
|
||||||
|
*hostName,
|
||||||
|
ip,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initializing bootstrap data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
hostBootstrap, err := loadHostBootstrap()
|
hostBootstrap, err := loadHostBootstrap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
nebulaHostCreds, err := nebula.NewHostCredentials(adm.Nebula.CACredentials, *hostName, ip)
|
newHostBootstrap.Hosts = hostBootstrap.Hosts
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new nebula host key/cert: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nebulaHostSignedPublicCreds, err := bootstrap.NewNebulaHostSignedPublicCredentials(
|
|
||||||
adm.Nebula.CACredentials,
|
|
||||||
nebulaHostCreds.Public,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating signed public credentials for host: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newHostBootstrap := bootstrap.Bootstrap{
|
|
||||||
AdminCreationParams: adm.CreationParams,
|
|
||||||
|
|
||||||
Hosts: hostBootstrap.Hosts,
|
|
||||||
HostName: *hostName,
|
|
||||||
}
|
|
||||||
|
|
||||||
newHostBootstrap.Nebula.CAPublicCredentials = adm.Nebula.CACredentials.Public
|
|
||||||
newHostBootstrap.Nebula.HostCredentials = nebulaHostCreds
|
|
||||||
newHostBootstrap.Nebula.SignedPublicCredentials = nebulaHostSignedPublicCreds
|
|
||||||
|
|
||||||
newHostBootstrap.Garage.RPCSecret = adm.Garage.RPCSecret
|
|
||||||
newHostBootstrap.Garage.AdminToken = randStr(32)
|
|
||||||
newHostBootstrap.Garage.GlobalBucketS3APICredentials = adm.Garage.GlobalBucketS3APICredentials
|
|
||||||
|
|
||||||
return newHostBootstrap.WriteTo(os.Stdout)
|
return newHostBootstrap.WriteTo(os.Stdout)
|
||||||
},
|
},
|
||||||
|
@ -2,10 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"isle/bootstrap"
|
"isle/bootstrap"
|
||||||
"isle/daemon"
|
"isle/daemon"
|
||||||
"isle/garage"
|
"isle/garage"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,10 +17,11 @@ func coalesceDaemonConfigAndBootstrap(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
host := bootstrap.Host{
|
host := bootstrap.Host{
|
||||||
Name: hostBootstrap.HostName,
|
HostAssigned: hostBootstrap.HostAssigned,
|
||||||
Nebula: bootstrap.NebulaHost{
|
HostConfigured: bootstrap.HostConfigured{
|
||||||
SignedPublicCredentials: hostBootstrap.Nebula.SignedPublicCredentials,
|
Nebula: bootstrap.NebulaHost{
|
||||||
PublicAddr: daemonConfig.VPN.PublicAddr,
|
PublicAddr: daemonConfig.VPN.PublicAddr,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ var subCmdNebulaShow = subCmd{
|
|||||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
caPublicCreds := hostBootstrap.Nebula.CAPublicCredentials
|
caPublicCreds := hostBootstrap.CAPublicCredentials
|
||||||
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caPublicCreds.CertPEM))
|
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caPublicCreds.CertPEM))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unmarshaling ca.crt: %w", err)
|
return fmt.Errorf("unmarshaling ca.crt: %w", err)
|
||||||
|
@ -58,9 +58,9 @@ func nebulaPmuxProcConfig(
|
|||||||
|
|
||||||
config := map[string]interface{}{
|
config := map[string]interface{}{
|
||||||
"pki": map[string]string{
|
"pki": map[string]string{
|
||||||
"ca": hostBootstrap.Nebula.CAPublicCredentials.CertPEM,
|
"ca": hostBootstrap.CAPublicCredentials.CertPEM,
|
||||||
"cert": hostBootstrap.Nebula.HostCredentials.Public.CertPEM,
|
"cert": hostBootstrap.PublicCredentials.CertPEM,
|
||||||
"key": hostBootstrap.Nebula.HostCredentials.PrivateKeyPEM,
|
"key": hostBootstrap.PrivateCredentials.PrivateKeyPEM,
|
||||||
},
|
},
|
||||||
"static_host_map": staticHostMap,
|
"static_host_map": staticHostMap,
|
||||||
"punchy": map[string]bool{
|
"punchy": map[string]bool{
|
||||||
|
72
go/nebula/keys.go
Normal file
72
go/nebula/keys.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SigningPrivateKey wraps an ed25519.PrivateKey to provide convenient JSON
|
||||||
|
// (un)marshaling methods.
|
||||||
|
type SigningPrivateKey ed25519.PrivateKey
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (k SigningPrivateKey) MarshalJSON() ([]byte, error) {
|
||||||
|
pemStr := cert.MarshalEd25519PrivateKey(ed25519.PrivateKey(k))
|
||||||
|
return json.Marshal(string(pemStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||||
|
func (k *SigningPrivateKey) UnmarshalJSON(b []byte) error {
|
||||||
|
var pemStr string
|
||||||
|
if err := json.Unmarshal(b, &pemStr); err != nil {
|
||||||
|
return fmt.Errorf("unmarshaling into string: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(pemStr))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshaling from PEM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*k = SigningPrivateKey(key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigningPublicKey wraps an ed25519.PublicKey to provide convenient JSON
|
||||||
|
// (un)marshaling methods.
|
||||||
|
type SigningPublicKey ed25519.PublicKey
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (k SigningPublicKey) MarshalJSON() ([]byte, error) {
|
||||||
|
pemStr := cert.MarshalEd25519PublicKey(ed25519.PublicKey(k))
|
||||||
|
return json.Marshal(string(pemStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Unmarshaler interface.
|
||||||
|
func (k *SigningPublicKey) UnmarshalJSON(b []byte) error {
|
||||||
|
var pemStr string
|
||||||
|
if err := json.Unmarshal(b, &pemStr); err != nil {
|
||||||
|
return fmt.Errorf("unmarshaling into string: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, _, err := cert.UnmarshalEd25519PublicKey([]byte(pemStr))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshaling from PEM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*k = SigningPublicKey(key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSigningPair generates and returns a new key pair which can be used
|
||||||
|
// for signing arbitrary blobs of bytes.
|
||||||
|
func GenerateSigningPair() (SigningPublicKey, SigningPrivateKey) {
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("generating ed25519 key: %w", err))
|
||||||
|
}
|
||||||
|
return SigningPublicKey(pub), SigningPrivateKey(priv)
|
||||||
|
}
|
52
go/nebula/keys_test.go
Normal file
52
go/nebula/keys_test.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSigningKeysJSON(t *testing.T) {
|
||||||
|
pub, priv := GenerateSigningPair()
|
||||||
|
|
||||||
|
t.Run("SigningPublicKey", func(t *testing.T) {
|
||||||
|
pubJSON, err := json.Marshal(pub)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(string(pubJSON), `"-----BEGIN `) {
|
||||||
|
t.Fatalf("pub key didn't marshal to PEM: %q", pubJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pub2 SigningPublicKey
|
||||||
|
if err := json.Unmarshal(pubJSON, &pub2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal([]byte(pub), []byte(pub2)) {
|
||||||
|
t.Fatalf("json unmarshaling got different result: %q", pub2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SigningPrivateKey", func(t *testing.T) {
|
||||||
|
privJSON, err := json.Marshal(priv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(string(privJSON), `"-----BEGIN `) {
|
||||||
|
t.Fatalf("priv key didn't marshal to PEM: %q", privJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
var priv2 SigningPrivateKey
|
||||||
|
if err := json.Unmarshal(privJSON, &priv2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal([]byte(priv), []byte(priv2)) {
|
||||||
|
t.Fatalf("json unmarshaling got different result: %q", priv2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -3,11 +3,7 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/ed25519"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@ -17,38 +13,33 @@ import (
|
|||||||
"golang.org/x/crypto/curve25519"
|
"golang.org/x/crypto/curve25519"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidSignature is returned from functions when a signature validation
|
|
||||||
// fails.
|
|
||||||
var ErrInvalidSignature = errors.New("invalid signature")
|
|
||||||
|
|
||||||
// HostPublicCredentials contains certificate and signing public keys which are
|
// HostPublicCredentials contains certificate and signing public keys which are
|
||||||
// able to be broadcast publicly.
|
// able to be broadcast publicly.
|
||||||
type HostPublicCredentials struct {
|
type HostPublicCredentials struct {
|
||||||
CertPEM string
|
CertPEM string
|
||||||
SigningKeyPEM string
|
SigningKey SigningPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostCredentials contains the certificate and private key files which will
|
// HostPrivateCredentials contains the private key files which will
|
||||||
// need to be present on a particular host. Each file is PEM encoded.
|
// need to be present on a particular host.
|
||||||
type HostCredentials struct {
|
type HostPrivateCredentials struct {
|
||||||
Public HostPublicCredentials
|
PrivateKeyPEM string
|
||||||
PrivateKeyPEM string
|
SigningPrivateKey SigningPrivateKey
|
||||||
SigningPrivateKeyPEM string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CAPublicCredentials contains certificate and signing public keys which are
|
// CAPublicCredentials contains certificate and signing public keys which are
|
||||||
// able to be broadcast publicly. The signing public key is the same one which
|
// able to be broadcast publicly. The signing public key is the same one which
|
||||||
// is embedded into the certificate.
|
// is embedded into the certificate.
|
||||||
type CAPublicCredentials struct {
|
type CAPublicCredentials struct {
|
||||||
CertPEM string
|
CertPEM string
|
||||||
SigningKeyPEM string // TODO remove redundant field
|
SigningKey SigningPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// CACredentials contains the certificate and private files which can be used to
|
// CACredentials contains the certificate and private files which can be used to
|
||||||
// create and validate HostCredentials. Each file is PEM encoded.
|
// create and validate HostCredentials. Each file is PEM encoded.
|
||||||
type CACredentials struct {
|
type CACredentials struct {
|
||||||
Public CAPublicCredentials
|
Public CAPublicCredentials
|
||||||
SigningPrivateKeyPEM string
|
SigningPrivateKey SigningPrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHostCertPEM generates and signs a new host certificate containing the
|
// NewHostCertPEM generates and signs a new host certificate containing the
|
||||||
@ -63,11 +54,6 @@ func NewHostCertPEM(
|
|||||||
return "", fmt.Errorf("unmarshaling public key PEM: %w", err)
|
return "", fmt.Errorf("unmarshaling public key PEM: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
caSigningKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.SigningPrivateKeyPEM))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("unmarshaling ca.key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.Public.CertPEM))
|
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.Public.CertPEM))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unmarshaling ca.crt: %w", err)
|
return "", fmt.Errorf("unmarshaling ca.crt: %w", err)
|
||||||
@ -104,7 +90,7 @@ func NewHostCertPEM(
|
|||||||
return "", fmt.Errorf("validating certificate constraints: %w", err)
|
return "", fmt.Errorf("validating certificate constraints: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := hostCert.Sign(caSigningKey); err != nil {
|
if err := signCert(&hostCert, caCreds.SigningPrivateKey); err != nil {
|
||||||
return "", fmt.Errorf("signing host cert with ca.key: %w", err)
|
return "", fmt.Errorf("signing host cert with ca.key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,46 +107,45 @@ func NewHostCertPEM(
|
|||||||
func NewHostCredentials(
|
func NewHostCredentials(
|
||||||
caCreds CACredentials, hostName string, ip net.IP,
|
caCreds CACredentials, hostName string, ip net.IP,
|
||||||
) (
|
) (
|
||||||
HostCredentials, error,
|
pub HostPublicCredentials, priv HostPrivateCredentials, err error,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// The logic here is largely based on
|
// The logic here is largely based on
|
||||||
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
|
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
|
||||||
|
|
||||||
signingPubKey, signingPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("generating ed25519 key: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
signingPrivKeyPEM := cert.MarshalEd25519PrivateKey(signingPrivKey)
|
|
||||||
signingPubKeyPEM := cert.MarshalEd25519PublicKey(signingPubKey)
|
|
||||||
|
|
||||||
var hostPub, hostKey []byte
|
var hostPub, hostKey []byte
|
||||||
{
|
{
|
||||||
var pubkey, privkey [32]byte
|
var pubkey, privkey [32]byte
|
||||||
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
|
if _, err = io.ReadFull(rand.Reader, privkey[:]); err != nil {
|
||||||
return HostCredentials{}, fmt.Errorf("reading random bytes to form private key: %w", err)
|
err = fmt.Errorf("reading random bytes to form private key: %w", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
curve25519.ScalarBaseMult(&pubkey, &privkey)
|
curve25519.ScalarBaseMult(&pubkey, &privkey)
|
||||||
hostPub, hostKey = pubkey[:], privkey[:]
|
hostPub, hostKey = pubkey[:], privkey[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingPubKey, signingPrivKey := GenerateSigningPair()
|
||||||
|
|
||||||
hostPubPEM := cert.MarshalX25519PublicKey(hostPub)
|
hostPubPEM := cert.MarshalX25519PublicKey(hostPub)
|
||||||
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
||||||
|
|
||||||
hostCertPEM, err := NewHostCertPEM(caCreds, string(hostPubPEM), hostName, ip)
|
hostCertPEM, err := NewHostCertPEM(caCreds, string(hostPubPEM), hostName, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return HostCredentials{}, fmt.Errorf("creating host certificate: %w", err)
|
err = fmt.Errorf("creating host certificate: %w", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return HostCredentials{
|
pub = HostPublicCredentials{
|
||||||
Public: HostPublicCredentials{
|
CertPEM: hostCertPEM,
|
||||||
CertPEM: hostCertPEM,
|
SigningKey: signingPubKey,
|
||||||
SigningKeyPEM: string(signingPubKeyPEM),
|
}
|
||||||
},
|
|
||||||
PrivateKeyPEM: string(hostKeyPEM),
|
priv = HostPrivateCredentials{
|
||||||
SigningPrivateKeyPEM: string(signingPrivKeyPEM),
|
PrivateKeyPEM: string(hostKeyPEM),
|
||||||
}, nil
|
SigningPrivateKey: signingPrivKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCACredentials generates a CACredentials. The domain should be the network's root domain,
|
// NewCACredentials generates a CACredentials. The domain should be the network's root domain,
|
||||||
@ -170,10 +155,7 @@ func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
|
|||||||
// The logic here is largely based on
|
// The logic here is largely based on
|
||||||
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/ca.go
|
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/ca.go
|
||||||
|
|
||||||
signingPubKey, signingPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
signingPubKey, signingPrivKey := GenerateSigningPair()
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("generating ed25519 key: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
expireAt := now.Add(2 * 365 * 24 * time.Hour)
|
expireAt := now.Add(2 * 365 * 24 * time.Hour)
|
||||||
@ -189,13 +171,10 @@ func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := caCert.Sign(signingPrivKey); err != nil {
|
if err := signCert(&caCert, signingPrivKey); err != nil {
|
||||||
return CACredentials{}, fmt.Errorf("signing caCert: %w", err)
|
return CACredentials{}, fmt.Errorf("signing caCert: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
signingPrivKeyPEM := cert.MarshalEd25519PrivateKey(signingPrivKey)
|
|
||||||
signingPubKeyPEM := cert.MarshalEd25519PublicKey(signingPubKey)
|
|
||||||
|
|
||||||
certPEM, err := caCert.MarshalToPEM()
|
certPEM, err := caCert.MarshalToPEM()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err)
|
return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err)
|
||||||
@ -203,10 +182,10 @@ func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
|
|||||||
|
|
||||||
return CACredentials{
|
return CACredentials{
|
||||||
Public: CAPublicCredentials{
|
Public: CAPublicCredentials{
|
||||||
CertPEM: string(certPEM),
|
CertPEM: string(certPEM),
|
||||||
SigningKeyPEM: string(signingPubKeyPEM),
|
SigningKey: signingPubKey,
|
||||||
},
|
},
|
||||||
SigningPrivateKeyPEM: string(signingPrivKeyPEM),
|
SigningPrivateKey: signingPrivKey,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,76 +205,3 @@ func IPFromHostCertPEM(hostCertPEM string) (net.IP, error) {
|
|||||||
|
|
||||||
return ips[0].IP, nil
|
return ips[0].IP, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignAndWrap signs the given bytes using the host key, and writes an
|
|
||||||
// encoded, versioned structure containing the signature and the given bytes.
|
|
||||||
func SignAndWrap(into io.Writer, signingKeyPEM string, b []byte) error {
|
|
||||||
|
|
||||||
key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(signingKeyPEM))
|
|
||||||
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: "NEBULA ED25519 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 input 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(signingPubKeyPEM string, b, sig []byte) error {
|
|
||||||
|
|
||||||
pubKey, _, err := cert.UnmarshalEd25519PublicKey([]byte(signingPubKeyPEM))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unmarshaling certificate as PEM: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ed25519.Verify(pubKey, b, sig) {
|
|
||||||
return ErrInvalidSignature
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ip net.IP
|
ip net.IP
|
||||||
ipNet *net.IPNet
|
ipNet *net.IPNet
|
||||||
caCredsA, caCredsB CACredentials
|
caCredsA, caCredsB CACredentials
|
||||||
hostCredsA, hostCredsB HostCredentials
|
hostPubCredsA, hostPubCredsB HostPublicCredentials
|
||||||
|
hostPrivCredsA, hostPrivCredsB HostPrivateCredentials
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -32,40 +30,14 @@ func init() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hostCredsA, err = NewHostCredentials(caCredsA, "foo", ip)
|
hostPubCredsA, hostPrivCredsA, err = NewHostCredentials(caCredsA, "foo", ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hostCredsB, err = NewHostCredentials(caCredsB, "bar", ip)
|
hostPubCredsB, hostPrivCredsB, err = NewHostCredentials(caCredsB, "bar", ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSignAndWrap(t *testing.T) {
|
|
||||||
|
|
||||||
b := []byte("foo bar baz")
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
|
|
||||||
if err := SignAndWrap(buf, hostCredsA.SigningPrivateKeyPEM, 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(hostCredsA.Public.SigningKeyPEM, b, gotSig); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ValidateSignature(hostCredsB.Public.SigningKeyPEM, b, gotSig); !errors.Is(err, ErrInvalidSignature) {
|
|
||||||
t.Fatalf("expected ErrInvalidSignature but got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
103
go/nebula/signed.go
Normal file
103
go/nebula/signed.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInvalidSignature is returned from functions when a signature validation
|
||||||
|
// fails.
|
||||||
|
var ErrInvalidSignature = errors.New("invalid signature")
|
||||||
|
|
||||||
|
func signCert(c *cert.NebulaCertificate, k SigningPrivateKey) error {
|
||||||
|
return c.Sign(ed25519.PrivateKey(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signed wraps an arbitrary value with a signature which was generated using a
|
||||||
|
// SigningPrivateKey. It can be JSON (un)marshaled while preserving all of its
|
||||||
|
// properties.
|
||||||
|
type Signed[T any] json.RawMessage
|
||||||
|
|
||||||
|
type signed[T any] struct {
|
||||||
|
Signature []byte
|
||||||
|
Body json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign will generate a Signed of the given value by first JSON marshaling it,
|
||||||
|
// and then signing the resulting bytes.
|
||||||
|
func Sign[T any](v T, k SigningPrivateKey) (Signed[T], error) {
|
||||||
|
var res Signed[T]
|
||||||
|
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("json marshaling: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := ed25519.PrivateKey(k).Sign(rand.Reader, b, crypto.Hash(0))
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("generating signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(signed[T]{Signature: sig, Body: json.RawMessage(b)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (s Signed[T]) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||||
|
func (s *Signed[T]) UnmarshalJSON(b []byte) error {
|
||||||
|
*s = b
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap produces the original value which Sign was called on, or returns
|
||||||
|
// ErrInvalidSignature if the signature is not valid for the value and public
|
||||||
|
// key.
|
||||||
|
func (s Signed[T]) Unwrap(pubK SigningPublicKey) (T, error) {
|
||||||
|
var (
|
||||||
|
res T
|
||||||
|
into signed[T]
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(s, &into); err != nil {
|
||||||
|
return res, fmt.Errorf("json unmarshaling outer Signed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ed25519.Verify(
|
||||||
|
ed25519.PublicKey(pubK), []byte(into.Body), into.Signature,
|
||||||
|
) {
|
||||||
|
return res, ErrInvalidSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(into.Body, &res); err != nil {
|
||||||
|
return res, fmt.Errorf("json unmarshaling: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnwrapUnsafe is like Unwrap, but it will not check the signature.
|
||||||
|
func (s Signed[T]) UnwrapUnsafe() (T, error) {
|
||||||
|
var (
|
||||||
|
res T
|
||||||
|
into signed[T]
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(s, &into); err != nil {
|
||||||
|
return res, fmt.Errorf("json unmarshaling outer Signed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(into.Body, &res); err != nil {
|
||||||
|
return res, fmt.Errorf("json unmarshaling: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
48
go/nebula/signed_test.go
Normal file
48
go/nebula/signed_test.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSigned(t *testing.T) {
|
||||||
|
type msg struct {
|
||||||
|
A int
|
||||||
|
B string
|
||||||
|
C bool
|
||||||
|
}
|
||||||
|
|
||||||
|
a := msg{1, "FOO", true}
|
||||||
|
|
||||||
|
signedA, err := Sign(a, hostPrivCredsA.SigningPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signedJSON, err := json.Marshal(signedA)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(string(signedJSON))
|
||||||
|
|
||||||
|
var signedB Signed[msg]
|
||||||
|
if err := json.Unmarshal(signedJSON, &signedB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = signedB.Unwrap(hostPubCredsB.SigningKey)
|
||||||
|
if !errors.Is(err, ErrInvalidSignature) {
|
||||||
|
t.Fatalf("expected ErrInvalidSignature but got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := signedB.Unwrap(hostPubCredsA.SigningKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a != b {
|
||||||
|
t.Fatalf("expected:%+v, got:%+v", a, b)
|
||||||
|
}
|
||||||
|
}
|
@ -12,5 +12,5 @@ source "$UTILS"/with-1-data-1-empty-node-cluster.sh
|
|||||||
bootstrap_file="$XDG_DATA_HOME/isle/bootstrap.json"
|
bootstrap_file="$XDG_DATA_HOME/isle/bootstrap.json"
|
||||||
|
|
||||||
[ "$(jq -rc <"$bootstrap_file" '.AdminCreationParams')" = "$(jq -rc <admin.json '.CreationParams')" ]
|
[ "$(jq -rc <"$bootstrap_file" '.AdminCreationParams')" = "$(jq -rc <admin.json '.CreationParams')" ]
|
||||||
[ "$(jq -rc <"$bootstrap_file" '.Nebula.CAPublicCredentials')" = "$(jq -rc <admin.json '.Nebula.CACredentials.Public')" ]
|
[ "$(jq -rc <"$bootstrap_file" '.CAPublicCredentials')" = "$(jq -rc <admin.json '.Nebula.CACredentials.Public')" ]
|
||||||
[ "$(jq -r <"$bootstrap_file" '.HostName')" = "primus" ]
|
[ "$(jq -r <"$bootstrap_file" '.SignedHostAssigned.Body.Name')" = "primus" ]
|
||||||
|
@ -5,10 +5,10 @@ adminBS="$XDG_DATA_HOME"/isle/bootstrap.json
|
|||||||
bs="$secondus_bootstrap" # set in with-1-data-1-empty-node-cluster.sh
|
bs="$secondus_bootstrap" # set in with-1-data-1-empty-node-cluster.sh
|
||||||
|
|
||||||
[ "$(jq -r <"$bs" '.AdminCreationParams')" = "$(jq -r <admin.json '.CreationParams')" ]
|
[ "$(jq -r <"$bs" '.AdminCreationParams')" = "$(jq -r <admin.json '.CreationParams')" ]
|
||||||
[ "$(jq -r <"$bs" '.HostName')" = "secondus" ]
|
[ "$(jq -r <"$bs" '.SignedHostAssigned.Body.Name')" = "secondus" ]
|
||||||
|
|
||||||
[ "$(jq -r <"$bs" '.Hosts.primus.Nebula.SignedPublicCredentials')" \
|
[ "$(jq -r <"$bs" '.Hosts.primus.PublicCredentials')" \
|
||||||
= "$(jq -r <"$adminBS" '.Nebula.SignedPublicCredentials')" ]
|
= "$(jq -r <"$adminBS" '.SignedHostAssigned.Body.PublicCredentials')" ]
|
||||||
|
|
||||||
[ "$(jq <"$bs" '.Hosts.primus.Garage.Instances|length')" = "3" ]
|
[ "$(jq <"$bs" '.Hosts.primus.Garage.Instances|length')" = "3" ]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user