Your values Fill in your own to see them in every example
On the app’s page in the hub, under Credentials.
That doesn’t look right yet. For example: q3Jx8vN2mZ0bT5sLwK7yHc4dR1fGaE9uPiO6nVjXkMs
Saved in this browser only, for every page of these docs. Nothing you type leaves this page.
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).
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) |
confirmation | method (same_device_passkey or phone_approval), and for a phone, device and key_custody |
The checks
- The signature verifies against the JWKS, and
typisverification+jwt. issishttps://id.trustaige.comandaudholds your client ID.exphasn’t passed.txn_hashequals the onebc-authorizereturned for this pending change, andauthorization_detailsmatches 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.jtiis theauth_req_idyou stored, and you haven’t accepted it before.subis the person signed in to your app who made the change.confirmation.methodis at least as strong as the change needs, if you require one:phone_approvalis stronger thansame_device_passkey.
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}`);
}
}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
}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 and
keyfunc (keyfunc.NewDefaultCtx
with the JWKS URL); the Python sample uses PyJWT
with cryptography for EdDSA.
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}`);
}
}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
}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}") 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.