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:
| Thing | Value |
|---|---|
Federation ConnectID | aca7266d-08db-4b4a-b550-6d686e9fa1cd |
Authorisation domain ConnectID | 3414e814-cc23-4af1-ba8a-14b5def25278 (the ONLY domain with a FederationID) |
| Authority | dd50cdae-67a0-4769-9fc4-14892de1fa25 |
Org ConnectID Dev Mockbanks | 582e9f1e-7a4d-44c1-9499-b3b117ea3a27 |
| Trust mark host | trustmark.directory.dev.connectid.com.au |
| Directory-hosted RP host | rp.directory.dev.connectid.com.au |
| Keystore host | keystore.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-90ffad8d8ccbholds 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'sopenid_relying_partybucket is empty even though its org holdsRP-CORE,RP-CLAIM-*and other RP roles. Anopenid_providersubject gets onlyopenid_providerrows.
So the three conditions for a metadata row to reach the wire are:
- The role is
RoleType: Federation. Directory-type roles are silently excluded (proof in §3.3). - The role lives in a domain with a non-null
FederationID. Roles in unrooted domains are silently excluded (proof in §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:
OpenidFederationEnabled | OpenidFederationEntityManagementType | Count |
|---|---|---|
true | openid_entity_federation_managed | 3,384 |
false | (field absent) | 97 |
true | openid_entity_self_managed | 1 (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:
| Field | Self-managed (16a4821b…) | Federation-managed (3166adf2…) |
|---|---|---|
OpenidFederationEnabled | true | true |
OpenidFederationEntityManagementType | openid_entity_self_managed | openid_entity_federation_managed |
ClientId | https://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) |
RtsClientCreated | false | true |
| Present only here | ClientUri, Environment | PolicyUri, TermsOfServiceUri |
LogoUri | participant-hosted | data.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:
| Dimension | Values in use | Count |
|---|---|---|
PolicyOperator | value | 90 |
add | 2 | |
default, one_of, subset_of, superset_of, essential | 0 | |
EntityType | openid_relying_party | 90 |
openid_provider | 2 | |
oauth_authorization_server, oauth_client | 0 | |
DataType | string | 89 |
boolean | 3 | |
number | 0 |
Two things follow directly.
oauth_authorization_serveris used nowhere in a working deployment. CBA's single existing metadata row is written against exactly that EntityType, and CBA's subordinate statements emit onlyfederation_entity,openid_providerandopenid_relying_partybuckets. ConnectID emits the same three buckets. Authoring capability rows againstoauth_authorization_serveris authoring into a bucket that does not exist. Useopenid_relying_partyfor agent identities andopenid_providerfor provider-side policy.one_ofhas no live precedent. CBA's one row uses it. It may work, but nothing on a mature instance exercises it. Prefervalueandadd, 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 statement | Federation roles held | Resulting 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-CORE | as 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.claimsacross four roles → one deduplicated array. Adding a role that repeats a claim another role already sets is safe. valuedoes not behave like OpenID Federation's replace-onlyvalueoperator across roles. Four roles each settingclaimswithvalueproduce 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"]}. Compare70a885dc(CIBA-RAR only) against76cf50f2(CIBA-RAR + RP-CORE): same effective value, different JSON type. Any consumer parsingmetadata_policymust 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:
| Pattern | Example roles | Rows each | What it encodes |
|---|---|---|---|
| Baseline | RP-CORE | 4 | scope openid, response_types [code], grant_types [authorization_code], claims [txn] |
| Security profile | RP-FAPI2-MTLS | 11 | the whole FAPI2 posture: token_endpoint_auth_method private_key_jwt, *_signing_alg PS256, three booleans |
| One claim per role | RP-CLAIM-EMAIL, RP-CLAIM-NAME, RP-CLAIM-BIRTHDATE, RP-CLAIM-PHONE_NUMBER, RP-CLAIM-ADDRESS, RP-CLAIM-CARD_PRIMARY_ACCOUNT_NUMBER | 1 to 3 | claims ["email"] etc. |
| Verified claim, two rows | RP-CLAIM-OVER18/21/25/65/16, RP-CLAIM-CBA_LOYALTY, RP-CLAIM-BENEFICIARY_ACCOUNT_* | 2 | claims ["verified_claims"] and claims_in_verified_claims ["over18"], the gate needs both |
| Channel / grant | RP-CHANNEL-CIBA, RP-CHANNEL-CIBA-GRANT | 1 | backchannel_token_delivery_mode poll; grant_types [urn:openid:params:grant-type:ciba] |
| Rich authorization request type | RP-RAR-PAYMENT_AUTHORISATION, RP-RAR-OPENID_CREDENTIAL, payments-v1 | 1 | authorization_details_types ["3ds:payment_authorisation"] |
| Directory-internal, never federated | AUTH, USER-MANAGEMENT, CERTIFICATE-MANAGEMENT, WORKBENCH, RS, IDP | 1 | scope 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
- Create the AuthorisationServer under the org (
POST …/authorisationservers, inline base64 SVG logo). - Deploy the OP so
{Issuer}/.well-known/openid-configurationreturns JSON with a mandatoryissuerkey, the directory fetches and validates it. - GET the full AS record, add
Issuer+OpenIDDiscoveryDocument, PUT the whole record back. - Get the OP's federation signing public key into the org keyset (§5).
- Serve
{Issuer}/.well-known/openid-federationasapplication/entity-statement+jwt, signed under that same kid, self-asserting the key in its ownjwks, withauthority_hintspointing at the CBA authority, andclient_registration_types_supported: ["automatic"]inmetadata.openid_provider. - Give the org an Active Federation-role claim so it lists and gets a mark.
- Verify with
/fetchthen/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:
keyType | use | Status | kid | x5c |
|---|---|---|---|---|
resource_server_signing | sig | Inactive | BmxI5DaC4gn9rkL_UnBX0h6-hrOVHwCGYgK4HI6j97o | yes |
resource_server_signing | sig | Inactive | 6ivDFG2wmvPwftT6U63r4vX6qthO4OmV-4zpmQcJOUE | yes |
resource_server_signing | sig | Active | D_HO-GHyLbxjy5z7ibSUkaouMbyQwSvAhc8YCngWN00 | yes |
resource_server_signing_key | sig | Active | 9cb8077729fe858841e767000bc9c4e0540210346729e619debeef5d700aba58 | no |
resource_server_signing_key | sig | Active | af3ad4313c46d56c8ebc9869b0b414e9f5945cb40a6e2530e1bb88bdf9a1f033 | no |
resource_server_signing_key | sig | Active | e4f694620d365347598db0c15679cb0d12d7e8cd82437a78490832e93cd23a48 | no |
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 nox5c. The CSR route (resource_server_signing) produces a base64url kid equal tox5thashS256, with anx5c. Exactly as the skill documents. 9cb8077729fe…is bank1's entity-configuration signing kid. It is in the org keyset as a BYOresource_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. SoftwareStatementIdslists 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:
| Subject | Vouched jwks contents | Count |
|---|---|---|
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-9049bd341ecf4be28dcfe95f41274d29 | 5 |
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. 0f5a7bbe → rtssigning 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):
TypeID | TypeName | ResourceType | Status |
|---|---|---|---|
dba0b603-a631-4c6d-9027-aa3850f60a54 | Redirect | authorisationServer | Active |
1e7080d2-938e-4e88-bf88-e0fd8074b08f | ConnectID Claims | authorisationServer | Active |
e44c4e11-658d-41c8-9b62-583b9aeed2c3 | ConnectID | authorisationServer | Active |
8321fe9a-868a-4cee-a808-1dc1c867493a | TDIF Accreditation | authorisationServer | Active |
1d3f5099-8f1c-48fa-89a2-9705f1072779 | Channel | authorisationServer | Active |
c9028671-60e1-416f-ae85-f7d967e7e650 | Test Certification | softwareStatement | Active |
09a36fd9-c31c-46a0-bcf2-992f7d03265b | Test -dev-release-oct25 | softwareStatement | Inactive |
Variants carry VariantID, VariantName, TypeID, CertificationDurationDays, Status. Examples: Redirect → FAPI2 Adv. OP w/Private Key, PAR (3650 days); Channel → web, 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
expmeans 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.
metadatacomes back resolved, with policy applied. For0f5a7bbetheopenid_relying_partyobject 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_markscomes back resolved, 4 entries, each a full signedtrust-mark+jwt.trust_chainhas 4 entries.- Only the requested
entity_typeappears inmetadata(bank1 resolved asopenid_providerreturned only that one bucket, though its entity configuration declares three).
Parameter tolerance, all OBSERVED 200:
| Variation | Result |
|---|---|
trust_anchor = the federation entity URL | 200 |
trust_anchor = the authority URL | 200 |
entity_type omitted entirely | 200 |
Called on the federation entity's /resolve instead of the authority's | 200 |
That is more forgiving than the skill implies. trust_anchor is still required.
9. Checklist to run against CBA, in order
- 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. - Author capability roles as
RoleType: Federation, in the federation-rootedCBA-Globaldomain, with rows againstopenid_relying_partyusingvalue(oradd). Neveroauth_authorization_server. - Model the envelope on the
authorization_details_typespattern: one role, one row, one capability, composed by union. - Register agent identities as
openid_entity_federation_managedand let the directory assignClientIdand host the entity configuration. - 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. - Do not invest in certification types or API families. Neither is load-bearing.
- Build every
metadata_policyconsumer to accept both a scalar and an array for the same claim.
10. What I could not determine
- Whether an authorisation server's
openid_providerpolicy comes from the org's claims or from somewhere else. TheVCI-ISSUERcorrelation is exact but it is one data point and a write would be needed to falsify it. - Whether
one_of,subset_of,superset_of,defaultandessentialwork at all. Zero live examples across 91 rows. CBA's one existing row usesone_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 tomrk-9049bd34…here, but only an inference. - Why
GET …/softwarestatements/{ss}/certifications403s. It 403s on both instances with the same session, so it is session-scoped rather than deployment-scoped, but I could not separateCertificationManager: falsefrom 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.