Files
backend/docs/ed25519-security-use-cases.md
Andriy Oblivantsev efe5907adc
CI / test (push) Successful in 3s
Update docs and defaults for tenerife.baby domain.
This replaces old momswap.produktor.duckdns.org references with tenerife.baby and refreshes the TypeScript integration guide to reflect the current asset upload, sharing, and relative-link flow.

Made-with: Cursor
2026-03-02 21:31:21 +00:00

224 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (ChallengeResponse)
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":"<base64url>"}`
2. Server returns `{"nonce":"...", "messageToSign":"login:<nonce>"}`
3. Client signs `login:<nonce>` 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\":\"<your_signature_base64url>\"}"
```
**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":"<base64url>"}`
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\":\"<your_pubkey_base64url>\",\"signature\":\"<signature_base64url>\"}"
```
**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": "<inviter_base64url>",
"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:` + `<publicKey>` + `:` + `<jti>`
```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 |