A node is known by a key it generated
The thing I had been calling blocked for weeks, built in an afternoon once it was pointed out that it was already decided. 08-connectivity says of the overlay keys: each node generates its own keypair, the private half never leaves the machine, the public half is published -- and says outright this IS ADR 0004's "a node holds its own identity". Nobody had applied it to node identity itself. identity now holds the public half of each node's key. Only the public half, which is the property worth having: a copy of this database is a list of who to believe, not a set of credentials, so compromise of a node really is compromise of only that node. Exactly one key is live per node, and re-enrolment revokes the one it replaced in the same transaction -- two live identities is the stolen-laptop case with the replaced machine still believed. Fault injection was worth the time here. Three findings. The unique index was defended by no test at all: sequential enrolment is already safe because the code revokes before inserting, so removing the constraint changed nothing. The constraint only matters when two enrolments race, and there is now a test that runs six at once and fails without it. My injection harness also lied to me. One injection matched nothing, changed no file, and reported NO BITE identically to a real one -- so a test that defends nothing and an injection that does nothing look the same. The harness now checksums the files and says NO-OP when they did not change. And one honest NO BITE left standing: making the key lookup return a zero key for an unknown node does not fail the test, because the signature check refuses it a line later. Two independent mechanisms, not a placebo. 61 tests, none skipped.
This commit is contained in:
@@ -150,3 +150,90 @@ func (i *Identity) Sign(ctx context.Context, message []byte) ([]byte, error) {
|
||||
func Verify(public ed25519.PublicKey, message, signature []byte) bool {
|
||||
return ed25519.Verify(public, message, signature)
|
||||
}
|
||||
|
||||
// NodeKey is the public half of a node's own keypair, as the mesh holds it.
|
||||
type NodeKey struct {
|
||||
ID string
|
||||
Node string
|
||||
Public ed25519.PublicKey
|
||||
Issued time.Time
|
||||
}
|
||||
|
||||
// ErrNotThisNode is what verification returns when a key is not the live one for a node.
|
||||
//
|
||||
// One error whether the key is unknown, revoked, or belongs to a different node. Whoever is
|
||||
// presenting a key that does not work is either a machine whose operator can be told out of band,
|
||||
// or something probing, and the second must not learn which.
|
||||
var ErrNotThisNode = errors.New("that key does not identify that node")
|
||||
|
||||
// RecordNodeKey writes down the public key the mesh will believe for a node.
|
||||
//
|
||||
// Any previous key for the node is revoked in the same transaction. Two live identities for one
|
||||
// node record is novox/hq ADR 0004's stolen-laptop case — the machine that was replaced going on
|
||||
// being believed — and the window between two statements is exactly when it would exist.
|
||||
func (i *Identity) RecordNodeKey(ctx context.Context, node string, public ed25519.PublicKey) (NodeKey, error) {
|
||||
if len(public) != ed25519.PublicKeySize {
|
||||
return NodeKey{}, fmt.Errorf(
|
||||
"a node key is %d bytes and this is %d: a node presents an Ed25519 public key",
|
||||
ed25519.PublicKeySize, len(public))
|
||||
}
|
||||
|
||||
tx, err := i.store.Pool().Begin(ctx)
|
||||
if err != nil {
|
||||
return NodeKey{}, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
|
||||
|
||||
if _, err := tx.Exec(ctx,
|
||||
`update node_key set revoked = now() where node = $1 and revoked is null`, node); err != nil {
|
||||
return NodeKey{}, err
|
||||
}
|
||||
|
||||
var k NodeKey
|
||||
var stored []byte
|
||||
err = tx.QueryRow(ctx,
|
||||
`insert into node_key (node, public) values ($1, $2) returning id, node, public, issued`,
|
||||
node, []byte(public)).Scan(&k.ID, &k.Node, &stored, &k.Issued)
|
||||
if err != nil {
|
||||
return NodeKey{}, err
|
||||
}
|
||||
k.Public = stored
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return NodeKey{}, err
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// LiveKey is the key currently identifying a node.
|
||||
func (i *Identity) LiveKey(ctx context.Context, node string) (NodeKey, error) {
|
||||
var k NodeKey
|
||||
var public []byte
|
||||
err := i.store.Pool().QueryRow(ctx,
|
||||
`select id, node, public, issued from node_key where node = $1 and revoked is null`,
|
||||
node).Scan(&k.ID, &k.Node, &public, &k.Issued)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return NodeKey{}, ErrNotThisNode
|
||||
}
|
||||
if err != nil {
|
||||
return NodeKey{}, err
|
||||
}
|
||||
k.Public = public
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// VerifyNode checks that something signed a challenge with the live key for a node.
|
||||
//
|
||||
// This is the whole of proving a node is that node, and it is the same operation the node performs
|
||||
// in the other direction on every declaration it receives. Nothing here is stored that could be
|
||||
// replayed: the mesh holds a public key, so a copy of this database proves nothing to anybody.
|
||||
func (i *Identity) VerifyNode(ctx context.Context, node string, challenge, signature []byte) error {
|
||||
key, err := i.LiveKey(ctx, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ed25519.Verify(key.Public, challenge, signature) {
|
||||
return ErrNotThisNode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user