Skip to content

Commit

Permalink
Merge pull request #78 from ssb-ngi-pointer/aliases
Browse files Browse the repository at this point in the history
Aliases: database and web/muxrpc handlers
  • Loading branch information
cryptix authored Mar 16, 2021
2 parents 90634b0 + ca3ee62 commit 206a776
Show file tree
Hide file tree
Showing 50 changed files with 3,071 additions and 255 deletions.
56 changes: 56 additions & 0 deletions aliases/confirm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT

// Package aliases implements the validation and signing features of https://ssb-ngi-pointer.github.io/rooms2/#alias
package aliases

import (
"bytes"

"golang.org/x/crypto/ed25519"

refs "go.mindeco.de/ssb-refs"
)

// Registration ties an alias to the ID of the user and the RoomID it should be registered on
type Registration struct {
Alias string
UserID refs.FeedRef
RoomID refs.FeedRef
}

// Sign takes the public key (belonging to UserID) and returns the signed confirmation
func (r Registration) Sign(privKey ed25519.PrivateKey) Confirmation {
var conf Confirmation
conf.Registration = r
msg := r.createRegistrationMessage()
conf.Signature = ed25519.Sign(privKey, msg)
return conf
}

// createRegistrationMessage returns the string of bytes that should be signed
func (r Registration) createRegistrationMessage() []byte {
var message bytes.Buffer
message.WriteString("=room-alias-registration:")
message.WriteString(r.RoomID.Ref())
message.WriteString(":")
message.WriteString(r.UserID.Ref())
message.WriteString(":")
message.WriteString(r.Alias)
return message.Bytes()
}

// Confirmation combines a registration with the corresponding signature
type Confirmation struct {
Registration

Signature []byte
}

// Verify checks that the confirmation is for the expected room and from the expected feed
func (c Confirmation) Verify() bool {
// re-construct the registration
message := c.createRegistrationMessage()

// check the signature matches
return ed25519.Verify(c.UserID.PubKey(), message, c.Signature)
}
67 changes: 67 additions & 0 deletions aliases/confirm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT

package aliases

import (
"bytes"
"testing"

"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/keys"
"github.com/stretchr/testify/require"
refs "go.mindeco.de/ssb-refs"
)

func TestConfirmation(t *testing.T) {
r := require.New(t)

// this is our room, it's not a valid feed but thats fine for this test
roomID := refs.FeedRef{
ID: bytes.Repeat([]byte("test"), 8),
Algo: "test",
}

// to make the test deterministic, decided by fair dice roll.
seed := bytes.Repeat([]byte("yeah"), 8)
// our user, who will sign the registration
userKeyPair, err := keys.NewKeyPair(bytes.NewReader(seed))
r.NoError(err)

// create and fill out the registration for an alias (in this case the name of the test)
var valid Registration
valid.RoomID = roomID
valid.UserID = userKeyPair.Feed
valid.Alias = t.Name()

// internal function to create the registration string
msg := valid.createRegistrationMessage()
want := "=room-alias-registration:@dGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHRlc3Q=.test:@Rt2aJrtOqWXhBZ5/vlfzeWQ9Bj/z6iT8CMhlr2WWlG4=.ed25519:TestConfirmation"
r.Equal(want, string(msg))

// create the signed confirmation
confirmation := valid.Sign(userKeyPair.Pair.Secret)

yes := confirmation.Verify()
r.True(yes, "should be valid for this room and feed")

// make up another id for the invalid test(s)
otherID := refs.FeedRef{
ID: bytes.Repeat([]byte("nope"), 8),
Algo: "test",
}

confirmation.RoomID = otherID
yes = confirmation.Verify()
r.False(yes, "should not be valid for another room")

confirmation.RoomID = roomID // restore
confirmation.UserID = otherID
yes = confirmation.Verify()
r.False(yes, "should not be valid for this room but another feed")

// puncture the signature to emulate an invalid one
confirmation.Signature[0] = confirmation.Signature[0] ^ 1

yes = confirmation.Verify()
r.False(yes, "should not be valid anymore")

}
42 changes: 42 additions & 0 deletions aliases/names.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT

package aliases

import (
"fmt"
)

// IsValid decides whether an alias is okay for use or not.
// The room spec defines it as _labels valid under RFC 1035_ ( https://ssb-ngi-pointer.github.io/rooms2/#alias-string )
// but that can be mostly any string since DNS is a 8bit binary protocol,
// as long as it's shorter then 63 charachters.
//
// Right now it's pretty basic set of characters (a-z, A-Z, 0-9).
// In theory we could be more liberal but there is a bunch of stuff to figure out,
// like homograph attacks (https://en.wikipedia.org/wiki/IDN_homograph_attack),
// if we would decide to allow full utf8 unicode.
func IsValid(alias string) bool {
if len(alias) > 63 {
return false
}

var valid = true
for _, char := range alias {
if char >= '0' && char <= '9' { // is an ASCII number
continue
}

if char >= 'a' && char <= 'z' { // is an ASCII char between a and z
continue
}

if char >= 'A' && char <= 'Z' { // is an ASCII upper-case char between a and z
continue
}

fmt.Println("found", char)
valid = false
break
}
return valid
}
31 changes: 31 additions & 0 deletions aliases/names_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT

