let-me-in

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 compact time counter, a public-key fingerprint, and an 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, finds the matching public key by fingerprint, and verifies the signature. 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: A4dQwA.tY8O0vXjKZ6F0Q2V8m3r0Jk1p9dW7aBcDeFgHiJkLmM.MEUCIQDx3k8Yp-R2vB...dGhpcyBpcyBhIHNpZ25hdHVyZQ
                  ────── ───────────────────────────────────────────── ──────────────────────────────────────────────────
                  counter key fingerprint                              ECDSA-SHA256 signature
                  (base64url)                                          (base64url, changes every period)

Counter = floor(unix_time / period), encoded as minimal big-endian bytes in base64url. The key identifier is base64url(SHA-256(SPKI_DER(public_key))). 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.