Ave provides digital signature capabilities using Ed25519 signing keys. Users can sign documents, messages, and transactions, and apps can request signatures via OAuth.
Signing Keys
Each Ave identity can have a signing key pair :
Public key : Shared publicly for signature verification
Private key : Encrypted with the user’s master key, stored securely
From signing.ts:89-149, users create signing keys:
// POST /api/signing/keys/:identityId
{
"publicKey" : "base64-encoded-public-key" ,
"encryptedPrivateKey" : "base64-encrypted-private-key"
}
The private key is encrypted client-side before transmission:
// Client-side key generation
const signingKeyPair = await crypto . subtle . generateKey (
{ name: 'Ed25519' },
true ,
[ 'sign' , 'verify' ]
);
// Export keys
const publicKey = await crypto . subtle . exportKey ( 'spki' , signingKeyPair . publicKey );
const privateKey = await crypto . subtle . exportKey ( 'pkcs8' , signingKeyPair . privateKey );
// Encrypt private key with master key
const masterKey = await loadMasterKey ();
const iv = crypto . getRandomValues ( new Uint8Array ( 12 ));
const encryptedPrivateKey = await crypto . subtle . encrypt (
{ name: 'AES-GCM' , iv },
masterKey ,
privateKey
);
// Send to Ave
await fetch ( `/api/signing/keys/ ${ identityId } ` , {
method: 'POST' ,
body: JSON . stringify ({
publicKey: arrayBufferToBase64 ( publicKey ),
encryptedPrivateKey: arrayBufferToBase64 ( encryptedPrivateKey )
})
});
Ave uses Ed25519 (Edwards-curve Digital Signature Algorithm) for its speed, security, and small signature size (64 bytes).
Signature Requests
OAuth apps can request signatures from users:
App creates request
The app calls Ave’s API with the payload to be signed, the user’s identity, and metadata.
User receives notification
Ave notifies the user (WebSocket, push notification, or in-app badge) of the pending signature request.
User reviews payload
The user sees the payload (human-readable if possible), app information, and metadata (e.g., “Sign in to Acme Corp”).
User signs or denies
If approved, the user’s browser decrypts the private key, signs the payload, and submits the signature to Ave.
App retrieves signature
The app polls Ave’s API for the signature status. Once signed, the signature is returned.
Creating a Signature Request
From signing.ts:503-572, apps create requests:
POST https://api.aveid.net/api/signing/request
Content-Type: application/json
{
"clientId" : "app_abc123",
"clientSecret" : "secret_xyz",
"identityId" : "identity_123",
"payload" : "Sign in to Acme Corp as [email protected] ",
"metadata" : {
"action" : "login",
"timestamp" : "2024-01-01T12:00:00Z",
"ip" : "192.168.1.1"
},
"expiresInSeconds" : 300
}
Response:
{
"requestId" : "req_abc123" ,
"expiresAt" : "2024-01-01T12:05:00Z" ,
"publicKey" : "base64-encoded-public-key"
}
From signing.ts:553-565:
const [ request ] = await db
. insert ( signatureRequests )
. values ({
identityId ,
appId: app . id ,
payload ,
metadata: metadata || {},
expiresAt: new Date ( Date . now () + expiresInSeconds * 1000 ),
})
. returning ();
Requests expire after the specified duration (default 5 minutes, max 1 hour).
Apps must authenticate with clientSecret to create signature requests. This prevents unauthorized apps from spamming users with requests.
Viewing Pending Requests
Users see pending requests in their dashboard:
GET https://api.aveid.net/api/signing/requests
Authorization: Bearer session_token
From signing.ts:214-266, returns:
{
"requests" : [
{
"id" : "req_abc123" ,
"payload" : "Sign in to Acme Corp" ,
"metadata" : {
"action" : "login" ,
"timestamp" : "2024-01-01T12:00:00Z"
},
"createdAt" : "2024-01-01T12:00:00Z" ,
"expiresAt" : "2024-01-01T12:05:00Z" ,
"app" : {
"id" : "app_abc123" ,
"name" : "Acme Corp" ,
"iconUrl" : "https://acme.com/icon.png" ,
"websiteUrl" : "https://acme.com"
},
"identity" : {
"id" : "identity_123" ,
"handle" : "alice" ,
"displayName" : "Alice Smith"
}
}
]
}
Signing a Request
From signing.ts:326-411, users sign requests:
POST https://api.aveid.net/api/signing/requests/:requestId/sign
Authorization: Bearer session_token
Content-Type: application/json
{
"signature" : "base64-encoded-signature"
}
Client-side signing process:
// 1. Load encrypted private key from Ave
const response = await fetch ( `/api/signing/requests/ ${ requestId } ` );
const { request , signingKey } = await response . json ();
// 2. Decrypt private key with master key
const masterKey = await loadMasterKey ();
const privateKeyBytes = await crypto . subtle . decrypt (
{ name: 'AES-GCM' , iv: /* stored IV */ },
masterKey ,
base64ToArrayBuffer ( signingKey . encryptedPrivateKey )
);
const privateKey = await crypto . subtle . importKey (
'pkcs8' ,
privateKeyBytes ,
{ name: 'Ed25519' },
false ,
[ 'sign' ]
);
// 3. Sign the payload
const payloadBytes = new TextEncoder (). encode ( request . payload );
const signatureBytes = await crypto . subtle . sign (
{ name: 'Ed25519' },
privateKey ,
payloadBytes
);
const signature = arrayBufferToBase64 ( signatureBytes );
// 4. Submit signature to Ave
await fetch ( `/api/signing/requests/ ${ requestId } /sign` , {
method: 'POST' ,
headers: { Authorization: `Bearer ${ sessionToken } ` },
body: JSON . stringify ({ signature })
});
From signing.ts:373-393, Ave verifies the signature before storing:
const isValid = await verifySignature (
result . request . payload ,
signature ,
result . signingKey . publicKey
);
if ( ! isValid ) {
return c . json ({ error: "Invalid signature" }, 400 );
}
await db
. update ( signatureRequests )
. set ({
status: "signed" ,
signature ,
resolvedAt: new Date (),
deviceId: user . deviceId ,
})
. where ( eq ( signatureRequests . id , requestId ));
Ave verifies signatures server-side to ensure integrity. Invalid signatures are rejected before being stored.
Denying a Request
Users can deny signature requests:
POST https://api.aveid.net/api/signing/requests/:requestId/deny
Authorization: Bearer session_token
From signing.ts:414-467, the request is marked as denied and logged in the activity log.
Polling for Signatures
Apps poll for signature status:
GET https://api.aveid.net/api/signing/request/:requestId/status?clientId=app_abc123
From signing.ts:574-619, returns:
{
"status" : "signed" ,
"signature" : "base64-encoded-signature" ,
"resolvedAt" : "2024-01-01T12:01:30Z"
}
Possible statuses:
pending: User hasn’t responded yet
signed: User signed the payload
denied: User rejected the request
expired: Request timed out
Apps typically poll every 2-3 seconds:
const pollForSignature = async ( requestId : string ) => {
return new Promise (( resolve , reject ) => {
const interval = setInterval ( async () => {
const response = await fetch (
`https://api.aveid.net/api/signing/request/ ${ requestId } /status?clientId=app_abc123`
);
const data = await response . json ();
if ( data . status === 'signed' ) {
clearInterval ( interval );
resolve ( data . signature );
} else if ( data . status === 'denied' ) {
clearInterval ( interval );
reject ( new Error ( 'User denied signature request' ));
} else if ( data . status === 'expired' ) {
clearInterval ( interval );
reject ( new Error ( 'Signature request expired' ));
}
}, 2000 );
});
};
Public Key Retrieval
Anyone can retrieve a user’s public signing key to verify signatures:
GET https://api.aveid.net/api/signing/public-key/:handle
From signing.ts:475-501, returns:
{
"handle" : "alice" ,
"publicKey" : "base64-encoded-public-key" ,
"createdAt" : "2024-01-01T10:00:00Z"
}
This allows third parties to:
Verify signatures offline
Build trust in signed documents
Implement “Login with Ave” flows
Signature Verification
Ave provides a public verification endpoint:
POST https://api.aveid.net/api/signing/verify
Content-Type: application/json
{
"message" : "Sign in to Acme Corp",
"signature" : "base64-encoded-signature",
"publicKey" : "base64-encoded-public-key"
}
From signing.ts:621-640, returns:
Verification uses Ed25519:
export async function verifySignature (
message : string ,
signature : string ,
publicKeyB64 : string
) : Promise < boolean > {
try {
const publicKey = await crypto . subtle . importKey (
'spki' ,
base64ToArrayBuffer ( publicKeyB64 ),
{ name: 'Ed25519' },
false ,
[ 'verify' ]
);
const messageBytes = new TextEncoder (). encode ( message );
const signatureBytes = base64ToArrayBuffer ( signature );
return await crypto . subtle . verify (
{ name: 'Ed25519' },
publicKey ,
signatureBytes ,
messageBytes
);
} catch {
return false ;
}
}
Use Cases
Login Signatures
Apps can use signatures for authentication:
// 1. App creates signature request
const { requestId } = await fetch ( 'https://api.aveid.net/api/signing/request' , {
method: 'POST' ,
body: JSON . stringify ({
clientId: 'app_123' ,
clientSecret: 'secret' ,
identityId: 'identity_123' ,
payload: `Login to Acme Corp at ${ new Date (). toISOString () } ` ,
metadata: { action: 'login' }
})
}). then ( r => r . json ());
// 2. Wait for user to sign
const signature = await pollForSignature ( requestId );
// 3. Verify signature server-side
const publicKey = await fetchPublicKey ( 'alice' );
const isValid = await verifySignature ( payload , signature , publicKey );
if ( isValid ) {
// Create session for user
createSession ( 'alice' );
}
Document Signatures
Sign PDF documents, contracts, or agreements:
// Hash the document
const documentHash = await crypto . subtle . digest (
'SHA-256' ,
documentBytes
);
const payload = `I, alice, agree to the terms on ${ new Date (). toISOString () } . ` +
`Document hash: ${ arrayBufferToHex ( documentHash ) } ` ;
// Request signature
const { requestId , signature } = await requestSignature ( payload );
// Store signature alongside document
await storeDocument ({
document: documentBytes ,
signature ,
signer: 'alice' ,
signedAt: new Date ()
});
Transaction Signatures
Sign cryptocurrency transactions or financial operations:
const transaction = {
from: 'alice' ,
to: 'bob' ,
amount: '100.00 USD' ,
timestamp: new Date (). toISOString ()
};
const payload = JSON . stringify ( transaction );
const signature = await requestSignature ( payload );
// Submit to blockchain or payment processor
await submitTransaction ({ transaction , signature });
Code Signing
Sign software releases or commits:
const releaseHash = await hashFile ( 'my-app-v1.0.0.tar.gz' );
const payload = `Release: my-app v1.0.0 \n ` +
`SHA-256: ${ releaseHash } \n ` +
`Signed by: alice \n ` +
`Date: ${ new Date (). toISOString () } ` ;
const signature = await requestSignature ( payload );
// Publish signature alongside release
await publishRelease ({
file: 'my-app-v1.0.0.tar.gz' ,
signature ,
publicKey: await fetchPublicKey ( 'alice' )
});
Key Rotation
Users can rotate their signing keys:
PUT https://api.aveid.net/api/signing/keys/:identityId
Authorization: Bearer session_token
Content-Type: application/json
{
"publicKey" : "base64-new-public-key",
"encryptedPrivateKey" : "base64-new-encrypted-private-key"
}
From signing.ts:153-206, this:
Deletes the old key
Stores the new key
Logs the rotation in activity log with severity: "warning"
Rotating signing keys invalidates all previous signatures . Only rotate keys if the old key is compromised or lost.
Security Properties
Private Key Protection
Private keys are never stored in plaintext
Encrypted with user’s master key (AES-256-GCM)
Only decrypted in-memory during signing
Never transmitted to Ave in plaintext
Signature Integrity
From signing.ts:373-382, Ave verifies every signature:
const isValid = await verifySignature (
result . request . payload ,
signature ,
result . signingKey . publicKey
);
if ( ! isValid ) {
return c . json ({ error: "Invalid signature" }, 400 );
}
This prevents:
Tampered signatures
Signatures from wrong keys
Replay attacks (timestamps in payload)
Request Expiration
Signature requests expire quickly:
// From signing.ts:510
expiresInSeconds : z . number (). min ( 60 ). max ( 3600 ). default ( 300 )
Minimum: 1 minute
Maximum: 1 hour
Default: 5 minutes
Expired requests are automatically marked:
// From signing.ts:600-611
if ( result . request . status === "pending" && new Date () > result . request . expiresAt ) {
await db
. update ( signatureRequests )
. set ({ status: "expired" , resolvedAt: new Date () })
. where ( eq ( signatureRequests . id , requestId ));
return c . json ({ status: "expired" , signature: null });
}
Demo Endpoint
Ave provides a demo endpoint for testing in the Playground:
POST https://api.aveid.net/api/signing/demo/request
Authorization: Bearer access_token
Content-Type: application/json
{
"payload" : "Test signature from Ave Playground"
}
From signing.ts:646-712, this uses OAuth access token authentication instead of client secret, making it easy to test from the browser.
Next Steps
OAuth Provider Learn how apps request signatures via OAuth
End-to-End Encryption Understand how private keys are encrypted