Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ WEBAUTHN_RP_ID=localhost
WEBAUTHN_RP_ORIGINS=http://localhost:5173
WEBAUTHN_RP_DISPLAY_NAME="VTA Farm"

# Optional VTA Wallet SIOPv2 login. Leave SIOP_RP_DID empty to keep the
# feature disabled. The DID must identify a dedicated RP identity whose
# did:webvh log is reachable over public HTTPS; do not reuse DID_HOSTING_DID.
SIOP_RP_DID=
SIOP_CHALLENGE_TTL_SECONDS=120
SIOP_CLOCK_SKEW_SECONDS=60
SIOP_DID_RESOLUTION_TIMEOUT_SECONDS=5
SIOP_MAX_BODY_BYTES=70000

# DID Hosting (optional — enables automatic did.jsonl upload after VTA setup)
#
# vtafarm-api's own keypair: `make gen-keypair`. Every full_stack session enrols
Expand Down
73 changes: 71 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ The database being shared has consequences worth reading once:
The API is now available at `http://localhost:8080`.
API docs: `http://localhost:8080/docs`

To exercise optional VTA Wallet login in Chrome, follow
[`docs/siop-browser-testing.md`](docs/siop-browser-testing.md).

4. (Optional) Generate a DID hosting keypair (required only if DID hosting is enabled):

```bash
Expand Down Expand Up @@ -103,14 +106,80 @@ Copy `.env.example` and adjust as needed:
| `DB_HOST` | `localhost` | The `make forward-db` tunnel to the shared dev database |
| `DB_NAME` | `vtafarm` | |
| `JWT_SECRET` | _(required)_ | HS256 signing secret — must match the team, see below |
| `SIOP_RP_DID` | _(empty)_ | Dedicated public RP DID; enables linked VTA Wallet login when set |
| `SIOP_CHALLENGE_TTL_SECONDS` | `120` | One-time wallet challenge lifetime |
| `SIOP_CLOCK_SKEW_SECONDS` | `60` | Allowed SIOP token clock skew |
| `SIOP_DID_RESOLUTION_TIMEOUT_SECONDS` | `5` | Public DID resolution timeout |
| `ORCHESTRATOR_RESUME` | `true` | Re-attach interrupted sessions at startup. Set `false` locally — see [`docs/shared-dev-database.md`](docs/shared-dev-database.md) |
| `CLUSTER_INGRESS_IP` | _(required)_ | External IP of the cluster's Traefik LoadBalancer |
| `CLOUDFLARE_API_TOKEN` | _(optional)_ | Required for VTA setup wizard |
| `CLOUDFLARE_ZONE_ID` | _(optional)_ | Required for VTA setup wizard |
| `KUBECONFIG` | _(empty)_ | Auto-detects `~/.kube/config` when empty |
| `K8S_NAMESPACE_PREFIX` | `vtafarm-user` | Per-user namespace: `vtafarm-user-{userID}` |

#### Generating JWT_SECRET
### SIOP RP DID

`SIOP_RP_DID` is the public identity of VTA Farm as a SIOPv2 relying party.
The wallet puts this value in the login token's `aud` claim; the API does not
sign with it and has no `SIOP_RP_PRIVATE_KEY` setting.

For local browser testing, put this development-only `did:key` in `.env`:

```dotenv
SIOP_RP_DID=did:key:z6MkkVc5EPGcCa3ZWB5i2YGX7BnLBm8vgf1qUwqTb9i87wLj
```

Restart the API and verify that wallet login is enabled:

```bash
curl http://localhost:8080/api/v1/auth/siop/metadata
```

The response should contain `"enabled":true` and the same `rp_did`. This DID is
a public test fixture, carries no VTA Farm private key, and is not suitable for
production.

#### Production RP DID

Production should use a dedicated `vtafarm-auth` VTA identity with persistent
key storage and a `did:webvh` history that resolves over public HTTPS. It may
use the existing DID-hosting deployment; a separate hosting service is not
required. Do not reuse `DID_HOSTING_DID`: that `did:key` and its
`DID_HOSTING_PRIVATE_KEY` are the privileged machine credential that
vtafarm-api uses to upload DID logs and manage hosting ACLs.

Provision the RP DID with PNM:

1. Select the VTA that is connected to the DID-hosting daemon:

```bash
pnm vta use <rp-vta-slug>
```

1. Find its registered hosting server ID:

```bash
pnm did-mgmt servers list
```

1. Create the RP context:

```bash
pnm contexts create \
--id vtafarm-auth \
--name vtafarm-auth
```

1. Create the RP DID:

```bash
pnm did-mgmt dids create \
--context vtafarm-auth \
--path vtafarm-auth \
--server <server-id>
```

### Generating JWT_SECRET

```bash
openssl rand -base64 32
Expand Down Expand Up @@ -237,7 +306,7 @@ Verify before going further — this is the step whose failure shows up several
minutes later as a mediator crash loop rather than as a TLS error.

