A secret the mesh is given, not one it made
The last of the four gaps ADR 0024 names. Everything the mesh handles today it generated itself, sealed to both ends, and discarded. An API key for a hosted service comes from a person, and carrying it needs a verb the mesh did not have. Accept seals it on the way in and keeps no plaintext — the same storage and the same property as a generated one, only a different origin. That is the whole difference from the arrangement being replaced, where an operator-supplied key sits in a column the control plane can read, which makes a copy of the database a copy of every account the mesh touches. The consequence is deliberate: the mesh cannot show it back. Somebody who loses the key gets a new one from wherever it came from. There is no reveal and there cannot be one, because a mesh that can reveal a secret is a mesh that holds it — asserted as a test, because it is a property somebody will eventually ask to break. An empty value is refused. A credential that exists, authenticates nowhere and looks exactly like a working one is the failure this whole mechanism is arranged to prevent.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
@@ -75,6 +76,42 @@ func Make(consumerKey, providerKey string) (Sealed, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Accept seals a value somebody supplied, rather than one the mesh made.
|
||||
//
|
||||
// **The mesh generates most of what it hands out and discards the plaintext.** An API key for a
|
||||
// hosted service does not work that way: it comes from a person, and the mesh's job is to carry it
|
||||
// to the machines that need it without being able to read it afterwards
|
||||
// (novox/hq ADR 0024).
|
||||
//
|
||||
// So the value is sealed on the way in and **the plaintext is not kept**. That is the whole of the
|
||||
// difference from the arrangement this replaces, where an operator-supplied key sits in a column
|
||||
// the control plane can read — which makes a copy of the database a copy of every account the mesh
|
||||
// touches.
|
||||
//
|
||||
// The consequence is deliberate and worth stating: **the mesh cannot show it back.** Somebody who
|
||||
// loses the key gets a new one from wherever it came from; there is no "reveal" and there cannot
|
||||
// be one, because a mesh that can reveal a secret is a mesh that holds it.
|
||||
func Accept(value string, consumerKey, providerKey string) (Sealed, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return Sealed{}, fmt.Errorf("there is nothing to seal")
|
||||
}
|
||||
if consumerKey == "" || providerKey == "" {
|
||||
return Sealed{}, fmt.Errorf("both ends need a sealing key before a secret can be kept")
|
||||
}
|
||||
forConsumer, err := Seal(consumerKey, []byte(value))
|
||||
if err != nil {
|
||||
return Sealed{}, err
|
||||
}
|
||||
forProvider, err := Seal(providerKey, []byte(value))
|
||||
if err != nil {
|
||||
return Sealed{}, err
|
||||
}
|
||||
return Sealed{
|
||||
ForConsumer: forConsumer, ForProvider: forProvider,
|
||||
ConsumerKey: consumerKey, ProviderKey: providerKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Seal closes a value to a node's public sealing key.
|
||||
func Seal(publicKey string, value []byte) (string, error) {
|
||||
public, err := base64.StdEncoding.DecodeString(publicKey)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -154,3 +155,62 @@ func TestSomethingThatIsNotAKeyIsRefused(t *testing.T) {
|
||||
t.Fatal("a secret was sealed to a key of the wrong length")
|
||||
}
|
||||
}
|
||||
|
||||
func TestASecretSomebodySuppliedIsKeptTheSameWay(t *testing.T) {
|
||||
// An API key comes from a person; the mesh's job is to carry it without being able to read it
|
||||
// afterwards. Same storage, same property, different origin.
|
||||
consumerPub, openConsumer := nodeKey(t)
|
||||
providerPub, openProvider := nodeKey(t)
|
||||
|
||||
sealed, err := Accept("sk-a-real-looking-key", consumerPub, providerPub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := openConsumer(sealed.ForConsumer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "sk-a-real-looking-key" {
|
||||
t.Fatalf("the value did not survive: %q", got)
|
||||
}
|
||||
if _, err := openProvider(sealed.ForProvider); err != nil {
|
||||
t.Fatal("the other end cannot open its copy")
|
||||
}
|
||||
// And what is stored is not the value, which is the whole point.
|
||||
for what, blob := range map[string]string{
|
||||
"the consumer's copy": sealed.ForConsumer, "the provider's copy": sealed.ForProvider,
|
||||
} {
|
||||
if strings.Contains(blob, "sk-a-real-looking-key") {
|
||||
t.Fatalf("%s holds the key in the clear", what)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSealingNothingIsRefused(t *testing.T) {
|
||||
// An empty key sealed and stored would be a credential that exists, authenticates nowhere,
|
||||
// and looks exactly like a working one.
|
||||
public, _ := nodeKey(t)
|
||||
for _, value := range []string{"", " ", "\n"} {
|
||||
if _, err := Accept(value, public, public); err == nil {
|
||||
t.Fatalf("%q was accepted as a secret", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAcceptedSecretCannotBeReadBack(t *testing.T) {
|
||||
// Stated as a test because it is a property somebody will ask to break. There is no field
|
||||
// holding the value and no function returning it — a mesh that can reveal a secret is a mesh
|
||||
// that holds it.
|
||||
public, _ := nodeKey(t)
|
||||
sealed, err := Accept("sk-something", public, public)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
said, err := json.Marshal(sealed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(said), "sk-something") {
|
||||
t.Fatalf("what is kept carries the secret: %s", said)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user