Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/EncDotNet.S57/EncDotNet.S57.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="EndDotNet.UnitTests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EncDotNet.Iso8211\EncDotNet.Iso8211.csproj" />
</ItemGroup>
Expand Down
41 changes: 41 additions & 0 deletions src/EncDotNet.S57/ExchangeSets/IS57ExchangeSetVerifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.Extensions.Logging;

namespace EncDotNet.S57.ExchangeSets;

/// <summary>
/// Verifies the integrity of the files referenced by an S-57 exchange set catalogue.
/// </summary>
/// <remarks>
/// <para>
/// Verification covers the per-file CRC checksums declared in the CATD <c>CRCS</c> subfield of
/// <c>CATALOG.031</c> (S-57 Edition 3.1 Part 3, clause 3.4) and, in future, S-63 digital
/// signatures. The two dimensions are reported independently on each
/// <see cref="S57FileVerificationResult"/>.
/// </para>
/// <para>
/// Implementations are non-throwing: per-file failures are surfaced as
/// <see cref="S57VerificationOutcome"/> values rather than exceptions.
/// </para>
/// </remarks>
public interface IS57ExchangeSetVerifier
{
/// <summary>
/// Verifies the integrity metadata in <paramref name="catalog"/> against the files located
/// under <paramref name="rootPath"/>.
/// </summary>
/// <param name="rootPath">The absolute path to the root directory of the exchange set.</param>
/// <param name="catalog">The parsed <c>CATALOG.031</c> whose entries to verify.</param>
/// <param name="trustAnchors">
/// Optional S-63 trust anchor options. Currently unused because signature verification is a
/// seam; supply when S-63 support is available.
/// </param>
/// <param name="logger">An optional logger for reporting verification warnings.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A result enumerating per-file verification outcomes without throwing.</returns>
Task<S57ExchangeSetVerificationResult> VerifyAsync(
string rootPath,
S57Catalog catalog,
S63TrustAnchorOptions? trustAnchors = null,
ILogger? logger = null,
CancellationToken cancellationToken = default);
}
34 changes: 34 additions & 0 deletions src/EncDotNet.S57/ExchangeSets/IS63SignatureVerifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace EncDotNet.S57.ExchangeSets;

/// <summary>
/// Verifies the S-63 digital signature of a file in an S-57 exchange set.
/// </summary>
/// <remarks>
/// <para>
/// This is a <strong>seam</strong>: the full S-63 ENC Data Protection Scheme (RSA/DSA signatures
/// over the SA-issued data server certificate chain) is not yet implemented and is intended to
/// land alongside S-63 decryption support. The interface is published now so that the
/// <see cref="IS57ExchangeSetVerifier"/> surface is stable in advance.
/// </para>
/// <para>
/// Implementations must be non-throwing and report failures via the returned
/// <see cref="S57VerificationOutcome"/>.
/// </para>
/// </remarks>
public interface IS63SignatureVerifier
{
/// <summary>
/// Verifies the detached S-63 signature associated with the file at <paramref name="filePath"/>.
/// </summary>
/// <param name="filePath">The absolute path to the file whose signature is being verified.</param>
/// <param name="trustAnchors">Trust anchor options controlling certificate chain validation.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>
/// The signature verification outcome. Returns <see cref="S57VerificationOutcome.NotSigned"/>
/// when no signature material is present for the file.
/// </returns>
Task<S57VerificationOutcome> VerifySignatureAsync(
string filePath,
S63TrustAnchorOptions trustAnchors,
CancellationToken cancellationToken = default);
}
91 changes: 91 additions & 0 deletions src/EncDotNet.S57/ExchangeSets/S57Crc32.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
namespace EncDotNet.S57.ExchangeSets;

/// <summary>
/// Computes the 32-bit cyclic redundancy check (CRC-32) used by S-57 exchange sets.
/// </summary>
/// <remarks>
/// <para>
/// S-57 Edition 3.1 Part 3, clause 3.4 defers the choice of CRC algorithm to the relevant
/// product specification. The ENC product specification (Appendix B.1) uses the ubiquitous
/// CRC-32 employed by the ZIP file format and Ethernet: the reflected polynomial
/// <c>0xEDB88320</c> (i.e. <c>0x04C11DB7</c> reversed), an initial value of all ones, and a
/// final XOR of all ones. This is the same algorithm exposed by <c>System.IO.Hashing.Crc32</c>
/// and <c>zlib</c>.
/// </para>
/// <para>
/// Implemented internally to avoid adding a package dependency for a single, well-known
/// table-based computation.
/// </para>
/// </remarks>
internal static class S57Crc32
{
private const uint Polynomial = 0xEDB88320u;

private static readonly uint[] Table = BuildTable();

private static uint[] BuildTable()
{
var table = new uint[256];
for (uint i = 0; i < 256; i++)
{
uint crc = i;
for (int bit = 0; bit < 8; bit++)
{
crc = (crc & 1) != 0 ? (crc >> 1) ^ Polynomial : crc >> 1;
}

table[i] = crc;
}

return table;
}

/// <summary>
/// Computes the CRC-32 of the bytes read from <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The stream to read to completion.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The computed CRC-32 value.</returns>
public static async Task<uint> ComputeAsync(Stream stream, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stream);

uint crc = 0xFFFFFFFFu;
var buffer = new byte[81920];
int bytesRead;

while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0)
{
for (int i = 0; i < bytesRead; i++)
{
crc = (crc >> 8) ^ Table[(crc ^ buffer[i]) & 0xFF];
}
}

