Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ policies:
| `etsi` | Service types, statuses, countries, credential types (audit) | ETSI TSL |
| `lote` | Entity types, statuses, territories, credential types (audit) | ETSI LoTE |
| `oidfed` | Entity types, trust marks, credential type mapping | OpenID Federation |
| `did` | Allowed domains, verifiable history | DID Web, DID Web VH |
| `decentralized_identifier` | Allowed domains, verifiable history | DID Web, DID Web VH |
| `mdociaca` | Issuer allowlist, IACA endpoint | mDOC IACA |
| `fidomds3` | AAGUID allowlist/blocklist | FIDO MDS3 |
| `constraints` (`require_key_binding`, `allowed_key_types`) | Generic; used by `emrtd-document-signer` | eMRTD |
Expand Down Expand Up @@ -821,6 +821,51 @@ When enabled, the response includes the relying party's identity extracted from

X5C enrichment is supported by both ETSI TSL and LoTE registries.

### Verifier Identifiers (`verifier_ids`)

A wallet that answers a request with a pseudonym function needs a stable *verifier identifier* string for the relying party. Go-Trust can return those in the evaluation response `context.verifier_ids`, derived only from material it has authenticated, so the wallet does not have to parse certificates, resolve DIDs or walk federation chains itself. The member is informational (never critical) and **off by default**: existing responses are unchanged unless a policy enables it.

```yaml
policies:
policies:
credential-verifier:
constraints:
return_verifier_ids: [semantic, x509_san_dns] # unknown type names are rejected at startup
```

```json
{
"decision": true,
"context": {
"verifier_ids": [
{"verifier_id_type": "semantic", "verifier_id": "semantic_id:VATSE-556000000101", "basis": "x5c-chain"},
{"verifier_id_type": "x509_san_dns", "verifier_id": "rp.example.com", "basis": "client-id-binding"}
]
}
}
```

The response lists everything the policy enables and go-trust can derive; a type it cannot establish is omitted. A request may narrow the response to one type with the optional request `context` member `verifier_id_type` (for example `"context": {"verifier_id_type": "x509_san_dns"}`). The hint only narrows: it cannot enable a type the policy did not. Clients treat `verifier_id_type` as opaque; the order of entries carries no meaning.

Types go-trust can derive (the exact value form is fixed per type). Type names follow the OpenID4VP `client_id` scheme names (`x509_san_dns`, `x509_san_uri`, `decentralized_identifier`, `openid_federation`); `semantic` is the only exception:

| Type | Value | Basis | Registries |
|---|---|---|---|
| `semantic` | `semantic_id:` + the ETSI semantic identifier from the **access certificate** leaf (ETSI TS 119 475 5.1.3 legal persons: `organizationIdentifier`; 5.1.5 natural persons: `serialNumber`). Must match `<3 letters><2 letters>-<value>`; the value is kept verbatim. | `x5c-chain` | x5c registries (`etsi`, `lote`) |
| `x509_san_dns` | the `client_id` of an `x509_san_dns` request, which must equal a dNSName SAN of the leaf (ASCII case-insensitively): the A-label form, lower case, one trailing dot removed; see the rules below | `client-id-binding` | x5c registries |
| `x509_san_uri` | the `client_id` of an `x509_san_uri` request, byte-for-byte (no case folding, no slash normalisation, not re-serialised through `net/url`) | `client-id-binding` | x5c registries |
| `decentralized_identifier` | the full DID of the resolved DID document, without fragment | `did-resolution` | `didweb`, `didwebvh`, `didjwks`, `did` |
| `openid_federation` | the Entity ID as in the validated trust chain, byte-for-byte | `oidf-trust-chain` | `oidfed` |

Rules:

