clarity-auth
v1.0.1
Published
ClarityAuth - ZK-based authentication for Midnight blockchain
Downloads
134
Maintainers
Readme
ClarityAuth - ZK Session Authentication for Midnight
A zero-knowledge authentication contract for Clarity DAO on Midnight blockchain. Enables privacy-preserving session management using ZK proofs.
Features
- Privacy-Preserving Authentication: User identity is never stored on-chain; only hashed authority identifiers are used
- Replay Protection: Each challenge can only be used once, preventing replay attacks
- Session Management: Create, verify, and revoke sessions with cryptographic proofs
- Audience Binding: Sessions are bound to specific DAO/app identifiers
- Authority Pattern: Uses Midnight's authority pattern since
msg.senderis not available
Architecture
Authority Pattern
Midnight does not expose msg.sender like Ethereum. Instead, ClarityAuth uses the "authority pattern":
- User's public key is retrieved via a witness function (
getPublicKey) - The public key is hashed to create an "authority" identifier
- The authority hash is stored on-chain (not the raw public key)
- Ownership is verified by proving knowledge of the public key that hashes to the stored authority
Contract State
// Replay protection: tracks used challenge hashes
ledger usedChallenges: Map<Bytes<32>, Boolean>;
// Active sessions indexed by session public key hash
ledger sessions: Map<Bytes<32>, SessionRecord>;
// Session counter for analytics
ledger sessionCounter: Counter;Session Record
struct SessionRecord {
authority: Bytes<32>; // Hash of user's publicKey (proves ownership)
expiresAt: Uint<64>; // Unix timestamp for session expiry
audience: Bytes<32>; // DAO/app identifier
createdAt: Uint<64>; // Creation timestamp
}Installation
npm installTarget Midnight Stack
This repo now targets the official Midnight compatibility matrix published on April 3, 2026:
compactdevtools0.5.1compact compile0.30.0@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]@midnight-ntwrk/[email protected]
Source: Midnight compatibility matrix
Building
Compile the Compact Contract
npm run build:contractThis compiles contract/src/clarity-auth.compact to build/.
Build TypeScript
npm run buildTesting
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run with coverage
npm run test:coverageDeployment
This repo owns the Midnight contract deployment flow. The Clarity app should only consume the deployed contract address.
Generate a Deployment Seed
npm run wallet:seed -- --network preprodThis prints a fresh MIDNIGHT_SERVER_WALLET_SEED=... value and its derived unshielded address. Fund that address before deploying.
Local Midnight Infrastructure
npm run midnight:local:up
npm run midnight:local:logsThis starts the local node, indexer, and proof server from docker-compose.midnight.yml.
Web Deploy UI
MIDNIGHT_PROOF_SERVER=http://127.0.0.1:6300 npm run deploy:webThen open the Vite page it prints, connect mnLace, and click Deploy contract.
If deployment succeeds, the page shows the exact env line to copy into the app repo:
NEXT_PUBLIC_MIDNIGHT_AUTH_CONTRACT_ADDRESS=<deployed-address>Notes
- For preprod, use a running local proof server. The currently working image is the one pinned in docker-compose.midnight.yml.
- Deployment now happens from the browser wallet, not from a CLI seed-wallet flow.
- The app repo at
/Users/justin/Desktop/code/claritydao-mvpshould not own deployment anymore; it should only read the deployed address from env.
Usage
TypeScript Integration
import {
createClarityAuthClient,
generateChallenge,
createSessionPkHash
} from 'clarity-auth';
// Build these from your app's connected Midnight wallet session.
const providers = {
privateStateProvider,
zkConfigProvider,
proofProvider,
publicDataProvider,
walletProvider,
midnightProvider,
};
const client = createClarityAuthClient({
providers,
contractAddress: 'abcd...contract-address-without-0x-prefix',
wallet: {
publicKey: walletPublicKeyBytes,
},
});
// Generate a challenge for replay protection
const challenge = client.generateChallenge();
// Create a session public key and hash it
const sessionPk = crypto.getRandomValues(new Uint8Array(32));
const sessionPkHash = await client.createSessionHash(sessionPk);
// Define audience (DAO identifier)
const audience = await hashBytes(new TextEncoder().encode('clarity-dao'));
// Create a session (expires in 1 hour)
const expiresAt = BigInt(Math.floor(Date.now() / 1000) + 3600);
const result = await client.authorizeSession({
challenge,
sessionPkHash,
expiresAt,
audience,
});
if (result.success) {
console.log('Session created:', result.txHash);
}For preprod browser use, this package expects your app to provide an already connected Midnight wallet session and the corresponding providers. This package does not currently bootstrap those providers from the browser wallet on its own.
Verifying Session Ownership
// Verify the caller owns a valid session
const verified = await client.verifySessionOwnership({
sessionPkHash,
audience,
});
if (verified.result) {
console.log('Session ownership verified');
}Revoking a Session
// Only the session owner can revoke
const revoked = await client.revokeSession({ sessionPkHash });
if (revoked.success) {
console.log('Session revoked');
}Contract Circuits
authorize_session
Creates a new authenticated session.
Parameters:
challenge: Bytes<32>- Unique challenge (prevents replay)sessionPkHash: Bytes<32>- Hash of session public keyexpiresAt: Uint<64>- Unix timestamp for expiryaudience: Bytes<32>- DAO/app identifier
Validation:
- Challenge must not have been used before
- Expiry must be in the future
- Session must not already exist
revoke_session
Revokes an existing session.
Parameters:
sessionPkHash: Bytes<32>- Session to revoke
Validation:
- Session must exist
- Caller must own the session (authority match)
is_session_valid
Checks if a session is valid.
Parameters:
sessionPkHash: Bytes<32>- Session to check
Returns: Boolean - True if session exists, not revoked, and not expired
verify_session_ownership
Proves the caller owns a valid session.
Parameters:
sessionPkHash: Bytes<32>- Session to verifyaudience: Bytes<32>- Expected audience
Validation:
- Session must exist and be valid
- Caller must own the session
- Audience must match
is_challenge_used
Checks if a challenge has been used.
Parameters:
challenge: Bytes<32>- Challenge to check
Returns: Boolean - True if challenge was used
session_exists
Checks if a session record exists.
Parameters:
sessionPkHash: Bytes<32>- Session to check
Returns: Boolean - True if session exists (may be revoked)
Witness Functions
Witnesses are implemented in TypeScript and run locally during ZK proof generation:
| Witness | Description |
|---------|-------------|
| getPublicKey() | Returns user's 32-byte public key from wallet |
| getCurrentTime() | Returns current Unix timestamp in seconds |
| computeAuthorityHash(pubKey) | Computes SHA-256 hash of public key |
Security Considerations
- Replay Protection: Each challenge can only be used once
- Authority Verification: Session ownership is cryptographically verified
- Session Expiry: Sessions have explicit expiry timestamps
- Audience Binding: Prevents cross-app session hijacking
- ZK Privacy: Raw public keys are never stored on-chain
Directory Structure
clarity-auth/
├── contract/
│ └── src/
│ └── clarity-auth.compact # Compact contract
├── cli/
│ └── src/
│ ├── index.ts # Main API
│ ├── types.ts # TypeScript types
│ ├── witnesses.ts # Witness implementations
│ └── deploy.ts # Deployment logic
├── tests/
│ └── clarity-auth.test.ts # Test suite
├── build/ # Compiled output
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── README.mdLicense
MIT
Contributing
Contributions are welcome. Please open an issue first to discuss proposed changes.
