package delivery import ( "crypto/ed25519" "encoding/base64" "fmt" ) // A composition's signature (novox/hq ADR 0539 clause 3): the controller signs each composition it writes with its // key, and the operator signs one made by hand with mesh-hand-compose with theirs. The node-engine trusts exactly these two // signers and applies nothing of a composition whose signature fails or whose signer is neither. 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 controller'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 }