bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
package main
|
||||
|
||||
// ops_seal.go — this machine's encryption key, and the credentials an action
|
||||
// op is handed sealed to it.
|
||||
//
|
||||
// An action op names the credentials its program needs (`credentials:
|
||||
// [registry]`), never their values. The host's vault seals each value to this
|
||||
// machine's X25519 key (NaCl sealed box, libsodium crypto_box_seal), bound to
|
||||
// the run: the sealed record carries the credential's name and the nonce of
|
||||
// the terms it rides with — an op_run's own nonce, or, for a run under a
|
||||
// standing grant (privilege user, so no verbs), the grant's, which any run of
|
||||
// that grant within its bounds may use. The bastion opens them at the run's start, hands
|
||||
// them to the program in its environment, scrubs them from its output, and
|
||||
// keeps them only on the run's own tmpfs, which is unmounted when the run ends
|
||||
// (and by the startup sweep if the bastion died mid-run). The pod relays
|
||||
// sealed bytes; only this machine can open them.
|
||||
//
|
||||
// The public key travels on every heartbeat, signed by this machine's own key
|
||||
// (owner.go), which is the key the person's device pins for receipts. The host
|
||||
// pins the encryption key the first time it sees it after anchoring, the same
|
||||
// trust-on-first-use the entity key has here, and a key that changes after
|
||||
// that is a different machine answering.
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
const (
|
||||
opEncKeyFile = "encryption_key.json"
|
||||
// opSealedMaxBytes bounds one sealed credential on the wire.
|
||||
opSealedMaxBytes = 16 << 10
|
||||
)
|
||||
|
||||
var opEncKeyMu sync.Mutex
|
||||
|
||||
type opEncKeyFileRec struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// opEncKey returns this machine's encryption key pair, creating it on first
|
||||
// use. It lives in the machine's record (masked from every op), 0600.
|
||||
func opEncKey() (*[32]byte, *[32]byte, error) {
|
||||
opEncKeyMu.Lock()
|
||||
defer opEncKeyMu.Unlock()
|
||||
d, err := configDir()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
p := filepath.Join(d, opEncKeyFile)
|
||||
if raw, err := os.ReadFile(p); err == nil {
|
||||
var f opEncKeyFileRec
|
||||
if json.Unmarshal(raw, &f) == nil {
|
||||
pub, e1 := base64.StdEncoding.DecodeString(f.PublicKey)
|
||||
priv, e2 := base64.StdEncoding.DecodeString(f.PrivateKey)
|
||||
if e1 == nil && e2 == nil && len(pub) == 32 && len(priv) == 32 {
|
||||
var a, b [32]byte
|
||||
copy(a[:], pub)
|
||||
copy(b[:], priv)
|
||||
return &a, &b, nil
|
||||
}
|
||||
}
|
||||
// A key file that does not read is never silently replaced: the host
|
||||
// pinned the key it held, and a new one would read as another machine.
|
||||
return nil, nil, fmt.Errorf("this machine's %s does not read; move it aside to make a new key (the host will see a new machine key)", opEncKeyFile)
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil, err
|
||||
}
|
||||
pub, priv, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
raw, _ := json.MarshalIndent(opEncKeyFileRec{
|
||||
PublicKey: base64.StdEncoding.EncodeToString(pub[:]),
|
||||
PrivateKey: base64.StdEncoding.EncodeToString(priv[:]),
|
||||
CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}, "", " ")
|
||||
if err := atomicWrite(p, raw); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_ = os.Chmod(p, 0o600)
|
||||
return pub, priv, nil
|
||||
}
|
||||
|
||||
// opEncKeySigningInput is what this machine's own key signs to vouch for its
|
||||
// encryption key, tagged like every other input that key signs (owner.go).
|
||||
func opEncKeySigningInput(pubB64 string) []byte {
|
||||
return []byte("encryption-key\n" + pubB64)
|
||||
}
|
||||
|
||||
// opEncKeyClaim is the heartbeat's statement of the encryption key: the key,
|
||||
// and this machine's signature over it. Nil when either key cannot be had.
|
||||
func opEncKeyClaim() map[string]any {
|
||||
pub, _, err := opEncKey()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
mpub, mpriv, err := loadOrCreateOwnerKey()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
b64 := base64.StdEncoding.EncodeToString(pub[:])
|
||||
return map[string]any{
|
||||
"public_key": b64,
|
||||
"machine_key": base64.StdEncoding.EncodeToString(mpub),
|
||||
"signature": base64.StdEncoding.EncodeToString(ed25519.Sign(mpriv, opEncKeySigningInput(b64))),
|
||||
}
|
||||
}
|
||||
|
||||
// opSealedCred is what the host seals: one credential, for one run.
|
||||
type opSealedCred struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Nonce string `json:"nonce"` // the terms' nonce: this run and no other
|
||||
Expires string `json:"expires"` // RFC3339; the run's terms bound it too
|
||||
}
|
||||
|
||||
// opOpenCreds opens the sealed credentials a run was handed and checks them
|
||||
// against the op: each is one the op declares, each is sealed for this run,
|
||||
// none is expired, none arrives twice, and every declared one is there.
|
||||
// dryRun: a dry run is handed none, and refuses any.
|
||||
func opOpenCreds(blobs []string, m *opManifest, nonce string, dryRun bool) (map[string]string, error) {
|
||||
if dryRun {
|
||||
if len(blobs) > 0 {
|
||||
return nil, errors.New("a dry run is never handed a credential")
|
||||
}
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
declared := map[string]bool{}
|
||||
for _, c := range m.Credentials {
|
||||
declared[c] = true
|
||||
}
|
||||
if len(blobs) > len(declared) {
|
||||
return nil, errors.New("the run was handed more credentials than the op declares")
|
||||
}
|
||||
pub, priv, err := opEncKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, b := range blobs {
|
||||
raw, err := base64.StdEncoding.DecodeString(b)
|
||||
if err != nil || len(raw) > opSealedMaxBytes {
|
||||
return nil, errors.New("a sealed credential is malformed")
|
||||
}
|
||||
plain, ok := box.OpenAnonymous(nil, raw, pub, priv)
|
||||
if !ok {
|
||||
return nil, errors.New("a sealed credential does not open with this machine's key")
|
||||
}
|
||||
var c opSealedCred
|
||||
if err := json.Unmarshal(plain, &c); err != nil {
|
||||
return nil, errors.New("a sealed credential does not parse")
|
||||
}
|
||||
if !declared[c.Name] {
|
||||
return nil, fmt.Errorf("the credential %q is not one this op declares", c.Name)
|
||||
}
|
||||
if c.Nonce != nonce {
|
||||
return nil, fmt.Errorf("the credential %q was sealed for another run", c.Name)
|
||||
}
|
||||
exp, err := time.Parse(time.RFC3339, c.Expires)
|
||||
if err != nil || !exp.After(time.Now()) {
|
||||
return nil, fmt.Errorf("the credential %q has expired", c.Name)
|
||||
}
|
||||
if _, dup := out[c.Name]; dup {
|
||||
return nil, fmt.Errorf("the credential %q arrived twice", c.Name)
|
||||
}
|
||||
if c.Value == "" {
|
||||
return nil, fmt.Errorf("the credential %q is empty", c.Name)
|
||||
}
|
||||
out[c.Name] = c.Value
|
||||
}
|
||||
for c := range declared {
|
||||
if _, ok := out[c]; !ok {
|
||||
return nil, fmt.Errorf("the op declares the credential %q and the run was not handed it", c)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// opSealCredFor seals a credential to a public key: the host's side, kept
|
||||
// here for the tests and for a person sealing by hand at the machine.
|
||||
func opSealCredFor(pub *[32]byte, c opSealedCred) (string, error) {
|
||||
raw, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err := box.SealAnonymous(nil, raw, pub, rand.Reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(out), nil
|
||||
}
|
||||
Reference in New Issue
Block a user