Trust Controller CBA vendor demonstration

Evidence artefactdocs/connectid-reference-patterns.md

ConnectID dev: working reference patterns to copy onto the CBA directory

Reference instance: https://web.directory.dev.connectid.com.au ($CID) Date of survey: 2026-08-10 Method: session-cookie GETs and public unauthenticated GETs only. No POST, PUT, PATCH or DELETE was issued at any point. Session (ralph.bragg@raidiam.com, SuperUser, SystemUser, CertificationManager: false) was alive at the start and still alive at the end (GET /users/me → 200 both times).

This is a recipe, not a survey. For each thing we have to create on CBA there is a working ConnectID example, the exact fields that matter, and a runnable curl. Everything under OBSERVED was read off the wire today. Everything under INFERENCE is reasoning, labelled.


0. Setup used for every command below

CID='https://web.directory.dev.connectid.com.au'
COOK="/private/tmp/claude-501/-Users-ralphbragg-Projects-cba/0b4c59c6-02a6-44f9-b9c6-73b9c9dd0393/scratchpad/cid-cookies.txt"
G() { curl -sS "$CID$1" -b "$(cat $COOK)" -H 'accept: application/json' -H "referer: $CID/organisations"; }
Q() { python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1],safe=''))" "$1"; }

# Public federation surface (no cookie needed for any of these)
FED='https://federation.directory.dev.connectid.com.au/federation_entity/aca7266d-08db-4b4a-b550-6d686e9fa1cd'
AUTH='https://authority.directory.dev.connectid.com.au/authority/dd50cdae-67a0-4769-9fc4-14892de1fa25'

