Skip to main content

Authentication

GHOST uses two authentication mechanisms: EIP 712 typed data signatures for user facing endpoints and API key authentication for internal CRE endpoints.

EIP 712 Typed Data Signatures

All user facing endpoints require an EIP 712 signature that proves the request was authorized by the specified Ethereum address. This eliminates the need for session tokens or JWTs while providing cryptographic proof of identity.

Domain Separator

const EIP712_DOMAIN = {
name: "GhostProtocol",
version: "0.0.1",
chainId: 11155111, // Sepolia
verifyingContract: "0xE588a6c73933BFD66Af9b4A07d48bcE59c0D2d13" // Vault address
};

Message Types

Each endpoint has a specific message type that defines the signed payload:

EndpointMessage TypeFields
Confirm DepositConfirmDepositslotId (string), timestamp (uint256)
Cancel LendCancelLendintentId (string), timestamp (uint256)
Submit BorrowSubmitBorrowtoken (address), amount (uint256), maxRate (string), collateralToken (address), collateralAmount (uint256), timestamp (uint256)
Cancel BorrowCancelBorrowintentId (string), timestamp (uint256)
Accept ProposalAcceptProposalproposalId (string), timestamp (uint256)
Reject ProposalRejectProposalproposalId (string), timestamp (uint256)
Repay LoanRepayLoanloanId (string), timestamp (uint256)
Claim Excess CollateralClaimExcessCollateralloanId (string), timestamp (uint256)

Signature Verification

The server recovers the signer address from the EIP 712 signature and verifies it matches the expected user:

const recoveredAddress = ethers.verifyTypedData(
EIP712_DOMAIN,
messageTypes,
messagePayload,
signature
);

if (recoveredAddress.toLowerCase() !== expectedAddress.toLowerCase()) {
throw new Error("Signature verification failed");
}

Timestamp Validation

Each signed message includes a timestamp field. The server enforces a 5 minute window:

const now = Math.floor(Date.now() / 1000);
const diff = Math.abs(now - timestamp);
if (diff > 300) {
throw new Error("Timestamp too far from current time");
}

This prevents replay attacks where a captured signature is resubmitted after the intended action window.

Client Side Signing

To sign a request, the client uses ethers.js or a compatible wallet:

import { ethers } from "ethers";

const wallet = new ethers.Wallet(privateKey);

const signature = await wallet.signTypedData(
{
name: "GhostProtocol",
version: "0.0.1",
chainId: 11155111,
verifyingContract: "0xE588a6c73933BFD66Af9b4A07d48bcE59c0D2d13",
},
{
ConfirmDeposit: [
{ name: "slotId", type: "string" },
{ name: "timestamp", type: "uint256" },
],
},
{
slotId: "abc123",
timestamp: Math.floor(Date.now() / 1000),
}
);

Internal API Key Authentication

CRE facing endpoints under /api/v1/internal/* are authenticated using a shared API key passed in the x-api-key header:

GET /api/v1/internal/pending-intents
x-api-key: <INTERNAL_API_KEY>

The API key is configured via the INTERNAL_API_KEY environment variable. All internal endpoints check this header before processing the request. If the key is missing or incorrect, the request is rejected with a 401 status.

This is a simpler authentication model appropriate for machine to machine communication where the CRE is the only caller.