Skip to content

mwaddip/libpam-web3

Repository files navigation

libpam-web3

Authenticate to Linux servers using wallet signatures. No passwords, no SSH keys — just your wallet.

Supported Chains

Chain Wallet Verification Package
EVM MetaMask Built-in secp256k1 ecrecover libpam-web3 + libpam-web3-evm
OPNet OPWallet ML-DSA-44/65/87 (post-quantum) libpam-web3 + libpam-web3-opnet
Cardano Eternl, Nami, Lace Ed25519 COSE_Sign1 (CIP-30) libpam-web3 + libpam-web3-cardano

New chains can be added as plugins — see Plugin Interface.

How It Works

User                              Server
  |                                 |
  |──── SSH login ────────────────> |
  |                                 |
  |<── OTP code + signing URL ───── |  "Code: 847293"
  |                                 |  "Sign at: https://host:34206?session=..."
  |                                 |
  | (opens URL, connects wallet,    |
  |  signs message in browser)      |
  |                                 |
  |                                 |── auth-svc writes .sig file
  |──── press Enter ──────────────> |
  |                                 |── verify signature
  |                                 |── match wallet against GECOS
  |                                 |
  |<── LOGIN SUCCESS ────────────── |

The signing URL port is derived deterministically from the chain name (1024 + crc32(chain) % 64511), so no URL configuration is needed.

Quick Start

1. Build

# Install Rust and PAM dev headers
sudo apt install libpam0g-dev

# Build core + all checked-out backends
./build.sh

# Or build core + specific backends
./build.sh --with-backends=evm

# Or core only (manual paste mode, no signing page)
./build.sh --with-backends=none

2. Install

# Core PAM module (always required)
sudo dpkg -i packaging/libpam-web3_*.deb

# Chain backend (pick one or more)
sudo dpkg -i plugins/evm/packaging/libpam-web3-evm_*.deb
sudo dpkg -i plugins/opnet/packaging/libpam-web3-opnet_*.deb
sudo dpkg -i plugins/cardano/packaging/libpam-web3-cardano_*.deb

3. Configure

Edit /etc/pam_web3/config.toml:

[machine]
id = "my-server"
secret_key = "0x..."  # openssl rand -hex 32

[auth]
otp_length = 6
otp_ttl_seconds = 300

4. Add a wallet user

# EVM
useradd -m -c "wallet=0xAbCd...1234" alice

# Cardano
useradd -m -c "wallet=addr1q9x...7f3k" bob

# OPNet
useradd -m -c "wallet=0x8a3f...beef" carol

5. Configure PAM and SSH

Add to /etc/pam.d/sshd (before @include common-auth):

auth [success=2 default=ignore] pam_succeed_if.so user != web3user
auth [success=1 default=die] pam_web3.so
@include common-auth

In /etc/ssh/sshd_config:

KbdInteractiveAuthentication yes
UsePAM yes

Restart: sudo systemctl restart sshd

Architecture

libpam-web3 (core)
├── PAM module (pam_web3.so)          — OTP, sessions, GECOS, plugin dispatch
├── Built-in EVM verifier             — secp256k1 ecrecover
└── Plugin interface                  — subprocess-based chain dispatch

plugins/evm/                          — Signing page + auth-svc (port 63108)
plugins/opnet/                        — Plugin + auth-svc (port 32448)
plugins/cardano/                      — Plugin + auth-svc (port 34206)

Each chain backend is a self-contained HTTPS server that:

  • Serves the signing page at GET /
  • Handles auth callbacks at POST /auth/callback/:session_id
  • Writes .sig files for PAM to read
  • Uses TLS certs generated by the core package's postinst
  • Listens on a port derived from crc32(chain_name) — no config needed

AI Agent Authentication

AI agents can authenticate programmatically without a browser — the wallet signature is just cryptography.

Python (EVM)

from eth_account import Account
from eth_account.messages import encode_defunct
import paramiko, re

def ssh_with_wallet(host, username, private_key_hex):
    transport = paramiko.Transport((host, 22))
    transport.connect(username=username)

    def auth_handler(title, instructions, prompts):
        responses = []
        for prompt, echo in prompts:
            match = re.search(r'Code: (\d+).*?Machine: (\S+)', prompt, re.DOTALL)
            if match:
                otp, machine_id = match.groups()
                message = f"Authenticate to {machine_id} with code: {otp}"
                signable = encode_defunct(text=message)
                signed = Account.sign_message(signable, private_key=private_key_hex)
                responses.append(signed.signature.hex())
            else:
                responses.append('')
        return responses

    transport.auth_interactive(username, auth_handler)

Node.js (EVM)

const { Client } = require('ssh2');
const { Wallet } = require('ethers');

const wallet = new Wallet('0x<private_key>');
const conn = new Client();

conn.on('keyboard-interactive', (name, instructions, lang, prompts, finish) => {
  const responses = prompts.map(p => {
    const match = p.prompt.match(/Code: (\d+).*?Machine: (\S+)/s);
    if (match) {
      const [, otp, machineId] = match;
      return wallet.signMessageSync(`Authenticate to ${machineId} with code: ${otp}`);
    }
    return '';
  });
  finish(responses);
});

conn.connect({ host: 'server.example.com', username: 'myuser', tryKeyboard: true });

Foundry (cast)

cast wallet sign "Authenticate to my-server with code: 123456" --private-key $WALLET_KEY
# Paste the output into the SSH prompt

GECOS Format

wallet=0x1234...abcd,nft=5
  • wallet=ADDRESS — required for authentication (case-insensitive match, chain-agnostic)
  • nft=TOKEN_ID — optional metadata
  • Can include other fields: Alice,wallet=0x...,nft=5

Security

Feature Description
No password storage Wallets are public addresses, no secrets stored
Replay protection OTP bound to machine ID + timestamp + HMAC
Time-limited OTP codes expire (default 5 minutes)
Cryptographic verification secp256k1 ecrecover (EVM), ML-DSA (OPNet), Ed25519 (Cardano)
Fail-secure Any error results in authentication denial
Memory-safe Core module written in Rust
Zero-config signing URLs Port derived from chain name, TLS from postinst

Adding a New Chain

See Plugin Interface Specification. In short:

  1. Create plugins/{chain}/ with a verification plugin, auth-svc, and signing page
  2. Port is 1024 + (crc32("{chain}") % 64511) — no coordination needed
  3. ./build.sh --with-backends={chain} builds it

License

MIT

About

Authenticate to Linux servers using your Ethereum wallet. PAM module for Web3 wallet-based SSH authentication.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors