GS1 Risotto Rice¶
This runnable example uses the supplied Dal Giardino product JSON-LD, including
its GS1 Digital Link identity, nested offer, shipping and return information,
reviews, ratings, ingredients and allergen statement. It makes real signatures
under the offline demonstration DID did:web:product-issuer.example. It does not
represent a credential issued or endorsed by Dal Giardino, GS1, or the reviewers.
Prerequisites and checkout¶
Use Linux and Bash, Rust 1.92 or later with Cargo, Python 3.11 or later, and Git.
The project has been validated with Rust 1.92 and Python 3.14. Initial dependency
installation/build needs internet access; the credential exercises run offline.
No web server, domain ownership, paid service, jq, or Python crypto package is
required for this local demonstration.
For a new checkout:
git clone https://github.com/chris-day/holon-vc.git
cd holon-vc
If you already have the repository, open a terminal in its root instead. Check
that cargo --version and python3 --version succeed. Create the documentation
virtual environment only if it does not already exist:
if [ ! -x .venv/bin/python ]; then python3 -m venv .venv; fi
.venv/bin/python -m pip install -r requirements-docs.txt
Choose either the automated route below or the manual walkthrough. Each creates its own isolated runtime directory. Commands require the example files from this revision of the repository.
Automated route¶
From the repository root on Linux:
cargo build --locked --release
.venv/bin/python scripts/product-workflow.py --output /tmp/holon-product-example
The output directory must not already exist. Omit --output to run all checks
in a temporary directory and remove the outputs afterward. The workflow uses
Python's standard library; it requires no additional Python packages.
Expected result:
PASS: 17 CLI operations; full Ed25519/P-256 credentials, three tamper rejections, real selective disclosure, product preservation and HTML embedding.
| Output | Purpose |
|---|---|
product.holon.json |
The supplied product inside a Holon record |
issuer.vc.json |
Complete VC with a real eddsa-rdfc-2022 proof |
selective.vc.json |
Complete VC with a real ecdsa-sd-2023 base proof |
derived.vc.json |
Verifiable disclosure of identity, ingredients and allergens |
issuer.report.json, selective.report.json, derived.report.json |
Verification results at generation time |
product.html |
Minimal page embedding the exact issued Ed25519 credential |
config.toml |
Generated context and schema pins using absolute local paths |
data/ |
Private keys, local policies, status registry, and staged public resources |
tampered-*.json |
Deliberately invalid credentials used for rejection checks |
Do not publish the entire output directory. The workflow generates a random password and never retains it, so retained private keys are not reusable after the run. This example is intended to be regenerated. Generated DID/status pins expire after five minutes; the credentials and context pin expire after one day. Saved reports are observations at generation time, not continuing verification.
To repeat verification immediately after the run:
target/release/holon-vc \
--data-dir /tmp/holon-product-example/data \
--config /tmp/holon-product-example/config.toml \
--offline --output-format json \
credential verify \
--credential /tmp/holon-product-example/issuer.vc.json \
--threshold trusted-assertion
Supplied product JSON-LD¶
The entire source is included directly from the executable example fixture:
{
"@context": [
"https://schema.org",
{
"gs1": {
"@id": "https://gs1.org/voc/",
"@prefix": true
}
}
],
"id": "https://id.gs1.org/01/09506000134352",
"type": [
"Product",
"gs1:Product"
],
"name": "Dal Giardino Risotto Rice with Mushrooms",
"url": "https://dalgiardino.com/risotto-rice-with-mushrooms/",
"sku": "DG-RRM-411-GB",
"gtin14": "09506000134352",
"brand": {
"type": "Brand",
"name": "Dal Giardino",
"url": "https://dalgiardino.com/"
},
"description": "A delicious mix of fragrant rice with Italian mushrooms.",
"image": {
"type": "ImageObject",
"url": "https://dalgiardino.com/risotto-rice-with-mushrooms/Dal_Giardino_Risotto_Rice_With_Nett_260.png",
"contentUrl": "https://dalgiardino.com/risotto-rice-with-mushrooms/Dal_Giardino_Risotto_Rice_With_Nett_260.png",
"caption": "Packshot of Dal Giardino Risotto Rice with Mushrooms, 411 g",
"width": 260,
"height": 260
},
"offers": {
"type": "Offer",
"url": "https://shop.dalgiardino.example/products/09506000134352",
"priceCurrency": "GBP",
"price": 4.49,
"availability": "https://schema.org/InStock",
"priceValidUntil": "2027-06-30",
"itemCondition": "https://schema.org/NewCondition",
"seller": {
"type": "Organization",
"name": "Dal Giardino Demonstration Store",
"url": "https://shop.dalgiardino.example/"
},
"shippingDetails": {
"type": "OfferShippingDetails",
"shippingRate": {
"type": "MonetaryAmount",
"value": 0,
"currency": "GBP"
},
"shippingDestination": {
"type": "DefinedRegion",
"addressCountry": "GB"
},
"deliveryTime": {
"type": "ShippingDeliveryTime",
"handlingTime": {
"type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"type": "QuantitativeValue",
"minValue": 2,
"maxValue": 4,
"unitCode": "DAY"
}
}
},
"hasMerchantReturnPolicy": {
"type": "MerchantReturnPolicy",
"applicableCountry": "GB",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnMethod": [
"https://schema.org/ReturnByMail",
"https://schema.org/ReturnInStore"
],
"returnFees": "https://schema.org/FreeReturn"
}
},
"review": [
{
"type": "Review",
"name": "Creamy and well seasoned",
"reviewBody": "Earthy mushroom flavour, a creamy texture and straightforward preparation for a quick supper.",
"datePublished": "2026-07-18",
"author": {
"type": "Person",
"name": "Maya Patel"
},
"reviewRating": {
"type": "Rating",
"ratingValue": 5,
"bestRating": 5,
"worstRating": 1
}
},
{
"type": "Review",
"name": "Convenient pantry meal",
"reviewBody": "A useful cupboard staple with a balanced mushroom taste; I added fresh parsley before serving.",
"datePublished": "2026-08-02",
"author": {
"type": "Person",
"name": "Oliver Evans"
},
"reviewRating": {
"type": "Rating",
"ratingValue": 4,
"bestRating": 5,
"worstRating": 1
}
}
],
"aggregateRating": {
"type": "AggregateRating",
"ratingValue": 4.5,
"bestRating": 5,
"worstRating": 1,
"reviewCount": 2,
"ratingCount": 2
},
"gs1:gtin": "09506000134352",
"gs1:brandName": "Dal Giardino",
"gs1:functionalName": "Risotto rice with mushrooms",
"gs1:netContent": {
"type": [
"QuantitativeValue",
"gs1:QuantitativeValue"
],
"value": 411,
"unitCode": "GRM"
},
"gs1:ingredientStatement": "Rice, Italian mushrooms, onion, sunflower oil, vegetable stock (salt, yeast extract, celery and herbs), garlic, parsley and black pepper.",
"gs1:allergenStatement": "Contains celery. Gluten free."
}
Manual walkthrough: every command¶
The blocks below are included from examples/product/walkthrough.sh, so the
published commands and the runnable version remain identical. Alternatively, run
bash examples/product/walkthrough.sh from the repository root and answer its
password prompt. It leaves artifacts in the directory printed at startup.
1. Build and choose a private workspace¶
Run all following blocks in order in the same Bash session, starting in the
repository root. mktemp chooses a new directory for every run. Choose a nonempty
password when prompted and retain it if you want to reuse these manual-run keys.
The password stays in a shell variable and is passed over stdin, never in command
arguments. Do not run the walkthrough with shell tracing (set -x).
set -euo pipefail
umask 077
cargo build --locked --release
PRODUCT_REPO="$PWD"
PRODUCT_RUN="$(mktemp -d /tmp/holon-product-manual.XXXXXX)"
export PRODUCT_REPO PRODUCT_RUN
PRODUCT_DID='did:web:product-issuer.example'
PRODUCT_SCHEMA='urn:example:product-holon:1.0'
printf 'Artifacts: %s\n' "$PRODUCT_RUN"
read -r -s -p 'Choose a password for the demonstration keys: ' PRODUCT_PASSWORD
printf '\n'
test -n "$PRODUCT_PASSWORD"
vc() {
"$PRODUCT_REPO/target/release/holon-vc" \
--data-dir "$PRODUCT_RUN/data" \
--config "$PRODUCT_RUN/config.toml" \
--offline --output-format json "$@"
}
sign_vc() {
printf '%s\n' "$PRODUCT_PASSWORD" | vc "$@" --password-stdin
}
2. Prepare the Holon and pinned configuration¶
This copies every supplied product value into claims.product and generates
actual SHA-256 pins, absolute paths, creation time and a one-day expiry. No
placeholder substitution is necessary. The input's inline context is represented
by the scoped, pinned context distributed with this example.
python3 - <<'PY'
import datetime as dt
import hashlib
import json
import os
from pathlib import Path
repo = Path(os.environ['PRODUCT_REPO'])
work = Path(os.environ['PRODUCT_RUN'])
example = repo / 'examples/product'
now = dt.datetime.now(dt.timezone.utc)
expires = (now + dt.timedelta(days=1)).isoformat()
(work / 'expires.txt').write_text(expires)
source = json.loads((example / 'product.jsonld').read_text())
product = {k: v for k, v in source.items() if k != '@context'}
holon = {
'id': 'urn:example:holon:product:09506000134352',
'type': 'ProductHolon', 'schemaVersion': '1.0',
'createdAt': now.isoformat(), 'claims': {'product': product},
'source': {'id': 'did:web:product-issuer.example', 'kind': 'demonstration-publisher'},
'evidence': [], 'relatedHolons': [],
}
(work / 'product.holon.json').write_text(json.dumps(holon, indent=2) + '\n')
def sha(name):
return hashlib.sha256((example / name).read_bytes()).hexdigest()
(work / 'config.toml').write_text(f'''
[contexts."urn:example:product-context:1.0"]
path = {json.dumps(str(example / 'context.jsonld'))}
sha256 = "{sha('context.jsonld')}"
expires = "{expires}"
media_type = "application/ld+json"
[schemas."urn:example:product-holon:1.0"]
full = {json.dumps(str(example / 'schema.json'))}
disclosure = {json.dumps(str(example / 'disclosure.schema.json'))}
context = "urn:example:product-context:1.0"
sha256 = "{sha('schema.json')}"
disclosure_sha256 = "{sha('disclosure.schema.json')}"
''')
PY
PRODUCT_EXPIRES="$(cat "$PRODUCT_RUN/expires.txt")"
3. Create keys, suites and local trust policies¶
These commands create two encrypted keys, associate them with their cryptosuites, and read the actual public-key fingerprints to configure local trust. This trust is explicitly granted for the demonstration; it does not establish brand ownership.
sign_vc key setup --id issuer --controller "$PRODUCT_DID" --algorithm ed25519 \
> "$PRODUCT_RUN/issuer-key.json"
sign_vc key setup --id selective --controller "$PRODUCT_DID" --algorithm p256 \
> "$PRODUCT_RUN/selective-key.json"
vc suite setup --name issuer \
--key "$PRODUCT_RUN/data/keys/private/issuer.json" \
--verification-method "$PRODUCT_DID#issuer" --cryptosuite eddsa-rdfc-2022
vc suite setup --name selective \
--key "$PRODUCT_RUN/data/keys/private/selective.json" \
--verification-method "$PRODUCT_DID#selective" --cryptosuite ecdsa-sd-2023
for PRODUCT_KEY in issuer selective; do
PRODUCT_FINGERPRINT="$(python3 -c \
'import json,sys; print(json.load(open(sys.argv[1]))["fingerprint"])' \
"$PRODUCT_RUN/$PRODUCT_KEY-key.json")"
vc trust add --id "$PRODUCT_KEY" --issuer "$PRODUCT_DID" \
--verification-method "$PRODUCT_DID#$PRODUCT_KEY" \
--fingerprint "$PRODUCT_FINGERPRINT" \
--credential-type HolonCredential --schema "$PRODUCT_SCHEMA" \
--purpose holon-assertion
done
4. Stage DID metadata and signed status lists¶
Both keys are created before staging the DID so it authorizes both verification
methods. These commands create local public artifacts and resource pins. They do
not upload anything or attempt to resolve the .example domain over the network.
Complete steps 5–7 within five minutes of this step. If you pause longer, use the refresh commands below before continuing.
sign_vc well-known generate --origin https://product-issuer.example \
--issuer-did "$PRODUCT_DID" --suite issuer
sign_vc status create --id products \
--url https://product-issuer.example/status/products --suite issuer
5. Issue and verify both full credentials¶
Each command must exit successfully. The verification reports should contain
"cryptographicallyValid": true, "status": "active", and
"decision": "trusted-assertion". The subject schema is this example's configured
product schema, not the bundled temperature-example schema.
for PRODUCT_SUITE in issuer selective; do
sign_vc credential issue --holon "$PRODUCT_RUN/product.holon.json" \
--schema-id "$PRODUCT_SCHEMA" --suite "$PRODUCT_SUITE" \
--status-list "$PRODUCT_RUN/data/status/products.json" \
--expires "$PRODUCT_EXPIRES" --output "$PRODUCT_RUN/$PRODUCT_SUITE.vc.json"
vc credential verify --credential "$PRODUCT_RUN/$PRODUCT_SUITE.vc.json" \
--threshold trusted-assertion --output "$PRODUCT_RUN/$PRODUCT_SUITE.report.json"
done
6. Derive and verify the reduced credential¶
Derivation uses the P-256 credential, and requires no signing password. The assertions below check successful verification and confirm that offers, reviews, aggregate ratings and SKU were omitted.
vc credential derive --credential "$PRODUCT_RUN/selective.vc.json" \
--reveal "$PRODUCT_REPO/examples/product/reveal.json" \
--output "$PRODUCT_RUN/derived.vc.json"
vc credential verify --credential "$PRODUCT_RUN/derived.vc.json" \
--threshold trusted-assertion --output "$PRODUCT_RUN/derived.report.json"
python3 - <<'PY'
import json, os
from pathlib import Path
work = Path(os.environ['PRODUCT_RUN'])
for name in ['issuer', 'selective', 'derived']:
report = json.loads((work / f'{name}.report.json').read_text())
assert report['decision'] == 'trusted-assertion'
assert report['cryptographicallyValid'] and report['status'] == 'active'
product = json.loads((work / 'derived.vc.json').read_text())['credentialSubject']['claims']['product']
assert product['gtin14'] == '09506000134352'
assert 'gs1:ingredientStatement' in product and 'gs1:allergenStatement' in product
assert not {'offers', 'review', 'aggregateRating', 'sku'} & product.keys()
print('PASS: all three credentials verify; selected claims retained and other claims omitted')
PY
7. Confirm that changing a signed price fails¶
A verification failure is the expected success condition here. The block requires exit code 5 and a failed cryptographic check, so an unrelated error does not count as a successful tamper test.
python3 - <<'PY'
import json, os
from pathlib import Path
work = Path(os.environ['PRODUCT_RUN'])
vc = json.loads((work / 'issuer.vc.json').read_text())
vc['credentialSubject']['claims']['product']['offers']['price'] = 0.01
(work / 'tampered-price.json').write_text(json.dumps(vc))
PY
if vc credential verify --credential "$PRODUCT_RUN/tampered-price.json" \
> "$PRODUCT_RUN/tampered-price.report.json"; then
printf '%s\n' 'ERROR: tampered credential unexpectedly accepted' >&2
exit 1
else
PRODUCT_EXIT=$?
test "$PRODUCT_EXIT" -eq 5
fi
python3 - <<'PY'
import json, os
from pathlib import Path
report = json.loads((Path(os.environ['PRODUCT_RUN']) / 'tampered-price.report.json').read_text())
assert not report['cryptographicallyValid']
print('PASS: changed price rejected cryptographically (exit 5)')
PY
8. Embed the issued credential in HTML¶
This writes an actual HTML page with the complete signed credential. Its visible product JSON is generated from the same credential. Open the printed file path in your browser. The password variable is cleared after this step.
python3 - <<'PY'
import html, json, os
from pathlib import Path
work = Path(os.environ['PRODUCT_RUN'])
vc = json.loads((work / 'issuer.vc.json').read_text())
product = vc['credentialSubject']['claims']['product']
embedded = json.dumps(vc, indent=2, ensure_ascii=True).replace('<', '\\u003c')
assert json.loads(embedded) == vc
page = f'''<!doctype html>
<html lang="en"><meta charset="utf-8">
<title>Product VC example</title>
<h1>{html.escape(product['name'])}</h1>
<p>Local demonstration issuer; no brand-owner endorsement. No live verification.</p>
<p><a href="issuer.vc.json">Full credential</a> | <a href="derived.vc.json">Disclosed credential</a></p>
<script id="product-credential" type="application/ld+json">{embedded}</script>
<pre>{html.escape(json.dumps(product, indent=2))}</pre>
</html>'''
(work / 'product.html').write_text(page)
print('PASS: HTML embeds the complete issued credential')
print(f'Open {work / "product.html"} in your browser')
PY
unset PRODUCT_PASSWORD
Refresh resources after a pause¶
For the manual walkthrough, reuse the same shell variables and password. If step 8 cleared the variable, read your original password again:
read -r -s -p 'Original demonstration key password: ' PRODUCT_PASSWORD
printf '\n'
sign_vc well-known generate --origin https://product-issuer.example \
--issuer-did "$PRODUCT_DID" --suite issuer --force
sign_vc status create --id products \
--url https://product-issuer.example/status/products --suite issuer --force
vc credential verify --credential "$PRODUCT_RUN/issuer.vc.json" \
--threshold trusted-assertion
unset PRODUCT_PASSWORD
Status refresh preserves existing allocations and revocations. It does not extend the credential or context's one-day expiry. After a day, start a fresh walkthrough. For the automated route, the random password is not retained: rerun it into a new directory instead.
Troubleshooting¶
| Symptom | Action |
|---|---|
Automated --output directory already exists |
Select a new directory; existing artifacts are never overwritten automatically. |
| Expired resource/status pin | Refresh the manual run as above, or regenerate the automated run. |
| Wrong key password | Use the password entered in manual step 1. Automated passwords are intentionally not retained. |
| Unknown context or missing schema | Pass the generated config.toml using the vc wrapper; do not issue with the default schema. |
trusted-assertion threshold is not met |
Complete step 3 with the actual generated fingerprints and the same data directory. |
| Output file already exists when repeating a step | Start a fresh walkthrough, or deliberately use the command's --force option for an output you intend to replace. |
Keep data/ private. Only selectively publish the credential and HTML files;
private keys, local trust policies and the status registry are not website files.
The staged public DID/status resources need their own deliberate deployment for
an online issuer. This walkthrough is fully local.
Build and preview this guide¶
bash scripts/docs.sh build
bash scripts/docs.sh serve
Open http://127.0.0.1:8000/Product-VC-example.html; stop the preview with Ctrl-C.
The static build is in site-docs/Product-VC-example.html.
What gets signed¶
The layout is:
VerifiableCredential + HolonCredential
issuer: did:web:product-issuer.example
credentialSubject:
id: urn:example:holon:product:09506000134352
type: ProductHolon
schemaVersion: 1.0
createdAt: actual generation timestamp
claims:
product: the supplied product object
credentialSchema: product Holon subject schema
credentialStatus: revocation and suspension entries
proof: generated Data Integrity proof
examples/product/product.jsonld preserves the supplied standalone JSON-LD.
The workflow copies its product properties without changing their values; only
the top-level @context is relocated into a pinned, scoped context. The original
product ID remains https://id.gs1.org/01/09506000134352. The Holon record and
the generated credential have separate identifiers.
examples/product/context.jsonld extends the built-in Holon context and scopes
the product terms to claims.product. Its Schema.org term definitions are a
subset of the official context,
retrieved 2026-09-28. That context maps terms to http://schema.org/ IRIs;
these mappings are preserved even though the context URL is HTTPS. The supplied
GS1 prefix remains https://gs1.org/voc/. In particular, the product's
value is Schema.org's value, not the temperature example's Holon value.
The approach uses standard JSON-LD scoped contexts.
The context deliberately contains only terms needed by this example. New product
fields require an explicit context/schema update. Both full and disclosure schemas
are local, pinned, self-contained subject schemas. They validate the example's
structure and field types; they are not a GS1 certification schema and do not
establish the accuracy, ownership, regulatory compliance, or current commercial
validity of the claims. In particular, priceValidUntil is a product claim;
credential validity checking does not independently enforce that offer date.
Applying the result to ExampleHolon.html¶
Replace the previous credential script with the complete contents of
issuer.vc.json, not an independently assembled proof:
<script id="product-credential" type="application/ld+json">
<!-- Insert HTML-safe serialization of the entire generated VC here. -->
</script>
The snippet above is a template, not valid credential JSON. product.html
demonstrates actual insertion: serialize the credential as JSON and escape <
as \u003c so product strings cannot terminate the script element. Do not HTML
entity-escape JSON quotes. Escaping must preserve the parsed credential object.
Read the visible product fields from
credentialSubject.claims.product; read issuer, validity and proof information
from the generated VC. Do not edit signed fields after issuance. The signature
covers the credential data, not surrounding HTML or the bytes of linked images.
The generated page explicitly states that it performs no live verification.
For a real deployment, configure an issuer identity and key controlled by the
publisher, host its DID and signed status lists, distribute the pinned context
and schemas, and establish verifier trust independently. The example's local
trust policy deliberately trusts the fresh demonstration key; a
trusted-assertion result is scoped to that policy. The .example domain and
URN profile identifiers here are not a public deployment.
Actual selective disclosure¶
The workflow separately issues a P-256 ecdsa-sd-2023 credential and derives a
credential retaining the product ID, types, name, GTINs, ingredients and allergens.
Offers, reviews, aggregate ratings and SKU are absent. The normal mandatory VC
and Holon identifiers, issuer, validity, schema and status metadata remain.
The selection is maintained in examples/product/reveal.json. The derived
credential verifies without signing again or supplying the issuer's private key.
It remains correlatable through its mandatory identifiers. Ed25519 credentials
cannot be selectively disclosed this way by deleting properties.
The automated workflow’s three negative checks change price, ingredient text and a review after issuance, and require cryptographic rejection. No unsigned placeholder or mock signature is used. The original product assertions, including reviews and offers, are all attributed to this demonstration publisher; there is no claim that the named reviewers independently signed them or that this is corroborated evidence.
Example files and code reference¶
The documentation source is examples/product/Product-VC-example.md. The site
page docs/Product-VC-example.md includes it directly. Edit this source when
changing the example; the command snippets and product JSON are also included
from their actual files rather than maintained as separate copies.
| File | Role |
|---|---|
examples/product/product.jsonld |
Complete standalone Schema.org/GS1 product input, without a VC envelope or proof. |
examples/product/context.jsonld |
Holon extension and scoped product vocabulary used during RDF processing and signing. |
examples/product/schema.json |
Full subject schema required for issuance and verification of the complete product. |
examples/product/disclosure.schema.json |
Subject schema for the reduced, derived credential. |
examples/product/reveal.json |
Seven explicit JSON Pointer selections for disclosure. |
examples/product/walkthrough.sh |
Interactive Bash implementation of the eight manual steps above. |
scripts/product-workflow.py |
Automated issuance, verification, negative checks, disclosure and HTML generation. |
tests/product_example.rs |
Cargo integration test that executes the automated workflow against the test-built CLI. |
Context and identifiers¶
context.jsonld first includes urn:holon:context:1.0, then defines
ProductHolon as urn:example:ProductHolon and product as
urn:example:product. The product property supplies its own scoped context.
Within that scope, id and type alias @id and @type; the supplied product
classes and properties map to Schema.org, and gs1: expands using the supplied
GS1 namespace. The full context is:
{
"@context": [
"urn:holon:context:1.0",
{
"ProductHolon": "urn:example:ProductHolon",
"product": {
"@id": "urn:example:product",
"@context": {
"schema": "http://schema.org/",
"gs1": {
"@id": "https://gs1.org/voc/",
"@prefix": true
},
"AggregateRating": {
"@id": "schema:AggregateRating"
},
"Brand": {
"@id": "schema:Brand"
},
"DefinedRegion": {
"@id": "schema:DefinedRegion"
},
"ImageObject": {
"@id": "schema:ImageObject"
},
"MerchantReturnPolicy": {
"@id": "schema:MerchantReturnPolicy"
},
"MonetaryAmount": {
"@id": "schema:MonetaryAmount"
},
"Offer": {
"@id": "schema:Offer"
},
"OfferShippingDetails": {
"@id": "schema:OfferShippingDetails"
},
"Organization": {
"@id": "schema:Organization"
},
"Person": {
"@id": "schema:Person"
},
"Product": {
"@id": "schema:Product"
},
"QuantitativeValue": {
"@id": "schema:QuantitativeValue"
},
"Rating": {
"@id": "schema:Rating"
},
"Review": {
"@id": "schema:Review"
},
"ShippingDeliveryTime": {
"@id": "schema:ShippingDeliveryTime"
},
"addressCountry": {
"@id": "schema:addressCountry"
},
"aggregateRating": {
"@id": "schema:aggregateRating"
},
"applicableCountry": {
"@id": "schema:applicableCountry"
},
"author": {
"@id": "schema:author"
},
"availability": {
"@id": "schema:availability"
},
"bestRating": {
"@id": "schema:bestRating"
},
"brand": {
"@id": "schema:brand"
},
"caption": {
"@id": "schema:caption"
},
"contentUrl": {
"@id": "schema:contentUrl"
},
"currency": {
"@id": "schema:currency"
},
"datePublished": {
"@id": "schema:datePublished"
},
"deliveryTime": {
"@id": "schema:deliveryTime"
},
"description": {
"@id": "schema:description"
},
"gtin14": {
"@id": "schema:gtin14"
},
"handlingTime": {
"@id": "schema:handlingTime"
},
"hasMerchantReturnPolicy": {
"@id": "schema:hasMerchantReturnPolicy"
},
"height": {
"@id": "schema:height"
},
"id": "@id",
"image": {
"@id": "schema:image"
},
"itemCondition": {
"@id": "schema:itemCondition"
},
"maxValue": {
"@id": "schema:maxValue"
},
"merchantReturnDays": {
"@id": "schema:merchantReturnDays"
},
"minValue": {
"@id": "schema:minValue"
},
"name": {
"@id": "schema:name"
},
"offers": {
"@id": "schema:offers"
},
"price": {
"@id": "schema:price"
},
"priceCurrency": {
"@id": "schema:priceCurrency"
},
"priceValidUntil": {
"@id": "schema:priceValidUntil"
},
"ratingCount": {
"@id": "schema:ratingCount"
},
"ratingValue": {
"@id": "schema:ratingValue"
},
"returnFees": {
"@id": "schema:returnFees"
},
"returnMethod": {
"@id": "schema:returnMethod"
},
"returnPolicyCategory": {
"@id": "schema:returnPolicyCategory"
},
"review": {
"@id": "schema:review"
},
"reviewBody": {
"@id": "schema:reviewBody"
},
"reviewCount": {
"@id": "schema:reviewCount"
},
"reviewRating": {
"@id": "schema:reviewRating"
},
"seller": {
"@id": "schema:seller"
},
"shippingDestination": {
"@id": "schema:shippingDestination"
},
"shippingDetails": {
"@id": "schema:shippingDetails"
},
"shippingRate": {
"@id": "schema:shippingRate"
},
"sku": {
"@id": "schema:sku"
},
"transitTime": {
"@id": "schema:transitTime"
},
"type": "@type",
"unitCode": {
"@id": "schema:unitCode"
},
"url": {
"@id": "schema:url"
},
"value": {
"@id": "schema:value"
},
"width": {
"@id": "schema:width"
},
"worstRating": {
"@id": "schema:worstRating"
}
}
}
}
]
}
The document passed for issuance contains only the approved context URL. Inline term definitions live inside the trusted, pinned context file. The example does not disable the CLI's rejection of inline contexts in input credentials.
Full and disclosure schema behavior¶
Both schemas use Draft 2020-12 and the same ID,
urn:example:product-holon:1.0, because the signed credential identifies one
profile with full and disclosure variants. They validate credentialSubject.
| Constraint | Full schema | Disclosure schema |
|---|---|---|
| Required Holon fields | id, type, schemaVersion, claims, createdAt |
id, type, schemaVersion, claims |
| Holon type/version | ProductHolon / 1.0 |
Same |
| Required claims | product |
product |
| Required product fields | Every property present in the supplied product | id, type, name, gtin14 |
| Required nested object fields | All supplied fields in each nested object | Nested fields become optional |
| Unknown object properties | Rejected | Rejected |
| GTIN strings | Exactly 14 decimal digits | Same when present |
| Product arrays | Nonempty arrays with typed items | Same when present |
The schemas retain types and constraints on disclosed values. Product strings must be nonempty and quantities are JSON numbers. The schemas do not verify a GTIN check digit, require both GTIN properties to agree, recompute review averages, or constrain every number to an integer or positive range. Product date strings are not checked as commercial expiry rules. Holon URI/date fields have their own format constraints. These are example profiles, not full GS1 business-rule validation. Optional evidence and related-Holon shapes are inherited from the Holon profile; both workflows supply empty arrays and make no corroboration claim.
Reveal selection¶
{
"version": "1.0",
"selectivePointers": [
"/credentialSubject/claims/product/id",
"/credentialSubject/claims/product/type",
"/credentialSubject/claims/product/name",
"/credentialSubject/claims/product/gtin14",
"/credentialSubject/claims/product/gs1:gtin",
"/credentialSubject/claims/product/gs1:ingredientStatement",
"/credentialSubject/claims/product/gs1:allergenStatement"
]
}
These seven pointers retain product identity, name, GTINs, ingredients and
allergens. The CLI additionally retains mandatory credential/Holon metadata.
The disclosure schema requires product id, type, name and gtin14, so a
new reveal selection must still satisfy that profile. Adding a pointer does not
add a claim to the source credential; the selected claim must already be signed.
Manual shell implementation¶
The setup section enables shell error handling, sets owner-only output
permissions and creates a fresh working directory. vc() supplies the same
binary, data directory, pinned configuration, offline mode and JSON output to
every command. sign_vc() adds password input over stdin only where required.
The remaining named snippet sections prepare files, configure keys and trust,
stage public resources, issue, derive, perform a price-tamper check and build HTML.
issuer-key.json and selective-key.json contain public setup reports used for
fingerprint extraction; encrypted private keys live under data/keys/private/.
expires.txt carries the generated one-day expiry into the issuance commands.
The script leaves its artifacts in the printed directory. It checks one tampered
price; the automated runner additionally checks ingredients and review text.
Automated Python implementation¶
main() accepts --binary (default target/release/holon-vc) and --output
(a new directory). Without --output, a temporary directory is cleaned up after
success. It generates an ephemeral password and one-day expiry. digest() hashes
context/schema bytes, write() serializes output JSON, and run() invokes the
CLI with fixed offline/configuration arguments, a 90-second timeout per command,
expected exit-code checking and JSON response parsing.
The 17 CLI operations comprise six key/suite/trust operations, two publication and status operations, four full issuance/verification operations, three negative verification operations and two disclosure/verification operations. The runner also compares both full credential product objects with the input, checks the derived product's required claims and omissions, and round-trips HTML-safe JSON. It never implements signing itself: cryptography is performed by the Rust CLI.
Regression check and maintenance¶
cargo test --locked --test product_example
bash -n examples/product/walkthrough.sh
bash scripts/docs.sh build
The Rust test starts python3 scripts/product-workflow.py --binary with Cargo's
built CLI path and requires successful completion. It tests the automated route;
it does not execute the interactive Bash script. Run the manual route separately
when changing its commands. Keep the schema, context and reveal selection aligned
when adding product fields; regenerate the runtime configuration to pin the new
file digests. Never edit already signed credentials to update claims.