package link_test import ( "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/hex" "errors" "testing" "time" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // A token another enrolment holds for the moment is "not now", not a refusal: the node asks again // with the same request, and nothing is spent (novox/hq issue 083). func TestAnEnrolmentMetByAHeldTokenIsAskedToTryAgain(t *testing.T) { inv := inventory.ForTest(t) ctx := t.Context() if _, err := inv.AddNode(ctx, "laptop"); err != nil { t.Fatal(err) } issued, err := inv.IssueToken(ctx, "laptop", time.Hour) if err != nil { t.Fatal(err) } if _, err := inv.Claim(ctx, issued.Secret, "another enrolment's key", false); err != nil { t.Fatal(err) } public, _, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } _, err = link.Enrolment{Inventory: inv}.Enrol(ctx, link.EnrolRequest{ Node: "laptop", Secret: issued.Secret, PublicKey: public}) if !errors.Is(err, link.ErrTryAgain) { t.Fatalf("an enrolment met by a held token was not asked to try again: %v", err) } // And a token that cannot be used at all is still refused outright. _, err = link.Enrolment{Inventory: inv}.Enrol(ctx, link.EnrolRequest{ Node: "laptop", Secret: "not-a-token", PublicKey: public}) if err == nil || errors.Is(err, link.ErrTryAgain) { t.Fatalf("a token that cannot be used was not refused outright: %v", err) } } // **A spent token cannot be replayed with a node's public key** (novox/hq issue 083, on review). A // public key is no secret: finishing an enrolment whose token that key spent takes proof the // presenter holds its private half, and a proof made with another key — or for another request — // is refused outright. func TestASpentTokenCannotBeReplayedWithAPublicKeyAlone(t *testing.T) { inv := inventory.ForTest(t) ctx := t.Context() if _, err := inv.AddNode(ctx, "laptop"); err != nil { t.Fatal(err) } issued, err := inv.IssueToken(ctx, "laptop", time.Hour) if err != nil { t.Fatal(err) } victim, victimPrivate, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } // The victim's enrolment spent the token. by := claimantOf(victim) if _, err := inv.Claim(ctx, issued.Secret, by, false); err != nil { t.Fatal(err) } if err := inv.Spend(ctx, issued.Secret, by); err != nil { t.Fatal(err) } // Someone with the leaked token and the victim's public key, and no proof. _, err = link.Enrolment{Inventory: inv}.Enrol(ctx, link.EnrolRequest{ Node: "laptop", Secret: issued.Secret, PublicKey: victim, SealingKey: "the attacker's"}) if err == nil || errors.Is(err, link.ErrTryAgain) { t.Fatalf("a spent token was taken again with a public key alone: %v", err) } // With a proof made by another key. _, forger, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } forged := ed25519.Sign(forger, link.EnrolProof(issued.Secret, victim, "", "the attacker's", "")) _, err = link.Enrolment{Inventory: inv}.Enrol(ctx, link.EnrolRequest{ Node: "laptop", Secret: issued.Secret, PublicKey: victim, SealingKey: "the attacker's", Proof: forged}) if err == nil { t.Fatal("a spent token was taken again with a proof made by another key") } // With the victim's own proof, but for another request — keys swapped in. theirs := ed25519.Sign(victimPrivate, link.EnrolProof(issued.Secret, victim, "", "the victim's", "")) _, err = link.Enrolment{Inventory: inv}.Enrol(ctx, link.EnrolRequest{ Node: "laptop", Secret: issued.Secret, PublicKey: victim, SealingKey: "the attacker's", Proof: theirs}) if err == nil { t.Fatal("a spent token was taken again with a proof made for another request") } // And the victim's own, proven request is not honoured when the broker redelivered it: the // first delivery may already have been answered. _, err = link.Enrolment{Inventory: inv}.Enrol(ctx, link.EnrolRequest{ Node: "laptop", Secret: issued.Secret, PublicKey: victim, SealingKey: "the victim's", Proof: theirs, Redelivered: true}) if err == nil { t.Fatal("a redelivered request finished an enrolment already spent") } } // claimantOf is the claimant the enrolment derives from a key, recomputed here. func claimantOf(public ed25519.PublicKey) string { sum := sha256.Sum256(public) return hex.EncodeToString(sum[:]) }