> ## Documentation Index
> Fetch the complete documentation index at: https://hs-df36fa00.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Provider Ownership Challenges and Key Verification

> Use signed Ed25519 ownership challenges to prove DID control when registering a new provider or rotating a key on a Watt ServiceNet node.

When a node requires ownership challenges, you must prove that you control the private key behind your `did:key` DID before the node accepts a registration or key rotation. The node issues a short-lived challenge — a random nonce — that you sign with your Ed25519 private key. The signed challenge is then included in the registration or rotation request, and the node verifies the signature against the public key encoded in your DID.

## When challenges are required

The node operator controls whether challenges are required via the `SERVICENET_REQUIRE_PROVIDER_OWNERSHIP_CHALLENGES` environment variable. Set it to `1` to enforce challenges, or leave it unset to allow open registration.

<Note>
  If you are connecting to a production or shared node, assume challenges are enforced — node operators may require them regardless of the default setting.
</Note>

## Challenge flow

<Steps>
  ### Request a challenge

  POST to `/v1/providers/ownership-challenges` with your `provider_did` and the `operation` you intend to perform — either `"register"` for a new provider or `"rotate_key"` to update an existing provider's DID.

  ```bash theme={null}
  curl -X POST http://127.0.0.1:8042/v1/providers/ownership-challenges \
    -H 'content-type: application/json' \
    -d '{
      "provider_did": "did:key:z6MkhaXgBZDvotD1X9gRrYkM5Xq9jYQqK6d8r8bQdE1mV2Xa",
      "operation": "register"
    }'
  ```

  The response contains the challenge you must sign:

  ```json theme={null}
  {
    "challenge_id": "a3f1c2d4-e5b6-7890-abcd-ef1234567890",
    "provider_id": "prv_8f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c",
    "provider_did": "did:key:z6MkhaXgBZDvotD1X9gRrYkM5Xq9jYQqK6d8r8bQdE1mV2Xa",
    "operation": "register",
    "challenge": "base64encodedRandomNonce==",
    "issued_at": "2025-01-15T10:00:00Z",
    "expires_at": "2025-01-15T10:05:00Z"
  }
  ```

  <Tip>
    If you omit `provider_id` from the challenge request, the node automatically generates a unique ID prefixed with `prv_`. Use the `provider_id` from the challenge response when you submit the registration request.
  </Tip>

  ### Sign the challenge with your Ed25519 private key

  Sign the `challenge` string (UTF-8 encoded) using the Ed25519 private key that corresponds to your `provider_did`. Encode the resulting 64-byte signature as standard base64.

  ```python theme={null}
  import base64
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

  # Load your Ed25519 private key
  private_key = Ed25519PrivateKey.from_private_bytes(your_key_bytes)

  # Sign the challenge string (UTF-8 encoded)
  challenge_bytes = challenge_string.encode('utf-8')
  signature = private_key.sign(challenge_bytes)
  ownership_signature = base64.b64encode(signature).decode()
  ```

  <Warning>
    Sign the raw `challenge` string exactly as returned in the response — do not base64-decode it first. The node verifies `challenge.as_bytes()` directly.
  </Warning>

  ### Register with the signed challenge

  POST to `/v1/providers/register` and include `ownership_challenge_id` and `ownership_signature` alongside your provider details. Use the `provider_id` from the challenge response.

  ```bash theme={null}
  curl -X POST http://127.0.0.1:8042/v1/providers/register \
    -H 'content-type: application/json' \
    -d '{
      "provider_id": "<PROVIDER_ID_FROM_STEP_1>",
      "provider_did": "did:key:z6MkhaXgBZDvotD1X9gRrYkM5Xq9jYQqK6d8r8bQdE1mV2Xa",
      "display_name": "Acme Labs",
      "ownership_challenge_id": "<CHALLENGE_ID_FROM_STEP_1>",
      "ownership_signature": "<BASE64_SIGNATURE_FROM_STEP_2>"
    }'
  ```

  A successful registration returns `201 Created` with the new `ProviderRecord`. Each challenge can only be consumed once — the node records `completed_at` on the challenge after it is verified and rejects any attempt to reuse it.
</Steps>

## Fetching a challenge by ID

If you need to inspect an issued challenge — for example, to check its `expires_at` or `provider_id` — retrieve it by ID:

```bash theme={null}
curl http://127.0.0.1:8042/v1/providers/ownership-challenges/<CHALLENGE_ID>
```

The response is the same `ProviderOwnershipChallenge` object returned when the challenge was created.

## Key rotation challenges

The same challenge flow applies when rotating a provider's DID key. Request a challenge with `"operation": "rotate_key"` and include the `provider_id` of the existing provider you are updating:

```bash theme={null}
curl -X POST http://127.0.0.1:8042/v1/providers/ownership-challenges \
  -H 'content-type: application/json' \
  -d '{
    "provider_id": "acme-labs",
    "provider_did": "did:key:z6MkNewKeyABCDEF...",
    "operation": "rotate_key"
  }'
```

Sign the returned challenge with the **new** key's private key, then include the challenge ID and signature in the rotation request. See [Rotate or Revoke a Provider's DID Key](/guides/rotate-revoke-provider) for the complete rotation flow.

## Challenge request fields

<ParamField body="provider_did" type="string" required>
  The Ed25519 `did:key` DID you want to associate with the provider. For `rotate_key` operations, this is the **new** DID you are rotating to.
</ParamField>

<ParamField body="operation" type="string" required>
  The operation you intend to complete after signing. Must be `"register"` or `"rotate_key"`.
</ParamField>

<ParamField body="provider_id" type="string">
  The provider ID to associate with this challenge. Optional for `register` operations — if omitted, the node generates a unique ID. Required for `rotate_key` operations to identify the existing provider being updated.
</ParamField>

<Warning>
  Challenges expire after 5 minutes (configurable via `SERVICENET_PROVIDER_CHALLENGE_TTL_SECS` on the node). If your challenge expires before you complete the registration or rotation, request a new one. Expired challenges are rejected with `400 Bad Request`.
</Warning>