- **Fail closed.** Nothing is emitted for a deny decision, when the matched profile's validation failed (normally only a warning), or when the identifier was not verified: `x509_san_dns`/`x509_san_uri` need `VerifyLeafBinding` to pass, `decentralized_identifier` needs a successful resolution and a matching key, `openid_federation` needs a validated trust chain and a matching key. An unverified `client_id` is never echoed. Resolution-only responses carry no `verifier_ids`.
- **`x509_san_dns` is the A-label, lower case.** Internationalised names are supported: a certificate carries them as A-labels (`xn--...`, RFC 5280), and a `client_id` that carries the A-label binds to the SAN and yields the A-label as the value. The `client_id` is compared with the SAN ASCII case-insensitively (the value emitted is always lower case, one trailing dot removed). A U-label `client_id` (`bücher.example`) is not converted: the PDP does no IDNA processing, because IDNA2003, IDNA2008 and UTS #46 differ for some names and a difference would be a different pseudonym, so no entry is emitted and the verifier must send the A-label. Also omitted, never repaired: an empty label (`a..b`, a leading dot, more than one trailing dot), whitespace or control characters, a label over 63 or a name over 253 octets, a wildcard `client_id`, and a `client_id` that binds only through a wildcard SAN (`*.example.com`), which names one of many hosts under one certificate. Shared test vectors: `pkg/registry/testdata/x509_san_dns_verifier_id_vectors.json`.
- **`semantic` comes from the access certificate only.** The registration certificate is not an input today, so wallets must not assume registration-certificate precedence. A `serialNumber` that is not a semantic identifier (for example a plain certificate serial) is never emitted as `semantic`. The subject kind comes from the WRPAC policy OIDs alone: a certificate with neither a natural- nor a legal-person policy (or with both) yields no `semantic`, because its `organizationIdentifier` could be a natural person's affiliation rather than the verifier. The type must fit the subject: natural-person types (`PNO`, `IDC`, `PAS`, `TAX`, `TIN`) are read from `serialNumber` of natural-person certificates only, and refused for legal-person ones.
- **Intermediary requests** (`allow_intermediaries`): the identifier is the end relying party's. `semantic` is taken from the RP leaf; `x509_san_dns` and `x509_san_uri` are omitted because the `client_id` of a brokered request cannot be shown to name the end relying party. The intermediary's own, unvalidated chain is never a source.
- The member is carried through by the strategies that return the matching registry's own response (`first_match`, `sequential`); the aggregating `all` and `best_match` strategies and composite registries build a new response and do not carry it.
- Not offered: `did_scid`, `service` and `spki_sha256` are not implemented yet and are rejected in `return_verifier_ids`.

## Deployment

### Docker
Expand Down
13 changes: 11 additions & 2 deletions cmd/gt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,7 @@ func configurePoliciesFromConfig(cfg *config.Config, registryMgr *registry.Regis
policy.Constraints = registry.PolicyConstraints{
AllowedKeyTypes: policyCfg.Constraints.AllowedKeyTypes,
RequireKeyBinding: policyCfg.Constraints.RequireKeyBinding,
ReturnVerifierIDs: policyCfg.Constraints.ReturnVerifierIDs,
}
}

Expand Down Expand Up @@ -1373,7 +1374,12 @@ func configurePoliciesFromConfig(cfg *config.Config, registryMgr *registry.Regis
}
}

policyMgr.RegisterPolicy(policy)
// config.Validate rejects unknown verifier id types at startup;
// RegisterPolicy re-checks so no caller can install an unvalidated one.
if err := policyMgr.RegisterPolicy(policy); err != nil {
logger.Fatal("Invalid policy",
logging.F("policy", name), logging.F("error", err.Error()))
}
policyCount++