return crc ^ 0xFFFFFFFFu;
}

/// <summary>
/// Computes the CRC-32 of the supplied bytes.
/// </summary>
/// <param name="data">The data to checksum.</param>
/// <returns>The computed CRC-32 value.</returns>
public static uint Compute(ReadOnlySpan<byte> data)
{
uint crc = 0xFFFFFFFFu;
foreach (byte b in data)
{
crc = (crc >> 8) ^ Table[(crc ^ b) & 0xFF];
}

return crc ^ 0xFFFFFFFFu;
}

/// <summary>
/// Formats a CRC-32 value as the 8-character, upper-case hexadecimal string used in the
/// CATD <c>CRCS</c> subfield (most-significant byte first).
/// </summary>
/// <param name="crc">The CRC-32 value.</param>
/// <returns>An 8-character upper-case hexadecimal string.</returns>
public static string Format(uint crc) => crc.ToString("X8");
}
31 changes: 31 additions & 0 deletions src/EncDotNet.S57/ExchangeSets/S57ExchangeSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,37 @@ public Task<S57Catalog> ReadCatalogAsync(string rootPath, ILogger? logger = null
return S57CatalogReader.ReadFromFileAsync(catalogPath, logger, cancellationToken);
}

/// <summary>
/// Verifies the integrity of the files referenced by this exchange set's <c>CATALOG.031</c>.
/// </summary>
/// <remarks>
/// This is an opt-in, non-throwing operation: it reads the catalogue and validates each
/// entry's CRC checksum against the corresponding file on disk, returning the per-file
/// outcomes. S-63 digital-signature verification is a seam and currently reports every file
/// as <see cref="S57VerificationOutcome.NotSigned"/>.
/// </remarks>
/// <param name="rootPath">The absolute path to the root directory of the exchange set.</param>
/// <param name="trustAnchors">Optional S-63 trust anchor options (currently unused).</param>
/// <param name="logger">An optional logger for reporting verification warnings.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A result enumerating per-file verification outcomes.</returns>
/// <exception cref="InvalidOperationException"><see cref="CatalogFileName"/> is <see langword="null"/>.</exception>
public async Task<S57ExchangeSetVerificationResult> VerifyAsync(
string rootPath,
S63TrustAnchorOptions? trustAnchors = null,
ILogger? logger = null,
CancellationToken cancellationToken = default)
{
if (CatalogFileName is null)
{
throw new InvalidOperationException("The exchange set does not contain a catalog file.");
}

var catalog = await ReadCatalogAsync(rootPath, logger, cancellationToken).ConfigureAwait(false);
var verifier = new S57ExchangeSetVerifier();
return await verifier.VerifyAsync(rootPath, catalog, trustAnchors, logger, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Reads the base cell file and applies any update files in order, returning the resulting document.
/// </summary>
Expand Down
40 changes: 40 additions & 0 deletions src/EncDotNet.S57/ExchangeSets/S57ExchangeSetVerificationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Collections.Immutable;

namespace EncDotNet.S57.ExchangeSets;

/// <summary>
/// The aggregate result of verifying all files referenced by an S-57 exchange set catalogue.
/// </summary>
public sealed class S57ExchangeSetVerificationResult
{
/// <summary>Gets the per-file verification results.</summary>
public required ImmutableArray<S57FileVerificationResult> FileResults { get; init; }

/// <summary>
/// Gets a value indicating whether the exchange set has no integrity violations: every
/// file's checksum is <see cref="S57VerificationOutcome.Ok"/> or
/// <see cref="S57VerificationOutcome.NoChecksum"/> (the CRC is optional in S-57, so its
/// absence is not a failure), and every signature is <see cref="S57VerificationOutcome.Ok"/>
/// or <see cref="S57VerificationOutcome.NotSigned"/>.
/// </summary>
public bool AllValid => FileResults.All(r =>
r.ChecksumOutcome is S57VerificationOutcome.Ok or S57VerificationOutcome.NoChecksum
&& r.SignatureOutcome is S57VerificationOutcome.Ok or S57VerificationOutcome.NotSigned);

/// <summary>Gets a value indicating whether at least one file's CRC checksum did not match.</summary>
public bool HasChecksumMismatches => FileResults.Any(r => r.ChecksumOutcome == S57VerificationOutcome.ChecksumMismatch);

/// <summary>Gets a value indicating whether at least one referenced file was missing on disk.</summary>
public bool HasMissingFiles => FileResults.Any(r => r.ChecksumOutcome == S57VerificationOutcome.FileMissing);

/// <summary>
/// Gets a value indicating whether at least one file had an invalid digital signature.
/// </summary>
public bool HasInvalidSignatures => FileResults.Any(r => r.SignatureOutcome == S57VerificationOutcome.SignatureInvalid);

/// <summary>
/// Gets a value indicating whether no file carries a digital signature (all are
/// <see cref="S57VerificationOutcome.NotSigned"/>). Unencrypted ENC exchange sets are unsigned.
/// </summary>
public bool IsUnsigned => FileResults.All(r => r.SignatureOutcome == S57VerificationOutcome.NotSigned);
}
Loading
Loading