package aliases

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestIsValid(t *testing.T) {
a := assert.New(t)

cases := []struct {
alias string
valid bool
}{
{"basic", true},
{"no spaces", false},
{"no.dots", false},
{"#*!(! nope", false},

// too long
{"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", false},
}

for i, tc := range cases {
yes := IsValid(tc.alias)
a.Equal(tc.valid, yes, "wrong for %d: %s", i, tc.alias)
}
}
10 changes: 7 additions & 3 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (
"github.com/unrolled/secure"
"go.cryptoscope.co/muxrpc/v2/debug"

"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite"
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
mksrv "github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
"github.com/ssb-ngi-pointer/go-ssb-room/web/handlers"
Expand Down Expand Up @@ -201,7 +201,10 @@ func runroomsrv() error {
}

// create the shs+muxrpc server
roomsrv, err := mksrv.New(db.AllowList, opts...)
roomsrv, err := mksrv.New(
db.AllowList,
db.Aliases,
opts...)
if err != nil {
return fmt.Errorf("failed to instantiate ssb server: %w", err)
}
Expand Down Expand Up @@ -238,10 +241,11 @@ func runroomsrv() error {
Domain: httpsDomain,
PortHTTPS: uint(portHTTP),
PortMUXRPC: uint(portMUXRPC),
PubKey: roomsrv.Whoami().PubKey(),
RoomID: roomsrv.Whoami(),
},
roomsrv.StateManager,
handlers.Databases{
Aliases: db.Aliases,
AuthWithSSB: db.AuthWithSSB,
AuthFallback: db.AuthFallback,
AllowList: db.AllowList,
Expand Down
133 changes: 133 additions & 0 deletions muxrpc/handlers/alias/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: MIT

// Package alias implements the muxrpc handlers for alias needs.
package alias

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"

kitlog "github.com/go-kit/kit/log"
"go.cryptoscope.co/muxrpc/v2"

"github.com/ssb-ngi-pointer/go-ssb-room/aliases"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
refs "go.mindeco.de/ssb-refs"
)

// Handler implements the muxrpc methods for alias registration and recvocation
type Handler struct {
logger kitlog.Logger
self refs.FeedRef

db roomdb.AliasService
}

// New returns a fresh alias muxrpc handler
func New(log kitlog.Logger, self refs.FeedRef, aliasDB roomdb.AliasService) Handler {
var h Handler
h.self = self
h.logger = log
h.db = aliasDB

return h
}

const sigSuffix = ".sig.ed25519"

// Register is an async muxrpc method handler for registering aliases.
// It receives two string arguments over muxrpc (alias and signature),
// checks the signature confirmation is correct (for this room and signed by the key of theconnection)
// If it is valid, it registers the alias on the roomdb and returns true. If not it returns an error.
func (h Handler) Register(ctx context.Context, req *muxrpc.Request) (interface{}, error) {
var args []string

err := json.Unmarshal(req.RawArgs, &args)
if err != nil {
return nil, fmt.Errorf("registerAlias: bad request: %w", err)
}

if n := len(args); n != 2 {
return nil, fmt.Errorf("registerAlias: expected two arguments got %d", n)
}

if !strings.HasSuffix(args[1], sigSuffix) {
return nil, fmt.Errorf("registerAlias: signature does not have the expected suffix")
}

// remove the suffix of the base64 string
sig := strings.TrimSuffix(args[1], sigSuffix)

var confirmation aliases.Confirmation
confirmation.RoomID = h.self
confirmation.Alias = args[0]
confirmation.Signature, err = base64.StdEncoding.DecodeString(sig)
if err != nil {
return nil, fmt.Errorf("registerAlias: bad signature encoding: %w", err)
}

// check alias is valid
if !aliases.IsValid(confirmation.Alias) {
return nil, fmt.Errorf("registerAlias: invalid alias")
}

// get the user from the muxrpc connection
userID, err := network.GetFeedRefFromAddr(req.RemoteAddr())
if err != nil {
return nil, err
}

confirmation.UserID = *userID

// check the signature
if !confirmation.Verify() {
return nil, fmt.Errorf("registerAlias: invalid signature")
}

err = h.db.Register(ctx, confirmation.Alias, confirmation.UserID, confirmation.Signature)
if err != nil {
return nil, fmt.Errorf("registerAlias: could not register alias: %w", err)
}

return true, nil
}

// Revoke checks that the alias is from that user before revoking the alias from the database.
func (h Handler) Revoke(ctx context.Context, req *muxrpc.Request) (interface{}, error) {
var args []string

err := json.Unmarshal(req.RawArgs, &args)
if err != nil {
return nil, fmt.Errorf("registerAlias: bad request: %w", err)
}

if n := len(args); n != 1 {
return nil, fmt.Errorf("registerAlias: expected two arguments got %d", n)
}

// get the user from the muxrpc connection
userID, err := network.GetFeedRefFromAddr(req.RemoteAddr())
if err != nil {
return nil, err
}

alias, err := h.db.Resolve(ctx, args[0])
if err != nil {
return nil, err
}

if !alias.Feed.Equal(userID) {
return nil, fmt.Errorf("revokeAlias: not your alias (moderators need to use the web dashboard of the room")
}

err = h.db.Revoke(ctx, alias.Name)
if err != nil {
return nil, err
}

return true, nil
}
3 changes: 1 addition & 2 deletions muxrpc/handlers/tunnel/server/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ type connectWithOriginArg struct {
Origin refs.FeedRef `json:"origin"` // this should be clear from the shs session already
}

func (h *handler) connect(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
func (h *Handler) connect(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
// unpack arguments

var args []connectArg
err := json.Unmarshal(req.RawArgs, &args)
if err != nil {
Expand Down
Loading

0 comments on commit 206a776

Please sign in to comment.