feat: Add MongoIdempotencyStoreOptions for MongoDB configuration
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat: Implement BsonJsonConverter for converting BsonDocument and BsonArray to JSON fix: Update project file to include MongoDB.Bson package test: Add GraphOverlayExporterTests to validate NDJSON export functionality refactor: Refactor Program.cs in Attestation Tool for improved argument parsing and error handling docs: Update README for stella-forensic-verify with usage instructions and exit codes feat: Enhance HmacVerifier with clock skew and not-after checks feat: Add MerkleRootVerifier and ChainOfCustodyVerifier for additional verification methods fix: Update DenoRuntimeShim to correctly handle file paths feat: Introduce ComposerAutoloadData and related parsing in ComposerLockReader test: Add tests for Deno runtime execution and verification test: Enhance PHP package tests to include autoload data verification test: Add unit tests for HmacVerifier and verification logic
This commit is contained in:
@@ -1,60 +1,136 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Provenance.Attestation;
|
||||
|
||||
static int PrintUsage()
|
||||
{
|
||||
Console.Error.WriteLine("Usage: stella-forensic-verify --payload <file> --signature-hex <hex> --key-hex <hex> [--key-id <id>] [--content-type <ct>]");
|
||||
return 1;
|
||||
}
|
||||
return await ToolEntrypoint.RunAsync(args, Console.Out, Console.Error, TimeProvider.System);
|
||||
|
||||
string? GetArg(string name)
|
||||
internal static class ToolEntrypoint
|
||||
{
|
||||
for (int i = 0; i < args.Length - 1; i++)
|
||||
private const int ExitInvalid = 1;
|
||||
private const int ExitUnverified = 2;
|
||||
|
||||
public static async Task<int> RunAsync(string[] args, TextWriter stdout, TextWriter stderr, TimeProvider timeProvider)
|
||||
{
|
||||
if (args[i].Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
return args[i + 1];
|
||||
var options = Parse(args);
|
||||
if (!options.Valid)
|
||||
{
|
||||
return Usage(stderr);
|
||||
}
|
||||
|
||||
byte[] payload;
|
||||
try
|
||||
{
|
||||
payload = options.PayloadPath == "-"
|
||||
? await ReadAllAsync(Console.OpenStandardInput())
|
||||
: await File.ReadAllBytesAsync(options.PayloadPath!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await stderr.WriteLineAsync($"read error: {ex.Message}");
|
||||
return ExitInvalid;
|
||||
}
|
||||
|
||||
byte[] signature;
|
||||
byte[] key;
|
||||
try
|
||||
{
|
||||
signature = Hex.FromHex(options.SignatureHex!);
|
||||
key = Hex.FromHex(options.KeyHex!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await stderr.WriteLineAsync($"hex parse error: {ex.Message}");
|
||||
return ExitInvalid;
|
||||
}
|
||||
|
||||
var signRequest = new SignRequest(payload, options.ContentType!, RequiredClaims: new[] { "predicateType" });
|
||||
var signResult = new SignResult(signature, options.KeyId!, options.SignedAt ?? DateTimeOffset.MinValue, null);
|
||||
|
||||
var verifier = new HmacVerifier(new InMemoryKeyProvider(options.KeyId!, key, options.NotAfter), timeProvider, options.MaxSkew);
|
||||
var verifyResult = await verifier.VerifyAsync(signRequest, signResult);
|
||||
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
valid = verifyResult.IsValid,
|
||||
reason = verifyResult.Reason,
|
||||
verifiedAt = verifyResult.VerifiedAt.ToUniversalTime().ToString("O"),
|
||||
keyId = options.KeyId,
|
||||
contentType = options.ContentType
|
||||
}, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false });
|
||||
|
||||
await stdout.WriteLineAsync(json);
|
||||
return verifyResult.IsValid ? 0 : ExitUnverified;
|
||||
}
|
||||
|
||||
private static async Task<byte[]> ReadAllAsync(Stream stream)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static int Usage(TextWriter stderr)
|
||||
{
|
||||
stderr.WriteLine("Usage: stella-forensic-verify --payload <file|-> --signature-hex <hex> --key-hex <hex> [--key-id <id>] [--content-type <ct>] [--signed-at <ISO>] [--not-after <ISO>] [--max-skew-minutes <int>]");
|
||||
stderr.WriteLine("Exit codes: 0 valid, 2 invalid signature/time, 1 bad args");
|
||||
return ExitInvalid;
|
||||
}
|
||||
|
||||
private static ParsedOptions Parse(string[] args)
|
||||
{
|
||||
string? GetArg(string name)
|
||||
{
|
||||
for (int i = 0; i < args.Length - 1; i++)
|
||||
{
|
||||
if (args[i].Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
return args[i + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var payload = GetArg("--payload");
|
||||
var sig = GetArg("--signature-hex");
|
||||
var key = GetArg("--key-hex");
|
||||
if (payload is null || sig is null || key is null)
|
||||
{
|
||||
return ParsedOptions.Invalid;
|
||||
}
|
||||
|
||||
DateTimeOffset? ParseDate(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
return DateTimeOffset.Parse(value!, null, System.Globalization.DateTimeStyles.RoundtripKind);
|
||||
}
|
||||
|
||||
TimeSpan ParseSkew(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return TimeSpan.FromMinutes(5);
|
||||
return TimeSpan.FromMinutes(double.Parse(value!, System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return new ParsedOptions(
|
||||
Valid: true,
|
||||
PayloadPath: payload,
|
||||
SignatureHex: sig,
|
||||
KeyHex: key,
|
||||
KeyId: GetArg("--key-id") ?? "hmac",
|
||||
ContentType: GetArg("--content-type") ?? "application/octet-stream",
|
||||
SignedAt: ParseDate(GetArg("--signed-at")),
|
||||
NotAfter: ParseDate(GetArg("--not-after")),
|
||||
MaxSkew: ParseSkew(GetArg("--max-skew-minutes"))
|
||||
);
|
||||
}
|
||||
|
||||
private sealed record ParsedOptions(
|
||||
bool Valid,
|
||||
string? PayloadPath = null,
|
||||
string? SignatureHex = null,
|
||||
string? KeyHex = null,
|
||||
string? KeyId = null,
|
||||
string? ContentType = null,
|
||||
DateTimeOffset? SignedAt = null,
|
||||
DateTimeOffset? NotAfter = null,
|
||||
TimeSpan MaxSkew = default)
|
||||
{
|
||||
public static readonly ParsedOptions Invalid = new(false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
string? payloadPath = GetArg("--payload");
|
||||
string? signatureHex = GetArg("--signature-hex");
|
||||
string? keyHex = GetArg("--key-hex");
|
||||
string keyId = GetArg("--key-id") ?? "hmac";
|
||||
string contentType = GetArg("--content-type") ?? "application/octet-stream";
|
||||
|
||||
if (payloadPath is null || signatureHex is null || keyHex is null)
|
||||
{
|
||||
return PrintUsage();
|
||||
}
|
||||
|
||||
byte[] payload = await System.IO.File.ReadAllBytesAsync(payloadPath);
|
||||
byte[] signature;
|
||||
byte[] key;
|
||||
try
|
||||
{
|
||||
signature = Hex.FromHex(signatureHex);
|
||||
key = Hex.FromHex(keyHex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"hex parse error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var request = new SignRequest(payload, contentType);
|
||||
var signResult = new SignResult(signature, keyId, DateTimeOffset.MinValue, null);
|
||||
|
||||
var verifier = new HmacVerifier(new InMemoryKeyProvider(keyId, key));
|
||||
var result = await verifier.VerifyAsync(request, signResult);
|
||||
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
valid = result.IsValid,
|
||||
reason = result.Reason,
|
||||
verifiedAt = result.VerifiedAt.ToUniversalTime().ToString("O")
|
||||
});
|
||||
Console.WriteLine(json);
|
||||
|
||||
return result.IsValid ? 0 : 2;
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
# stella-forensic-verify (preview)
|
||||
|
||||
Minimal dotnet tool for offline HMAC verification of provenance payloads.
|
||||
Minimal .NET 10 global tool for offline verification of provenance payloads signed with an HMAC key. No network access; deterministic JSON output.
|
||||
|
||||
## Usage
|
||||
|
||||
/mnt/e/dev/git.stella-ops.org /mnt/e/dev/git.stella-ops.org
|
||||
/mnt/e/dev/git.stella-ops.org
|
||||
```
|
||||
stella-forensic-verify \
|
||||
--payload payload.bin # or '-' to read stdin
|
||||
--signature-hex DEADBEEF... # hex-encoded HMAC
|
||||
--key-hex 001122... # hex-encoded HMAC key
|
||||
[--key-id hmac] # optional key id
|
||||
[--content-type application/octet-stream]
|
||||
[--signed-at 2025-11-21T12:00:00Z]
|
||||
[--not-after 2025-12-31T23:59:59Z]
|
||||
[--max-skew-minutes 5]
|
||||
```
|
||||
|
||||
Outputs deterministic JSON:
|
||||
Output (single line, deterministic field order):
|
||||
|
||||
```
|
||||
{"valid":true,"reason":"verified","verifiedAt":"2025-11-22T12:00:00.0000000Z","keyId":"hmac","contentType":"application/octet-stream"}
|
||||
```
|
||||
|
||||
## Exit codes
|
||||
- 0: signature valid
|
||||
- 2: signature invalid
|
||||
- 1: bad arguments/hex parse failure
|
||||
- 2: signature/time invalid
|
||||
- 1: bad arguments or hex parse failure
|
||||
|
||||
## Offline kit packaging (manual)
|
||||
1. `dotnet pack src/Provenance/StellaOps.Provenance.Attestation.Tool/StellaOps.Provenance.Attestation.Tool.csproj -c Release -o out/tools`
|
||||
2. Copy the produced nupkg into the offline kit under `tools/`.
|
||||
3. Install in air-gap host: `dotnet tool install --global --add-source tools stella-forensic-verify --version <pkg-version>`.
|
||||
4. Document expected SHA256 of the nupkg alongside the kit manifest.
|
||||
|
||||
Reference in New Issue
Block a user