EncDotNet.S100.ExchangeSets
Reader for S-100 Exchange Set catalogues, dataset/support file discovery, and digital signature verification.
Overview
This library parses S-100 Exchange Set CATALOG.XML files and provides access to the datasets and support files within an exchange set. Key types include:
ExchangeSet— opens and navigates an exchange set through anIAssetSource.ExchangeCatalogue— the parsed catalogue metadata.ExchangeCatalogueReader— XML parser for the exchange catalogue.DatasetDiscoveryMetadata— metadata for each dataset in the exchange set (file name, bounding box, product specification).SupportFileDiscoveryMetadata— metadata for support files.CatalogueDiscoveryMetadata— metadata for embedded catalogues.
File path resolution
Producers lay out exchange sets in different ways, and the catalogue can
describe a file's location in several forms. ExchangeSet normalizes all
of them into a single source-relative path:
- A separate
<filePath>directory element combined with a bare<fileName>(e.g. UKHO S-101:filePath=101GB00502793,fileName=101GB00502793.000). - A full path folded into
<fileName>, optionally with afile:/URI prefix (e.g.file:/S-101/DATASET_FILES/101AU005BTB01.000). - Windows-style separators and a leading slash in
<filePath>(e.g. NOAA S-102:\S102\PBC_UTM11N_MLLW_LALB).
Use DatasetDiscoveryMetadata.RelativePath (and the equivalent on the
support/catalogue metadata types) — or the static
ExchangeSet.ResolveRelativePath(filePath, fileName) — to obtain the path
to pass to an IAssetSource. ExchangeSet.NormalizeFileName handles the
file:/ prefix, backslash separators, and leading slashes on a bare file
name.
The reader also recognizes dataset/support/catalogue discovery items that
are wrapped in product-specific elements and namespaces
(e.g. S102_DatasetDiscoveryMetadata in http://www.iho.int/s102/2.0/xc),
not just the generic S100_DatasetDiscoveryMetadata.
Legacy S100EC catalogue layout
Modern S-100 (Edition 5.x, Part 17) nests discovery records inside a
wrapper element such as <datasetDiscoveryMetadata>. Some products —
notably JCOMM/IHO S-411 sample sets (namespace
http://www.iho.int/S100EC) — instead place the
S100_DatasetDiscoveryMetadata records directly under the catalogue
root with no wrapper. The reader tolerates both layouts: when the
wrapper is present its children are read, otherwise the root is scanned
directly.
Digital Signature Verification
The library implements the S-100 Part 15 Data Protection Scheme for signature verification and a complementary checksum/integrity dimension. Exchange sets may include multiple per-file signatures (DSA or ECDSA over SHA-256) embedded in CATALOG.XML, including signatures over unencrypted, compressed, or encrypted data and distribution signatures that sign another signature's ASN.1 R/S bytes. Independently of any signature, each file's SHA-256 digest is computed and its presence/readability confirmed, so even an unsigned exchange set can be checked for missing or corrupt files.
Model types
| Type | Description | S-100 Part 15 ref |
|---|---|---|
DigitalSignatureAlgorithm |
Legacy DSA/ECDSA plus the Part 15 file-transfer ECDSA384SHA2 algorithm |
§15-8.4, §15-8.7 |
DigitalSignatureValue |
Parsed legacy, signature-on-data, or signature-on-signature value (id, certificateRef, raw signature bytes, and form-specific metadata) | §15-8.8, §15-8.11.3–6 |
DigitalSignatureKind / SignatureDataStatus |
Signature form and the unencrypted/compressed/encrypted representation covered by a data signature | §15-8.11.3–6 |
SignatureVerificationResult |
Per-signature outcome and structured failure reason | §15-8.8 |
CertificateBlock |
Certificate collection from the catalogue (scheme administrator ID + certificate entries) | §15-5 |
CertificateEntry |
Individual X.509 certificate (id, issuer, DER-encoded bytes) | §15-5.2 |
CryptographicHash |
Parsed hash MRN urn:mrn:iho:s100:hash:<alg>:<hex> used to integrity-check a resource |
§15-8.10, Table 15-12 |
These are surfaced as properties on DatasetDiscoveryMetadata, SupportFileDiscoveryMetadata, and CatalogueDiscoveryMetadata (via DigitalSignatureAlgorithm, ordered DigitalSignatures, the compatibility DigitalSignatureValue?, and ExpectedHash?), and on ExchangeCatalogue (via CertificateBlock?).
ExchangeSetVerifier verifies every signature on a resource. Signature IDs are
unique catalogue-wide; signatureRef chains are resolved within the same
resource entry, support forward references, and reject missing references,
duplicates, cross-resource references, and cycles explicitly. The aggregate
FileVerificationResult.Outcome remains compatible with existing callers,
while SignatureResults exposes each signature's result.
Verification API
// Create a verifier
IExchangeSetVerifier verifier = new ExchangeSetVerifier();
// Configure trust anchors (optional — pass trusted SA root certificates)
var trust = new TrustAnchorOptions
{
// For development/testing, skip certificate chain validation:
AllowUntrustedCertificates = true,
// For production, supply IHO SA root certificates:
// TrustedRoots = [saRootCert],
};
// Verify an exchange set
ExchangeSetVerificationResult result = await verifier.VerifyAsync(
assetSource, // IAssetSource (filesystem or ZIP)
catalogue, // ExchangeCatalogue (from ExchangeCatalogueReader)
trust,
cancellationToken);
// Inspect results
if (result.IsUnsigned)
{
// No signatures present — exchange set is unsigned
}
else if (result.AllValid)
{
// All files have valid signatures
}
else if (result.HasInvalidSignatures)
{
// At least one file has an invalid or untrusted signature
foreach (var file in result.FileResults)
{
Console.WriteLine($"{file.FileName}: {file.Outcome} — {file.Detail}");
}
}
Verification outcomes
Each FileVerificationResult reports two independent dimensions: the digital-signature outcome (Outcome) and the checksum/integrity outcome (ChecksumOutcome). A file may, for example, report a valid checksum while being unsigned. ComputedSha256 carries the file's SHA-256 digest (lower-case hex) — useful for the unsigned case. Both dimensions use the same VerificationOutcome enum:
VerificationOutcome |
Dimension | Meaning |
|---|---|---|
Ok |
both | Signature valid (and certificate trusted), or computed digest matched the declared hash |
NotSigned |
signature | No digital signature present for this file |
SignatureInvalid |
signature | Signature does not match the file contents |
CertificateUntrusted |
signature | Signature is valid but the certificate is not trusted |
CertificateExpired |
signature | Certificate has expired |
CertificateNotFound |
signature | Referenced certificate not found in the catalogue |
FileMissing |
both | Referenced file not found in the asset source (incomplete set) |
Error |
both | Unexpected error during verification |
NoChecksum |
checksum | File present and readable, but no declared hash to compare against |
ChecksumMismatch |
checksum | Computed digest does not match the declared cryptographic hash |
The
VerificationOutcomemembers are append-only: their names and ordinals are stable so downstream consumers (including the S-57 exchange-set bridge) can mirror them.
ExchangeSetVerificationResult exposes aggregate helpers across both dimensions: AllValid, HasInvalidSignatures, IsUnsigned (signature side) and HasChecksumMismatches, HasMissingFiles, IntegrityVerified (checksum side).
How a missing checksum is treated
S-100 integrity is delivered by Part 15 signatures, and the specification mandates no per-resource checksum element, so a "no checksum present" case (NoChecksum) must not count as a failure. This is a deliberate, documented decision:
AllValidis a strict signature-only predicate — it requires every file's signature to beOk, ignores the checksum dimension, and is thereforefalsefor an unsigned set. Callers pair it withIsUnsignedto tell "signed and all valid" apart from "unsigned". It is not the overall integrity verdict.IntegrityVerifiedis the integrity verdict:trueunless a file is missing or a declared checksum mismatched.NoChecksumdoes not fail it.- The
s100 validateexit code follows the same rule — a file fails only onChecksumMismatch/FileMissing/Error/ invalid signature (and, under--strict, alsoNotSigned/NoChecksum).
This mirrors the sibling S-57 implementation (EncDotNet #6), whose AllValid likewise treats a missing CRC as non-failing (the CATALOG.031 self-reference legitimately has none) and fails only on mismatch, missing file, error, or invalid signature — keeping the two repos' semantics consistent for any future shared/bridge abstraction.
Checksum / integrity verification
S-100 has no per-resource CRC element like S-57's CATALOG.031; the digital signature itself "serves the dual purpose of a checksum against the unencrypted data file" (Part 15 §15-8.9). The only standalone digest construct is the optional cryptographic hash MRN urn:mrn:iho:s100:hash:<alg>:<hex> (§15-8.10, Table 15-12), which real catalogues rarely carry and for which the specification defines no fixed catalogue slot. Accordingly:
- Every file is hashed (streaming SHA-256) and checked for presence/readability, so an unsigned set can still be checked for missing or corrupt files.
- When the catalogue declares a hash MRN for a resource (discovered best-effort by
ExchangeCatalogueReaderand surfaced asExpectedHash), the computed digest is compared against it (Ok/ChecksumMismatch); otherwise the file reportsNoChecksum.
Part 15 confidentiality (decryption)
The confidentiality dimension of Part 15 — reading encrypted datasets — is implemented at the library level under the EncDotNet.S100.ExchangeSets.Protection namespace. Signing/authoring and viewer/CLI wiring remain out of scope.
| Type | Role | S-100 Part 15 ref |
|---|---|---|
S100Cipher |
AES-128 primitives: single-block key wrap/unwrap (EncryptBlock/DecryptBlock) and dataset modified-CBC DecryptDataset/EncryptDataset |
§15-6 |
HardwareId |
16-byte Data Client system id (HW_ID) |
§15-7.3.1.1 |
UserPermit |
46-char user permit: parse/validate (CRC-32), Create, and DecryptHardwareId(M_KEY) |
§15-7.3 |
DataPermit |
One datasetPermit record (encryptedKey, mandatory expiry, edition/issue identity) |
§15-7.4.4 |
PermitFile / PermitGroup / PermitHeader |
PERMIT.XML parser (namespace-tolerant 5.0/5.1) with TryGetPermit lookup |
§15-7.4 |
StandaloneDigitalSignatureReader / PermitSignatureVerifier |
Parses PERMIT.SIGN, validates its certificate chain and ECDSA P-384/SHA-384 signature, and exposes the permit only after authentication |
§15-7.4.5, §15-8.11.2 |
IDatasetKeyProvider / PermitKeyProvider |
Resolves a cell key from an authenticated permit and enforces catalogue edition, issue-date, and expiry applicability | §15-7.4.4 |
DecryptingAssetSource |
IAssetSource decorator that decrypts (and optionally decompresses) keyed files transparently |
§15-5, §15-6 |
DatasetPermitException / DatasetDecryptionException |
Permit-policy refusal (with a PermitEvaluationResult), and a permitted dataset whose cell key can't decrypt it |
§15-6, §15-7.4.4 |
Crypto details (all pinned to the §15 worked examples in unit tests): AES-128, PKCS#7 padding, and the §15-6.2.4 modified CBC mode (a random block is prepended before encryption and discarded on decryption, so no IV is transmitted). Cell keys and hardware ids are exactly one AES block and are wrapped with single-block ECB. Compression (§15-5.2) is ZIP/DEFLATE, applied before encryption; DecryptingAssetSource unzips the single-entry archive when decompress is set.
using EncDotNet.S100.ExchangeSets.Protection;
// Hardware id either recovered from a user permit (needs the OEM M_KEY) or held by the client.
HardwareId hwId = UserPermit.Parse(userPermitText).DecryptHardwareId(manufacturerKey);
// Authenticate the licence before any permit key can be used.
await using Stream permitXml = File.OpenRead("PERMIT.XML");
await using Stream permitSign = File.OpenRead("PERMIT.SIGN");
PermitAuthenticationResult authentication =
await PermitSignatureVerifier.AuthenticateAsync(
permitXml, permitSign, "PERMIT.XML", trustAnchors);
PermitFile permits = authentication.PermitFile
?? throw new InvalidDataException(authentication.Verification.Detail);
// Catalogue metadata constrains permit edition, issue date, and expiry.
var keys = new PermitKeyProvider(permits, hwId, catalogue);
// Wrap any IAssetSource so encrypted datasets read as plaintext.
using IAssetSource source = new DecryptingAssetSource(fileSystemOrZipSource, keys, decompress: true);
await using Stream plaintext = await source.OpenAsync("S-101/101GB40079ABCDEF.000");
Errors. A dataset its permit doesn't authorize throws DatasetPermitException
before any decryption; Evaluation.Outcome says why (for example
EditionMismatch or IssuedAfterExpiry). A permit's encryptedKey is a bare
AES block with no checksum, so a wrong hardware id isn't detected when the
key is unwrapped. It shows up when that key fails to decrypt the dataset, and
DecryptingAssetSource then throws DatasetDecryptionException. The exception
names the dataset (DatasetPath) and points to the hardware id, the
manufacturer key used to recover it, or a permit issued for a different Data
Client. It derives from CryptographicException, and the original failure is its
InnerException. Detection relies on the PKCS#7 padding check, so about one
wrong key in 256 decrypts without an error and returns unreadable content
instead.
Legacy signatures over the unencrypted resource can verify an encrypted
exchange set by passing a DecryptingAssetSource to
ExchangeSetVerifier.VerifyAsync(...). Catalogues using the explicit Part 15
signature forms should instead use the stage-aware resolver below so one
resource can carry signatures over multiple representations.
For catalogues using the explicit Part 15 signature forms, pass the raw asset source to the verifier and supply the authenticated key provider to its stage-aware resolver:
var contentResolver = new Part15SignatureContentResolver(keys);
var verifier = new ExchangeSetVerifier(contentResolver);
ExchangeSetVerificationResult verification =
await verifier.VerifyAsync(fileSystemOrZipSource, catalogue, trust);
An encrypted signature hashes the stored ciphertext without requiring a
permit. A compressed signature decrypts only when necessary, and an
unencrypted signature decrypts and/or decompresses according to the
discovery metadata. This lets all representations and chained distribution
signatures coexist on one resource.
PermitFile.Read(...) remains available for metadata inspection, but returns an unauthenticated permit that PermitKeyProvider rejects. Production key use must flow through PermitSignatureVerifier.AuthenticateAsync(...).
Viewer/CLI workflows for supplying permits and keys remain separate from the library-level signature and decryption support.
For a step-by-step walkthrough, including code that generates a test protected
exchange set, see
Reading protected exchange sets. The
EncDotNet.S100 facade's S100ExchangeSet.WithDecryption wraps
DecryptingAssetSource for you.
CLI
The s100 validate command verifies an exchange set when given a CATALOG.XML, a directory containing one, or a .zip whose root holds one:
s100 validate exchangeset/CATALOG.XML
s100 validate ./exchangeset # folder
s100 validate exchangeset.zip --format json
It prints a per-file signature/checksum table (or JSON), exits 0 when no file fails, and 6 (the shared findings exit code) on any failure. --strict additionally fails unsigned files and files with no declared checksum.
The same command also verifies S-57 / S-63 exchange sets (a folder containing a CATALOG.031, or the CATALOG.031 file itself) by routing through EncDotNet.S100.Datasets.S57.S57ExchangeSetVerification, which checks each file's CRC-32 and maps the upstream EncDotNet.S57 result onto this same ExchangeSetVerificationResult model and exit-code semantics (NoChecksum / NotSigned non-failing). See the S-57 bridge README.
s100 validate s57set/CATALOG.031
s100 validate ./s57set --format json # folder containing CATALOG.031
Trust anchor model
TrustAnchorOptions controls how certificate trust is evaluated:
TrustedRoots— a list ofX509Certificate2instances representing trusted Scheme Administrator (SA) root certificates. A signing certificate'sIssuerfield is matched against these roots.AllowUntrustedCertificates— whentrue, signatures are verified for correctness but certificate chain validation is skipped. This is useful during development or when loading exchange sets from unknown sources.
The IHO publishes test SA certificates for interoperability testing. For production use, supply the official IHO SA root certificate.
Scope and limitations
- Verification only — signing/authoring of exchange sets is not yet implemented.
- Decryption, permit authentication, and signature metadata are implemented — Part 15 confidentiality and all catalogue-level signature forms are supported at the library level. Viewer/CLI permit-entry UX remains out of scope here.
- Checksum reference is opportunistic — S-100 mandates no per-resource hash, so
NoChecksumis the common (and non-failing) result for unsigned sets; hash-MRN placement is discovered best-effort. - File hashing uses streaming SHA-256 to avoid loading large HDF5 files into memory.
Installation
dotnet add package EncDotNet.S100.ExchangeSets