# Signing a Comms.ID request token for direct HTTP

Every hosted call carries `Authorization: Bearer <token>`: a short-lived EdDSA (Ed25519) JWT
signed with your app's private key. The private key stays on your server; Comms.ID only ever
sees the public key you registered in the Companion App. The npm clients and
`npx comms-id token <product>` sign for you; this page is for any other language.

The token has the header `{"alg":"EdDSA"}` and these claims:

| Claim | Value |
| --- | --- |
| `iss` | your app id (`COMMS_ID_CLIENT_ID`) |
| `aud`, `scope` | the operation's `x-comms-id-auth` in its OpenAPI document, for example Company: `asic-registers`, `asic-registers:read` |
| `sub`, `jti` | a new random UUID for each token |
| `iat`, `exp` | now, and now plus at most 120 seconds |

Make a new token for each request (or reuse one for at most its two minutes). Each example
reads `COMMS_ID_CLIENT_ID` and `COMMS_ID_PRIVATE_JWK` (the private JWK you generated) from the
environment and prints one token for the audience and scope you pass.

```sh
TOKEN=$(python3 sign.py asic-registers asic-registers:read)
curl -s https://api.comms.id/company/v1/lookup \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"acn":"004085616"}'
```

Use `https://api-test.comms.id` with a development app's key for TEST (synthetic replies).

## Python

```python
"""Sign a Comms.ID request token (EdDSA JWT) with your app's private key.

Requires: pip install cryptography
Environment: COMMS_ID_CLIENT_ID (your app id), COMMS_ID_PRIVATE_JWK (the private JWK you
generated; it never leaves your server). Usage: python3 sign.py <audience> <scope>
Each operation's audience and scope are in its OpenAPI document under x-comms-id-auth.
"""
import base64, json, os, sys, time, uuid
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey


def b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()


def token(client_id: str, private_jwk: dict, audience: str, scope: str, lifetime: int = 120) -> str:
    seed = base64.urlsafe_b64decode(private_jwk["d"] + "=" * (-len(private_jwk["d"]) % 4))
    key = Ed25519PrivateKey.from_private_bytes(seed)
    now = int(time.time())
    header = b64url(json.dumps({"alg": "EdDSA"}, separators=(",", ":")).encode())
    claims = {"iss": client_id, "sub": str(uuid.uuid4()), "jti": str(uuid.uuid4()),
              "aud": audience, "scope": scope, "iat": now, "exp": now + min(lifetime, 120)}
    payload = b64url(json.dumps(claims, separators=(",", ":")).encode())
    signing_input = f"{header}.{payload}".encode()
    return f"{header}.{payload}.{b64url(key.sign(signing_input))}"


if __name__ == "__main__":
    print(token(os.environ["COMMS_ID_CLIENT_ID"].strip(), json.loads(os.environ["COMMS_ID_PRIVATE_JWK"]), sys.argv[1], sys.argv[2]))
```

## Go

```go
// Sign a Comms.ID request token (EdDSA JWT) with your app's private key. Standard library only.
//
// Environment: COMMS_ID_CLIENT_ID (your app id), COMMS_ID_PRIVATE_JWK (the private JWK you
// generated; it never leaves your server). Usage: go run sign.go <audience> <scope>
// Each operation's audience and scope are in its OpenAPI document under x-comms-id-auth.
package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"time"
)

func uuid4() string {
	b := make([]byte, 16)
	_, _ = rand.Read(b)
	b[6] = (b[6] & 0x0f) | 0x40
	b[8] = (b[8] & 0x3f) | 0x80
	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}

func Token(clientID string, privateJWK map[string]string, audience, scope string) (string, error) {
	seed, err := base64.RawURLEncoding.DecodeString(privateJWK["d"])
	if err != nil || len(seed) != ed25519.SeedSize {
		return "", fmt.Errorf("COMMS_ID_PRIVATE_JWK is not an Ed25519 private key")
	}
	key := ed25519.NewKeyFromSeed(seed)
	now := time.Now().Unix()
	header, _ := json.Marshal(map[string]string{"alg": "EdDSA"})
	claims, _ := json.Marshal(map[string]any{
		"iss": clientID, "sub": uuid4(), "jti": uuid4(),
		"aud": audience, "scope": scope, "iat": now, "exp": now + 120,
	})
	signingInput := base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(claims)
	return signingInput + "." + base64.RawURLEncoding.EncodeToString(ed25519.Sign(key, []byte(signingInput))), nil
}

func main() {
	var jwk map[string]string
	if err := json.Unmarshal([]byte(os.Getenv("COMMS_ID_PRIVATE_JWK")), &jwk); err != nil {
		fmt.Fprintln(os.Stderr, "COMMS_ID_PRIVATE_JWK is not JSON")
		os.Exit(1)
	}
	token, err := Token(strings.TrimSpace(os.Getenv("COMMS_ID_CLIENT_ID")), jwk, os.Args[1], os.Args[2])
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Println(token)
}
```

## PHP

```php
<?php
// Sign a Comms.ID request token (EdDSA JWT) with your app's private key. Needs PHP's sodium
// extension (bundled since PHP 7.2).
//
// Environment: COMMS_ID_CLIENT_ID (your app id), COMMS_ID_PRIVATE_JWK (the private JWK you
// generated; it never leaves your server). Usage: php sign.php <audience> <scope>
// Each operation's audience and scope are in its OpenAPI document under x-comms-id-auth.

function comms_id_b64url(string $data): string {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function comms_id_uuid4(): string {
    $b = random_bytes(16);
    $b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
    $b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}

function comms_id_token(string $clientId, array $privateJwk, string $audience, string $scope): string {
    $seed = base64_decode(strtr($privateJwk['d'], '-_', '+/'), true);
    if ($seed === false || strlen($seed) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) {
        throw new RuntimeException('COMMS_ID_PRIVATE_JWK is not an Ed25519 private key');
    }
    $secret = sodium_crypto_sign_secretkey(sodium_crypto_sign_seed_keypair($seed));
    $now = time();
    $header = comms_id_b64url(json_encode(['alg' => 'EdDSA']));
    $payload = comms_id_b64url(json_encode([
        'iss' => $clientId, 'sub' => comms_id_uuid4(), 'jti' => comms_id_uuid4(),
        'aud' => $audience, 'scope' => $scope, 'iat' => $now, 'exp' => $now + 120,
    ]));
    $signature = sodium_crypto_sign_detached("$header.$payload", $secret);
    return "$header.$payload." . comms_id_b64url($signature);
}

if (PHP_SAPI === 'cli' && realpath($argv[0]) === __FILE__) {
    echo comms_id_token(trim(getenv('COMMS_ID_CLIENT_ID')), json_decode(getenv('COMMS_ID_PRIVATE_JWK'), true), $argv[1], $argv[2]), "\n";
}
```

Each example is tested against the production verifier: its token is accepted, and a token
for another audience or with a changed signature is refused (platform
`infra/signing-examples.test.ts`).
