diff --git a/go/delivery/compose_candidate_test.go b/go/delivery/compose_candidate_test.go index 196ac89..7c66dce 100644 --- a/go/delivery/compose_candidate_test.go +++ b/go/delivery/compose_candidate_test.go @@ -101,6 +101,13 @@ func TestComposeCandidateAndComposedPlanRoundTripAndRefuse(t *testing.T) { if SameComposed(planned, Composed{Node: "ace", Module: "netcheck", FP: fp, Resources: []json.RawMessage{}}) { t.Error("a plan change is not a new composition value") } + signed := strings.Replace(string(raw), `"resources"`, `"signature":{"signer":"x","sig":"y"},"resources"`, 1) + if c, err := DecodeComposed([]byte(signed)); err != nil || c.Node != "ace" { + t.Fatalf("a composition the ended controller signed does not decode: %v", err) + } + if strings.Contains(string(raw), "signature") { + t.Error("a composition is written with a signature") + } refused := Composed{Node: "ace", Module: "netcheck", FP: fp, Refused: "the setting domain has no value"} if raw, err = EncodeComposed(refused); err != nil { t.Fatal(err) diff --git a/go/delivery/composed_signature.go b/go/delivery/composed_signature.go deleted file mode 100644 index 0b4abd7..0000000 --- a/go/delivery/composed_signature.go +++ /dev/null @@ -1,100 +0,0 @@ -package delivery - -import ( - "crypto/ed25519" - "encoding/base64" - "fmt" -) - -// A composition's signature (novox/hq ADR 0539 clause 3): the signer of a composition signs it with its key (the ended -// controller signed those it wrote, the operator one made by hand). The node-engine trusts the signers it is given and -// applies nothing of a composition whose signature fails or whose signer is none of them. The signature is an -// ed25519 signature, as a declaration's is, of the composition's digest (ComposedDigestOf) after a context line, so a -// signature made for anything else never verifies here. -const composedSigningContext = "novox composition v1\n" - -// Signature is who signed a composition and the signature: Signer the signer's ed25519 public key and Sig the -// signature, both in standard base64. -type Signature struct { - Signer string `json:"signer"` - Sig string `json:"sig"` -} - -func (s Signature) check() error { - if key, err := base64.StdEncoding.DecodeString(s.Signer); err != nil || len(key) != ed25519.PublicKeySize { - return fmt.Errorf("delivery: a composition's signer %q is not an ed25519 public key in base64", s.Signer) - } - if sig, err := base64.StdEncoding.DecodeString(s.Sig); err != nil || len(sig) != ed25519.SignatureSize { - return fmt.Errorf("delivery: a composition's signature is not an ed25519 signature in base64") - } - return nil -} - -// SignComposed is c signed with key: its digest signed, and the key's public half named as the signer. A signature c -// already carries is replaced. -func SignComposed(c Composed, key ed25519.PrivateKey) (Composed, error) { - if len(key) != ed25519.PrivateKeySize { - return Composed{}, fmt.Errorf("delivery: the key to sign a composition with is not an ed25519 private key") - } - return SignComposedBy(c, key.Public().(ed25519.PublicKey), func(message []byte) ([]byte, error) { - return ed25519.Sign(key, message), nil - }) -} - -// SignComposedBy is SignComposed for a signer that keeps its private half to itself (the controller's identity store -// signs a message and hands back the signature): sign signs the message it is given with the key whose public half is -// signer. A signature that does not verify against signer is refused, so a composition is never written signed by a -// key other than the one it names. -func SignComposedBy(c Composed, signer ed25519.PublicKey, sign func(message []byte) ([]byte, error)) (Composed, error) { - if len(signer) != ed25519.PublicKeySize { - return Composed{}, fmt.Errorf("delivery: the signer of a composition is not an ed25519 public key") - } - c.Signature = nil - digest, err := ComposedDigestOf(c) - if err != nil { - return Composed{}, err - } - message := []byte(composedSigningContext + digest) - sig, err := sign(message) - if err != nil { - return Composed{}, err - } - if !ed25519.Verify(signer, message, sig) { - return Composed{}, fmt.Errorf("delivery: the signature made for the composition of %q on %s does not verify against its signer", c.Module, c.Node) - } - c.Signature = &Signature{Signer: base64.StdEncoding.EncodeToString(signer), Sig: base64.StdEncoding.EncodeToString(sig)} - return c, nil -} - -// VerifyComposed is c's digest when c is signed by one of the trusted keys and its signature verifies against its -// digest; otherwise it refuses, saying why, and nothing of c is to be applied: an unsigned composition, one whose -// signer is not a trusted key, and one changed after it was signed. -func VerifyComposed(c Composed, trusted []ed25519.PublicKey) (string, error) { - if c.Signature == nil { - return "", fmt.Errorf("delivery: the composition of %q on %s is unsigned, and only a signed one is applied", c.Module, c.Node) - } - if err := c.Signature.check(); err != nil { - return "", err - } - signer, _ := base64.StdEncoding.DecodeString(c.Signature.Signer) - var key ed25519.PublicKey - for _, t := range trusted { - if t.Equal(ed25519.PublicKey(signer)) { - key = t - } - } - if key == nil { - return "", fmt.Errorf("delivery: the composition of %q on %s is signed by %s, which is not a trusted signer (the vault's key or the operator's)", - c.Module, c.Node, c.Signature.Signer) - } - digest, err := ComposedDigestOf(c) - if err != nil { - return "", err - } - sig, _ := base64.StdEncoding.DecodeString(c.Signature.Sig) - if !ed25519.Verify(key, []byte(composedSigningContext+digest), sig) { - return "", fmt.Errorf("delivery: the composition of %q on %s does not match its signature: it was changed after it was signed, or signed for another", - c.Module, c.Node) - } - return digest, nil -} diff --git a/go/delivery/composed_signature_test.go b/go/delivery/composed_signature_test.go deleted file mode 100644 index 3a531d0..0000000 --- a/go/delivery/composed_signature_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package delivery - -import ( - "crypto/ed25519" - "crypto/rand" - "crypto/x509" - "encoding/json" - "encoding/pem" - "strings" - "testing" - "time" -) - -func aSigningKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) { - t.Helper() - pub, priv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - return pub, priv -} - -func aComposition() Composed { - return Composed{Node: "ace", Module: "m", FP: fp2of('a'), Resources: []json.RawMessage{ - json.RawMessage(`{"id":"m.f","type":"file","module":"m","path":"/etc/m","content":"x"}`)}} -} - -// 1943/AC5: a composition signed by the controller's key or by the operator's verifies against the two trusted keys, -// keeps its digest, and survives its encoding; one signed by another key, unsigned, or changed by one byte after it was -// signed does not verify, and nothing of it is to be applied. -func TestACompositionIsSignedByATrustedKey(t *testing.T) { - controllerPub, controller := aSigningKey(t) - operatorPub, operator := aSigningKey(t) - _, stranger := aSigningKey(t) - trusted := []ed25519.PublicKey{controllerPub, operatorPub} - c := aComposition() - want, _ := ComposedDigestOf(c) - - for name, key := range map[string]ed25519.PrivateKey{"controller": controller, "operator": operator} { - signed, err := SignComposed(c, key) - if err != nil { - t.Fatalf("%s: %v", name, err) - } - raw, err := EncodeComposed(signed) - if err != nil { - t.Fatal(err) - } - back, err := DecodeComposed(raw) - if err != nil { - t.Fatal(err) - } - digest, err := VerifyComposed(back, trusted) - if err != nil || digest != want { - t.Errorf("%s's signature: %q, %v; want the digest %s", name, digest, err, want) - } - if d, _ := ComposedDigestOf(back); d != want { - t.Errorf("%s's signature changed the digest: %s", name, d) - } - - changed := strings.Replace(string(raw), `"content":"x"`, `"content":"y"`, 1) - if changed == string(raw) { - t.Fatal("the test changed nothing") - } - if tampered, err := DecodeComposed([]byte(changed)); err != nil { - t.Fatal(err) - } else if _, err := VerifyComposed(tampered, trusted); err == nil || !strings.Contains(err.Error(), "signature") { - t.Errorf("%s: a composition changed by one byte verified: %v", name, err) - } - } - - byStranger, _ := SignComposed(c, stranger) - if _, err := VerifyComposed(byStranger, trusted); err == nil || !strings.Contains(err.Error(), "not a trusted") { - t.Errorf("a stranger's signature = %v, want refused as no trusted signer", err) - } - if _, err := VerifyComposed(c, trusted); err == nil || !strings.Contains(err.Error(), "unsigned") { - t.Errorf("an unsigned composition = %v, want refused", err) - } - signed, _ := SignComposed(c, controller) - forged := signed - forged.Signature = &Signature{Signer: signed.Signature.Signer, Sig: byStranger.Signature.Sig} - if _, err := VerifyComposed(forged, trusted); err == nil { - t.Errorf("a stranger's signature under the controller's name verified") - } - if _, err := EncodeComposed(Composed{Node: "ace", Module: "m", FP: fp2of('a'), Resources: c.Resources, - Signature: &Signature{Signer: "not base64!", Sig: "x"}}); err == nil { - t.Errorf("a signature naming no key was encoded") - } -} - -// 1943/AC5: a signer that keeps its private half signs through a function, and the composition verifies as one signed -// with the key; a function that signs with another key than the one named is refused. -func TestACompositionIsSignedByAKeptKey(t *testing.T) { - pub, priv := aSigningKey(t) - _, other := aSigningKey(t) - c := aComposition() - signed, err := SignComposedBy(c, pub, func(m []byte) ([]byte, error) { return ed25519.Sign(priv, m), nil }) - if err != nil { - t.Fatal(err) - } - if _, err := VerifyComposed(signed, []ed25519.PublicKey{pub}); err != nil { - t.Errorf("a composition signed through the function does not verify: %v", err) - } - if _, err := SignComposedBy(c, pub, func(m []byte) ([]byte, error) { return ed25519.Sign(other, m), nil }); err == nil { - t.Errorf("a signature by another key than the one named was accepted") - } - - // L3 s1 (ADR 0550 §2): a message the vault or the controller signed under a kind's context verifies against the - // signers a node trusts and names which; an unknown key and another kind's context are refused. - vaultPub, vault := aSigningKey(t) - controllerPub, controller := aSigningKey(t) - trusted := []Signer{{Name: "operator", Key: controllerPub}, {Name: SignerVault, Key: vaultPub}} - enrol, _ := SignContext(KindEnrolment) - certCtx, _ := SignContext(KindNodeCertificate) - msg := []byte("the reply") - for _, row := range []struct { - name string - key ed25519.PrivateKey - signed string - checked string - want string - }{ - {"the vault's key verifies", vault, enrol, enrol, SignerVault}, - {"the controller's key verifies", controller, enrol, enrol, "operator"}, - {"an unknown key is refused", other, enrol, enrol, ""}, - {"the wrong context is refused", vault, certCtx, enrol, ""}, - } { - got, err := VerifyBy(trusted, row.checked, msg, SignMessage(row.key, row.signed, msg)) - if row.want == "" && err == nil { - t.Errorf("%s: verified as %s", row.name, got.Name) - } - if row.want != "" && (err != nil || got.Name != row.want) { - t.Errorf("%s: %+v, %v", row.name, got.Name, err) - } - } - if _, err := SignContext("hand-over"); err == nil { - t.Error("a kind the vault does not sign has a context") - } - // The signers' key reads back only when its fingerprint is its key's, and what a node trusts names both holders. - k := NewSignerKey(vaultPub, time.Unix(0, 0)) - raw, _ := json.Marshal(k) - if back, err := DecodeSignerKey(raw); err != nil || back.Fingerprint != SignerFingerprint(vaultPub) { - t.Errorf("the signers' key: %+v, %v", back, err) - } - k.Fingerprint = SignerFingerprint(controllerPub) - if raw, _ := json.Marshal(k); true { - if _, err := DecodeSignerKey(raw); err == nil { - t.Error("a signers' key naming another fingerprint was taken") - } - } - if got := Trusts(trusted, nil); len(got) != 2 || !TrustsKey(got, SignerVault, SignerFingerprint(vaultPub)) || - !TrustsKey(got, "operator", SignerFingerprint(controllerPub)) { - t.Errorf("trusts: %v", got) - } - // A certificate the vault's authority signs for a node's name and serving key checks against a bundle holding that - // authority, and not for another name, another key, or against the controller's alone; a bundle given again adds no - // certificate twice. - authority := func(pub ed25519.PublicKey, priv ed25519.PrivateKey) string { - tpl := AuthorityTemplate(pub, time.Now().Add(-time.Hour)) - der, err := x509.CreateCertificate(nil, tpl, tpl, pub, priv) - if err != nil { - t.Fatal(err) - } - return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) - } - vaultCA, controllerCA := authority(vaultPub, vault), authority(controllerPub, controller) - parent, _ := ParseCertificate(vaultCA) - serving, _ := aSigningKey(t) - der, err := x509.CreateCertificate(nil, NodeCertificateTemplate("ace.internal", serving, time.Now().Add(-time.Hour)), parent, serving, vault) - if err != nil { - t.Fatal(err) - } - leaf := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) - bundle := AuthorityBundle(controllerCA, AuthorityBundle(controllerCA, vaultCA)) - if n := strings.Count(bundle, "BEGIN CERTIFICATE"); n != 2 { - t.Errorf("the bundle holds %d certificates, want 2", n) - } - now := time.Now() - if err := CheckNodeCertificate(leaf, []string{bundle}, "ace.internal", serving, now); err != nil { - t.Errorf("the vault's certificate does not check: %v", err) - } - for name, err := range map[string]error{ - "another name": CheckNodeCertificate(leaf, []string{vaultCA}, "g14.internal", serving, now), - "another key": CheckNodeCertificate(leaf, []string{vaultCA}, "ace.internal", controllerPub, now), - "the controller's alone": CheckNodeCertificate(leaf, []string{controllerCA}, "ace.internal", serving, now), - } { - if err == nil { - t.Errorf("%s: checked", name) - } - } -} diff --git a/go/delivery/composed_value.go b/go/delivery/composed_value.go index 787f88d..e3535f0 100644 --- a/go/delivery/composed_value.go +++ b/go/delivery/composed_value.go @@ -57,9 +57,6 @@ type Composed struct { // (ADR 0593 §6): a refused composition holds no resource, filled file, data or plan, and has no digest // (ComposedDigestOf refuses it); only on a module's key. The node keeps running what it ran of the module. Refused string - // Signature is the controller's or, made by hand, the operator's (ADR 0539 clause 3, SignComposed); it signs the - // digest and is never part of it. - Signature *Signature } // composedWire is a composition as it is written. @@ -74,7 +71,9 @@ type composedWire struct { Own *NodeOwn `json:"own,omitempty"` Plan *catalogue.Plan `json:"plan,omitempty"` Refused string `json:"refused,omitempty"` - Signature *Signature `json:"signature,omitempty"` + // Signature is read and dropped: a composition is unsigned (ADR 0593 §6), and a value the ended controller signed + // still decodes. Nothing writes it. + Signature json.RawMessage `json:"signature,omitempty"` } // canonicalJSON is raw with its object keys sorted and its spacing dropped, its numbers kept as written. @@ -165,11 +164,6 @@ func (c Composed) check() error { } } } - if c.Signature != nil { - if err := c.Signature.check(); err != nil { - return err - } - } return nil } @@ -185,7 +179,7 @@ func (c Composed) wire() (composedWire, error) { return composedWire{}, err } w := composedWire{V: Version, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data, Filled: c.Filled, - Plan: c.Plan, Refused: c.Refused, Signature: c.Signature, Resources: make([]json.RawMessage, len(c.Resources))} + Plan: c.Plan, Refused: c.Refused, Resources: make([]json.RawMessage, len(c.Resources))} for i, r := range c.Resources { canon, err := canonicalJSON(r) if err != nil { @@ -224,7 +218,7 @@ func DecodeComposed(data []byte) (Composed, error) { return Composed{}, fmt.Errorf("delivery: a composition does not fit its shape: %w", err) } c := Composed{V: w.V, Node: w.Node, Module: w.Module, FP: w.FP, Data: w.Data, Resources: w.Resources, - Filled: w.Filled, Plan: w.Plan, Refused: w.Refused, Signature: w.Signature} + Filled: w.Filled, Plan: w.Plan, Refused: w.Refused} if w.Own != nil { c.Own = *w.Own } @@ -234,15 +228,14 @@ func DecodeComposed(data []byte) (Composed, error) { return c, nil } -// ComposedDigestOf is a composition's digest, "sha256:", of its encoding without its signature and its plan. A want names it -// (ADR 0539 clause 1), the controller writes it beside the composition, and the signature signs it. +// ComposedDigestOf is a composition's digest, "sha256:", of its encoding without its plan. A want names it (ADR 0539 +// clause 1), and the node's engine writes it beside the composition (ADR 0593 §6). // // A refused composition has no digest: it is refused, naming the module (ADR 0593 §6). func ComposedDigestOf(c Composed) (string, error) { if c.Refused != "" { return "", fmt.Errorf("delivery: the composition of %s on %s is refused (%s) and has no digest", c.Module, c.Node, c.Refused) } - c.Signature = nil c.Plan = nil raw, err := EncodeComposed(c) if err != nil { @@ -252,8 +245,8 @@ func ComposedDigestOf(c Composed) (string, error) { return "sha256:" + hex.EncodeToString(sum[:]), nil } -// SameComposed says whether two compositions are the same value, their signatures included, so the controller writes -// only when they differ. +// SameComposed says whether two compositions are the same value, their plans included, so the engine writes only when +// they differ. func SameComposed(a, b Composed) bool { ra, errA := EncodeComposed(a) rb, errB := EncodeComposed(b)