let-me-in

Protect any web service with ECDSA time-signed headers.
No shared secrets. No passwords in config files.

How it Works

1

Generate Key Pair

The Chrome extension generates an ECDSA P-256 key pair. The private key is encrypted with your passkey (Touch ID / Windows Hello) and never leaves the browser.

2

Deploy Public Key

Copy the public key PEM and drop it into the verifier's keys folder. The verifier picks it up automatically — no restart needed.

3

Requests Get Signed

Every matching request gets an X-Auth-Signature header containing a time counter and ECDSA signature. The signature changes every 30 seconds.

4

Verifier Checks

Traefik or NGINX forwards the request to the verifier. It checks the time window and verifies the signature with your public key. Valid? 200. Invalid? 401.

let-me-in architecture diagram

Header Format

HTTP Request
GET /api/v1/data HTTP/1.1
Host: merchant.example.com
X-Auth-Signature: 3a7b2f0.MEUCIQDx3k8Yp-R2vB...dGhpcyBpcyBhIHNpZ25hdHVyZQ
                  ───────  ──────────────────────────────────────────────────
                  counter  ECDSA-SHA256 signature (base64url)
                  (hex)    changes every period

Counter = floor(unix_time / period). Both extension and server compute it independently from their clocks. Server accepts ±1 window for clock skew.

Download

Chrome Extension

Generates keys, signs requests, manages URL patterns.

Download .zip
Install
1. Unzip let-me-in-extension.zip
2. Open chrome://extensions
3. Enable "Developer mode"
4. Click "Load unpacked" → select the folder

Verifier (Docker)

ForwardAuth service. Works with Traefik and NGINX.

docker pull docker.pwypp.com/tools/let-me-in/verifier:latest
Or build from source
git clone <repo> && cd let-me-in/verifier
docker build -t let-me-in-verifier .

Setup: Traefik

Traefik's ForwardAuth middleware delegates authentication to the verifier.

1. Run the verifier

docker-compose.yml
services:
  let-me-in:
    image: docker.pwypp.com/tools/let-me-in/verifier
    container_name: let-me-in-verifier
    restart: unless-stopped
    volumes:
      - ./keys:/etc/let-me-in/keys:ro
    environment:
      HEADER_NAME: X-Auth-Signature
      PERIOD: "30"
      WINDOW: "1"

2. Configure Traefik middleware

traefik-dynamic.yml
http:
  middlewares:
    let-me-in:
      forwardAuth:
        address: "http://let-me-in-verifier:8080/verify"

  routers:
    my-service:
      rule: "Host(`app.example.com`)"
      middlewares:
        - let-me-in
      service: backend
      entryPoints:
        - websecure

3. Drop your public key

# Copy PEM from the extension and save it:
cat > keys/mike.pem <<'EOF'
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
EOF

# Verifier auto-reloads — no restart needed

Setup: NGINX

NGINX's auth_request directive works the same way — subrequest to the verifier.

1. Run the verifier

Same Docker container as above.

2. Configure NGINX

nginx.conf
upstream let_me_in {
    server let-me-in-verifier:8080;
}

server {
    listen 443 ssl;
    server_name app.example.com;

    # Protected location
    location / {
        auth_request /let-me-in-verify;
        proxy_pass http://backend;
    }

    # Internal auth subrequest
    location = /let-me-in-verify {
        internal;
        proxy_pass http://let_me_in/verify;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Auth-Signature $http_x_auth_signature;
    }
}

Configuration

Verifier Environment Variables

VariableDefaultDescription
KEYS_DIR/etc/let-me-in/keysDirectory to watch for .pem public key files
HEADER_NAMEX-Auth-SignatureHTTP header to read the signed token from
PERIOD30Time window in seconds (must match extension)
WINDOW1Clock skew tolerance (±N periods)
LISTEN:8080HTTP listen address

Key Management

Security

Asymmetric Keys

ECDSA P-256 — the same curve used in TLS. Private key never leaves the browser. Public key on the server can't forge signatures.

Time-Bound Tokens

Each signature is valid for one time window (default 30s). Intercepted tokens expire before they can be reused.

Passkey Protection

Private key encrypted with AES-256-GCM. Decryption requires biometric authentication (Touch ID / Windows Hello) via WebAuthn.

No Shared Secrets

Unlike TOTP or API keys, compromising a server reveals nothing useful. The public key can't sign requests.