# Check the proof

> Check a Countersign proof on your server before you act. Every claim it carries, the checks to make, and code that makes them.

Source: https://docs.trustaige.com/countersign/proof

Replace these placeholders with your organization's own values:

- `<your-client-id>`: App client ID, for example `q3Jx8vN2mZ0bT5sLwK7yHc4dR1fGaE9uPiO6nVjXkMs`. On the app’s page in the hub, under Credentials.

The proof is a JWT signed with the keys at
`https://id.trustaige.com/.well-known/jwks.json`, with the algorithm your app
registered for its ID tokens (`EdDSA` unless you chose another). Its header
`typ` is `verification+jwt`, so it can never pass as an ID token
([RFC 8725 §3.11](https://www.rfc-editor.org/rfc/rfc8725#section-3.11)).
Check all of it before you change anything: a proof that fails one check is
no proof.

## What it carries

| Claim | |
|---|---|
| `iss` | `https://id.trustaige.com` |
| `aud` | Your app's client ID |
| `sub` | The person: the same `sub` as their ID token for your app |
| `jti` | The `auth_req_id` this proof answers |
| `iat`, `nbf`, `exp` | Valid for two minutes from the person's answer |
| `auth_time` | When the person answered |
| `authorization_details` | The action, as Trustaige received it |
| `txn_hash` | The same value `bc-authorize` returned for the request |
| `binding_message`, `nonce` | Echoed, when you sent them |
| `acr`, `amr` | How strongly the person proved it was them; `amr` includes `mca` for a phone approval ([RFC 8176](https://www.rfc-editor.org/rfc/rfc8176)) |
| `confirmation` | `method` (`same_device_passkey` or `phone_approval`), and for a phone, `device` and `key_custody` |

## The checks

1. The signature verifies against the JWKS, and `typ` is `verification+jwt`.
2. `iss` is `https://id.trustaige.com` and `aud` holds your client ID.
3. `exp` hasn't passed.
4. `txn_hash` equals the one `bc-authorize` returned for this pending change,
   and `authorization_details` matches your record of it by value. Compare the
   parsed JSON, not the bytes: another JSON library may write the same action
   differently, so never rebuild the hash yourself.
5. `jti` is the `auth_req_id` you stored, and you haven't accepted it before.
6. `sub` is the person signed in to your app who made the change.
7. `confirmation.method` is at least as strong as the change needs, if you
   require one: `phone_approval` is stronger than `same_device_passkey`.

```js title="check-proof.js"
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { isDeepStrictEqual } from 'node:util';

const jwks = createRemoteJWKSet(new URL('https://id.trustaige.com/.well-known/jwks.json'));
const strength = { same_device_passkey: 1, phone_approval: 2 };

export async function checkProof(proof, change) {
  const { payload } = await jwtVerify(proof, jwks, {
    issuer: 'https://id.trustaige.com',
    audience: '<your-client-id>',
    typ: 'verification+jwt',
  });
  if (payload.txn_hash !== change.txnHash) throw new Error('the proof is for a different action');
  if (!isDeepStrictEqual(payload.authorization_details, change.details)) throw new Error('the proof is for a different action');
  if (payload.jti !== change.authReqId) throw new Error('the proof answers a different request');
  if (!(await usedProofs.add(payload.jti))) throw new Error('the proof was already used');
  if (payload.sub !== change.sub) throw new Error('the proof was made by someone else');
  if ((strength[payload.confirmation.method] ?? 0) < (strength[change.required] ?? 1)) {
    throw new Error(`this change needs ${change.required}`);
  }
}
```

```go title="proof.go"
type proofClaims struct {
	AuthorizationDetails json.RawMessage `json:"authorization_details"`
	TxnHash              string          `json:"txn_hash"`
	Confirmation         struct {
		Method string `json:"method"`
	} `json:"confirmation"`
	jwt.RegisteredClaims
}

var strength = map[string]int{"same_device_passkey": 1, "phone_approval": 2}

func checkProof(ctx context.Context, jwks keyfunc.Keyfunc, proof string, change Change) error {
	claims := &proofClaims{}
	tok, err := jwt.ParseWithClaims(proof, claims, jwks.Keyfunc,
		jwt.WithIssuer("https://id.trustaige.com"),
		jwt.WithAudience("<your-client-id>"),
		jwt.WithExpirationRequired(),
		jwt.WithValidMethods([]string{"EdDSA", "ES256", "RS256", "PS256"}))
	if err != nil {
		return fmt.Errorf("the proof is not valid: %w", err)
	}
	var got, want any
	_ = json.Unmarshal(claims.AuthorizationDetails, &got)
	_ = json.Unmarshal(change.Details, &want)
	switch {
	case tok.Header["typ"] != "verification+jwt":
		return errors.New("not a Countersign proof")
	case claims.TxnHash != change.TxnHash || !reflect.DeepEqual(got, want):
		return errors.New("the proof is for a different action")
	case claims.ID != change.AuthReqID:
		return errors.New("the proof answers a different request")
	case !usedProofs.Add(ctx, claims.ID):
		return errors.New("the proof was already used")
	case claims.Subject != change.Sub:
		return errors.New("the proof was made by someone else")
	case strength[claims.Confirmation.Method] < strength[change.Required]:
		return fmt.Errorf("this change needs %s", change.Required)
	}
	return nil
}
```

```python title="proof.py"
import jwt

jwks = jwt.PyJWKClient("https://id.trustaige.com/.well-known/jwks.json")
STRENGTH = {"same_device_passkey": 1, "phone_approval": 2}

def check_proof(proof: str, change) -> None:
    if jwt.get_unverified_header(proof).get("typ") != "verification+jwt":
        raise ValueError("not a Countersign proof")
    key = jwks.get_signing_key_from_jwt(proof)
    claims = jwt.decode(
        proof, key.key,
        algorithms=["EdDSA", "ES256", "RS256", "PS256"],
        audience="<your-client-id>",
        issuer="https://id.trustaige.com",
        options={"require": ["exp", "jti", "sub"]},
    )
    if claims["txn_hash"] != change.txn_hash or claims["authorization_details"] != change.details:
        raise ValueError("the proof is for a different action")
    if claims["jti"] != change.auth_req_id:
        raise ValueError("the proof answers a different request")
    if not used_proofs.add(claims["jti"]):
        raise ValueError("the proof was already used")
    if claims["sub"] != change.sub:
        raise ValueError("the proof was made by someone else")
    if STRENGTH.get(claims["confirmation"]["method"], 0) < STRENGTH.get(change.required, 1):
        raise ValueError(f"this change needs {change.required}")
```

The Go sample uses [`golang-jwt`](https://github.com/golang-jwt/jwt) and
[`keyfunc`](https://github.com/MicahParks/keyfunc) (`keyfunc.NewDefaultCtx`
with the JWKS URL); the Python sample uses [PyJWT](https://pyjwt.readthedocs.io)
with `cryptography` for `EdDSA`.

## Two devices, or one

A phone approval proves the answer came from Trustaige Identity on the
person's enrolled phone, unlocked by their face or fingerprint for that one
request. It is a second device only when your page is open somewhere else.
Nothing in the request tells Trustaige where your page is open, so if a change
must be approved on a separate device, refuse a phone approval when your own
page is running on a phone.

## Try it

**Try it on yourself**, on the app's **Countersign** tab in the hub, runs
every check above against a real proof and shows each one ticked. A trial's
proof is addressed to `trustaige-countersign-trial`, which no app accepts, so
your own check 2 refuses it.
