Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions FEDERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,17 @@ By default, tootik uses `draft-cavage-http-signatures` when it signs outgoing re

## Application Actor

tootik creates a special user named `nobody`, which acts as an [Application Actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md). Its key is used to sign outgoing requests not initiated by a particular user.
tootik creates a special user named `actor`, which acts as an [Application Actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md). Its key is used to sign outgoing requests not initiated by a particular user.

There are multiple ways to "discover" this actor:

1. Using [WebFinger](https://www.rfc-editor.org/rfc/rfc7033), just like any other user:

```sh
curl https://example.org/.well-known/webfinger?resource=acct:nobody@example.org
curl https://example.org/.well-known/webfinger?resource=acct:actor@example.org
```

2. For compatibility with servers that allow discovery of the Application Actor, the domain is an alias of `nobody`:
2. For compatibility with servers that allow discovery of the Application Actor, the domain is an alias of `actor`:

```sh
curl https://example.org/.well-known/webfinger?resource=acct:[email protected]
Expand All @@ -85,15 +85,15 @@ curl -H "accept: application/activity+json" https://example.org
curl https://example.org/actor
```

5. The `links` array returned by `/.well-known/nodeinfo` links to `nobody`, as [FEP-2677](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md) requires
5. The `links` array returned by `/.well-known/nodeinfo` links to `actor`, as [FEP-2677](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md) requires

```sh
curl https://example.org/.well-known/nodeinfo
```

The `sharedInbox` of non-portable actors points to `nobody`'s inbox, to reduce the number of requests from servers that deduplicate outgoing requests by `sharedInbox` during wide delivery of posts.
`actor` advertises support for RFC9421 and Ed25519 using [FEP-844e](https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md), to encourage other servers to use these capabilities when talking to tootik.

`nobody` advertises support for RFC9421 and Ed25519 using [FEP-844e](https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md), to encourage other servers to use these capabilities when talking to tootik.
Before v0.21.0, tootik used to call this actor `nobody` and set the `sharedInbox` of non-portable actors to `nobody`'s inbox, to reduce the number of requests from servers that deduplicate outgoing requests by `sharedInbox` during wide delivery of posts.

## Forwarding

Expand Down Expand Up @@ -149,14 +149,14 @@ Support for data portability comes into play in 5 main areas:

## Registration

Since v0.21.0, tootik no longer offers choice between 'traditional' and portable actors: all newly registered users are portable actors.

A portable actor is created by generating or supplying a pre-generated, base58-encoded Ed25519 private key during registration. The key, like the user's `preferredUsername`, must be unique per tootik instance.

No matter if the key was generated by tootik or provided by the user, the user can recover it through the settings page.

tootik does not support the [FEP-ae97](https://codeberg.org/fediverse/fep/src/branch/main/fep/ae97/fep-ae97.md) registration flow.

Registration of portable actors is disabled by default (see `EnablePortableActorRegistration`).

## Discovery

Portable actors can be looked up normally, over [WebFinger](https://www.rfc-editor.org/rfc/rfc7033):
Expand Down
24 changes: 12 additions & 12 deletions cfg/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ import (
type Config struct {
DatabaseOptions string

RequireRegistration bool
RequireInvitation bool
MaxInvitationsPerUser *int
InvitationTimeout time.Duration
RegistrationInterval time.Duration
CertificateApprovalTimeout time.Duration
UserNameRegex string
CompiledUserNameRegex *regexp.Regexp `json:"-"`
ForbiddenUserNameRegex string
CompiledForbiddenUserNameRegex *regexp.Regexp `json:"-"`
EnablePortableActorRegistration bool
RequireRegistration bool
RequireInvitation bool
MaxInvitationsPerUser *int
InvitationTimeout time.Duration
RegistrationInterval time.Duration
CertificateApprovalTimeout time.Duration
UserNameRegex string
CompiledUserNameRegex *regexp.Regexp `json:"-"`
ForbiddenUserNameRegex string
CompiledForbiddenUserNameRegex *regexp.Regexp `json:"-"`
EnableNonPortableActorRegistration bool

MaxPostsLength int
MaxPostsPerDay int64
Expand Down Expand Up @@ -165,7 +165,7 @@ func (c *Config) FillDefaults() {
c.CompiledUserNameRegex = regexp.MustCompile(c.UserNameRegex)

if c.ForbiddenUserNameRegex == "" {
c.ForbiddenUserNameRegex = `^(root|localhost|ip6-.*|.*(admin|tootik).*)$`
c.ForbiddenUserNameRegex = `^(actor|nobody|root|localhost|ip6-.*|.*(admin|tootik).*)$`
}

c.CompiledForbiddenUserNameRegex = regexp.MustCompile(c.ForbiddenUserNameRegex)
Expand Down
4 changes: 2 additions & 2 deletions cluster/community_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestCluster_PostInCommunity(t *testing.T) {
cluster := NewCluster(t, "a.localdomain", "b.localdomain", "g.localdomain")
defer cluster.Stop()

if _, _, err := user.Create(t.Context(), "g.localdomain", cluster["g.localdomain"].DB, cluster["g.localdomain"].Config, "stuff", ap.Group, nil); err != nil {
if _, _, err := user.CreatePortable(t.Context(), "g.localdomain", cluster["g.localdomain"].DB, cluster["g.localdomain"].Config, "stuff", ap.Group, nil); err != nil {
t.Fatal("Failed to create community")
}

Expand Down Expand Up @@ -93,7 +93,7 @@ func TestCluster_ReplyInCommunity(t *testing.T) {
cluster := NewCluster(t, "a.localdomain", "b.localdomain", "g.localdomain")
defer cluster.Stop()

if _, _, err := user.Create(t.Context(), "g.localdomain", cluster["g.localdomain"].DB, cluster["g.localdomain"].Config, "stuff", ap.Group, nil); err != nil {
if _, _, err := user.CreatePortable(t.Context(), "g.localdomain", cluster["g.localdomain"].DB, cluster["g.localdomain"].Config, "stuff", ap.Group, nil); err != nil {
t.Fatal("Failed to create community")
}

Expand Down
12 changes: 4 additions & 8 deletions cluster/followers_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestCluster_FollowersSyncMissingRemoteFollow(t *testing.T) {
Config: cluster["a.localdomain"].Config,
DB: cluster["a.localdomain"].DB,
Resolver: cluster["a.localdomain"].Resolver,
Keys: cluster["a.localdomain"].NobodyKeys,
Keys: cluster["a.localdomain"].AppActorKeys,
Inbox: cluster["a.localdomain"].Inbox,
}
if _, err := syncer.ProcessBatch(t.Context()); err != nil {
Expand Down Expand Up @@ -128,7 +128,7 @@ func TestCluster_FollowersSyncMissingLocalFollow(t *testing.T) {
Config: cluster["a.localdomain"].Config,
DB: cluster["a.localdomain"].DB,
Resolver: cluster["a.localdomain"].Resolver,
Keys: cluster["a.localdomain"].NobodyKeys,
Keys: cluster["a.localdomain"].AppActorKeys,
Inbox: cluster["a.localdomain"].Inbox,
}
if _, err := syncer.ProcessBatch(t.Context()); err != nil {
Expand All @@ -148,8 +148,6 @@ func TestCluster_FollowersSyncMissingRemoteFollowPortableActor(t *testing.T) {
cluster := NewCluster(t, "a.localdomain", "b.localdomain")
defer cluster.Stop()

cluster["b.localdomain"].Config.EnablePortableActorRegistration = true

pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
Expand Down Expand Up @@ -198,7 +196,7 @@ func TestCluster_FollowersSyncMissingRemoteFollowPortableActor(t *testing.T) {
Config: cluster["a.localdomain"].Config,
DB: cluster["a.localdomain"].DB,
Resolver: cluster["a.localdomain"].Resolver,
Keys: cluster["a.localdomain"].NobodyKeys,
Keys: cluster["a.localdomain"].AppActorKeys,
Inbox: cluster["a.localdomain"].Inbox,
}
if _, err := syncer.ProcessBatch(t.Context()); err != nil {
Expand All @@ -218,8 +216,6 @@ func TestCluster_FollowersSyncMissingLocalFollowPortableActor(t *testing.T) {
cluster := NewCluster(t, "a.localdomain", "b.localdomain")
defer cluster.Stop()

cluster["b.localdomain"].Config.EnablePortableActorRegistration = true

pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
Expand Down Expand Up @@ -268,7 +264,7 @@ func TestCluster_FollowersSyncMissingLocalFollowPortableActor(t *testing.T) {
Config: cluster["a.localdomain"].Config,
DB: cluster["a.localdomain"].DB,
Resolver: cluster["a.localdomain"].Resolver,
Keys: cluster["a.localdomain"].NobodyKeys,
Keys: cluster["a.localdomain"].AppActorKeys,
Inbox: cluster["a.localdomain"].Inbox,
}
if _, err := syncer.ProcessBatch(t.Context()); err != nil {
Expand Down
38 changes: 28 additions & 10 deletions cluster/invitation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func TestServer_InvitationHappyFlow(t *testing.T) {
alice := s.Register(aliceKeypair).OK()

s.Config.RequireInvitation = true
s.Config.EnableNonPortableActorRegistration = true

bobCode := "70bc9fdf-74a4-41e5-973d-08ba3fd23d74"
carolCode := "ded3626c-ea4b-44cc-adf3-18510e7634e1"
Expand All @@ -38,7 +39,9 @@ func TestServer_InvitationHappyFlow(t *testing.T) {
FollowInput("➕ Generate", bobCode).
Contains(Line{Type: Text, Text: "Code: " + bobCode})

s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK()
accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode)
accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate")
s.HandleInput(bobKeypair, accept.Path, "n").Follow("😈 My profile").OK()

alice.
Follow("⚙️ Settings").
Expand All @@ -47,7 +50,9 @@ func TestServer_InvitationHappyFlow(t *testing.T) {
FollowInput("➕ Generate", carolCode).
Contains(Line{Type: Text, Text: "Code: " + carolCode})

s.HandleInput(carolKeypair, "/users/invitations/accept", carolCode).Follow("😈 My profile").OK()
accept = s.HandleInput(carolKeypair, "/users/invitations/accept", carolCode)
accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate")
s.HandleInput(carolKeypair, accept.Path, "generate").Follow("😈 My profile").OK()
}

func TestServer_WrongCode(t *testing.T) {
Expand All @@ -56,6 +61,7 @@ func TestServer_WrongCode(t *testing.T) {
alice := s.Register(aliceKeypair).OK()

s.Config.RequireInvitation = true
s.Config.EnableNonPortableActorRegistration = true

bobCode := "70bc9fdf-74a4-41e5-973d-08ba3fd23d74"
carolCode := "ded3626c-ea4b-44cc-adf3-18510e7634e1"
Expand All @@ -68,7 +74,9 @@ func TestServer_WrongCode(t *testing.T) {

s.HandleInput(bobKeypair, "/users/invitations/accept", carolCode).Error("40 Invalid invitation code")

s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK()
accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode)
accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate")
s.HandleInput(bobKeypair, accept.Path, "generate").Follow("😈 My profile").OK()
}

func TestServer_ExpiredCode(t *testing.T) {
Expand All @@ -92,7 +100,10 @@ func TestServer_ExpiredCode(t *testing.T) {
s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Error("40 Invalid invitation code")

s.Config.InvitationTimeout = time.Hour
s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK()

accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode)
accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate")
s.HandleInput(bobKeypair, accept.Path, "generate").Follow("😈 My profile").OK()

case <-t.Context().Done():
t.Fail()
Expand All @@ -114,7 +125,9 @@ func TestServer_CodeReuse(t *testing.T) {
FollowInput("➕ Generate", bobCode).
Contains(Line{Type: Text, Text: "Code: " + bobCode})

s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK()
accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode)
accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate")
s.HandleInput(bobKeypair, accept.Path, "generate").Follow("😈 My profile").OK()

s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Error("40 Invalid invitation code")
s.HandleInput(carolKeypair, "/users/invitations/accept", bobCode).Error("40 Invalid invitation code")
Expand All @@ -128,6 +141,7 @@ func TestServer_InvitationLimit(t *testing.T) {
s.Config.RequireInvitation = true
limit := 1
s.Config.MaxInvitationsPerUser = &limit
s.Config.EnableNonPortableActorRegistration = true

bobCode := "70bc9fdf-74a4-41e5-973d-08ba3fd23d74"
carolCode := "ded3626c-ea4b-44cc-adf3-18510e7634e1"
Expand All @@ -145,7 +159,9 @@ func TestServer_InvitationLimit(t *testing.T) {
alice.Goto("/users/invitations/generate").
Error("40 Reached the maximum number of invitations")

s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK()
accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode)
accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate")
s.HandleInput(bobKeypair, accept.Path, "n").Follow("😈 My profile").OK()

alice.
Follow("⚙️ Settings").
Expand All @@ -156,7 +172,9 @@ func TestServer_InvitationLimit(t *testing.T) {
NotContains(Line{Type: Link, Text: "➕ Generate", URL: "/users/invitations/generate"}).
Contains(Line{Type: Text, Text: "Reached the maximum number of invitations."})

s.HandleInput(carolKeypair, "/users/invitations/accept", carolCode).Follow("😈 My profile").OK()
accept = s.HandleInput(carolKeypair, "/users/invitations/accept", carolCode)
accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate")
s.HandleInput(carolKeypair, accept.Path, "generate").Follow("😈 My profile").OK()

limit = 3
alice.
Expand Down Expand Up @@ -251,9 +269,9 @@ func TestServer_InvitationCreateAcceptDelete(t *testing.T) {
t.Fatalf("Not found")
}

s.
HandleInput(bobKeypair, "/users/invitations/accept", code).
OK()
accept := s.HandleInput(bobKeypair, "/users/invitations/accept", code)
accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate")
s.HandleInput(bobKeypair, accept.Path, "generate").OK()

page.
Follow("➖ Revoke").
Expand Down
2 changes: 1 addition & 1 deletion cluster/move_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestCluster_MovedAccount(t *testing.T) {
Domain: "b.localdomain",
DB: cluster["b.localdomain"].DB,
Resolver: cluster["b.localdomain"].Resolver,
Keys: cluster["b.localdomain"].NobodyKeys,
Keys: cluster["b.localdomain"].AppActorKeys,
Inbox: cluster["b.localdomain"].Inbox,
}
if err := mover.Run(t.Context()); err != nil {
Expand Down
13 changes: 0 additions & 13 deletions cluster/portability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ func TestCluster_ReplyForwardingPortableActors(t *testing.T) {
cluster := NewCluster(t, "a.localdomain", "b.localdomain", "c.localdomain")
defer cluster.Stop()

cluster["a.localdomain"].Config.EnablePortableActorRegistration = true
cluster["b.localdomain"].Config.EnablePortableActorRegistration = true
cluster["c.localdomain"].Config.EnablePortableActorRegistration = true

alice := cluster["a.localdomain"].RegisterPortable(aliceKeypair).OK()
bob := cluster["b.localdomain"].RegisterPortable(bobKeypair).OK()
carol := cluster["c.localdomain"].RegisterPortable(carolKeypair).OK()
Expand Down Expand Up @@ -103,10 +99,6 @@ func TestCluster_Gateways(t *testing.T) {
cluster := NewCluster(t, "a.localdomain", "b.localdomain", "c.localdomain")
defer cluster.Stop()

cluster["a.localdomain"].Config.EnablePortableActorRegistration = true
cluster["b.localdomain"].Config.EnablePortableActorRegistration = true
cluster["c.localdomain"].Config.EnablePortableActorRegistration = true

pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
Expand Down Expand Up @@ -195,11 +187,9 @@ func TestCluster_ForwardedLegacyReply(t *testing.T) {
cluster := NewCluster(t, "a.localdomain", "b.localdomain", "c.localdomain")
defer cluster.Stop()

cluster["a.localdomain"].Config.EnablePortableActorRegistration = true
cluster["b.localdomain"].Config.RFC9421Threshold = 1
cluster["b.localdomain"].Config.Ed25519Threshold = 1
cluster["b.localdomain"].Config.DisableIntegrityProofs = true
cluster["c.localdomain"].Config.EnablePortableActorRegistration = true

alice := cluster["a.localdomain"].RegisterPortable(aliceKeypair).OK()
bob := cluster["b.localdomain"].Register(bobKeypair).OK()
Expand Down Expand Up @@ -234,9 +224,6 @@ func TestCluster_ClientSideSigning(t *testing.T) {
cluster := NewCluster(t, "a.localdomain", "b.localdomain", "c.localdomain")
defer cluster.Stop()

cluster["a.localdomain"].Config.EnablePortableActorRegistration = true
cluster["c.localdomain"].Config.EnablePortableActorRegistration = true

pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
Expand Down
Loading
Loading