Skip to main content
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:
1

App creates request

The app calls Ave’s API with the payload to be signed, the user’s identity, and metadata.
2

User receives notification

Ave notifies the user (WebSocket, push notification, or in-app badge) of the pending signature request.
3

User reviews payload

The user sees the payload (human-readable if possible), app information, and metadata (e.g., “Sign in to Acme Corp”).
4

User signs or denies

If approved, the user’s browser decrypts the private key, signs the payload, and submits the signature to Ave.
5

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:
{
  "valid": true
}
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

Build docs developers (and LLMs) love