From e64486b0e6ab573ee089aaaae7915406f8a5a745 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:42:41 +0200 Subject: [PATCH] Sign a composition through a signer that keeps its private half (issue 1943, S5) The controller's identity store signs a message and never hands its key out, as it signed declarations. --- go/delivery/composed_signature.go | 26 +++++++++++++++++++++++--- go/delivery/composed_signature_test.go | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/go/delivery/composed_signature.go b/go/delivery/composed_signature.go index df7219f..1860a5a 100644 --- a/go/delivery/composed_signature.go +++ b/go/delivery/composed_signature.go @@ -36,13 +36,33 @@ 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 } - sig := ed25519.Sign(key, []byte(composedSigningContext+digest)) - c.Signature = &Signature{Signer: base64.StdEncoding.EncodeToString(key.Public().(ed25519.PublicKey)), - Sig: base64.StdEncoding.EncodeToString(sig)} + 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 } diff --git a/go/delivery/composed_signature_test.go b/go/delivery/composed_signature_test.go index d0606c5..816e8a8 100644 --- a/go/delivery/composed_signature_test.go +++ b/go/delivery/composed_signature_test.go @@ -83,3 +83,21 @@ func TestACompositionIsSignedByATrustedKey(t *testing.T) { 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") + } +}