Skip to content

feat: SimpulseID credentials — schema-first domain layer on harbour-credentials#24

Merged
jdsika merged 130 commits intomainfrom
feat/gx-compliance-nested
Apr 4, 2026
Merged

feat: SimpulseID credentials — schema-first domain layer on harbour-credentials#24
jdsika merged 130 commits intomainfrom
feat/gx-compliance-nested

Conversation

@jdsika
Copy link
Copy Markdown
Collaborator

@jdsika jdsika commented Nov 27, 2025

Summary

Schema-first SimpulseID credential and identity framework for the ENVITED-X Data Space, built as a domain layer on top of harbour-credentials. Implements EVES-008 (SimpulseID Credential and Identity Framework) with evidence protocol per EVES-009 (Evidence-Based Consent Using Verifiable Presentations).

91 files changed, 9,675 insertions, 797 deletions across 125 commits.


Specification compliance

Standard Status Reference
EVES-008 Implemented 5 credential types, did:ethr on Base, Gaia-X alignment, schema-first pipeline
EVES-009 Implemented Evidence VPs use HARBOUR_DELEGATE challenge format (credential.issue action)
W3C VC Data Model v2 Compliant JSON-LD contexts, @context ordering, credentialStatus
Gaia-X Trust Framework 25.11 Compliant LegalPerson/NaturalPerson composition via harbour layer
OID4VP Aligned KB-JWT transaction_data_hashes for challenge binding

Design Decisions

Why a domain layer, not a standalone repo?

SimpulseID extends harbour-credentials rather than reimplementing credential infrastructure. Harbour provides the cryptographic signing/verification layer (ES256, SD-JWT, delegation), the W3C VC v2 base types, and the Gaia-X compliance model. SimpulseID adds only ENVITED-specific semantics: credential types, membership chains, program metadata, and legal form enums. This separation means harbour can serve other ecosystems (not just ENVITED), while SimpulseID stays focused on ASCS e.V. governance.

Why harbourCredential IRI reference instead of inline Gaia-X data?

Each SimpulseID credential subject (Participant, Administrator, User) carries a mandatory harbourCredential IRI pointing to a separate Harbour Gaia-X compliance credential. This avoids duplicating Gaia-X data (registration numbers, addresses, legal names) across every SimpulseID credential. The Harbour credential serves as the "baseline of trust" — if Gaia-X compliance is revoked, all SimpulseID credentials referencing it become invalid via the CRSet revocation mechanism. Membership credentials (AscsBaseMembership, AscsEnvitedMembership) do NOT carry harbourCredential because they attest to a relationship, not an identity.

Why OrganizationParticipant instead of Participant?

The LinkML schema originally used class_uri: simpulseid:Participant, but the JSON-LD context generator emitted "SimpulseidParticipant": {"@id": "Participant"} — and the bare term Participant was already claimed by gx:Participant from the Gaia-X imports. This caused the SHACL shape to never fire on participant credential subjects (the type resolved to the wrong namespace). The fix was renaming to simpulseid:OrganizationParticipant, which generates a unique context term. This is documented in the EVES-008 spec with an explanatory note.

Why urn:uuid for membership credential subjects?

Membership credentials use urn:uuid: identifiers instead of participant DIDs for credentialSubject.id. If two membership credentials (base + ENVITED) shared a participant DID as their subject ID, loading both into an RDF graph would merge all properties onto a single node — conflating the base membership's hostingOrganization with the ENVITED membership's baseMembershipCredential. The urn:uuid: pattern keeps each membership as an independent graph node. The schema:member property on the membership object points back to the participant DID.

Why member vs memberOf (not interchangeable)?

schema:member is used on the membership object, pointing TO the member: AscsBaseMembership.member = <BMW DID>. schema:memberOf is used on the person, pointing TO the organization they belong to: Administrator.memberOf = [<BMW DID>]. This follows schema.org semantics and is enforced by SHACL shapes — swapping them would produce validation failures.

Why program metadata in DID service endpoints?

Program definitions (AdministratorProgram, UserProgram, BaseMembershipProgram, EnvitedMembershipProgram) are embedded as serviceEndpoint objects inside ProgramMetadataService entries on program DIDs. This is a DID Core §5.4 pattern: the program DID is externally controlled (via the controller property pointing to the issuing participant DID) and serves its metadata through standard DID resolution. The alternative — separate JSON files or a centralized API — would break the decentralized resolution model.

