Skip to content

Commit 680c862

Browse files
author
Volodymyr Shvets
committed
perf(schema): sign GeoJSON geometry as @JSON literal to fix pathological VC canonicalization
Signing a VC that embeds a GeoJSON geometry is dominated by JSON-LD canonicalization. The generated schema context never activates the geojson `@list` scoped context for the geometry field, so `coordinates` falls through to the default `@vocab` and every coordinate is expanded per-value into RDF, which is super-linear (~O(n^2)). A large MultiPolygon takes tens of seconds and tens-to-hundreds of MB to sign; concurrent signings stack the cost. Fix: when a schema context contains `#GeoJSON`, map the root `coordinates`/`bbox` terms to `@type: @json` so the geometry is signed as a single opaque JCS literal instead of exploded into RDF lists (root-level term -> applies at every nesting depth). Cuts canonicalization from ~22s to ~1.5s on a large real schema. Gated by `geoJsonCoordinatesAsJson`, passed by schema-publish-helper as `entity !== EVC`: EVC schemas keep the old context because BBS+ selective- disclosure derive flattens nested arrays inside an @JSON literal, corrupting the revealed geometry. `@type: @json` changes only JSON-LD canonicalization, not the stored JSON document, so consumers reading the geometry as JSON are unaffected. The change applies to newly published schema contexts; existing credentials keep verifying against their own published context.
1 parent 1a88b4b commit 680c862

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

common/src/helpers/schemas-to-context.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export function schemasToContext(
1515
type?: string | undefined;
1616
// tslint:disable-next-line:completed-docs
1717
rootTerms?: any;
18+
// Map GeoJSON `coordinates`/`bbox` to a single `@type: @json` literal instead of
19+
// expanding them per-coordinate into RDF (huge JSON-LD canonicalization cost on sign).
20+
// MUST be false for EVC schemas: BBS+ selective-disclosure derive flattens @json values.
21+
// tslint:disable-next-line:completed-docs
22+
geoJsonCoordinatesAsJson?: boolean;
1823
}
1924
): {
2025
// tslint:disable-next-line:completed-docs
@@ -32,5 +37,16 @@ export function schemasToContext(
3237
replacedContext['@context'][contextEntry[0]] = contextEntry[1];
3338
}
3439
}
40+
if (contextSettings?.geoJsonCoordinatesAsJson && additionalContexts && additionalContexts.has('#GeoJSON')) {
41+
// GeoJSON `coordinates`/`bbox` are deeply-nested numeric arrays. Expanding them into
42+
// RDF (per-coordinate) makes JSON-LD canonicalization super-linear — a large MultiPolygon
43+
// takes tens of seconds and tens-to-hundreds of MB to sign. Map them to a single @json
44+
// literal (root-level, so it applies at every nesting depth) so the geometry is signed as
45+
// an opaque JCS value instead of exploded into RDF lists. Requires JSON-LD 1.1 (set above).
46+
// Gated by the caller: EVC schemas must NOT use this — BBS+ selective-disclosure derive
47+
// flattens nested arrays inside an @json literal, corrupting the revealed geometry.
48+
replacedContext['@context'].coordinates = { '@id': 'https://purl.org/geojson/vocab#coordinates', '@type': '@json' };
49+
replacedContext['@context'].bbox = { '@id': 'https://purl.org/geojson/vocab#bbox', '@type': '@json' };
50+
}
3551
return replacedContext;
3652
}

guardian-service/src/helpers/import-helpers/schema/schema-publish-helper.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ISchemaDocument,
77
ModuleStatus,
88
Schema,
9+
SchemaEntity,
910
SchemaHelper,
1011
SchemaStatus,
1112
SentinelHubContext
@@ -52,7 +53,11 @@ export function generateSchemaContext(item: SchemaCollection) {
5253
checkSchemaProps(item, itemDocument);
5354
const defsArray = itemDocument.$defs ? Object.values(itemDocument.$defs) : [];
5455
const additionalContexts = getAdditionalContexts(itemDocument);
55-
return schemasToContext([...defsArray, itemDocument], additionalContexts);
56+
return schemasToContext([...defsArray, itemDocument], additionalContexts, {
57+
// Sign GeoJSON geometry as an opaque @json literal (fast canonicalization) — but NOT for
58+
// EVC schemas, where BBS+ selective-disclosure derive would flatten the revealed geometry.
59+
geoJsonCoordinatesAsJson: item.entity !== SchemaEntity.EVC,
60+
});
5661
}
5762

5863
export function generatePackage(options: {
@@ -96,7 +101,10 @@ export function generatePackage(options: {
96101
Array.from(defsArray.values()),
97102
additionalContexts,
98103
{
99-
vocab: 'https://w3id.org/traceability/#undefinedTerm'
104+
vocab: 'https://w3id.org/traceability/#undefinedTerm',
105+
// Only optimize GeoJSON-as-@json when NO packaged schema is EVC (selective disclosure),
106+
// since the package shares one context and @json breaks BBS+ derive of geometry.
107+
geoJsonCoordinatesAsJson: schemas.every((s) => s.entity !== SchemaEntity.EVC)
100108
}
101109
);
102110

0 commit comments

Comments
 (0)