A CLI tool for building TS11-compliant Catalogue of Attestations sites from credential schemas discovered across GitHub repositories.
registry-cli discovers credential type metadata (VCTMs) from configured
source repositories, validates them against the TS11 schema-meta model, and
produces a static site with:
- Per-credential detail pages with TS11 metadata
- Rendered rulebooks (from co-located
rulebook.md) - A JSON API (
/api/v1/schemas.json) with per-schema and per-organization endpoints - An OpenAPI specification
- Optional JWS signing of all API responses
go install github.com/sirosfoundation/registry-cli/cmd/registry-cli@latestregistry-cli build \
--sources sources.yaml \
--output dist \
--base-url https://registry.siros.org \
--templates templates-go \
--static staticregistry-cli sign \
--input dist/api/v1 \
--key-mode devThe sources.yaml file declares where to find credential repositories:
defaults:
branch: vctm
sources:
# Auto-discover by GitHub topic
- "github:topic/vctm?org=sirosfoundation"
# Explicit repository
- "git:https://github.com/org/repo.git"
# Local directory with explicit organization label
- url: "file:///path/to/local/credentials"
organization: "MyOrg"
# Remote repo with custom org label
- url: "git:https://github.com/other/repo.git"
organization: "CustomLabel"Source entries can be plain strings or structs with url and optional
organization fields. Local (file://) sources default to organization
"Local" if no label is provided.
Each source entry may specify a layout field to select a repo plugin that
understands the repository's directory structure. If omitted, the default
plugin is used.
sources:
# Default layout (omitted or explicit)
- url: "git:https://github.com/sirosfoundation/demo-credentials.git"
branch: vctm
# EUDI-style rulebook catalog
- url: "git:https://github.com/webuild-consortium/webuild-attestation-rulebooks-catalog.git"
organization: "webuild-consortium"
layout: "rulebook-catalog"
options:
attestation_los: "iso_18045_high"
binding_type: "key"| Field | Type | Default | Description |
|---|---|---|---|
url |
string | (required) | Source URL (git:, file://, or meta-source like github:topic/vctm) |
organization |
string | (inferred from URL) | Organization label for grouping |
branch |
string | manifest default | Git branch to clone |
path |
string | (repo root) | Restrict discovery to a subdirectory |
layout |
string | "default" |
Repo plugin name (see below) |
options |
map | (empty) | Plugin-specific key-value configuration |
Handles the standard SIROS credential layout with co-located files per credential:
<slug>.schema-meta.yaml (TS11 governance — required for TS11 API)
<slug>.vctm.json (SD-JWT VC Type Metadata)
<slug>.mdoc.json (mDOC metadata)
<slug>.vc.json (W3C VC schema)
rulebook.md (co-located governance document)
Also supports:
- Markdown files with
vct:YAML front matter (auto-converted to all formats) - Legacy bare
.vctmor.jsonfiles without schema-meta
Handles EUDI ARF-style attestation rulebook catalog repositories such as webuild-attestation-rulebooks-catalog and eudi-doc-attestation-rulebooks-catalog.
Expected directory structure:
data-schemas/
sd-jwt/<name>-sd-jwt.json (JSON Schema 2020-12 validation definitions)
mdoc/<name>-mdoc.json (mDOC validation schemas)
rulebooks/
rb-<slug>/README.md (EUDI-template governance document)
Discovery rule:
A credential type exists for every slug that has at least one data schema:
data-schemas/sd-jwt/<name>-sd-jwt.json and/or data-schemas/mdoc/<name>-mdoc.json.
The slug is the file name without its -sd-jwt.json / -mdoc.json suffix and
without a leading dsNNN- (case is kept: ds009-eInvoice becomes eInvoice).
A type with an SD-JWT VC schema follows the SD-JWT flow below, with its mdoc
schema attached as a second format. A type with only an mdoc schema follows the
mdoc-only flow.
What it does for a type with an SD-JWT VC schema:
- Extracts
vctfromproperties.vct.constorproperties.vct.examples[0] - Converts JSON Schema properties → VCTM claims:
requiredarray →mandatory: true- Descriptions with "selectively disclosable" →
sd: "always" - Property names → display labels (snake_case → Title Case)
- Maps
rulebooks/rb-<slug>/README.mdas the governance rulebook - Attaches the matching mDOC schema from
data-schemas/mdoc/as a second format - Normalizes legacy
dsNNN-filename prefixes (e.g.,ds002-pid→pid) - Discovers card and logo artwork per credential and publishes it with the site (see below)
What it does for an mdoc-only type:
- Publishes the mdoc schema as
<org>/<slug>.mdoc.json - Publishes the TS11 entry with
supportedFormats: ["mso_mdoc"], aschemaURIsentry for the mdoc file and, with the sourceoptions(attestation_los,binding_type,version), the same governance metadata as the other types;rulebookURIpoints at the rendered rulebook whenrulebooks/rb-<slug>/README.mdexists - Publishes the catalogue entry (
.well-known/vctm-registry.json, with the.mdoc.jsonfile as its metadata link) and the credential detail page, which shows the display name, description, document type, format downloads, rulebook link, source and the raw mdoc schema - Publishes the artwork found by the asset conventions below through the same validated asset path (
<org>/<slug>-svg_template.svg,-svg_template_dark.svg,-logo.*) - Skips a type whose mdoc schema is not valid JSON, logs a warning and continues with the other types
A type without VCTM has no claims, so it appears in the TS11 schemas API, the catalogue and the organisation page, while the attribute catalogue lists the claims of VCTM-based types.
mdoc schema fields that are read:
| Field | Used for |
|---|---|
title |
display name on the detail page, organisation page and catalogue entry (the slug when absent) |
description |
description on the detail page and catalogue entry |
docType |
document type shown on the detail page: a top-level docType string, properties.docType.const, or the first docType property with a const found in the document (WE BUILD schemas declare it under $defs) |
Asset conventions:
Each credential <slug> may carry artwork. The plugin looks in these places, first match wins per asset:
| Asset | Preferred location | Also found in |
|---|---|---|
| Card (light) | assets/<slug>/card.svg |
data-schemas/sd-jwt/<slug>-card.svg, display/<slug>-card.svg, display/<slug>.svg |
| Card (dark) | assets/<slug>/card-dark.svg |
|
| Logo | assets/<slug>/logo.svg or logo.png |
data-schemas/sd-jwt/<slug>-logo.{svg,png}, display/<slug>-logo.{svg,png} |
Published URLs: every asset is copied next to the credential's VCTM and the VCTM references it by absolute URL (--base-url with a trailing slash trimmed):
<out>/<org>/<slug>-svg_template.svg https://<base-url>/<org>/<slug>-svg_template.svg (card.svg)
<out>/<org>/<slug>-svg_template_dark.svg https://<base-url>/<org>/<slug>-svg_template_dark.svg (card-dark.svg)
<out>/<org>/<slug>-logo.svg|png https://<base-url>/<org>/<slug>-logo.svg|png (logo.svg|png)
The VCTM lists the cards under display[].rendering.svg_templates[] (light and dark colour scheme) and the logo under display[].rendering.simple.logo. Published asset types are svg, png, jpg, jpeg and webp. Credential format files keep their own names and content; an asset is always written under its <slug>-<key> name.
SVG cards are checked before publication, see SVG and Image URI Validation. A card that fails a check is skipped with a logged warning and its reference is removed from the VCTM, so the published VCTM contains working links only.
Plugin options:
| Option | Description |
|---|---|
attestation_los |
Default TS11 Level of Surety for all credentials in this source |
binding_type |
Default TS11 binding type for all credentials in this source |
version |
Default schema version |
Without attestation_los and binding_type, credentials will appear on the
site but not in the TS11 API (same as publishing without schema-meta).
Plugins implement the repoplugin.Plugin interface:
package repoplugin
type Plugin interface {
Name() string
Description() string
Discover(ctx Context) ([]DiscoveredCredential, error)
}Plugins self-register via init() and are compiled in by importing the package:
import _ "github.com/sirosfoundation/registry-cli/pkg/repoplugin/myplugin"See pkg/repoplugin/rulebookcatalog/ for a complete example.
Each credential must have a co-located .schema-meta.yaml file (or .schema-meta.json) providing TS11 Catalogue of Attestations compliance metadata. Only two fields are required; all others are inferred automatically.
| Field | Values | Description |
|---|---|---|
attestation_los |
iso_18045_high, iso_18045_moderate, iso_18045_enhanced-basic, iso_18045_basic |
Attestation Level of Surety per ISO 18045. Friendly aliases (high, moderate, enhanced-basic, basic, substantial, low) are normalized automatically. |
binding_type |
key, biometric, claim, none |
How the credential is bound to the holder. Friendly aliases (cnf → key, holder → key) are normalized. |
| Field | Type | Default | Description |
|---|---|---|---|
version |
string | "0.1.0" |
Schema version (semver). Can also be derived from git tags. |
rulebook_uri |
string | (auto-generated) | URL to attestation rulebook. If omitted, registry-cli auto-detects from co-located rulebook.md. |
trusted_authorities |
array | (empty) | Trust framework references per TS11 §4.3.3. See Trusted Authorities below. |
The following fields are inferred automatically at build time:
| Field | Source |
|---|---|
id |
UUID v5 deterministically derived from org/slug |
supportedFormats |
Detected from co-located files: .vctm.json → dc+sd-jwt, .mdoc.json → mso_mdoc, .vc.json → jwt_vc_json |
schemaURIs |
Generated from supportedFormats and registry base URL |
attestation_los: iso_18045_high
binding_type: keyattestation_los: iso_18045_high
binding_type: key
version: "1.0.0"
rulebook_uri: https://example.com/credentials/my-credential/rulebook
trusted_authorities:
- framework_type: etsi_tl
value: "https://tl.etsi.org/export/trustlist.xml"
is_lote: false
- framework_type: eidas
value: "https://eidas.ec.europa.eu"
is_lote: false
trust_mark_id: "https://eidas.ec.europa.eu/markers/high"
trust_mark_issuers:
- "https://issuer.example.com"Trusted authority entries define governance frameworks and trust marks:
| Field | Type | Required | Description |
|---|---|---|---|
framework_type |
string | Yes | Identifier for the trust framework (e.g., etsi_tl, eidas, custom-scheme) |
value |
string | Yes | Trust list URL or authority endpoint |
is_lote |
boolean | No | Whether this is a List of Trusted Entities (LOTE) |
trust_mark_id |
string | No | URI identifying the trust mark |
trust_mark_issuers |
array of strings | No | URLs of entities authorized to issue this trust mark |
Place the .schema-meta.yaml file in the same directory and with the same base name as your credential files:
credentials/
├── my-credential.md (markdown source)
├── my-credential.schema-meta.yaml (TS11 metadata)
├── my-credential.vctm.json (auto-generated or pre-built)
├── my-credential.mdoc.json (auto-generated or pre-built)
└── my-credential.vc.json (auto-generated or pre-built)
Credentials discovered without a .schema-meta.yaml file appear in the human-readable site but are excluded from TS11 API responses (/api/v1/schemas.json). This allows incremental migration to TS11 compliance.
registry-cli build also writes /api/v1/orgs.json (an index of contributing organizations) and /api/v1/orgs/<org>/schemas.json (that organization's TS11-compliant schemas, same shape as /api/v1/schemas.json). This is a SIROS extension, not part of the normative TS11 API — TS11 has no "organization" concept, and its SchemaMeta JSON Schema forbids additional properties, so filtering is expressed as a separate resource collection rather than a new field on the schema object. Every schema returned from an org-scoped list is identical to the one served from the global list.
There is no static resource for a set of organizations — fetch each one's list and merge client-side.
| Package | Purpose |
|---|---|
pkg/discovery |
Source resolution, GitHub topic search, repo cloning |
pkg/repoplugin |
Repo layout plugin interface and registry |
pkg/repoplugin/defaultlayout |
Default plugin: co-located schema-meta + format files |
pkg/repoplugin/rulebookcatalog |
EUDI-style catalog plugin: JSON Schema → VCTM conversion |
pkg/schemameta |
TS11 schema-meta parsing, inference, validation |
pkg/assets |
SVG validation and publication of layout plugin assets |
pkg/render |
HTML template rendering, markdown conversion |
pkg/jwssign |
JWS signing (dev, SoftHSM, YubiHSM) via PKCS#11 |
pkg/apihandler |
TS11 REST API with filtering, pagination, JWS-signed responses |
pkg/ts11compliance |
TS11 specification compliance test suite |
pkg/mdcred |
Markdown-based credential conversion |
registry-cli convert transforms Markdown credential definitions into the following formats:
| Format | Aliases | Output file | Description |
|---|---|---|---|
vctm |
sd-jwt, sdjwt |
.vctm.json |
SD-JWT VC Type Metadata |
mddl |
mdoc, mso_mdoc |
.mdoc.json |
mDOC MDDL (ISO 18013-5) |
w3c |
jwt_vc_json |
.vc.json |
W3C VCDM 2.0 credential schema |
jsonschema |
json-schema, schema |
.schema.json |
Standalone JSON Schema (draft-07) |
By default all formats are generated. Use --format or formats: front matter to select specific formats.
registry-cli serve --sources sources.yaml --output dist --port 8080# Build and run with docker compose
docker compose up --build
# Or build the image directly
docker build -t registry-cli:latest .
docker run -p 8080:8080 -v ./sources:/data/sources:ro registry-cli:latestregistry-cli processes potentially untrusted content from external credential repositories (rulebooks, VCTM metadata). All user-generated content is sanitized to prevent XSS attacks:
Attestation rulebooks are converted from Markdown to HTML and sanitized using a strict content security policy:
Allowed elements: paragraphs, headings, bold, italic, links, tables, lists, code blocks, blockquotes
Blocked elements: script tags, objects, embeds, SVGs, style tags, iframe, form
Blocked attributes: event handlers (onclick, onerror, etc.), style attributes, javascript: URIs, data: URIs
Example attack prevention:
<!-- Blocked: scripts are stripped -->
<script>alert('xss')</script>
<!-- Blocked: event handlers are removed -->
<img src=x onerror="alert(1)">
<!-- Blocked: style attributes prevent CSS injection -->
<p style="position:fixed;...">Overlay</p>
<!-- Blocked: javascript: URIs are filtered -->
[Click me](javascript:alert(1))
<!-- Allowed: safe links are preserved -->
[Documentation](https://example.com/docs)Credential metadata (VCTM) may reference images for logos and backgrounds:
- Allowed:
https://andhttp://URIs pointing to external image files - Blocked:
data:URIs (inline data),blob:URIs,javascript:protocols,file://URIs
Invalid image URIs are silently removed from rendered credential pages. The implementation is in pkg/render/vctm.go::SanitizeVCTM().
Published SVG assets. The registry serves SVG cards and logos discovered by layout plugins from its own origin, so every SVG is validated with a real XML tokenizer (encoding/xml, pkg/assets/svg.go::ValidateSVG) before it is published. A card is published when it:
- is a well-formed XML document with an
<svg>root element of at most 2 MiB; - contains only static graphic elements (no
<script>,<foreignObject>,<iframe>,<object>,<embed>); - carries no event-handler attributes (
onload,onclick, anyon*); - uses
href,xlink:hrefandsrconly for same-document#idfragments, which also covers<use>of external documents; - contains no
javascript:ordata:URIs, and no externalurl(...),@importor entity declarations instyleelements and attributes.
A rejected card is skipped with a logged warning that names the file and the reason, and its reference is removed from the VCTM. Absolute https:// and http:// asset URIs pass SanitizeVCTM, so published assets appear on the rendered credential pages.
Comprehensive security tests verify sanitization effectiveness:
go test ./pkg/render -run "Sanitize" -vTests cover:
- XSS via script tags and event handlers
- CSS injection via style attributes
- SVG and object attacks
- Data URI attacks
- JavaScript protocol handlers
- Safe content preservation (links, tables, lists)
When publishing credentials with rulebooks:
- Avoid HTML in Markdown: Write rulebooks using standard Markdown syntax
- Don't embed SVGs: Reference SVGs via URLs only, never inline
- Use safe links: Only use
https://links to trusted documentation - No styling: Don't use HTML style attributes or CSS; format using Markdown
- No JavaScript: Avoid any JavaScript references or data URIs
Example safe rulebook structure:
# PID Attestation Rulebook
## Overview
This rulebook governs Personal ID credentials.
- [Framework documentation](https://example.com/pid-framework)
- [Technical specifications](https://example.com/pid-spec)
## Requirements
- Attestation LoS: high
- Binding type: key
## See also
- [PID Rulebook v1.5](https://example.com/pid-rulebook-1.5)make test # Unit tests
make test-softhsm # Include SoftHSM PKCS#11 integration tests
make coverage # Coverage report
make lint # Run linterSee LICENSE.