Ctrl K
ComunidadGuías…

internal/mtprotoedge/passkey_e2e_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package mtprotoedge

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net"
	"testing"
	"time"

	"go.uber.org/zap/zaptest"

	"github.com/gotd/log/logzap"
	"github.com/iamxvbaba/td/clock"
	"github.com/iamxvbaba/td/exchange"
	"github.com/iamxvbaba/td/session"
	"github.com/iamxvbaba/td/telegram"
	"github.com/iamxvbaba/td/telegram/dcs"
	"github.com/iamxvbaba/td/tg"
	"github.com/iamxvbaba/td/transport"

	"telesrv/internal/app/account"
	"telesrv/internal/app/auth"
	"telesrv/internal/app/contacts"
	"telesrv/internal/app/dialogs"
	"telesrv/internal/app/help"
	passkeyapp "telesrv/internal/app/passkey"
	"telesrv/internal/app/updates"
	"telesrv/internal/app/users"
	"telesrv/internal/rpc"
	"telesrv/internal/store/memory"
	"telesrv/internal/webauthn/webauthntest"
)

const passkeyTestOrigin = "android:apk-key-hash:e2e-test"

// publicKeyOptions extracts challenge / rpId / user.id from DataJSON (top-level publicKey).
type publicKeyOptions struct {
	PublicKey struct {
		Challenge string `json:"challenge"`
		RPID      string `json:"rpId"`
		RP        struct {
			ID string `json:"id"`
		} `json:"rp"`
		User struct {
			ID string `json:"id"`
		} `json:"user"`
	} `json:"publicKey"`
}

func parsePasskeyOptions(t *testing.T, data string) publicKeyOptions {
	t.Helper()
	var opts publicKeyOptions
	if err := json.Unmarshal([]byte(data), &opts); err != nil {
		t.Fatalf("parse passkey options %q: %v", data, err)
	}
	return opts
}

func decodeB64URL(t *testing.T, s string) []byte {
	t.Helper()
	b, err := base64.RawURLEncoding.DecodeString(s)
	if err != nil {
		if b, err = base64.URLEncoding.DecodeString(s); err != nil {
			t.Fatalf("base64url decode %q: %v", s, err)
		}
	}
	return b
}

