- Register by signing service key: GET /v1/service-key, POST /v1/auth/register-by-signature - Login auto-attempts register first for new users - Web: default API URL momswap.produktor.duckdns.org, /libs/ static handler - Docker: webbuild stage for geo-api-client, copy web+libs to runtime - Bin scripts: test.sh, run.sh, up.sh, down.sh - docs/ed25519-security-use-cases.md: use cases, message formats, examples - SERVICE_PUBLIC_KEY env (defaults to ADMIN_PUBLIC_KEY) Made-with: Cursor
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
# Ed25519 Security Use Cases
|
||||
|
||||
This document describes how Ed25519 key pairs are used for identity, authentication, and secure access to feature collection data.
|
||||
This document describes how the Momswap Geo backend uses Ed25519 signatures for authentication and registration.
|
||||
|
||||
## 1) Identification by Public Key
|
||||
## Overview
|
||||
|
||||
Each user identity is represented by an Ed25519 public key. The backend never needs the private key.
|
||||
- **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
|
||||
@@ -13,9 +16,13 @@ flowchart LR
|
||||
backendApi -->|"registers identity by public key"| usersTable[(UsersByPublicKey)]
|
||||
```
|
||||
|
||||
## 2) Authentication via Challenge Signature
|
||||
## Use Cases
|
||||
|
||||
Authentication is challenge-response: client signs server nonce with private key; server verifies with stored public key.
|
||||
### 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
|
||||
@@ -32,36 +39,117 @@ sequenceDiagram
|
||||
A-->>C: {accessToken}
|
||||
```
|
||||
|
||||
## 3) Secure Data Storing (Write Feature Collection Data)
|
||||
**Flow:**
|
||||
|
||||
Only an authenticated token linked to a verified public key can create collections and features for that owner key.
|
||||
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":"..."}`
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
requestCreate[CreateCollectionOrFeatureRequest] --> bearerCheck[BearerTokenValidation]
|
||||
bearerCheck --> sessionStore[(Sessions)]
|
||||
sessionStore --> ownerKey[ResolvedOwnerPublicKey]
|
||||
ownerKey --> ownershipRule[OwnershipValidation]
|
||||
ownershipRule --> writeData[(CollectionsAndFeatures)]
|
||||
ownershipRule --> reject403[RejectForbidden]
|
||||
**Example (curl):**
|
||||
|
||||
```bash
|
||||
# Step 1: Get challenge
|
||||
CHALLENGE=$(curl -s -X POST https://momswap.produktor.duckdns.org/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://momswap.produktor.duckdns.org/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"publicKey\":\"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI\",\"nonce\":\"$NONCE\",\"signature\":\"<your_signature_base64url>\"}"
|
||||
```
|
||||
|
||||
## 4) Secure Data Receiving (Read Feature Collection Data)
|
||||
**Example (TypeScript):**
|
||||
|
||||
Reads are filtered by ownership. A token from one key cannot read another key's collections/features.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
requestRead[ReadCollectionsOrFeaturesRequest] --> validateToken[ValidateBearerToken]
|
||||
validateToken --> ownerKey[OwnerPublicKeyFromSession]
|
||||
ownerKey --> queryOwned[QueryOnlyOwnerRows]
|
||||
queryOwned --> responseData[ReturnOwnedCollectionsFeatures]
|
||||
ownerKey --> denyOther[RejectOtherOwnerAccess]
|
||||
```ts
|
||||
const challenge = await client.createChallenge(publicKey);
|
||||
const signature = await signMessage(privateKey, challenge.messageToSign);
|
||||
const token = await client.loginWithSignature(publicKey, privateKey);
|
||||
```
|
||||
|
||||
## 5) Invitation-Based Identity Expansion
|
||||
---
|
||||
|
||||
Existing users can issue signed invitations. New users prove ownership of their own private key at registration.
|
||||
### 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://momswap.produktor.duckdns.org/v1/service-key | jq -r '.publicKey')
|
||||
|
||||
# Step 2: Sign $SERVICE_KEY with your private key, then:
|
||||
curl -s -X POST https://momswap.produktor.duckdns.org/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
|
||||
@@ -73,9 +161,63 @@ flowchart LR
|
||||
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
|
||||
|
||||
- Private keys stay client-side.
|
||||
- Public keys are stable user identifiers.
|
||||
- Session tokens are granted only after valid Ed25519 signature verification.
|
||||
- Collection/feature access is enforced by owner key derived from session identity.
|
||||
- **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 |
|
||||
|
||||
Reference in New Issue
Block a user