# Ed25519 Security Use Cases This document describes how the Momswap Geo backend uses Ed25519 signatures for authentication and registration. ## Overview - **Identity** = Ed25519 public key (base64url, 43 chars, no padding) - **All keys** use RFC 8032 Ed25519; Go `crypto/ed25519` on server, `@noble/ed25519` in TypeScript client - **Signatures** are base64url-encoded (64 bytes → 86 chars unpadded) - **Private keys never leave the client**; server only verifies signatures ```mermaid flowchart LR userClient[UserClient] -->|"holds private key locally"| privateKey[PrivateKey] userClient -->|"shares public key"| backendApi[BackendAPI] backendApi -->|"registers identity by public key"| usersTable[(UsersByPublicKey)] ``` ## Use Cases ### 1. Login (Challenge–Response) Proves the client controls the private key for a registered public key. **Message to sign:** `login:` + nonce (exact UTF-8 string) ```mermaid sequenceDiagram participant C as Client participant A as API participant U as UsersStore C->>A: POST /v1/auth/challenge {publicKey} A-->>C: {nonce, messageToSign} C->>C: sign(messageToSign, privateKey) C->>A: POST /v1/auth/login {publicKey, nonce, signature} A->>U: load user by publicKey A->>A: verify signature with publicKey A-->>C: {accessToken} ``` **Flow:** 1. `POST /v1/auth/challenge` with `{"publicKey":""}` 2. Server returns `{"nonce":"...", "messageToSign":"login:"}` 3. Client signs `login:` with private key 4. `POST /v1/auth/login` with `{"publicKey","nonce","signature"}` 5. Server verifies and returns `{"accessToken":"..."}` **Example (curl):** ```bash # Step 1: Get challenge CHALLENGE=$(curl -s -X POST https://tenerife.baby/v1/auth/challenge \ -H "Content-Type: application/json" \ -d '{"publicKey":"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI"}') NONCE=$(echo "$CHALLENGE" | jq -r '.nonce') # Step 2: Sign "login:$NONCE" with your private key, then: curl -s -X POST https://tenerife.baby/v1/auth/login \ -H "Content-Type: application/json" \ -d "{\"publicKey\":\"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI\",\"nonce\":\"$NONCE\",\"signature\":\"\"}" ``` **Example (TypeScript):** ```ts const challenge = await client.createChallenge(publicKey); const signature = await signMessage(privateKey, challenge.messageToSign); const token = await client.loginWithSignature(publicKey, privateKey); ``` --- ### 2. Register by Signing Service Key Registers a new user without an invitation. User proves key ownership by signing the API's advertised public key. **Message to sign:** The service public key string (exact value from `GET /v1/service-key`) **Flow:** 1. `GET /v1/service-key` → `{"publicKey":""}` 2. Client signs that exact string with private key 3. `POST /v1/auth/register-by-signature` with `{"publicKey","signature"}` 4. Server verifies and registers the user **Requires:** `SERVICE_PUBLIC_KEY` or `ADMIN_PUBLIC_KEY` must be set. **Example (curl):** ```bash # Step 1: Fetch service key SERVICE_KEY=$(curl -s https://tenerife.baby/v1/service-key | jq -r '.publicKey') # Step 2: Sign $SERVICE_KEY with your private key, then: curl -s -X POST https://tenerife.baby/v1/auth/register-by-signature \ -H "Content-Type: application/json" \ -d "{\"publicKey\":\"\",\"signature\":\"\"}" ``` **Example (TypeScript):** ```ts await client.registerBySigningServiceKey(publicKey, privateKey); ``` --- ### 3. Create Invitation An authenticated user creates a signed invitation for new users. **Message to sign:** `invite:` + base64url(payload) Payload JSON: ```json { "jti": "unique-token-id", "inviterPublicKey": "", "inviteePublicKey": "", "expiresAtUnix": 1735689600, "maxUses": 1 } ``` **Flow:** 1. Build payload, base64url-encode it → `payloadB64` 2. Sign `invite:` + payloadB64 with inviter's private key 3. `POST /v1/invitations` (with Bearer token) with `{"invitePayloadB64","inviteSignature"}` **Example (TypeScript):** ```ts const payload = { jti, inviterPublicKey, expiresAtUnix, maxUses }; const payloadB64 = bytesToBase64Url(textToBytes(JSON.stringify(payload))); const inviteSig = await signMessage(inviterPrivateKey, `invite:${payloadB64}`); await client.createInvitation(payload, inviterPrivateKey); ``` --- ### 4. Register with Invitation New user registers using a valid invitation and proves key ownership. **Messages to sign:** 1. Invite: inviter signs `invite:` + payloadB64 (validated on create) 2. Proof: invitee signs `register:` + `` + `:` + `` ```mermaid flowchart LR inviter[InviterUser] -->|"sign invite payload"| invitePayload[InvitePayloadAndSignature] newUser[NewUser] -->|"submit public key + invite + proof signature"| registerEndpoint[RegisterEndpoint] registerEndpoint --> verifyInvite[VerifyInviteSignatureAndRules] registerEndpoint --> verifyProof[VerifyNewUserProofSignature] verifyInvite --> usersStore[(UsersStore)] verifyProof --> usersStore ``` **Flow:** 1. Obtain invitation (payloadB64, inviteSignature, jti) from inviter 2. Sign `register:` + yourPublicKey + `:` + jti with your private key 3. `POST /v1/auth/register` with `{"publicKey","invitePayloadB64","inviteSignature","proofSignature"}` **Example (TypeScript):** ```ts const proofSig = await signMessage(privateKey, `register:${publicKey}:${jti}`); await client.registerWithInvitation({ publicKey, privateKey, invitePayloadB64, inviteSignature, jti }); ``` --- ### 5. Secure Data Access (Collections & Features) Only an authenticated token linked to a verified public key can create and read collections/features for that owner. ```mermaid flowchart TD request[CreateOrReadRequest] --> bearerCheck[BearerTokenValidation] bearerCheck --> sessionStore[(Sessions)] sessionStore --> ownerKey[ResolvedOwnerPublicKey] ownerKey --> ownershipRule[OwnershipValidation] ownershipRule --> data[(CollectionsAndFeatures)] ownershipRule --> reject403[RejectForbidden] ``` --- ## Message Format Summary | Use Case | Message to Sign | |-----------------------|------------------------------------------------------| | Login | `login:` + nonce | | Register (service) | Service public key (exact string from API) | | Create invitation | `invite:` + base64url(payload) | | Register (invitation) | `register:` + publicKey + `:` + jti | ## Security Notes - **Replay protection:** Challenges are single-use and expire (5 min TTL). Invitations have expiry and max-use limits. - **Key binding:** Login challenge is bound to the public key that requested it. Invitation proof binds the new user's key to the JTI. - **Canonical encoding:** Use UTF-8 for all messages; base64url without padding for keys and signatures. - **Storage:** Client stores keys in `localStorage`; consider an encrypted wrapper for higher-security deployments. - **Private keys stay client-side.** Session tokens are granted only after valid Ed25519 signature verification. ## API Endpoints Reference | Method | Endpoint | Auth | Purpose | |--------|---------------------------------|------|----------------------------| | GET | /v1/service-key | No | Fetch service pubkey | | POST | /v1/auth/challenge | No | Get login nonce | | POST | /v1/auth/login | No | Exchange signature for JWT | | POST | /v1/auth/register-by-signature | No | Register (sign service key)| | POST | /v1/auth/register | No | Register with invitation | | POST | /v1/invitations | Yes | Create invitation |