logger.Debug("Registered policy from config",
Expand All @@ -1387,7 +1393,10 @@ func configurePoliciesFromConfig(cfg *config.Config, registryMgr *registry.Regis
if cfg.Policies.DefaultPolicy != "" {
defaultPolicy := policyMgr.GetPolicy(cfg.Policies.DefaultPolicy)
if defaultPolicy != nil {
policyMgr.SetDefaultPolicy(defaultPolicy)
if err := policyMgr.SetDefaultPolicy(defaultPolicy); err != nil {
logger.Fatal("Invalid default policy",
logging.F("policy", cfg.Policies.DefaultPolicy), logging.F("error", err.Error()))
}
logger.Info("Default policy set from config",
logging.F("policy", cfg.Policies.DefaultPolicy))
} else {
Expand Down
1 change: 1 addition & 0 deletions cmd/gt/policy_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func TestConfigurePoliciesFromConfigCopiesEveryField(t *testing.T) {
Constraints: &config.PolicyConstraintsConfig{
AllowedKeyTypes: []string{"x5c"},
RequireKeyBinding: true,
ReturnVerifierIDs: []string{"semantic", "x509_san_dns"},
},
ETSI: &config.ETSIPolicyConfig{
ServiceTypes: []string{"http://uri.etsi.org/TrstSvc/Svctype/CA/QC"},
Expand Down
1 change: 1 addition & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ A few `server` settings can also be set via CLI flag (`gt -host`, `-port`, `-ext
| `policies.policies.<name>.registries` | — | string list | Registries limits evaluation to specific registry names. If empty, all registries are considered. |
| `policies.policies.<name>.constraints.require_key_binding` | — | boolean | RequireKeyBinding requires that a key be provided and validated. |
| `policies.policies.<name>.constraints.allowed_key_types` | — | string list | AllowedKeyTypes restricts accepted key types (e.g., ["x5c", "jwk"]) |
| `policies.policies.<name>.constraints.return_verifier_ids` | — | string list | ReturnVerifierIDs lists the verifier identifier types to return in the evaluation response context member verifier_ids. Supported types: semantic, x509_san_dns, x509_san_uri, decentralized_identifier, openid_federation. Empty (default) returns none. Unknown names are rejected at startup. |
| `policies.policies.<name>.oidfed.required_trust_marks` | — | string list | RequiredTrustMarks specifies trust mark types that MUST be present |
| `policies.policies.<name>.oidfed.entity_types` | — | string list | EntityTypes filters by OpenID Federation entity types |
| `policies.policies.<name>.oidfed.max_chain_depth` | — | integer | MaxChainDepth limits trust chain resolution depth |
Expand Down
30 changes: 29 additions & 1 deletion docs/swagger/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ const docTemplate = `{
]
},
"context": {
"description": "Optional context (MUST NOT be critical)",
"description": "Optional context (MUST NOT be critical). Recognised member: verifier_id_type narrows context.verifier_ids in the response to that one type",
"type": "object",
"additionalProperties": {
"type": "string"
Expand Down Expand Up @@ -333,6 +333,13 @@ const docTemplate = `{
"trust_metadata": {
"description": "TrustMetadata contains the resolved trust metadata for the subject.\nFor DID resolution: contains the DID Document\nFor OpenID Federation: contains the Entity Configuration\nThis field is set when trust_metadata is requested or for resolution-only requests.",
"type": "object"
},
"verifier_ids": {
"description": "VerifierIDs lists verifier identifiers the PDP derived from authenticated\nmaterial, for wallets that need a stable identifier string for the relying\nparty (for example as input to a pseudonym function). It is present only\nwhen the policy enables it (constraints.return_verifier_ids), the decision\nis true, and the identifier was verified. Informational, never critical.",
"type": "array",
"items": {
"$ref": "#/definitions/authzen.VerifierID"
}
}
}
},
Expand Down Expand Up @@ -421,6 +428,27 @@ const docTemplate = `{
"example": "key"
}
}
},
"authzen.VerifierID": {
"description": "A verifier identifier derived by the PDP from authenticated material",
"type": "object",
"properties": {
"basis": {
"description": "Basis states how the value was verified, for example \"x5c-chain\",\n\"client-id-binding\", \"did-resolution\" or \"oidf-trust-chain\".",
"type": "string",
"example": "x5c-chain"
},
"verifier_id": {
"description": "ID is the identifier in the canonical form defined for the type.",
"type": "string",
"example": "semantic_id:VATSE-556000000101"
},
"verifier_id_type": {
"description": "Type is an opaque type string, for example \"semantic\" or \"x509_san_dns\".",
"type": "string",
"example": "semantic"
}
}
}
},
"tags": [
Expand Down
30 changes: 29 additions & 1 deletion docs/swagger/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@
]
},
"context": {
"description": "Optional context (MUST NOT be critical)",
"description": "Optional context (MUST NOT be critical). Recognised member: verifier_id_type narrows context.verifier_ids in the response to that one type",
"type": "object",
"additionalProperties": {
"type": "string"
Expand Down Expand Up @@ -353,6 +353,13 @@
"trust_metadata": {
"description": "TrustMetadata contains the resolved trust metadata for the subject.\nFor DID resolution: contains the DID Document\nFor OpenID Federation: contains the Entity Configuration\nThis field is set when trust_metadata is requested or for resolution-only requests.",
"type": "object"
},
"verifier_ids": {
"description": "VerifierIDs lists verifier identifiers the PDP derived from authenticated\nmaterial, for wallets that need a stable identifier string for the relying\nparty (for example as input to a pseudonym function). It is present only\nwhen the policy enables it (constraints.return_verifier_ids), the decision\nis true, and the identifier was verified. Informational, never critical.",
"type": "array",
"items": {
"$ref": "#/definitions/authzen.VerifierID"
}
}
}
},
Expand Down Expand Up @@ -441,6 +448,27 @@
"example": "key"
}
}
},
"authzen.VerifierID": {
"description": "A verifier identifier derived by the PDP from authenticated material",
"type": "object",
"properties": {
"basis": {
"description": "Basis states how the value was verified, for example \"x5c-chain\",\n\"client-id-binding\", \"did-resolution\" or \"oidf-trust-chain\".",
"type": "string",
"example": "x5c-chain"
},
"verifier_id": {
"description": "ID is the identifier in the canonical form defined for the type.",
"type": "string",
"example": "semantic_id:VATSE-556000000101"
},
"verifier_id_type": {
"description": "Type is an opaque type string, for example \"semantic\" or \"x509_san_dns\".",
"type": "string",
"example": "semantic"
}
}
}
},
"tags": [
Expand Down
30 changes: 29 additions & 1 deletion docs/swagger/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ definitions:
context:
additionalProperties:
type: string
description: Optional context (MUST NOT be critical)
description: Optional context (MUST NOT be critical). Recognised member: verifier_id_type narrows context.verifier_ids in the response to that one type
type: object
resource:
allOf:
Expand Down Expand Up @@ -86,6 +86,16 @@ definitions:
For OpenID Federation: contains the Entity Configuration
This field is set when trust_metadata is requested or for resolution-only requests.
type: object
verifier_ids:
description: |-
VerifierIDs lists verifier identifiers the PDP derived from authenticated
material, for wallets that need a stable identifier string for the relying
party (for example as input to a pseudonym function). It is present only
when the policy enables it (constraints.return_verifier_ids), the decision
is true, and the identifier was verified. Informational, never critical.
items:
$ref: '#/definitions/authzen.VerifierID'
type: array
type: object
authzen.PDPMetadata:
description: Policy Decision Point metadata for service discovery
Expand Down Expand Up @@ -165,6 +175,24 @@ definitions:
example: key
type: string
type: object
authzen.VerifierID:
description: A verifier identifier derived by the PDP from authenticated material
properties:
basis:
description: |-
Basis states how the value was verified, for example "x5c-chain",
"client-id-binding", "did-resolution" or "oidf-trust-chain".
example: x5c-chain
type: string
verifier_id:
description: ID is the identifier in the canonical form defined for the type.
example: semantic_id:VATSE-556000000101
type: string
verifier_id_type:
description: Type is an opaque type string, for example "semantic" or "x509_san_dns".
example: semantic
type: string
type: object
host: localhost:6001
info:
contact:
Expand Down
7 changes: 7 additions & 0 deletions example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,13 @@ policies:
# # Reject resolution-only requests, which return resolved metadata
# # without any key having been presented.
# require_key_binding: true
# # Verifier identifiers returned in the response context
# # (verifier_ids), for wallets that need a stable identifier for the
# # relying party. Off by default. Types: semantic (access certificate
# # only), x509_san_dns, x509_san_uri, decentralized_identifier,
# # openid_federation. Unknown names are rejected at startup. A request can narrow the response to one type with
# # context.verifier_id_type.
# return_verifier_ids: [semantic, x509_san_dns]

etsi:
service_types:
Expand Down
Loading
Loading