**Test against the origin, not the hostname.** Managed and platform records are
*proxied* through Cloudflare, so plain `curl https://<hostname>` reports
_proxied_ through Cloudflare, so plain `curl https://<hostname>` reports
Cloudflare's edge certificate (issuer: Google Trust Services) and Cloudflare's
status code — it tells you nothing about the cluster. Pin the node IP:

Expand Down
57 changes: 57 additions & 0 deletions docs/siop-browser-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# VTA Wallet SIOP browser test

VTA Wallet login is disabled until `SIOP_RP_DID` is set. Production must use
the dedicated VTA Farm RP `did:webvh` whose history resolves over public HTTPS.
Do not reuse `DID_HOSTING_DID`.

For a local browser-contract test, a public `did:key` can be used as the
audience because the RP does not sign with it. This exercises wallet discovery,
challenge persistence, VTA token minting, persona verification, account
linking, and the role-matched cookie. It is not a substitute for the production
`did:webvh` consent and resolution check.

## Start locally

With the database tunnel running, start the API with a dev-only RP DID:

```bash
SIOP_RP_DID=did:key:z6MkkVc5EPGcCa3ZWB5i2YGX7BnLBm8vgf1qUwqTb9i87wLj
make dev
```

Start the frontend in its repository:

```bash
pnpm dev
```

Open `http://localhost:5173/login`. The API metadata can be checked without a
session:

```bash
curl http://localhost:8080/api/v1/auth/siop/metadata
```

It should return `enabled: true`, and Chrome should show **Continue with VTA
Wallet** when the extension exposes both `walletProfile` and `proxyLogin`.

## User flow

1. Sign in with the existing passkey.
2. Open **Settings → VTA Wallet identities**.
3. Select **Link**, choose a wallet persona, and approve the one-time assertion.
4. Confirm the identity appears in the list, then log out.
5. On `/login`, select **Continue with VTA Wallet** and approve the assertion.
6. Confirm the browser reaches `/portal` using the `vtafarm_user` httpOnly
cookie. No SIOP token should appear in local or session storage.
7. Replay and wrong-persona attempts must fail. Unlink the identity and confirm
wallet login then reports that it is not linked.

## Admin flow

Repeat the same sequence at **Admin → Security** and `/admin/login`. The admin
flow uses only admin routes and sets only `vtafarm_admin`; linking a user
identity does not authorize the admin route.

An account with a linked VTA Wallet identity cannot delete its final passkey.
Unlink every wallet identity first if the passkey must be removed.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/ic3software/vtafarm-api
go 1.26.3

require (
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467
github.com/gin-contrib/cors v1.7.7
github.com/gin-gonic/gin v1.12.0
github.com/go-webauthn/webauthn v0.17.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand Down
5 changes: 5 additions & 0 deletions helm/vtafarm-api/templates/vtafarm-api/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ data:
WEBAUTHN_RP_ID: {{ .Values.webauthn.rpID | default (include "app.frontendHost" .) | quote }}
WEBAUTHN_RP_ORIGINS: {{ .Values.webauthn.rpOrigins | default (include "app.frontendOrigin" .) | quote }}
WEBAUTHN_RP_DISPLAY_NAME: {{ .Values.webauthn.rpDisplayName | quote }}
SIOP_RP_DID: {{ .Values.siop.rpDID | quote }}
SIOP_CHALLENGE_TTL_SECONDS: {{ .Values.siop.challengeTTLSeconds | quote }}
SIOP_CLOCK_SKEW_SECONDS: {{ .Values.siop.clockSkewSeconds | quote }}
SIOP_DID_RESOLUTION_TIMEOUT_SECONDS: {{ .Values.siop.didResolutionTimeoutSeconds | quote }}
SIOP_MAX_BODY_BYTES: {{ .Values.siop.maxBodyBytes | quote }}
CORS_ALLOWED_ORIGINS: {{ .Values.cors.allowedOrigins | default (include "app.frontendOrigin" .) | quote }}
9 changes: 9 additions & 0 deletions helm/vtafarm-api/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ webauthn:
rpID: ""
rpOrigins: ""

# Optional VTA Wallet SIOPv2 login. rpDID must be a dedicated public RP DID;
# empty keeps metadata disabled and all SIOP routes fail closed.
siop:
rpDID: ""
challengeTTLSeconds: "120"
clockSkewSeconds: "60"
didResolutionTimeoutSeconds: "5"
maxBodyBytes: "70000"

# Browser origins allowed to call the API with credentials. Defaults to the
# frontend's; the localhost dev servers are always allowed on top of it.
cors:
Expand Down
Loading
Loading