Complete signature flow with Ed25519 and security guarantees.
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
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
The frontend calls:
GET /api/v1/users/meIf not logged in → OAuth2 redirect If logged in → Display signature button
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"
}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
The frontend displays:
- ✅ Signature confirmed
- Timestamp
- Link to signatures list
{
"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)
GET /api/v1/documents/policy_2025/signaturesThe signature list endpoint has access restrictions to protect user privacy:
| User Type | What They See |
|---|---|
| Document owner (created_by) | All signatures with emails |
| Admin (in ACKIFY_ADMIN_EMAILS) | All signatures with emails |
| Authenticated user (not owner) | Only their own signature (if they signed) |
| Non-authenticated | Empty list |
Note: The signature count is always visible to everyone via the
signatureCountfield in document responses. Only the detailed list (with email addresses) is restricted.
Example responses:
As document owner/admin:
{
"data": [
{"userEmail": "alice@example.com", "signedAt": "..."},
{"userEmail": "bob@example.com", "signedAt": "..."},
{"userEmail": "charlie@example.com", "signedAt": "..."}
]
}As authenticated non-owner (who has signed):
{
"data": [
{"userEmail": "bob@example.com", "signedAt": "..."}
]
}As non-authenticated:
{
"data": []
}The same access control applies to the expected signers endpoint (/expected-signers).
import "crypto/ed25519"
func VerifySignature(publicKey ed25519.PublicKey, payload, signature []byte) bool {
hash := sha256.Sum256(payload)
return ed25519.Verify(publicKey, hash[:], signature)
}UNIQUE (doc_id, user_sub)Behavior:
- If the user tries to sign twice → 409 Conflict error
- The frontend detects this and displays "Already signed"
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.
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
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
The unique nonce prevents signature reuse:
nonce := fmt.Sprintf("%s-%d", userSub, time.Now().UnixNano())Signatures are limited to 100 requests/minute per IP.
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
Document: "GDPR Training 2025"
Expected signers: 50 employees
Features:
- Completion tracking (42/50 = 84%)
- Automatic email reminders
- Signature export
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.
See API Documentation for all signature-related endpoints.