package spindle
import (
"context"
"crypto/ed25519"
"crypto/rand"
"fmt"
"strings"
"testing"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
gossh "golang.org/x/crypto/ssh"
"tangled.org/core/api/org_tangled"
"tangled.org/core/spindle/config"
"tangled.org/core/spindle/db"
)
type debugKeyEnv struct {
*ensureEnv
}
func newDebugKeyEnv(t *testing.T, collaborators ...syntax.DID) *debugKeyEnv {
t.Helper()
env := newEnsureEnv(t)
env.serve("repo", collaborators...)
env.ensure(t)
dir := env.s.res.Directory().(*identity.MockDirectory)
for _, did := range append([]syntax.DID{testStranger}, collaborators...) {
dir.Insert(identity.Identity{
DID: did,
Services: map[string]identity.ServiceEndpoint{
"atproto_pds": {Type: "AtprotoPersonalDataServer", URL: env.knot.srv.URL},
},
})
}
return &debugKeyEnv{env}
}
const testStranger = syntax.DID("did:plc:stranger")
func newSSHKey(t *testing.T) gossh.PublicKey {
t.Helper()
pub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
key, err := gossh.NewPublicKey(pub)
if err != nil {
t.Fatal(err)
}
return key
}
func (e *debugKeyEnv) giveKey(who syntax.DID, rkey string, key gossh.PublicKey, usages ...string) {
e.knot.put(who, org_tangled.KeySshKeyNSID, rkey, org_tangled.KeySshKey{
LexiconTypeID: org_tangled.KeySshKeyNSID,
Name: rkey,
CreatedAt: "2026-01-01T00:00:00Z",
PublicKey: strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))) + " laptop",
Usages: usages,
})
}
func (e *debugKeyEnv) allowed(key gossh.PublicKey) bool {
return e.s.DebugKeyAllowed(context.Background(), testRepoDid, key)
}
func TestDebugKeyOfTheOwnerGetsIn(t *testing.T) {
e := newDebugKeyEnv(t)
key := newSSHKey(t)
e.giveKey(testOwner, "k1", key)
if !e.allowed(key) {
t.Fatal("the owner's key was refused")
}
}
func TestDebugKeyOfACollaboratorGetsIn(t *testing.T) {
e := newDebugKeyEnv(t, testBob)
key := newSSHKey(t)
e.giveKey(testBob, "k1", key)
if !e.allowed(key) {
t.Fatal("a collaborator's key was refused")
}
}
func TestDebugKeyOfAStrangerStaysOut(t *testing.T) {
e := newDebugKeyEnv(t, testBob)
key := newSSHKey(t)
e.giveKey(testStranger, "k1", key)
e.giveKey(testBob, "k1", newSSHKey(t))
if e.allowed(key) {
t.Fatal("a key of someone without push access got in")
}
}
func TestDebugKeyWhoseRecordIsGoneStaysOut(t *testing.T) {
e := newDebugKeyEnv(t)
key := newSSHKey(t)
e.giveKey(testOwner, "k1", key)
e.knot.erase(testOwner, org_tangled.KeySshKeyNSID, "k1")
if e.allowed(key) {
t.Fatal("a key whose record was deleted got in")
}
}
func TestDebugKeyOfABannedCollaboratorStaysOut(t *testing.T) {
e := newDebugKeyEnv(t, testBob)
key := newSSHKey(t)
e.giveKey(testBob, "k1", key)
if err := e.s.db.PutBan(db.BanEntry{SubjectDid: testBob}); err != nil {
t.Fatal(err)
}
if e.allowed(key) {
t.Fatal("a banned collaborator's key got in")
}
}
func TestDebugKeyOfAPausedOwnersRepoStaysOut(t *testing.T) {
e := newDebugKeyEnv(t)
key := newSSHKey(t)
e.giveKey(testOwner, "k1", key)
e.s.cfg.Server.Admission = config.AdmissionClosed
if e.allowed(key) {
t.Fatal("a key got in on a repo whose owner isn't admitted")
}
}
func TestDebugKeyIsRefusedWhenThePDSErrors(t *testing.T) {
e := newDebugKeyEnv(t, testBob)
key := newSSHKey(t)
e.giveKey(testOwner, "k1", key)
e.knot.failCollection = org_tangled.KeySshKeyNSID
if e.allowed(key) {
t.Fatal("a key got in while its holder's pds was failing")
}
}
func TestDebugKeyMatchesByTheMarshalledKeyNotTheComment(t *testing.T) {
e := newDebugKeyEnv(t)
key := newSSHKey(t)
e.giveKey(testOwner, "k1", key)
// a different comment and whitespace around the same key is still the same key
e.knot.put(testOwner, org_tangled.KeySshKeyNSID, "k1", org_tangled.KeySshKey{
Name: "k1",
CreatedAt: "2026-01-01T00:00:00Z",
PublicKey: " " + strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))) + " other comment \n",
})
if !e.allowed(key) {
t.Fatal("the same key under another comment was refused")
}
}
func TestDebugKeyThatOnlySignsIsNotAnAuthKey(t *testing.T) {
e := newDebugKeyEnv(t)
key := newSSHKey(t)
e.giveKey(testOwner, "k1", key, "sign")
if e.allowed(key) {
t.Fatal("a signing-only key got in")
}
}
func TestDebugKeysAreCachedPerDidForAMoment(t *testing.T) {
e := newDebugKeyEnv(t)
key := newSSHKey(t)
e.giveKey(testOwner, "k1", key)
if !e.allowed(key) {
t.Fatal("the owner's key was refused")
}
before := e.knot.count()
for range 5 {
if !e.allowed(key) {
t.Fatal("the owner's key was refused on a repeat")
}
}
if got := e.knot.count(); got != before {
t.Fatalf("%d pds requests for repeats inside the cache window, want none", got-before)
}
}
func TestDebugKeyLookupStopsAfterAFewHolders(t *testing.T) {
var collaborators []syntax.DID
for i := range 3 * debugKeyHolderCap {
collaborators = append(collaborators, syntax.DID(fmt.Sprintf("did:plc:collab%03d", i)))
}
e := newDebugKeyEnv(t, collaborators...)
before := e.knot.count()
if e.allowed(newSSHKey(t)) {
t.Fatal("a key nobody holds got in")
}
if got := e.knot.count() - before; got > debugKeyHolderCap {
t.Fatalf("%d pds requests for one check, want at most %d", got, debugKeyHolderCap)
}
}