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
7.7 KiB
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/ed25519on server,@noble/ed25519in TypeScript client - Signatures are base64url-encoded (64 bytes → 86 chars unpadded)
- Private keys never leave the client; server only verifies signatures
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)
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:
POST /v1/auth/challengewith{"publicKey":"<base64url>"}- Server returns
{"nonce":"...", "messageToSign":"login:<nonce>"} - Client signs
login:<nonce>with private key POST /v1/auth/loginwith{"publicKey","nonce","signature"}- Server verifies and returns
{"accessToken":"..."}
Example (curl):
# 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):
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:
GET /v1/service-key→{"publicKey":"<base64url>"}- Client signs that exact string with private key
POST /v1/auth/register-by-signaturewith{"publicKey","signature"}- Server verifies and registers the user
Requires: SERVICE_PUBLIC_KEY or ADMIN_PUBLIC_KEY must be set.
Example (curl):
# 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):
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:
{
"jti": "unique-token-id",
"inviterPublicKey": "<inviter_base64url>",
"inviteePublicKey": "",
"expiresAtUnix": 1735689600,
"maxUses": 1
}
Flow:
- Build payload, base64url-encode it →
payloadB64 - Sign
invite:+ payloadB64 with inviter's private key POST /v1/invitations(with Bearer token) with{"invitePayloadB64","inviteSignature"}
Example (TypeScript):
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:
- Invite: inviter signs
invite:+ payloadB64 (validated on create) - Proof: invitee signs
register:+<publicKey>+:+<jti>
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:
- Obtain invitation (payloadB64, inviteSignature, jti) from inviter
- Sign
register:+ yourPublicKey +:+ jti with your private key POST /v1/auth/registerwith{"publicKey","invitePayloadB64","inviteSignature","proofSignature"}
Example (TypeScript):
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.
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 |