# Decode any entity statement / trust mark / resolve response
JWT() { python3 -c "
import sys,json,base64
p=sys.stdin.read().strip().split('.')
for i in (0,1):
    b=p[i]+'='*(-len(p[i])%4)
    print(json.dumps(json.loads(base64.urlsafe_b64decode(b)),indent=1))"; }

The real IDs on this instance, all OBSERVED:

ThingValue
Federation ConnectIDaca7266d-08db-4b4a-b550-6d686e9fa1cd
Authorisation domain ConnectID3414e814-cc23-4af1-ba8a-14b5def25278 (the ONLY domain with a FederationID)
Authoritydd50cdae-67a0-4769-9fc4-14892de1fa25
Org ConnectID Dev Mockbanks582e9f1e-7a4d-44c1-9499-b3b117ea3a27
Trust mark hosttrustmark.directory.dev.connectid.com.au
Directory-hosted RP hostrp.directory.dev.connectid.com.au
Keystore hostkeystore.directory.dev.connectid.com.au

1. The correction that matters most, up front

The capability-envelope spike concluded that metadata_policy only lands on a federation-managed, directory-hosted entity. That conclusion is wrong on the evidence here, and the real rule is different and easier to satisfy.

OBSERVED. https://auth.bank1.directory.dev.connectid.com.au is a self-hosted OpenID Provider. It publishes its own entity configuration, from its own host, and the directory does not host it. Its subordinate statement nevertheless carries a populated policy:

"metadata_policy": {
  "federation_entity": {},
  "openid_provider": { "scope": { "add": ["openid", "profile"] } },
  "openid_relying_party": {}
}

Those two add rows are exactly the two metadata rows on the VCI-ISSUER role, which the Mockbanks org holds as an Active claim. So policy reaches a self-hosted entity fine.

OBSERVED, and this is the actual rule. Policy is driven by the claims on the subject record, not on the organisation, and only for the bucket matching the subject's kind:

  • Software statement 3166adf2-b90b-4939-9b0a-90ffad8d8ccb holds one claim at software statement level: RP-CORE. Its org holds 12 Active Federation roles. Its subordinate statement carries exactly RP-CORE's four rows and nothing else.
  • The same is true of every one of the 14 listed RPs in that org (full table in §3.4). The policy is always precisely the union of the roles held by the software statement.
  • bank1's openid_relying_party bucket is empty even though its org holds RP-CORE, RP-CLAIM-* and other RP roles. An openid_provider subject gets only openid_provider rows.

So the three conditions for a metadata row to reach the wire are:

  1. The role is RoleType: Federation. Directory-type roles are silently excluded (proof in §3.3).
  2. The role lives in a domain with a non-null FederationID. Roles in unrooted domains are silently excluded (proof in §3.3).
  3. The claim is Active and attached to the subject, the software statement for an RP; the organisation for an authorisation server.

INFERENCE (one clean data point, not proven). For an authorisation server the source is the organisation's claims, because bank1 has no software statement of its own and the VCI-ISSUER claim exists only at org level. I could not falsify this read-only.

Why the CBA spike saw nothing. The spike attached the new role to CBA-123 at organisation level and then read the subordinate statement of a software statement (base44-app). On this evidence that combination emits nothing by design. Redo that test with the claim attached to the software statement. That is a one-call change and it is the single highest-value thing to try first on CBA.


2. Federation-managed versus self-managed software statements

2.1 What the deployment actually uses

OBSERVED, across all 48 organisations and 3,482 software statements on ConnectID dev:

OpenidFederationEnabledOpenidFederationEntityManagementTypeCount
trueopenid_entity_federation_managed3,384
false(field absent)97
trueopenid_entity_self_managed1 (and it is Inactive)

The one self-managed statement is 16a4821b-f016-4b9b-82d9-550cd535ccc1 in org 3408dc63-c123-4cfa-a0ee-328edc8c82bf ("ConnectID Wallet"). It is Inactive and does not appear in the authority's /list. There is no working self-managed software statement anywhere on this instance to copy.

Read that as a product signal. A directory with 1,420 listed relying parties uses federation-managed for every single one of them. CBA's base44-app being self-managed is the outlier, not the norm.

2.2 The field-level difference

OBSERVED, self-managed record versus a working federation-managed one:

FieldSelf-managed (16a4821b…)Federation-managed (3166adf2…)
OpenidFederationEnabledtruetrue
OpenidFederationEntityManagementTypeopenid_entity_self_managedopenid_entity_federation_managed
ClientIdhttps://connectid.dev.attest.raidiam.com/connectid-wallet (the participant's own URL)https://rp.directory.dev.connectid.com.au/openid_relying_party/3166adf2-b90b-4939-9b0a-90ffad8d8ccb (directory-assigned)
RtsClientCreatedfalsetrue
Present only hereClientUri, EnvironmentPolicyUri, TermsOfServiceUri
LogoUriparticipant-hosteddata.directory.dev.connectid.com.au/logos/{org}/softwarestatements/{ss}.png

The decisive field is ClientId. For a federation-managed statement the directory assigns ClientId = https://rp.directory.<host>/openid_relying_party/{SoftwareStatementId} and that URL IS the federation entity identifier. You do not choose it.

2.3 What federation-managed hosting actually buys you

OBSERVED. The directory serves the entity configuration itself:

curl -sS 'https://rp.directory.dev.connectid.com.au/openid_relying_party/3166adf2-b90b-4939-9b0a-90ffad8d8ccb/.well-known/openid-federation' | JWT

200, content-type: application/entity-statement+jwt, header {"alg":"PS256","kid":"mrk-9049bd341ecf4be28dcfe95f41274d29","typ":"entity-statement+jwt"}, lifetime 3600s, iss == sub. Body:

{
  "authority_hints": ["https://authority.directory.dev.connectid.com.au/authority/dd50cdae-67a0-4769-9fc4-14892de1fa25"],
  "jwks": { "keys": [ { "kid": "mrk-9049bd341ecf4be28dcfe95f41274d29", ... } ] },
  "metadata": {
    "federation_entity": { ... },
    "openid_relying_party": {
      "client_id": "https://rp.directory.dev.connectid.com.au/openid_relying_party/3166adf2-b90b-4939-9b0a-90ffad8d8ccb",
      "client_name": "test cm - 8 Jul 2024",
      "client_registration_types": ["automatic"],
      "jwks_uri": "https://keystore.directory.dev.connectid.com.au/582e9f1e-7a4d-44c1-9499-b3b117ea3a27/3166adf2-b90b-4939-9b0a-90ffad8d8ccb/application.jwks",
      "sector_identifier_uri": "https://keystore.directory.dev.connectid.com.au/582e9f1e-7a4d-44c1-9499-b3b117ea3a27/3166adf2-b90b-4939-9b0a-90ffad8d8ccb/redirect_uris.json",
      "organisation_id": "582e9f1e-7a4d-44c1-9499-b3b117ea3a27",
      "organisation_country": "AU",
      "organisation_company_register": "ABN",
      "organisation_registration_number": "80 648 970 103",
      "redirect_uris": ["https://connectid.com.au/cb"],
      "software_id": "3166adf2-b90b-4939-9b0a-90ffad8d8ccb",
      "software_version": "1.00",
      "subject_type": "public",
      "logo_uri": "...", "origin_uris": [], "software_api_webhook_uri": [], "software_flags": []
    }
  },
  "trust_marks": [ { "trust_mark": "<trust-mark+jwt>", ... } ]
}

mrk-9049bd341ecf4be28dcfe95f41274d29 is the directory's own KMS signing key for hosted entities. It is not in the org keyset (GET /organisations/{org}/certificates returns six keys and this is not one of them), yet it appears in every federation-managed RP's subordinate statement jwks so the chain validates.

Correction to the participant skill. The skill records mrk-9049bd34… as "a stale mis-upload… inert, do not copy it as a pattern". On this instance that kid is the directory's hosted-entity signer and is entirely load-bearing. I did not observe it inside any org keyset, so I cannot say whether a separate mis-uploaded copy also exists somewhere; what I can say is that the kid itself is not junk.

So the practical trade: federation-managed means zero hosting work (no well-known to serve, no signing key to hold, no key rotation) and the metadata is authored entirely by the Trust Controller. Self-managed means you serve and sign your own entity configuration and you must get a vouched key into the org keyset (§5).

2.4 Recipe for CBA

Register agent identities as federation-managed software statements. Not because policy requires it (§1 shows it does not) but because it is the only pattern with a working reference, it removes the "leaf serves HTML at its well-known" failure that currently blocks CBA's base44-app, and it makes the capability envelope a record the Trust Controller hosts and the agent cannot alter, which is exactly the TC-27 control story.

Then attach the capability roles to the software statement, not just to the org.


3. Roles and their metadata rows: the part to imitate

3.1 Enumerate

G /references/authorisationdomains/ConnectID/authorisationdomainroles
G /references/authorisationdomains/ConnectID/authorisationdomainroles/{RoleIdentifier}/metadata

OBSERVED: 42 roles in the ConnectID domain. 28 RoleType: Federation, 14 RoleType: Directory. Exclusive is false on all 42. 91 metadata rows in total.

3.2 The whole vocabulary actually in use

This is the headline for authoring CBA capability roles. Across a mature 42-role deployment:

DimensionValues in useCount
PolicyOperatorvalue90
add2
default, one_of, subset_of, superset_of, essential0
EntityTypeopenid_relying_party90
openid_provider2
oauth_authorization_server, oauth_client0
DataTypestring89
boolean3
number0

Two things follow directly.

  1. oauth_authorization_server is used nowhere in a working deployment. CBA's single existing metadata row is written against exactly that EntityType, and CBA's subordinate statements emit only federation_entity, openid_provider and openid_relying_party buckets. ConnectID emits the same three buckets. Authoring capability rows against oauth_authorization_server is authoring into a bucket that does not exist. Use openid_relying_party for agent identities and openid_provider for provider-side policy.
  2. one_of has no live precedent. CBA's one row uses it. It may work, but nothing on a mature instance exercises it. Prefer value and add, which have 92 working examples between them.

3.3 Two silent exclusions, both proven

Directory-type roles contribute nothing. Software statement 0f5a7bbe-1f36-45e6-92d2-f74ac7f37f04 holds five Active claims: AGDIS_IXP_BRIDGE, CIBA-RAR Test Role, RP-CLAIM-CBA_LOYALTY, RP-CORE (all Federation) and pki-test-harness (Directory). pki-test-harness carries scope value "directory:software:cert:management". The resolved scope is "openid", the Directory role's row is absent. It also produces no trust mark: the resolve response carries exactly four marks, one per Federation role.

Roles in an unrooted domain contribute nothing. Software statement a6860a4d-f22a-4f97-920d-2c8a1d288086 holds RP-CORE plus Test Role 1 from domain Test Domain 1, which has no FederationID. Its policy is byte-identical to a statement holding RP-CORE alone.

3.4 Union semantics, from real data

Every row below is OBSERVED. Read the roles column as what the software statement holds.

Software statementFederation roles heldResulting openid_relying_party policy
3166adf2…RP-CORE{"claims":{"value":["txn"]},"grant_types":{"value":["authorization_code"]},"response_types":{"value":["code"]},"scope":{"value":"openid"}}
70a885dc…CIBA-RAR Test Role{"authorization_details_types":{"value":["3ds:payment_authorisation"]},"claims":{"value":["txn","address","card_primary_account_number"]},"claims_in_verified_claims":{"value":["over18"]},"grant_types":{"value":["authorization_code","client_credentials","implicit"]},"response_types":{"value":["code","code id_token"]},"scope":{"value":"openid"}}
76cf50f2…CIBA-RAR + RP-COREas above but "scope":{"value":["openid"]}
4d84afd8…AGDIS_IXP_BRIDGE + CIBA-RAR + RP-CLAIM-CBA_LOYALTY + RP-CORE + RP-RAR-OPENID_CREDENTIAL (+ pki-test-harness, Directory){"authorization_details_types":{"value":["openid_credential","3ds:payment_authorisation"]},"claims":{"value":["verified_claims","txn","address","card_primary_account_number"]},"claims_in_verified_claims":{"value":["cba_loyalty","over18"]},"grant_types":{"value":["authorization_code","client_credentials","implicit"]},"response_types":{"value":["code","code id_token"]},"scope":{"value":["openid"]}}

The rules this establishes:

  • Rows union per (EntityType, ClaimName) and deduplicate. claims across four roles → one deduplicated array. Adding a role that repeats a claim another role already sets is safe.
  • value does not behave like OpenID Federation's replace-only value operator across roles. Four roles each setting claims with value produce a union, not last-write-wins. Confirmed on three independent statements.
  • The JSON shape flips between scalar and array depending on how many ROWS contributed, not how many values. One row for scope{"value":"openid"}. Two rows both carrying the identical single value "openid"{"value":["openid"]}. Compare 70a885dc (CIBA-RAR only) against 76cf50f2 (CIBA-RAR + RP-CORE): same effective value, different JSON type. Any consumer parsing metadata_policy must accept both a scalar and an array for the same claim. This is a real trap for the wiring tool and for any policy engine downstream.
  • The resolver flattens on the way out: 0f5a7bbe's policy "scope":{"value":["openid"]} appears in the resolve response as "scope": "openid".

3.5 The row shapes to copy

OBSERVED, verbatim. Note Value (singular, scalar) and Values (array) are distinct fields and a role mixes both freely.

// RP-CORE, the baseline every RP holds. Array form.
{"EntityType":"openid_relying_party","ClaimName":"response_types","PolicyOperator":"value",
 "DataType":"string","Values":["code"],"MetadataId":"be7b27ad-11a3-432e-a20b-47808a74f0ae",
 "DomainRoleName":"RP-CORE"}

// Scalar form, same role, same operator.
{"EntityType":"openid_relying_party","ClaimName":"scope","PolicyOperator":"value",
 "DataType":"string","Value":"openid","MetadataId":"a00964b0-099b-4004-8ff4-c1bc70ad82ee",
 "DomainRoleName":"RP-CORE"}

// Boolean, the value is the STRING "true", exactly as the skill warns.
{"EntityType":"openid_relying_party","ClaimName":"require_signed_request_object",
 "PolicyOperator":"value","DataType":"boolean","Value":"true",
 "MetadataId":"c557a737-8dac-466b-b2e3-938902a97062","DomainRoleName":"RP-FAPI2-MTLS"}

// The only `add` rows in the directory, and the only openid_provider rows.
{"EntityType":"openid_provider","ClaimName":"scope","PolicyOperator":"add",
 "DataType":"string","Values":["profile"],"MetadataId":"51be53da-9422-4b84-a6a4-f3f3e5debde2",
 "DomainRoleName":"VCI-ISSUER"}

3.6 How a real deployment splits policy across roles

This is the design pattern worth copying wholesale for the capability envelope. ConnectID splits into one baseline role plus many single-purpose entitlement roles, each holding one or two rows:

PatternExample rolesRows eachWhat it encodes
BaselineRP-CORE4scope openid, response_types [code], grant_types [authorization_code], claims [txn]
Security profileRP-FAPI2-MTLS11the whole FAPI2 posture: token_endpoint_auth_method private_key_jwt, *_signing_alg PS256, three booleans
One claim per roleRP-CLAIM-EMAIL, RP-CLAIM-NAME, RP-CLAIM-BIRTHDATE, RP-CLAIM-PHONE_NUMBER, RP-CLAIM-ADDRESS, RP-CLAIM-CARD_PRIMARY_ACCOUNT_NUMBER1 to 3claims ["email"] etc.
Verified claim, two rowsRP-CLAIM-OVER18/21/25/65/16, RP-CLAIM-CBA_LOYALTY, RP-CLAIM-BENEFICIARY_ACCOUNT_*2claims ["verified_claims"] and claims_in_verified_claims ["over18"], the gate needs both
Channel / grantRP-CHANNEL-CIBA, RP-CHANNEL-CIBA-GRANT1backchannel_token_delivery_mode poll; grant_types [urn:openid:params:grant-type:ciba]
Rich authorization request typeRP-RAR-PAYMENT_AUTHORISATION, RP-RAR-OPENID_CREDENTIAL, payments-v11authorization_details_types ["3ds:payment_authorisation"]
Directory-internal, never federatedAUTH, USER-MANAGEMENT, CERTIFICATE-MANAGEMENT, WORKBENCH, RS, IDP1scope directory:…, RoleType: Directory

The authorization_details_types pattern is the closest existing analogue to a capability envelope. One role, one row, one RAR type. It composes by union, it is governed centrally, and it is entirely independent of any scope the client requests. That is the shape to author CBA capability roles in.

3.7 Full metadata dump for the roles worth imitating

RP-CORE (4)
  openid_relying_party  response_types   value  string  ["code"]
  openid_relying_party  scope            value  string  "openid"
  openid_relying_party  claims           value  string  ["txn"]
  openid_relying_party  grant_types      value  string  ["authorization_code"]

RP-FAPI2-MTLS (11)
  openid_relying_party  require_signed_request_object              value  boolean "true"
  openid_relying_party  tls_client_certificate_bound_access_tokens value  boolean "true"
  openid_relying_party  require_pushed_authorization_requests      value  boolean "true"
  openid_relying_party  authorization_signed_response_alg          value  string  "PS256"
  openid_relying_party  token_endpoint_auth_signing_alg            value  string  "PS256"
  openid_relying_party  id_token_signed_response_alg               value  string  "PS256"
  openid_relying_party  request_object_signing_alg                 value  string  "PS256"
  openid_relying_party  token_endpoint_auth_method                 value  string  "private_key_jwt"
  openid_relying_party  scope                                      value  string  ["openid","profile"]
  openid_relying_party  grant_types                                value  string  ["authorization_code"]
  openid_relying_party  response_types                             value  string  ["code"]

CIBA-RAR Test Role (10)
  openid_relying_party  claims                      value string ["txn"] / ["address"] / ["card_primary_account_number"]
  openid_relying_party  claims_in_verified_claims   value string ["over18"]
  openid_relying_party  authorization_details_types value string ["3ds:payment_authorisation"]
  openid_relying_party  grant_types                 value string ["authorization_code","client_credentials","implicit"] and ["authorization_code"]
  openid_relying_party  response_types              value string ["code","code id_token"] and ["code"]
  openid_relying_party  scope                       value string "openid"

RP-CLAIM-OVER18 (2)          RP-CLAIM-CBA_LOYALTY (2)
  claims ["verified_claims"]   claims ["verified_claims"]
  claims_in_verified_claims ["over18"]   claims_in_verified_claims ["cba_loyalty"]

VCI-ISSUER (2), the only openid_provider rows, the only `add` operator
  openid_provider  scope  add  string  ["openid"]
  openid_provider  scope  add  string  ["profile"]

RP-CHANNEL-CIBA (1)   backchannel_token_delivery_mode value string "poll"
RP-CHANNEL-CIBA-GRANT (1) grant_types value string ["urn:openid:params:grant-type:ciba"]
RP-RAR-PAYMENT_AUTHORISATION (1) authorization_details_types value string ["3ds:payment_authorisation"]
RP-RAR-OPENID_CREDENTIAL (1)     authorization_details_types value string ["openid_credential"]
payments-v1 (1)                  authorization_details_types value string ["payments-v1"]
VC-WALLET (1)  token_endpoint_authentication_mechanism value string "attest_jwt_client_auth"

Roles with zero metadata rows (they exist purely to issue a trust mark, no policy): AGDIS_IXP_BRIDGE, BYO-KEYS, IDP, RS, VC-WALLET-PROVIDER, Test Auth Role, TEST-BPAYSD-587. This is a useful pattern in itself: a role can be a pure boolean accreditation with no metadata consequence.


4. A resolvable OpenID Provider: the exact recipe

Mock Bank A is the working example. OBSERVED end to end.

4.1 The AuthorisationServer record

GET $CID/organisations/582e9f1e-7a4d-44c1-9499-b3b117ea3a27/authorisationservers/f017164e-57e8-4c6a-b0cb-d031fdd8bdf7

{
  "AuthorisationServerId": "f017164e-57e8-4c6a-b0cb-d031fdd8bdf7",
  "OrganisationId": "582e9f1e-7a4d-44c1-9499-b3b117ea3a27",
  "CustomerFriendlyName": "Mock Bank A",
  "CustomerFriendlyDescription": "Mock Bank A",
  "CustomerFriendlyLogoUri": "https://web.directory.dev.connectid.com.au/logos/582e9f1e-.../...",
  "DeveloperPortalUri": "https://connectid.com.au/",
  "Issuer": "https://auth.bank1.directory.dev.connectid.com.au",
  "OpenIDDiscoveryDocument": "https://auth.bank1.directory.dev.connectid.com.au/.well-known/openid-configuration",
  "PayloadSigningCertLocationUri": "https://auth.bank1.directory.dev.connectid.com.au",
  "AutoRegistrationSupported": true,
  "AutoRegistrationNotificationWebhook": "https://auth.bank1.directory.dev.connectid.com.au/ssonboard/webhook",
  "SupportsRedirect": true, "SupportsCiba": false, "SupportsDCR": false,
  "OmitFromEcosystem": false, "Status": "Active",
  "CreatedAt": "2023-08-04T16:35:24.837Z",
  "ApiResources": [ … ], "AuthorisationServerCertifications": [ … ]
}

The three fields that make it resolvable: Issuer, OpenIDDiscoveryDocument (literally {Issuer}/.well-known/openid-configuration), and Status: Active. Confirms the skill's full-record GET-then-PUT requirement, the record has 12 top-level fields plus two arrays.

4.2 The entity configuration it must serve

curl -sS 'https://auth.bank1.directory.dev.connectid.com.au/.well-known/openid-federation' | JWT

200, content-type: application/entity-statement+jwt, 10,646 bytes. Header: {"alg":"PS256","kid":"9cb8077729fe858841e767000bc9c4e0540210346729e619debeef5d700aba58","typ":"entity-statement+jwt"}. Lifetime 7200s. iss == sub == "https://auth.bank1.directory.dev.connectid.com.au".

Top-level claims: sub, jwks, authority_hints, metadata, trust_marks, iss, iat, exp.

"authority_hints": ["https://authority.directory.dev.connectid.com.au/authority/dd50cdae-67a0-4769-9fc4-14892de1fa25"]

jwks contains exactly one key: kty RSA, alg PS256, use sig, kid 9cb8077729fe858841e767000bc9c4e0540210346729e619debeef5d700aba58, the same kid as the JWS header. metadata declares three entity types: federation_entity, openid_provider, openid_credential_issuer.

metadata.openid_provider carries exactly these 14 keys:

authorization_endpoint, client_registration_types_supported, grant_types_supported,
id_token_signing_alg_values_supported, issuer, jwks_uri,
pushed_authorization_request_endpoint, request_parameter_supported,
require_pushed_authorization_requests, response_types_supported, subject_types_supported,
token_endpoint, token_endpoint_auth_methods_supported, userinfo_endpoint

with "client_registration_types_supported": ["automatic"] present, exactly as the skill requires. Note it does NOT carry scope, scope appears only in the resolved output, injected by the VCI-ISSUER policy's add operator. That is a clean, live demonstration of policy adding metadata the leaf never asserted.

4.3 Recipe, in order

  1. Create the AuthorisationServer under the org (POST …/authorisationservers, inline base64 SVG logo).
  2. Deploy the OP so {Issuer}/.well-known/openid-configuration returns JSON with a mandatory issuer key, the directory fetches and validates it.
  3. GET the full AS record, add Issuer + OpenIDDiscoveryDocument, PUT the whole record back.
  4. Get the OP's federation signing public key into the org keyset (§5).
  5. Serve {Issuer}/.well-known/openid-federation as application/entity-statement+jwt, signed under that same kid, self-asserting the key in its own jwks, with authority_hints pointing at the CBA authority, and client_registration_types_supported: ["automatic"] in metadata.openid_provider.
  6. Give the org an Active Federation-role claim so it lists and gets a mark.
  7. Verify with /fetch then /resolve.

5. Certificates and keys: where a vouched key actually comes from

This closes the CBA open question. GET $CID/organisations/{org}/certificates is readable here and returns the org keyset. OBSERVED, six entries for Mockbanks:

keyTypeuseStatuskidx5c
resource_server_signingsigInactiveBmxI5DaC4gn9rkL_UnBX0h6-hrOVHwCGYgK4HI6j97oyes
resource_server_signingsigInactive6ivDFG2wmvPwftT6U63r4vX6qthO4OmV-4zpmQcJOUEyes
resource_server_signingsigActiveD_HO-GHyLbxjy5z7ibSUkaouMbyQwSvAhc8YCngWN00yes
resource_server_signing_keysigActive9cb8077729fe858841e767000bc9c4e0540210346729e619debeef5d700aba58no
resource_server_signing_keysigActiveaf3ad4313c46d56c8ebc9869b0b414e9f5945cb40a6e2530e1bb88bdf9a1f033no
resource_server_signing_keysigActivee4f694620d365347598db0c15679cb0d12d7e8cd82437a78490832e93cd23a48no

Record fields: ExpiryDateTime, OrganisationId, SoftwareStatementIds, Status, ValidFromDateTime, e, keyType, kid, kty, n, use, x5c, x5thashS256, x5u.

The mechanism, observed end to end:

  • The BYO PEM upload (resource_server_signing_key) produces a 64-char hex kid and no x5c. The CSR route (resource_server_signing) produces a base64url kid equal to x5thashS256, with an x5c. Exactly as the skill documents.
  • 9cb8077729fe… is bank1's entity-configuration signing kid. It is in the org keyset as a BYO resource_server_signing_key, it is the kid in bank1's JWS header, and it is in the authority's subordinate statement jwks for bank1. That is the complete vouching path, with the hex-sha256-of-DER-SPKI derivation confirmed by a real 64-hex kid on a live self-hosted OP.
  • Every key carries a public PEM at x5u = https://keystore.directory.dev.connectid.com.au/{orgId}/{kid}.pem.
  • SoftwareStatementIds lists which statements each key covers, the Active keys each cover ~42 statements, i.e. effectively the whole org. This is the org-wide scope the skill warns about, visible in the data.

Two different jwks selections, OBSERVED:

SubjectVouched jwks contentsCount
openid_provider (auth.bank1…)org-level sig keys plus per-software-statement rtssigning certs (e.g. EH2i9qXu…, which is SS 0f5a7bbe's own rtssigning key)18
openid_relying_party (rp.directory…/{ss})the 4 Active org-level sig keys plus the directory's hosted-entity key mrk-9049bd341ecf4be28dcfe95f41274d295

Note GET /organisations/{org}/certificates shows 6 keys but an AS's subordinate statement vouches 18. The org /certificates view is not the full vouching set, per-software-statement rtssigning certs are vouched but not listed there. Read them per statement: GET /organisations/{org}/softwarestatements/{ss}/certificates (e.g. 0f5a7bbertssigning EH2i9qXu… sig Active, rtstransport UrQ_XQFt… enc Active).

What this means for CBA. CBA's org keyset is empty yet its subordinate statement for base44-app vouches mrk-b9c3a7da76bf4865bcbce2e1e1a4e7d6. INFERENCE, by direct analogy with mrk-9049bd34… here: that is the CBA directory's own hosted-entity KMS key, not a participant key, and it is invisible in /organisations/{org}/certificates for exactly the same reason. If so, nothing is missing on CBA, there is simply no participant key yet because no certificate has ever been issued and no PEM uploaded. Not proven; the CBA-side test is to upload one BYO PEM and watch whether its hex kid joins that jwks.


6. Certification types and API families: we almost certainly do not need either

6.1 Certification types

OBSERVED, 7 types (GET /references/certificationtypes):

TypeIDTypeNameResourceTypeStatus
dba0b603-a631-4c6d-9027-aa3850f60a54RedirectauthorisationServerActive
1e7080d2-938e-4e88-bf88-e0fd8074b08fConnectID ClaimsauthorisationServerActive
e44c4e11-658d-41c8-9b62-583b9aeed2c3ConnectIDauthorisationServerActive
8321fe9a-868a-4cee-a808-1dc1c867493aTDIF AccreditationauthorisationServerActive
1d3f5099-8f1c-48fa-89a2-9705f1072779ChannelauthorisationServerActive
c9028671-60e1-416f-ae85-f7d967e7e650Test CertificationsoftwareStatementActive
09a36fd9-c31c-46a0-bcf2-992f7d03265bTest -dev-release-oct25softwareStatementInactive

Variants carry VariantID, VariantName, TypeID, CertificationDurationDays, Status. Examples: RedirectFAPI2 Adv. OP w/Private Key, PAR (3650 days); Channelweb, app; ConnectID Claims → one variant per claim (over18, cba_loyalty, beneficiary_account_au, …).

Certification records live inline on the AS as AuthorisationServerCertifications:

{"CertificationId":"5fab5a26-125f-41a9-8612-07e221857bfb",
 "AuthorisationServerId":"f017164e-57e8-4c6a-b0cb-d031fdd8bdf7",
 "ProfileType":"Redirect","ProfileVariant":"FAPI2 Adv. OP w/Private Key, PAR","ProfileVersion":1,
 "CertificationStatus":"Certified","CertificationURI":"https://www.example.com",
 "CertificationStartDate":"07/09/2024","CertificationExpirationDate":"05/09/2034","Status":"Active"}

bank1 holds 16 of them. None of them appears anywhere in the resolve response. The resolved openid_provider metadata carries no ProfileType, no ProfileVariant, no certification of any kind. Certifications are a directory-side conformity record only, not runtime-resolvable. Confirmed against a real, heavily certified OP.

The two softwareStatement types on this instance are both named "Test…". The entire production accreditation model here is Domain/Role trust marks, not certifications.

Correction to the CBA recon. The recon recorded GET /organisations/{org}/softwarestatements/{ss}/certifications → 403 on CBA and inferred the route might not be enabled on that deployment. It returns 403 here too, on a directory where certifications demonstrably exist and are populated. So the 403 is the session's CertificationManager: false flag, not a CBA deployment gap. Same session identity, same result, different instance, that is the control.

6.2 API families

OBSERVED: exactly one family on the whole instance.

{"ID":"b1d6da73-4e05-432d-b4aa-19318291327e","Name":"payments","Version":"1.0.0",
 "ConformanceTest":false,"Mandatory":false,"RequiresWellKnown":false,"Status":"Active",
 "CreatedAt":"2026-03-02T15:15:29.922927Z"}

It has no MetadataSchema field at all. A directory with 1,420 listed relying parties runs on one trivial API family with no metadata schema. The whole MetadataSchema / {values:[…]} / Cascade: false apparatus in the skill is unexercised here and irrelevant to a federation build. CBA having zero API families costs us nothing.


7. Trust marks: the constraint is product-wide, not CBA-specific

This is the answer that changes what we can claim, so it is stated plainly.

TMT='https://trustmark.directory.dev.connectid.com.au/trustmark/aca7266d-08db-4b4a-b550-6d686e9fa1cd/ConnectID/RP-CORE'
SUB='https://rp.directory.dev.connectid.com.au/openid_relying_party/0f5a7bbe-1f36-45e6-92d2-f74ac7f37f04'
curl -sS "$AUTH/trust-mark?trust_mark_type=$(Q "$TMT")&sub=$(Q "$SUB")" | JWT

200, typ: trust-mark+jwt, alg: PS256, kid: mrk-99ad2a45c68142a0ac368daae817fd67. Complete payload, nothing omitted:

{
  "iat": 1784975482,
  "iss": "https://authority.directory.dev.connectid.com.au/authority/dd50cdae-67a0-4769-9fc4-14892de1fa25",
  "sub": "https://rp.directory.dev.connectid.com.au/openid_relying_party/0f5a7bbe-1f36-45e6-92d2-f74ac7f37f04",
  "trust_mark_type": "https://trustmark.directory.dev.connectid.com.au/trustmark/aca7266d-08db-4b4a-b550-6d686e9fa1cd/ConnectID/RP-CORE"
}

Four claims. No exp. No id. No claims body. Identical on the mark embedded in bank1's own entity configuration, on all four marks in 0f5a7bbe's resolve response, and on the live trust-mark GET. This is a product-wide property, confirmed on a mature production-shaped deployment. It is not CBA misconfiguration and it is not something we can configure away.

Consequences, unchanged from the spike but now confirmed rather than suspected:

  • A capability envelope cannot ride inside a trust mark. It must ride in metadata_policy.
  • A verifier reading capability at call time must resolve, not merely read marks. A mark is a boolean "this subject holds this Domain/Role" and nothing more.
  • Marks are not independently expirable. No exp means revocation depends on the trust mark status endpoint. GET $AUTH/trust-mark-status?trust_mark_type=…&sub=… returns 404 {"error":"invalid_request","error_message":"uri_not_supported"} here, identical to CBA. Per OpenID Federation the status endpoint is a POST with form parameters, so this is probably correct behaviour, but I did not test the POST (read-only mandate) so trust mark status remains UNVERIFIED on both instances.

Trust mark type template, confirmed identical in shape to CBA: {TRUSTMARK_HOST}/trustmark/{FederationID}/{Domain}/{Role}, with the role name percent-encoded (ConnectID/CIBA-RAR%20Test%20Role was OBSERVED live, spaces in role names survive as %20 inside the type identifier).

What produces a mark: one Active RoleType: Federation claim in a federation-rooted domain, attached to the subject. 0f5a7bbe holds five Active claims and gets exactly four marks; the missing one is the Directory-type pki-test-harness.


8. Resolve: it works, and here is the shape

RP='https://rp.directory.dev.connectid.com.au/openid_relying_party/0f5a7bbe-1f36-45e6-92d2-f74ac7f37f04'
curl -sS "$AUTH/resolve?sub=$(Q "$RP")&trust_anchor=$(Q "$FED")&entity_type=openid_relying_party" | JWT

200, content-type: application/resolve-response+jwt, typ: resolve-response+jwt, kid: mrk-99ad2a45c68142a0ac368daae817fd67. Lifetime 287s.

Claims: exp, iat, iss, metadata, sub, trust_chain, trust_marks.

  • metadata comes back resolved, with policy applied. For 0f5a7bbe the openid_relying_party object carries the leaf's own fields (client_id, client_name, jwks_uri, redirect_uris, organisation_*, software_id, …) plus the four policy-injected claims: authorization_details_types, claims, claims_in_verified_claims, grant_types, response_types, scope.
  • trust_marks comes back resolved, 4 entries, each a full signed trust-mark+jwt.
  • trust_chain has 4 entries.
  • Only the requested entity_type appears in metadata (bank1 resolved as openid_provider returned only that one bucket, though its entity configuration declares three).

Parameter tolerance, all OBSERVED 200:

VariationResult
trust_anchor = the federation entity URL200
trust_anchor = the authority URL200
entity_type omitted entirely200
Called on the federation entity's /resolve instead of the authority's200

That is more forgiving than the skill implies. trust_anchor is still required.


9. Checklist to run against CBA, in order

  1. Attach a role claim to the software statement, not the org, and re-read /fetch. This is the spike's missing variable and the cheapest test we have.
  2. Author capability roles as RoleType: Federation, in the federation-rooted CBA-Global domain, with rows against openid_relying_party using value (or add). Never oauth_authorization_server.
  3. Model the envelope on the authorization_details_types pattern: one role, one row, one capability, composed by union.
  4. Register agent identities as openid_entity_federation_managed and let the directory assign ClientId and host the entity configuration.
  5. For any self-hosted OP: upload the federation signing public key as a BYO PEM to resource_server_signing_key, confirm the hex kid appears in /fetch, and sign the entity configuration under that kid.
  6. Do not invest in certification types or API families. Neither is load-bearing.
  7. Build every metadata_policy consumer to accept both a scalar and an array for the same claim.

10. What I could not determine

  • Whether an authorisation server's openid_provider policy comes from the org's claims or from somewhere else. The VCI-ISSUER correlation is exact but it is one data point and a write would be needed to falsify it.
  • Whether one_of, subset_of, superset_of, default and essential work at all. Zero live examples across 91 rows. CBA's one existing row uses one_of, so we are relying on it with no working precedent anywhere.
  • Trust mark status. GET is rejected on both instances; the POST form was not tested under the read-only mandate.
  • Whether the CBA subordinate statement's mrk-b9c3a7da… is the directory's own hosted-entity key. Strong analogy to mrk-9049bd34… here, but only an inference.
  • Why GET …/softwarestatements/{ss}/certifications 403s. It 403s on both instances with the same session, so it is session-scoped rather than deployment-scoped, but I could not separate CertificationManager: false from a route-level restriction read-only.
  • Whether a self-managed software statement can ever be made to resolve. The single self-managed statement on this instance is Inactive and unlisted, so there is no working example of that path anywhere on ConnectID dev.

Back to the coverage page