// TestPasskeyEndToEnd verifies passkey end to end: device A (logged in) registers
// a passkey, device B (a brand-new auth key) logs in via
// initPasskeyLogin/finishPasskeyLogin using a software authenticator assertion,
// then getPasskeys/deletePasskey round-trips. The whole chain verifies a real
// signature (the software authenticator signs with a real private key).
func TestPasskeyEndToEnd(t *testing.T) {
	const (
		dc        = 2
		phone     = "+8613800139000"
		wantPhone = "8613800139000"
		code      = "12345"
		rpID      = "telesrv.test"
	)

	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("gen rsa: %v", err)
	}
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	tcpAddr := ln.Addr().(*net.TCPAddr)

	userStore := memory.NewUserStore()
	authKeyStore := memory.NewAuthKeyStore()
	helpStore := memory.NewHelpStore()
	dialogStore := memory.NewDialogStore()
	messageStore := memory.NewMessageStore(dialogStore)
	updateEventStore := memory.NewUpdateEventStore()
	passkeyService := passkeyapp.NewService(memory.NewPasskeyStore(), memory.NewPasskeyChallengeStore(), rpID, dc)

	deps := rpc.Deps{
		Auth: auth.NewService(userStore, memory.NewAuthorizationStore(), memory.NewCodeStore(), authKeyStore, memory.NewTempAuthKeyBindingStore(authKeyStore), code,
			auth.WithLoginMessages(messageStore, dialogStore),
			auth.WithLoginCodeDelivery(memory.NewLoginCodeDeliveryStore(messageStore, updateEventStore))),
		Account: account.NewService(memory.NewPasswordStore(), account.WithUsers(userStore)),
		Help:    help.NewService(helpStore, helpStore),
		Users:   users.NewService(userStore),
		Updates: updates.NewService(memory.NewUpdateStateStore(), updateEventStore),

		Contacts: contacts.NewService(memory.NewContactStore()),
		Dialogs:  dialogs.NewService(dialogStore),
		Passkey:  passkeyService,
	}
	router := rpc.New(rpc.Config{DC: dc, IP: tcpAddr.IP.String(), Port: tcpAddr.Port}, deps, zaptest.NewLogger(t), clock.System)
	srv := New(Options{Logger: zaptest.NewLogger(t), DC: dc, RSAKey: rsaKey, AuthKeys: authKeyStore, LayerRPC: router})

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	serveErr := make(chan error, 1)
	go func() { serveErr <- srv.Serve(ctx, ln) }()
	defer func() {
		cancel()
		<-serveErr
	}()

	newClient := func() *telegram.Client {
		return telegram.NewClient(1, "hash", telegram.Options{
			PublicKeys:     []exchange.PublicKey{{RSA: &rsaKey.PublicKey}},
			Resolver:       dcs.Plain(dcs.PlainOptions{Protocol: transport.Intermediate}),
			DCList:         dcs.List{Options: []tg.DCOption{{ID: dc, IPAddress: tcpAddr.IP.String(), Port: tcpAddr.Port, Static: true}}},
			Logger:         logzap.New(zaptest.NewLogger(t).Named("client")),
			SessionStorage: &session.StorageMemory{},
			UpdateHandler:  telegram.UpdateHandlerFunc(func(context.Context, tg.UpdatesClass) error { return nil }),
		})
	}

	authn, err := webauthntest.New()
	if err != nil {
		t.Fatalf("new authenticator: %v", err)
	}
	var userHandle string

	// Device A: register + register passkey + getPasskeys.
	deviceA := newClient()
	if err := deviceA.Run(ctx, func(ctx context.Context) error {
		raw := tg.NewClient(deviceA)
		sent, err := raw.AuthSendCode(ctx, &tg.AuthSendCodeRequest{PhoneNumber: phone, APIID: 1, APIHash: "hash", Settings: tg.CodeSettings{}})
		if err != nil {
			return err
		}
		hash := sent.(*tg.AuthSentCode).PhoneCodeHash
		if _, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{PhoneNumber: phone, PhoneCodeHash: hash, PhoneCode: code}); err != nil {
			return err
		}
		if _, err := raw.AuthSignUp(ctx, &tg.AuthSignUpRequest{PhoneNumber: phone, PhoneCodeHash: hash, FirstName: "Pass"}); err != nil {
			return err
		}

		// 1) Registration options → the software authenticator creates an attestation.
		regOpts, err := raw.AccountInitPasskeyRegistration(ctx)
		if err != nil {
			return err
		}
		opts := parsePasskeyOptions(t, regOpts.Options.Data)
		if opts.PublicKey.RP.ID != rpID {
			return fmt.Errorf("registration rp.id = %q, want %q", opts.PublicKey.RP.ID, rpID)
		}
		userHandle = string(decodeB64URL(t, opts.PublicKey.User.ID)) // "2:<userId>"
		challenge := decodeB64URL(t, opts.PublicKey.Challenge)
		clientData, attObj, err := authn.Register(rpID, passkeyTestOrigin, challenge)
		if err != nil {
			return err
		}
		credB64 := authn.CredentialIDB64()
		cred := tg.InputPasskeyCredentialPublicKey{
			ID:    credB64,
			RawID: credB64,
			Response: &tg.InputPasskeyResponseRegister{
				ClientData:      tg.DataJSON{Data: string(clientData)},
				AttestationData: attObj,
			},
		}
		pk, err := raw.AccountRegisterPasskey(ctx, cred)
		if err != nil {
			return err
		}
		if pk.ID != credB64 {
			return fmt.Errorf("registered passkey id = %q, want %q", pk.ID, credB64)
		}

		// 2) getPasskeys should contain this passkey.
		list, err := raw.AccountGetPasskeys(ctx)
		if err != nil {
			return err
		}
		if len(list.Passkeys) != 1 || list.Passkeys[0].ID != credB64 {
			return fmt.Errorf("getPasskeys = %+v, want 1 passkey id=%q", list.Passkeys, credB64)
		}
		return nil
	}); err != nil {
		t.Fatalf("device A: %v", err)
	}

	// Device B: a brand-new auth key, logging in with the passkey.
	deviceB := newClient()
	if err := deviceB.Run(ctx, func(ctx context.Context) error {
		raw := tg.NewClient(deviceB)
		loginOpts, err := raw.AuthInitPasskeyLogin(ctx, &tg.AuthInitPasskeyLoginRequest{APIID: 1, APIHash: "hash"})
		if err != nil {
			return err
		}
		opts := parsePasskeyOptions(t, loginOpts.Options.Data)
		if opts.PublicKey.RPID != rpID {
			return fmt.Errorf("login rpId = %q, want %q", opts.PublicKey.RPID, rpID)
		}
		challenge := decodeB64URL(t, opts.PublicKey.Challenge)
		clientData, authData, sig, err := authn.Assert(rpID, passkeyTestOrigin, challenge)
		if err != nil {
			return err
		}
		credB64 := authn.CredentialIDB64()
		res, err := raw.AuthFinishPasskeyLogin(ctx, &tg.AuthFinishPasskeyLoginRequest{
			Credential: tg.InputPasskeyCredentialPublicKey{
				ID:    credB64,
				RawID: credB64,
				Response: &tg.InputPasskeyResponseLogin{
					ClientData:        tg.DataJSON{Data: string(clientData)},
					AuthenticatorData: authData,
					Signature:         sig,
					UserHandle:        userHandle,
				},
			},
		})
		if err != nil {
			return err
		}
		authz, ok := res.(*tg.AuthAuthorization)
		if !ok {
			return fmt.Errorf("finishPasskeyLogin result = %T, want *tg.AuthAuthorization", res)
		}
		self, ok := authz.User.(*tg.User)
		if !ok || !self.Self || self.Phone != wantPhone {
			return fmt.Errorf("passkey login user = %+v, want self phone=%s", authz.User, wantPhone)
		}

		// The second assertion (with an incremented counter) should still pass.
		loginOpts2, err := raw.AuthInitPasskeyLogin(ctx, &tg.AuthInitPasskeyLoginRequest{APIID: 1, APIHash: "hash"})
		if err != nil {
			return err
		}
		ch2 := decodeB64URL(t, parsePasskeyOptions(t, loginOpts2.Options.Data).PublicKey.Challenge)
		cd2, ad2, sig2, err := authn.Assert(rpID, passkeyTestOrigin, ch2)
		if err != nil {
			return err
		}
		if _, err := raw.AuthFinishPasskeyLogin(ctx, &tg.AuthFinishPasskeyLoginRequest{
			Credential: tg.InputPasskeyCredentialPublicKey{
				ID: credB64, RawID: credB64,
				Response: &tg.InputPasskeyResponseLogin{
					ClientData: tg.DataJSON{Data: string(cd2)}, AuthenticatorData: ad2, Signature: sig2, UserHandle: userHandle,
				},
			},
		}); err != nil {
			return fmt.Errorf("second passkey login: %w", err)
		}
		return nil
	}); err != nil {
		t.Fatalf("device B: %v", err)
	}

	// Device A connects again: delete the passkey → getPasskeys is empty.
	deviceC := newClient()
	if err := deviceC.Run(ctx, func(ctx context.Context) error {
		raw := tg.NewClient(deviceC)
		sent, err := raw.AuthSendCode(ctx, &tg.AuthSendCodeRequest{PhoneNumber: phone, APIID: 1, APIHash: "hash", Settings: tg.CodeSettings{}})
		if err != nil {
			return err
		}
		if _, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{PhoneNumber: phone, PhoneCodeHash: sent.(*tg.AuthSentCode).PhoneCodeHash, PhoneCode: code}); err != nil {
			return err
		}
		deleted, err := raw.AccountDeletePasskey(ctx, authn.CredentialIDB64())
		if err != nil {
			return err
		}
		if !deleted {
			return fmt.Errorf("deletePasskey returned false")
		}
		list, err := raw.AccountGetPasskeys(ctx)
		if err != nil {
			return err
		}
		if len(list.Passkeys) != 0 {
			return fmt.Errorf("getPasskeys after delete = %+v, want empty", list.Passkeys)
		}
		return nil
	}); err != nil {
		t.Fatalf("device C: %v", err)
	}
}