Why closed SHACL shapes with exclude_imports?

The SHACL generator uses closed=True (unexpected properties trigger ClosedConstraintComponent) and exclude_imports=True (only SimpulseID-defined classes get shapes). This means:

  • Harbour/Gaia-X base types are NOT re-validated by SimpulseID shapes (harbour's own shapes handle those)
  • Any typo or extra property in a credential is caught immediately
  • The sh:ignoredProperties (rdf:type) declaration allows JSON-LD @type to coexist with closed shapes

Why HARBOUR_DELEGATE for evidence VPs?

Evidence VPs in SimpulseID use harbour's delegation module with the credential.issue action type. The nonce format is <random> HARBOUR_DELEGATE <SHA-256(TransactionData)>, where TransactionData includes the credential ID being consented to. This replaced an earlier custom format (<random> <hash>) that bypassed the delegation module and was not parseable by parse_delegation_challenge(). The HARBOUR_DELEGATE format aligns with EVES-009 §2 and makes evidence VPs verifiable through the standard delegation verification pipeline.

Why signer DIDs vs resource DIDs?

Two distinct DID patterns serve different roles:

  • Signer DIDs (participants, users): Self-sovereign — carry verificationMethod with P-256 JsonWebKey, referenced from authentication and assertionMethod. No document-level controller. These DIDs sign credentials and evidence VPs.
  • Resource DIDs (programs, services): Externally controlled — carry a controller property pointing to the governing participant DID, no local verificationMethod. Authority is delegated via the controller chain, not local keys.

This separation is enforced by integrity tests (test_signer_did_has_verification_method, test_resource_did_has_controller).


Changes by category

LinkML schemas (3 files, +560)

  • simpulseid-core.yaml — 5 credential types, 5 subject types, 6 program metadata types, SimpulseIdLegalForm enum (21 values across DE/US/UK jurisdictions)
  • importmap.json — 70+ cross-directory import mappings (harbour → gaia-x → service-characteristics)
  • Mandatory harbourCredential IRI on Participant/Administrator/User subjects
  • Membership chain enforcement: member (required), baseMembershipCredential (required on ENVITED)

Generated artifacts (1 file, +148)

  • OWL ontology (simpulseid-core.owl.ttl)
  • SHACL shapes (simpulseid-core.shacl.ttl) — closed shapes with sh:in enum enforcement
  • JSON-LD context (simpulseid-core.context.jsonld) with type: @type alias injection
  • Generated via src/generate_artifacts.py with exclude_imports=True

Python source (5 files, +466)

  • generate_artifacts.py — LinkML → OWL/SHACL/JSON-LD pipeline with importmap resolution
  • sign_examples.py — evidence VP signing with HARBOUR_DELEGATE challenge format via delegation module (credential.issue action)
  • verify_signed_examples.py — JWT + evidence VP verification against DID keyring

Tests (13 files, +1,995)

  • test_shacl_failures.py — 30 mutation-based SHACL tests across 7 test classes:
    • MinCountConstraintComponent (11 cases: issuer, validFrom, credentialStatus, harbourCredential, member, baseMembershipCredential, givenName)
    • ClosedConstraintComponent (5 cases: credential, subject, DID, program endpoint)
    • InConstraintComponent (2 cases: invalid legalForm enum values)
    • DID mutations (4 cases: missing serviceEndpoint, unexpected properties, trust anchor)
    • Program metadata mutations (2 cases: admin + membership program closed shapes)
  • test_shacl_validation.py — 15 positive baselines (credentials + DIDs) + 7 invalid example regression tests
  • test_examples_integrity.py — 30+ structural assertions: Gaia-X LegalPerson/NaturalPerson composition, DID key linkage (P-256 JsonWebKey in authentication + assertionMethod), cross-document IRI resolution (subject DIDs, memberOf, hostingOrganization, member, baseMembershipCredential, revocation registry), program metadata required fields, IRI type pollution regression guard (issue fix: DID document fixtures fail SHACL validation when loaded as data #28)
  • test_evidence_proof.py — evidence VP signing and verification parametrized across all 5 credential types, full proof chain (evidence VP → outer VC), sign-verify fixture roundtrip

Examples (21 files, +992/−223)

  • 5 credential examples with Gaia-X composition and CRSet revocation entries
  • 10 did:ethr DID documents: 2 signer (participant), 2 signer (user), 4 program, 1 trust anchor service, 1 revocation registry service
  • Evidence VPs with HARBOUR_DELEGATE nonce format and did:key ephemeral holders

Documentation (17 files, +2,116)

  • MkDocs Material site: architecture, credential data model, DID identity system
  • Specification reference copies (DID Core, did:ethr method, Gaia-X ICAM 25.11, SD-JWT)
  • artifacts/README.md documenting the generation pipeline and artifact structure

Wallet manifests (6 files, +525/−78)

  • DIF/Altme-compatible wallet rendering manifests for all 5 credential types
  • Display mappings for credential fields to wallet UI

CI/CD and build (12 files, +1,380/−17)

  • GitHub Actions pipeline: standards-and-syntax, lint-markdown, generate-validate (3 OS), test-python (3 OS × 2 Python), test-ts (3 OS), credential story (3 OS)
  • cliff.toml for git-cliff release changelog generation
  • dependabot.yml for automated pip and github-actions dependency updates
  • npm cache for markdownlint-cli2, yarn cache for TypeScript tests
  • cd-release.yml with semantic versioning, pre-release detection, GitHub Pages docs deployment
  • cd-docs.yml with path-based triggering for documentation changes

Submodule pins

  • harbour-credentials: OMB v0.1.6 (CI artifact generation verification, cross-platform matrix), w3id.org at upstream master, EVES-009 compliance fixes (revocation warning, transaction freshness)
  • w3id.org (top-level): upstream master (d004a453) with merged reachhaven/harbour and ascs-ev/simpulse-id namespace redirect rules

Test plan

  • make story passes (generate → sign → verify → validate)
  • make test simpulseid — all SHACL + integrity + evidence tests green (156 passed)
  • make lint passes (pre-commit hooks green)
  • CI pipeline passes on all OS × Python matrix
  • git submodule status --recursive clean after fresh clone

jdsika and others added 25 commits July 2, 2025 08:55
Signed-off-by: Carlo van Driesten <carlo.van-driesten@bmw.de>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Signed-off-by: Carlo van Driesten <carlo.van-driesten@bmw.de>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Signed-off-by: Carlo van Driesten <carlo.van-driesten@bmw.de>
Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Signed-off-by: felix hoops <9974641+jfelixh@users.noreply.github.com>
Signed-off-by: Carlo van Driesten <carlo.van-driesten@bmw.de>
Signed-off-by: felix hoops <9974641+jfelixh@users.noreply.github.com>
Signed-off-by: felix hoops <9974641+jfelixh@users.noreply.github.com>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
jdsika added 8 commits March 28, 2026 15:27
Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
RDFS inference materialises rdfs:label from schema:name (schema.org
declares schema:name rdfs:subPropertyOf rdfs:label). Classes with
sh:closed SHACL shapes must declare rdfs:label as an allowed property
to prevent validation failures when an RDFS-aware validator is used.

Add label attribute (slot_uri: rdfs:label) to ProgramEndpoint mixin,
BaseMembershipProgram, EnvitedMembershipProgram, ProgramPublisher,
and ProgramEcosystem — matching the pattern used in Gaia-X and
Harbour schemas. Also add rdfs prefix and apply ruff format.

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Rename publisher → programPublisher in simpulseid-core schema to
eliminate a name collision with the gaia-x publisher slot. Both
schemas define publisher with different from_schema URIs, causing
LinkML's merge_dicts to raise ValueError during schema loading.

The slot_uri remains sdo:publisher so OWL, SHACL, and JSON-LD
output all use schema:publisher as the RDF property. DID fixtures
are unaffected — they define their own inline context mapping.

This removes the _context_gen_without_harbour monkey-patch that
temporarily replaced linkml.utils.mergeutils.merge_dicts at runtime
to suppress the conflict (-41 lines net).

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Merge 5 credential type definitions from simpulseid-credentials.yaml
into simpulseid-core.yaml, eliminating the need for the
filter_shacl_to_namespace() post-processing hack.

Changes:
- Move ParticipantCredential, AdministratorCredential, UserCredential,
  AscsBaseMembershipCredential, AscsEnvitedMembershipCredential into
  simpulseid-core.yaml
- Delete simpulseid-credentials.yaml and its importmap entry
- Replace filter_shacl_to_namespace() hack (~30 lines) with built-in
  exclude_imports=True flag on ShaclGenerator
- Remove rdflib dependency from generate_artifacts.py
- Fix pre-commit hooks to use venv python (python3 unavailable on Windows)

SHACL output now includes 18 shapes (15 simpulseid: + 3 schema: for
locally-defined classes with external class_uri). The 3 schema.org
shapes validate inlined objects (ProgramPublisher, ProgramEcosystem,
ProgramEndpoint) which was previously over-filtered.

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Update all simpulseid examples and tests to use prefixed CURIEs for
domain types, matching the harbour convention:
- Bare 'ParticipantCredential' → 'simpulseid:ParticipantCredential'
- Bare 'HarbourVerifiableCredential' → 'harbour:VerifiableCredential'
- Bare 'CRSetEntry' → 'harbour:CRSetEntry'
- etc.

Convention: W3C terms stay bare, all domain types use prefixed CURIEs.
Pin harbour-credentials to 18f0443 (class_uri + delegation namespace).

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
@jdsika jdsika force-pushed the feat/gx-compliance-nested branch from 6c7ec0a to b4f7b72 Compare March 31, 2026 10:48
jdsika and others added 7 commits March 31, 2026 19:13
RDFS inference infers the parent type via rdfs:subClassOf, making
the explicit harbour:Evidence entry redundant when a child type
(harbour:CredentialEvidence) is already present.

Also bumps harbour-credentials submodule pin to include:
- Redundant type removal in harbour examples
- SHACL test refactoring to use OMB ShaclValidator with RDFS inference
- TypeScript DELEGATED_EVIDENCE_TYPES whitelist fix

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
- Bump harbour-credentials to 0ff4763
- serviceEndpoint: range uri (DID Core §5.4)
- transaction_data: range JsonLiteral (OID4VP §8.4)
- Add standards compliance rules to AGENTS.md and CLAUDE.md

Resolves SHACL closed-shape violations caused by linkml:Any
range triggering RDFS inference on IRI-valued endpoints.

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Updates submodule to 7a7e41e which fixes cross-runtime
interop tests on Windows (temp file approach for yarn node).

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
- Add gitlab-ci-local usage instructions to AGENTS.md, CLAUDE.md, and
  .github/copilot-instructions.md
- Document install requirements (npm, rsync, Docker/Podman)
- Note corporate proxy limitation for Docker image pulls

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
- Makefile: add linkml submodule to _install_dev (single source of
  truth via OMB submodule pin)
- Makefile: add REPO_ROOT variable, replace fragile ../../../../ paths
  with absolute references in validate targets
- Makefile: fix ruff ordering in _format_default (check then format)
- .pre-commit-config.yaml: fix jsonld-lint to include .jsonld files
- .github/workflows/ci.yml: use make setup instead of make install dev
- pyproject.toml: remove direct linkml git URL
- submodules/harbour-credentials: bump with Makefile alignment fixes

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Test across OS: ubuntu-latest, macos-latest, windows-latest

Matrix applied to:
- test-python: 3 OS × 2 Python (3.12, 3.13) = 6 jobs
- test-ts: 3 OS = 3 jobs
- generate-validate and standards-and-syntax remain ubuntu-only

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
- Enable core.longpaths on Windows before ALL recursive submodule
  checkouts (linkml snapshot files exceed 260-char Windows path limit)
- Use --global instead of --system for git config (more reliable in CI)
- TS test job: use shallow harbour-only checkout instead of recursive
  (TS tests don't need OMB/linkml chain, avoids long path issue)

Closes #27

Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
@jdsika jdsika force-pushed the feat/gx-compliance-nested branch from c5ec6f3 to 4002259 Compare April 2, 2026 20:43
jdsika added 6 commits April 3, 2026 20:39
…ollision

Membership program DIDs inlined hostingOrganization with @id + @type on
the ASCS participant DID IRI, polluting it with rdf:type schema:Organization
in the merged graph. The closed Organization shape rejected DID properties.

SimpulseidParticipant class_uri (simpulseid:Participant) collided with
gx:Participant in the JSON-LD context — the SHACL shape never fired on
participant subjects. Changed to simpulseid:OrganizationParticipant.

- Rewrite membership program DIDs: move metadata into serviceEndpoint,
  hostingOrganization as plain URI, remove non-standard root program property
- Split Makefile validate into credential and DID fixture passes
- Re-enable DID_FIXTURES in Makefile
- Add DID fixture SHACL, structural invariant, cross-document IRI, and
  mutation-based negative SHACL tests

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Update participant subject type in invalid test fixtures from the CURIE
simpulseid:Participant to the bare class name SimpulseidParticipant,
matching the valid credential pattern and resolving correctly to
simpulseid:OrganizationParticipant via the generated context.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Pin ontology-management-base to v0.1.6 release in harbour-credentials
(CI artifact generation verification, cross-platform test matrix,
Makefile CI detection fixes). Sync both w3id.org forks with upstream
master — namespace redirect rules merged upstream.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Extend the mutation-based SHACL test suite to cover audit findings:

- Add User-missing-validFrom and Administrator-missing-givenName to
  parametrized mandatory field tests (nested harbour NaturalPerson shape)
- Add TestEnumConstraints: verify sh:in constraint on SimpulseIdLegalForm
  rejects invalid values (InConstraintComponent)
- Add TestProgramMetadataMutations: closed shape tests for
  AdministratorProgram and BaseMembershipProgram endpoints
- Add program metadata integrity assertions: required fields per
  program type (policy docs for admin/user, governance docs for membership)
- Parametrize evidence VP signing across all 5 credential types
  (was only Participant + BaseMembership)

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
- Add cliff.toml for git-cliff changelog generation (required by
  cd-release.yml, was silently failing with continue-on-error)
- Add npm cache for markdownlint-cli2 in lint-markdown job
- Add yarn cache for TypeScript test job via setup-node cache param
- Add dependabot.yml for automated pip and github-actions updates

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Refactor sign_examples.py to use harbour's delegation module for
evidence VP nonce generation instead of a custom format. The nonce
now follows the HARBOUR_DELEGATE challenge format:

  <random> HARBOUR_DELEGATE <sha256(transaction_data)>

This makes evidence VPs parseable by parse_delegation_challenge() and
aligns with EVES-009 §2 (challenge = hash of serialized message).

The action type is 'credential.issue' (already registered in harbour's
ACTION_LABELS) with the credential ID as transaction context.

Also pins harbour-credentials with EVES-009 compliance improvements:
revocation status warning and optional transaction freshness checks
in verify_sd_jwt_vp().

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
@jdsika
Copy link
Copy Markdown
Collaborator Author

jdsika commented Apr 4, 2026

Audit resolution — 6 new commits pushed

Submodule pins

  • 95a4568 chore: pin harbour-credentials with OMB v0.1.6 and sync w3id.org

Test coverage gaps closed

  • f8d74e3 test: close SHACL test coverage gaps
    • +2 parametrized mandatory field cases (User-missing-validFrom, Administrator-missing-givenName)
    • +2 enum constraint tests (SimpulseIdLegalForm sh:in violation)
    • +2 program metadata closed shape tests (Admin/Membership endpoints)
    • +8 program integrity assertions (required fields per program type)
    • Evidence VP signing parametrized across all 5 credential types

CI/CD

  • 56ec55c ci: add cliff.toml, Node.js caching, and dependabot

EVES-009 compliance

  • d6b7c74 fix: align evidence VP nonce with EVES-009 delegation challenge format
    • Evidence VPs now use <nonce> HARBOUR_DELEGATE <hash> via delegation module
    • Action type: credential.issue (registered in harbour ACTION_LABELS)
    • Harbour: added revocation warning + optional transaction freshness in verify_sd_jwt_vp()

Verification

  • make story passes end-to-end
  • All SHACL tests pass (60 passed)
  • All evidence + integrity tests pass (96 passed)
  • Harbour SD-JWT VP tests pass (23 passed)
  • Pre-commit hooks green

jdsika added 5 commits April 4, 2026 14:23
- Move corepack enable before actions/setup-node so yarn 4 is
  available when setup-node resolves the yarn cache directory
  (yarn 1.x rejects packageManager: yarn@4.13.0)
- Replace Unicode arrow in sign_examples.py print output with ASCII
  to avoid cp1252 encoding error on Windows CI runners

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Update harbour-credentials submodule pointer to main (0a5205d) which
includes the squash-merged Gaia-X Loire compliance, delegated signing,
and did:ethr migration.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Switch submodule branch tracking from fix/simpulse-id-credentials
to main now that the harbour PR has been merged.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
- Pin harbour-credentials to v1.0.0 release tag
- Replace setup-node yarn cache with manual actions/cache after
  corepack enable (setup-node cache: yarn invokes yarn 1.x internally
  before corepack takes effect on Windows)
- Fix remaining Unicode arrow in sign_examples.py for Windows cp1252

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
@jdsika jdsika merged commit 1bf76f3 into main Apr 4, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants