494 lines
12 KiB
Markdown
494 lines
12 KiB
Markdown
# Milestone 4: Authentication System
|
|
|
|
**Status**: Decided
|
|
**Goal**: Secure developer authentication and app signing infrastructure.
|
|
|
|
## Decision
|
|
|
|
**Custom JWT + OAuth2** with Go standard library crypto:
|
|
|
|
```
|
|
OAuth2: golang.org/x/oauth2 (GitHub, Google)
|
|
JWT: github.com/golang-jwt/jwt/v5
|
|
Signing: crypto/ed25519 (stdlib)
|
|
Password Hash: golang.org/x/crypto/argon2
|
|
API Key Hash: golang.org/x/crypto/bcrypt
|
|
```
|
|
|
|
### Rationale
|
|
|
|
1. **Go stdlib crypto** - Ed25519 built into Go, no external deps
|
|
2. **Simple JWT** - golang-jwt is battle-tested, minimal
|
|
3. **Stateless tokens** - No token store needed (SQLite handles refresh token revocation)
|
|
4. **OAuth-first** - GitHub OAuth for most developers, minimal password handling
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
Authentication covers two areas:
|
|
1. **Developer authentication** - Login to portal, API access
|
|
2. **App signing** - Package integrity and developer verification
|
|
|
|
---
|
|
|
|
## Developer Authentication
|
|
|
|
### Methods Required
|
|
|
|
| Method | Use Case | Priority |
|
|
|--------|----------|----------|
|
|
| OAuth2 (GitHub) | Primary login | P0 |
|
|
| OAuth2 (Google) | Alternative login | P1 |
|
|
| Email + Password | Fallback | P2 |
|
|
| API Keys | CLI tools, CI/CD | P0 |
|
|
|
|
### OAuth2 Flow
|
|
|
|
```
|
|
┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐
|
|
│ Browser │────►│ Portal │────►│ Provider │────►│ Callback│
|
|
└─────────┘ └─────────┘ │(GitHub) │ └────┬────┘
|
|
└──────────┘ │
|
|
▼
|
|
┌─────────────┐
|
|
│ Create/Link │
|
|
│ Account │
|
|
└─────────────┘
|
|
```
|
|
|
|
### OAuth2 Implementation
|
|
|
|
#### GitHub OAuth
|
|
|
|
```
|
|
Authorization URL: https://github.com/login/oauth/authorize
|
|
Token URL: https://github.com/login/oauth/access_token
|
|
User Info: https://api.github.com/user
|
|
Scopes: read:user, user:email
|
|
```
|
|
|
|
#### Google OAuth
|
|
|
|
```
|
|
Authorization URL: https://accounts.google.com/o/oauth2/v2/auth
|
|
Token URL: https://oauth2.googleapis.com/token
|
|
User Info: https://www.googleapis.com/oauth2/v2/userinfo
|
|
Scopes: openid, email, profile
|
|
```
|
|
|
|
### Session Management
|
|
|
|
#### JWT Tokens
|
|
|
|
```json
|
|
{
|
|
"sub": "dev_uuid",
|
|
"email": "dev@example.com",
|
|
"iat": 1704067200,
|
|
"exp": 1704153600,
|
|
"type": "access"
|
|
}
|
|
```
|
|
|
|
| Token Type | Lifetime | Storage |
|
|
|------------|----------|---------|
|
|
| Access Token | 1 hour | Memory/Cookie |
|
|
| Refresh Token | 30 days | HttpOnly Cookie |
|
|
|
|
#### Token Refresh Flow
|
|
|
|
```
|
|
1. Access token expires
|
|
2. Client sends refresh token
|
|
3. Server validates refresh token
|
|
4. Issue new access + refresh tokens
|
|
5. Invalidate old refresh token
|
|
```
|
|
|
|
### API Key Authentication
|
|
|
|
#### Key Format
|
|
|
|
```
|
|
mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
│ │ └── 32 random bytes (base62)
|
|
│ └── Environment (live/test)
|
|
└── Prefix (mosis key)
|
|
```
|
|
|
|
#### Key Storage
|
|
|
|
```sql
|
|
-- Only store hash, never the key itself
|
|
INSERT INTO api_keys (
|
|
developer_id,
|
|
name,
|
|
key_hash, -- bcrypt or argon2
|
|
key_prefix, -- "mk_live_abc" for display
|
|
permissions
|
|
) VALUES (...);
|
|
```
|
|
|
|
#### Key Permissions
|
|
|
|
```json
|
|
{
|
|
"permissions": [
|
|
"apps:read",
|
|
"apps:write",
|
|
"versions:upload",
|
|
"telemetry:read"
|
|
]
|
|
}
|
|
```
|
|
|
|
### Rate Limiting
|
|
|
|
| Endpoint Category | Limit | Window |
|
|
|-------------------|-------|--------|
|
|
| Auth endpoints | 10 | 1 minute |
|
|
| API (authenticated) | 1000 | 1 hour |
|
|
| API (per key) | 100 | 1 minute |
|
|
| Upload | 10 | 1 hour |
|
|
|
|
---
|
|
|
|
## App Signing
|
|
|
|
### Key Generation
|
|
|
|
#### Algorithm: Ed25519
|
|
|
|
```
|
|
Private key: 32 bytes (256 bits)
|
|
Public key: 32 bytes (256 bits)
|
|
Signature: 64 bytes (512 bits)
|
|
```
|
|
|
|
#### Why Ed25519?
|
|
- Fast signing and verification
|
|
- Small key and signature sizes
|
|
- No configuration choices (secure by default)
|
|
- Widely supported
|
|
|
|
### Key Generation Flow
|
|
|
|
```bash
|
|
# Developer generates keypair locally
|
|
mosis keys generate
|
|
|
|
# Output:
|
|
# Private key saved to: ~/.mosis/signing_key.pem
|
|
# Public key saved to: ~/.mosis/signing_key.pub
|
|
#
|
|
# Fingerprint: SHA256:xxxxx
|
|
#
|
|
# IMPORTANT: Keep your private key secure!
|
|
# Upload your public key to the developer portal.
|
|
```
|
|
|
|
### Key Format
|
|
|
|
#### Private Key (PEM)
|
|
|
|
```
|
|
-----BEGIN PRIVATE KEY-----
|
|
MC4CAQAwBQYDK2VwBCIEIGxxxxx...
|
|
-----END PRIVATE KEY-----
|
|
```
|
|
|
|
#### Public Key (PEM)
|
|
|
|
```
|
|
-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEAxxxxx...
|
|
-----END PUBLIC KEY-----
|
|
```
|
|
|
|
### Signing Flow
|
|
|
|
```
|
|
1. Build package (ZIP all files)
|
|
2. Generate MANIFEST.MF with SHA-256 hashes
|
|
3. Sign MANIFEST.MF with private key
|
|
4. Store signature in META-INF/CERT.SIG
|
|
5. Include public key in META-INF/CERT.PEM
|
|
```
|
|
|
|
#### MANIFEST.MF Example
|
|
|
|
```
|
|
Manifest-Version: 1.0
|
|
Created-By: mosis-cli 1.0.0
|
|
Package-Id: com.developer.myapp
|
|
Version-Code: 1
|
|
|
|
Name: manifest.json
|
|
SHA-256-Digest: K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=
|
|
|
|
Name: assets/main.rml
|
|
SHA-256-Digest: uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=
|
|
```
|
|
|
|
### Verification Flow
|
|
|
|
```
|
|
1. Extract MANIFEST.MF from package
|
|
2. Extract CERT.SIG (signature)
|
|
3. Extract CERT.PEM (public key)
|
|
4. Verify signature of MANIFEST.MF using public key
|
|
5. Verify CERT.PEM matches registered developer key
|
|
6. Verify each file hash matches MANIFEST.MF entry
|
|
```
|
|
|
|
### Key Registration
|
|
|
|
```
|
|
Developer Portal:
|
|
├── Go to Settings > Signing Keys
|
|
├── Click "Add Key"
|
|
├── Paste public key (PEM format)
|
|
├── Verify fingerprint matches local
|
|
└── Key is now registered
|
|
|
|
Server stores:
|
|
├── public_key (PEM text)
|
|
├── fingerprint (SHA256 of public key)
|
|
├── created_at
|
|
└── is_active
|
|
```
|
|
|
|
### Key Rotation
|
|
|
|
```
|
|
1. Generate new keypair
|
|
2. Register new public key in portal
|
|
3. Sign new versions with new key
|
|
4. (Optional) Revoke old key after transition period
|
|
```
|
|
|
|
### Trust Model
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────┐
|
|
│ Trust Chain │
|
|
├─────────────────────────────────────────────────┤
|
|
│ │
|
|
│ Developer │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ Private Key ──signs──► Package │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ Public Key ──registered──► Portal │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ Portal ──verifies──► Signature │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ Device ──trusts──► Portal-verified packages │
|
|
│ │
|
|
└─────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### Password Storage (if used)
|
|
|
|
```
|
|
Algorithm: Argon2id
|
|
Memory: 64 MB
|
|
Iterations: 3
|
|
Parallelism: 4
|
|
Salt: 16 bytes random
|
|
```
|
|
|
|
### Token Security
|
|
|
|
- Access tokens: Short-lived, in-memory only
|
|
- Refresh tokens: HttpOnly, Secure, SameSite=Strict
|
|
- API keys: Hashed with bcrypt, shown once on creation
|
|
|
|
### Key Security
|
|
|
|
- Private keys never leave developer's machine
|
|
- Public keys verified via fingerprint
|
|
- Key compromise: Revoke immediately, re-sign apps
|
|
|
|
### Audit Logging
|
|
|
|
```sql
|
|
INSERT INTO auth_audit_log (
|
|
developer_id,
|
|
action, -- login, logout, key_create, key_revoke
|
|
ip_address,
|
|
user_agent,
|
|
success,
|
|
failure_reason,
|
|
timestamp
|
|
) VALUES (...);
|
|
```
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### Authentication
|
|
|
|
```
|
|
POST /auth/oauth/github # Start GitHub OAuth
|
|
GET /auth/oauth/github/callback # GitHub callback
|
|
POST /auth/oauth/google # Start Google OAuth
|
|
GET /auth/oauth/google/callback # Google callback
|
|
POST /auth/refresh # Refresh tokens
|
|
POST /auth/logout # Invalidate tokens
|
|
GET /auth/me # Get current user
|
|
```
|
|
|
|
### API Keys
|
|
|
|
```
|
|
GET /api-keys # List keys
|
|
POST /api-keys # Create key
|
|
DELETE /api-keys/:id # Revoke key
|
|
```
|
|
|
|
### Signing Keys
|
|
|
|
```
|
|
GET /signing-keys # List keys
|
|
POST /signing-keys # Register key
|
|
DELETE /signing-keys/:id # Revoke key
|
|
GET /signing-keys/:id/verify # Verify a signature
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation (Go)
|
|
|
|
### Dependencies
|
|
|
|
```go
|
|
import (
|
|
// OAuth2
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/github"
|
|
"golang.org/x/oauth2/google"
|
|
|
|
// JWT
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
// Cryptography (all stdlib)
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
|
|
// Password/Key hashing
|
|
"golang.org/x/crypto/argon2"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
```
|
|
|
|
### OAuth2 Config
|
|
|
|
```go
|
|
var githubOAuth = &oauth2.Config{
|
|
ClientID: os.Getenv("GITHUB_CLIENT_ID"),
|
|
ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
|
|
Endpoint: github.Endpoint,
|
|
Scopes: []string{"read:user", "user:email"},
|
|
RedirectURL: "https://portal.mosis.dev/auth/github/callback",
|
|
}
|
|
```
|
|
|
|
### JWT Generation
|
|
|
|
```go
|
|
func generateAccessToken(developerID string) (string, error) {
|
|
claims := jwt.MapClaims{
|
|
"sub": developerID,
|
|
"type": "access",
|
|
"iat": time.Now().Unix(),
|
|
"exp": time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return token.SignedString([]byte(os.Getenv("JWT_SECRET")))
|
|
}
|
|
```
|
|
|
|
### Ed25519 Signing
|
|
|
|
```go
|
|
func signManifest(manifest []byte, privateKey ed25519.PrivateKey) []byte {
|
|
return ed25519.Sign(privateKey, manifest)
|
|
}
|
|
|
|
func verifySignature(manifest, signature []byte, publicKey ed25519.PublicKey) bool {
|
|
return ed25519.Verify(publicKey, manifest, signature)
|
|
}
|
|
```
|
|
|
|
### API Key Hashing
|
|
|
|
```go
|
|
func hashAPIKey(key string) (string, error) {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(key), bcrypt.DefaultCost)
|
|
return string(hash), err
|
|
}
|
|
|
|
func verifyAPIKey(key, hash string) bool {
|
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(key)) == nil
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Deliverables
|
|
|
|
- [x] Auth approach decided (OAuth2 + JWT + API Keys)
|
|
- [x] Crypto libraries selected (Go stdlib + golang-jwt)
|
|
- [ ] OAuth2 integration (GitHub) - P0
|
|
- [ ] OAuth2 integration (Google) - P1
|
|
- [ ] JWT token management
|
|
- [ ] API key generation and validation
|
|
- [ ] Ed25519 key generation (CLI tool)
|
|
- [ ] Signature creation and verification
|
|
- [ ] Key registration API
|
|
- [ ] Audit logging
|
|
|
|
---
|
|
|
|
## Test Cases
|
|
|
|
| Test | Description |
|
|
|------|-------------|
|
|
| OAuthLogin | Complete OAuth flow |
|
|
| TokenRefresh | Refresh expired access token |
|
|
| InvalidToken | Reject tampered JWT |
|
|
| APIKeyAuth | Authenticate with API key |
|
|
| KeyGeneration | Generate valid Ed25519 keypair |
|
|
| SignPackage | Sign and verify package |
|
|
| InvalidSignature | Reject tampered package |
|
|
| KeyRevocation | Revoked key fails verification |
|
|
|
|
---
|
|
|
|
## Open Questions
|
|
|
|
1. ~~Support for hardware security keys (YubiKey)?~~ → Defer to post-MVP
|
|
2. ~~Multi-factor authentication for portal?~~ → Defer to post-MVP
|
|
3. Team accounts with role-based access? → Consider for v1.1
|
|
4. ~~Key escrow for enterprise customers?~~ → Not needed for self-hosted
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749)
|
|
- [JWT RFC 7519](https://tools.ietf.org/html/rfc7519)
|
|
- [Ed25519 paper](https://ed25519.cr.yp.to/ed25519-20110926.pdf)
|
|
- [OWASP Authentication Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|