- Implement PKCE (Proof Key for Code Exchange) with S256 method - Add crypto/pkce module with code verifier and challenge generation - Modify OAuth flow to include code_challenge in authorization requests - Update HandleCallback to validate code_verifier during token exchange - Extend session lifetime from 7 to 30 days - Add comprehensive unit tests for PKCE functions - Maintain backward compatibility with fallback for non-PKCE sessions - Add detailed logging for OAuth flow with PKCE tracking PKCE enhances security by preventing authorization code interception attacks, as recommended by OAuth 2.1 and OIDC standards. feat: add encrypted refresh token storage with automatic cleanup - Add oauth_sessions table for storing encrypted refresh tokens - Implement AES-256-GCM encryption for refresh tokens using cookie secret - Create OAuth session repository with full CRUD operations - Add SessionWorker for automatic cleanup of expired sessions - Configure cleanup to run every 24h for sessions older than 37 days - Modify OAuth flow to store refresh tokens after successful authentication - Track client IP and user agent for session security validation - Link OAuth sessions to user sessions via session ID - Add comprehensive encryption tests with security validations - Integrate SessionWorker into server lifecycle with graceful shutdown This enables persistent OAuth sessions with secure token storage, reducing the need for frequent re-authentication from 7 to 30 days.
5.0 KiB
Cryptographic Signatures
Complete signature flow with Ed25519 and security guarantees.
Principle
Ackify uses Ed25519 (elliptic curve) to create non-repudiable cryptographic signatures.
Guarantees:
- ✅ Non-repudiation - The signature proves the signer's identity
- ✅ Integrity - SHA-256 hash detects any modification
- ✅ Immutable timestamp - PostgreSQL triggers prevent backdating
- ✅ Uniqueness - One signature per user/document
Signature Flow
1. User accesses the document
https://sign.company.com/?doc=policy_2025
The Vue.js frontend loads and displays:
- Document title (if metadata exists)
- Number of existing signatures
- "Sign this document" button
2. Session verification
The frontend calls:
GET /api/v1/users/me
If not logged in → OAuth2 redirect If logged in → Display signature button
3. Signature
When clicking "Sign", the frontend:
- Gets a CSRF token:
GET /api/v1/csrf
- Sends the signature:
POST /api/v1/signatures
Content-Type: application/json
X-CSRF-Token: abc123
{
"doc_id": "policy_2025"
}
4. Backend Processing
The backend (Go):
- Verifies the session - User authenticated
- Generates Ed25519 signature:
payload := fmt.Sprintf("%s:%s:%s:%s", docID, userSub, userEmail, timestamp) hash := sha256.Sum256([]byte(payload)) signature := ed25519.Sign(privateKey, hash[:]) - Calculates prev_hash - Hash of the last signature (chaining)
- Inserts into database:
INSERT INTO signatures (doc_id, user_sub, user_email, signed_at, payload_hash, signature, nonce, prev_hash) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - Returns the signature to the frontend
5. Confirmation
The frontend displays:
- ✅ Signature confirmed
- Timestamp
- Link to signatures list
Signature Structure
{
"docId": "policy_2025",
"userEmail": "alice@company.com",
"userName": "Alice Smith",
"signedAt": "2025-01-15T14:30:00Z",
"payloadHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"signature": "ed25519:3045022100...",
"nonce": "abc123xyz",
"prevHash": "sha256:prev..."
}
Fields:
payloadHash- SHA-256 of the payload (doc_id:user_sub:email:timestamp)signature- Ed25519 signature in base64nonce- Anti-replay protectionprevHash- Hash of the previous signature (blockchain-like)
Signature Verification
Manual (via API)
GET /api/v1/documents/policy_2025/signatures
Returns all signatures with:
- Signer email
- Timestamp
- Hash + signature
Programmatic (Go)
import "crypto/ed25519"
func VerifySignature(publicKey ed25519.PublicKey, payload, signature []byte) bool {
hash := sha256.Sum256(payload)
return ed25519.Verify(publicKey, hash[:], signature)
}
PostgreSQL Constraints
One signature per user/document
UNIQUE (doc_id, user_sub)
Behavior:
- If the user tries to sign twice → 409 Conflict error
- The frontend detects this and displays "Already signed"
Immutability of created_at
PostgreSQL trigger:
CREATE TRIGGER prevent_signatures_created_at_update
BEFORE UPDATE ON signatures
FOR EACH ROW
EXECUTE FUNCTION prevent_created_at_update();
Guarantee: Impossible to backdate a signature.
Chaining (Blockchain-like)
Each signature references the previous one via prev_hash:
Signature 1 → hash1
Signature 2 → hash2 (prev_hash = hash1)
Signature 3 → hash3 (prev_hash = hash2)
Tampering detection:
- If a signature is modified, the
prev_hashof the next one no longer matches - Allows detection of any history modification
Security
Ed25519 Private Key
Auto-generated on first startup or via:
ACKIFY_ED25519_PRIVATE_KEY=$(openssl rand -base64 64)
Important:
- The private key never leaves the server
- Stored in memory only (not in database)
- Backup required if you want to keep the same key after redeployment
Anti-Replay Protection
The unique nonce prevents signature reuse:
nonce := fmt.Sprintf("%s-%d", userSub, time.Now().UnixNano())
Rate Limiting
Signatures are limited to 100 requests/minute per IP.
Use Cases
Policy Read Validation
Document: "Security Policy 2025"
URL: https://sign.company.com/?doc=security_policy_2025
Workflow:
- Admin sends the link to employees
- Each employee clicks, reads, and signs
- Admin sees completion in
/admin
Training Acknowledgment
Document: "GDPR Training 2025"
Expected signers: 50 employees
Features:
- Completion tracking (42/50 = 84%)
- Automatic email reminders
- Signature export
Contractual Acknowledgment
Document: "Terms of Service v3"
Checksum: SHA-256 of the PDF
Verification:
- User calculates the PDF checksum
- Compares with stored metadata
- Signs if identical
See Checksums for more details.
API Reference
See API Documentation for all signature-related endpoints.