|
|
|
|
@@ -3,6 +3,7 @@ using System.IO.Compression;
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
|
using StellaOps.Cli.Services;
|
|
|
|
|
using Xunit;
|
|
|
|
|
@@ -111,6 +112,164 @@ public sealed class DevPortalBundleVerifierTests : IDisposable
|
|
|
|
|
Assert.True(result.Portable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyBundleAsync_PortableV1_ReturnsProfileAndNoErrorCode()
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = CreatePortableV1Bundle();
|
|
|
|
|
|
|
|
|
|
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.Equal("verified", result.Status);
|
|
|
|
|
Assert.Equal(DevPortalVerifyExitCode.Success, result.ExitCode);
|
|
|
|
|
Assert.True(result.Portable);
|
|
|
|
|
Assert.Equal("portable-v1", result.Profile);
|
|
|
|
|
Assert.Null(result.ErrorCode);
|
|
|
|
|
|
|
|
|
|
var json = result.ToJson();
|
|
|
|
|
Assert.Contains("\"profile\":\"portable-v1\"", json, StringComparison.Ordinal);
|
|
|
|
|
Assert.DoesNotContain("\"errorCode\":", json, StringComparison.Ordinal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyBundleAsync_PortableV1_ReturnsManifestSignatureMissing_WhenDetachedSignatureIsAbsent()
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = CreatePortableV1Bundle(
|
|
|
|
|
mutateAfterSign: files => files.Remove("manifest.sig"));
|
|
|
|
|
|
|
|
|
|
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.Equal("failed", result.Status);
|
|
|
|
|
Assert.Equal(DevPortalVerifyExitCode.SignatureFailure, result.ExitCode);
|
|
|
|
|
Assert.Equal("ERR_MANIFEST_SIGNATURE_MISSING", result.ErrorCode);
|
|
|
|
|
Assert.Contains("\"errorCode\":\"ERR_MANIFEST_SIGNATURE_MISSING\"", result.ToJson(), StringComparison.Ordinal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyBundleAsync_PortableV1_ReturnsManifestSignatureInvalid_WhenDetachedPayloadDoesNotMatchManifest()
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = CreatePortableV1Bundle(
|
|
|
|
|
mutateAfterSign: files =>
|
|
|
|
|
{
|
|
|
|
|
var manifestNode = JsonNode.Parse(files["manifest.json"])!.AsObject();
|
|
|
|
|
manifestNode["createdUtc"] = "2026-02-10T12:00:00Z";
|
|
|
|
|
files["manifest.json"] = Encoding.UTF8.GetBytes(manifestNode.ToJsonString());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.Equal("failed", result.Status);
|
|
|
|
|
Assert.Equal(DevPortalVerifyExitCode.SignatureFailure, result.ExitCode);
|
|
|
|
|
Assert.Equal("ERR_MANIFEST_SIGNATURE_INVALID", result.ErrorCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyBundleAsync_PortableV1_ReturnsManifestSchema_WhenSpecVersionIsInvalid()
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = CreatePortableV1Bundle(
|
|
|
|
|
mutateBeforeSign: (manifest, _) => manifest["specVersion"] = "2.0");
|
|
|
|
|
|
|
|
|
|
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.Equal("failed", result.Status);
|
|
|
|
|
Assert.Equal(DevPortalVerifyExitCode.SignatureFailure, result.ExitCode);
|
|
|
|
|
Assert.Equal("ERR_MANIFEST_SCHEMA", result.ErrorCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyBundleAsync_PortableV1_ReturnsDssePayloadDigest_WhenPayloadDigestMismatchIsDetected()
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = CreatePortableV1Bundle(
|
|
|
|
|
mutateBeforeSign: (manifest, _) =>
|
|
|
|
|
{
|
|
|
|
|
var digests = manifest["digests"]!.AsObject();
|
|
|
|
|
var payloadDigest = digests["dssePayloadDigest"]!.AsObject();
|
|
|
|
|
payloadDigest["sha256"] = new string('c', 64);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.Equal("failed", result.Status);
|
|
|
|
|
Assert.Equal(DevPortalVerifyExitCode.SignatureFailure, result.ExitCode);
|
|
|
|
|
Assert.Equal("ERR_DSSE_PAYLOAD_DIGEST", result.ErrorCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyBundleAsync_PortableV1_ReturnsRekorRootMismatch_WhenRootHashIsInvalid()
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = CreatePortableV1Bundle(
|
|
|
|
|
mutateBeforeSign: (manifest, _) =>
|
|
|
|
|
{
|
|
|
|
|
var rekor = manifest["rekor"]!.AsObject();
|
|
|
|
|
rekor["rootHash"] = "bad-root";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.Equal("failed", result.Status);
|
|
|
|
|
Assert.Equal(DevPortalVerifyExitCode.SignatureFailure, result.ExitCode);
|
|
|
|
|
Assert.Equal("ERR_REKOR_ROOT_MISMATCH", result.ErrorCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyBundleAsync_PortableV1_ReturnsRekorReferenceUncovered_WhenTileCoverageIsIncomplete()
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = CreatePortableV1Bundle(
|
|
|
|
|
mutateBeforeSign: (manifest, _) =>
|
|
|
|
|
{
|
|
|
|
|
var rekor = manifest["rekor"]!.AsObject();
|
|
|
|
|
var tileRefs = rekor["tileRefs"]!.AsArray();
|
|
|
|
|
var firstTile = tileRefs[0]!.AsObject();
|
|
|
|
|
firstTile["covers"] = new JsonArray();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.Equal("failed", result.Status);
|
|
|
|
|
Assert.Equal(DevPortalVerifyExitCode.SignatureFailure, result.ExitCode);
|
|
|
|
|
Assert.Equal("ERR_REKOR_REFERENCE_UNCOVERED", result.ErrorCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyBundleAsync_PortableV1_ReturnsRekorTileMissing_WhenTileRefPathIsNotDeclaredInFiles()
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = CreatePortableV1Bundle(
|
|
|
|
|
mutateBeforeSign: (manifest, _) =>
|
|
|
|
|
{
|
|
|
|
|
var files = manifest["files"]!.AsObject();
|
|
|
|
|
files.Remove("rekor/tile.tar");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.Equal("failed", result.Status);
|
|
|
|
|
Assert.Equal(DevPortalVerifyExitCode.SignatureFailure, result.ExitCode);
|
|
|
|
|
Assert.Equal("ERR_REKOR_TILE_MISSING", result.ErrorCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task VerifyBundleAsync_PortableV1_ReturnsParquetFingerprintMismatch_WhenSchemaFingerprintIsMissing()
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = CreatePortableV1Bundle(
|
|
|
|
|
mutateBeforeSign: (manifest, files) =>
|
|
|
|
|
{
|
|
|
|
|
var parquetBytes = Encoding.UTF8.GetBytes("parquet-placeholder");
|
|
|
|
|
files["components.parquet"] = parquetBytes;
|
|
|
|
|
|
|
|
|
|
var manifestFiles = manifest["files"]!.AsObject();
|
|
|
|
|
manifestFiles["components.parquet"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["sha256"] = ComputeSha256Hex(parquetBytes),
|
|
|
|
|
["size"] = parquetBytes.Length
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.Equal("failed", result.Status);
|
|
|
|
|
Assert.Equal(DevPortalVerifyExitCode.SignatureFailure, result.ExitCode);
|
|
|
|
|
Assert.Equal("ERR_PARQUET_FINGERPRINT_MISMATCH", result.ErrorCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ToJson_OutputsKeysSortedAlphabetically()
|
|
|
|
|
{
|
|
|
|
|
@@ -284,16 +443,216 @@ public sealed class DevPortalBundleVerifierTests : IDisposable
|
|
|
|
|
return bundlePath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string CreatePortableV1Bundle(
|
|
|
|
|
Action<JsonObject, Dictionary<string, byte[]>>? mutateBeforeSign = null,
|
|
|
|
|
Action<Dictionary<string, byte[]>>? mutateAfterSign = null)
|
|
|
|
|
{
|
|
|
|
|
var bundlePath = Path.Combine(_tempDir, $"portable-v1-{Guid.NewGuid():N}.tgz");
|
|
|
|
|
var files = new Dictionary<string, byte[]>(StringComparer.Ordinal);
|
|
|
|
|
|
|
|
|
|
var artifactDigest = new string('a', 64);
|
|
|
|
|
var rootHash = new string('b', 64);
|
|
|
|
|
var legacyBundleId = "c3d4e5f6-a7b8-9012-cdef-345678901234";
|
|
|
|
|
|
|
|
|
|
var canonicalBomBytes = Encoding.UTF8.GetBytes("{\"schemaVersion\":\"1.0\",\"entries\":[]}");
|
|
|
|
|
files["canonical_bom.json"] = canonicalBomBytes;
|
|
|
|
|
var canonicalBomDigest = ComputeSha256Hex(canonicalBomBytes);
|
|
|
|
|
|
|
|
|
|
var dssePayloadBytes = Encoding.UTF8.GetBytes("{\"subject\":\"portable-v1\"}");
|
|
|
|
|
var dsseEnvelope = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["payloadType"] = "application/vnd.in-toto+json",
|
|
|
|
|
["payload"] = Convert.ToBase64String(dssePayloadBytes),
|
|
|
|
|
["signatures"] = new JsonArray
|
|
|
|
|
{
|
|
|
|
|
new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["keyid"] = "test-key",
|
|
|
|
|
["sig"] = Convert.ToBase64String(Encoding.UTF8.GetBytes("dsse-signature"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
files["dsse_envelope.json"] = Encoding.UTF8.GetBytes(dsseEnvelope.ToJsonString());
|
|
|
|
|
var dssePayloadDigest = ComputeSha256Hex(dssePayloadBytes);
|
|
|
|
|
|
|
|
|
|
var tileTarBytes = Encoding.UTF8.GetBytes("portable-tile");
|
|
|
|
|
files["rekor/tile.tar"] = tileTarBytes;
|
|
|
|
|
|
|
|
|
|
var manifest = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["spec_version"] = "1.0",
|
|
|
|
|
["specVersion"] = "1.0",
|
|
|
|
|
["createdUtc"] = "2025-12-07T10:30:00Z",
|
|
|
|
|
["artifact"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["name"] = "evidence/portable-v1",
|
|
|
|
|
["version"] = "1.0.0",
|
|
|
|
|
["digest"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["sha256"] = artifactDigest
|
|
|
|
|
},
|
|
|
|
|
["mediaType"] = "application/vnd.stellaops.evidence.bundle+json"
|
|
|
|
|
},
|
|
|
|
|
["files"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["canonical_bom.json"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["sha256"] = canonicalBomDigest,
|
|
|
|
|
["size"] = canonicalBomBytes.Length
|
|
|
|
|
},
|
|
|
|
|
["dsse_envelope.json"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["sha256"] = ComputeSha256Hex(files["dsse_envelope.json"]),
|
|
|
|
|
["size"] = files["dsse_envelope.json"].Length
|
|
|
|
|
},
|
|
|
|
|
["rekor/tile.tar"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["sha256"] = ComputeSha256Hex(tileTarBytes),
|
|
|
|
|
["size"] = tileTarBytes.Length
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
["digests"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["canonicalBomSha256"] = canonicalBomDigest,
|
|
|
|
|
["dssePayloadDigest"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["sha256"] = dssePayloadDigest
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
["rekor"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["logId"] = "rekor.sigstore.dev",
|
|
|
|
|
["apiVersion"] = "2",
|
|
|
|
|
["tileRefs"] = new JsonArray
|
|
|
|
|
{
|
|
|
|
|
new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["path"] = "rekor/tile.tar",
|
|
|
|
|
["covers"] = new JsonArray
|
|
|
|
|
{
|
|
|
|
|
$"SHA256:{artifactDigest.ToUpperInvariant()}",
|
|
|
|
|
$"SHA256:{canonicalBomDigest.ToUpperInvariant()}"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
["rootHash"] = rootHash
|
|
|
|
|
},
|
|
|
|
|
["verifiers"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["rekorPub"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["keyMaterial"] = "rekor-test-key"
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
["compatibility"] = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["legacyBundleId"] = legacyBundleId
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mutateBeforeSign?.Invoke(manifest, files);
|
|
|
|
|
|
|
|
|
|
var manifestJson = manifest.ToJsonString();
|
|
|
|
|
files["manifest.json"] = Encoding.UTF8.GetBytes(manifestJson);
|
|
|
|
|
|
|
|
|
|
var canonicalManifestBytes = CanonicalizeJson(manifestJson);
|
|
|
|
|
var manifestSignature = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["payload"] = Convert.ToBase64String(canonicalManifestBytes),
|
|
|
|
|
["signatures"] = new JsonArray
|
|
|
|
|
{
|
|
|
|
|
new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["keyid"] = "manifest-key",
|
|
|
|
|
["sig"] = Convert.ToBase64String(Encoding.UTF8.GetBytes("manifest-signature"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
files["manifest.sig"] = Encoding.UTF8.GetBytes(manifestSignature.ToJsonString());
|
|
|
|
|
|
|
|
|
|
mutateAfterSign?.Invoke(files);
|
|
|
|
|
|
|
|
|
|
CreateTgzBundle(bundlePath, files);
|
|
|
|
|
return bundlePath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static byte[] CanonicalizeJson(string json)
|
|
|
|
|
{
|
|
|
|
|
using var document = JsonDocument.Parse(json);
|
|
|
|
|
using var stream = new MemoryStream();
|
|
|
|
|
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false });
|
|
|
|
|
WriteCanonicalElement(writer, document.RootElement);
|
|
|
|
|
writer.Flush();
|
|
|
|
|
return stream.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void WriteCanonicalElement(Utf8JsonWriter writer, JsonElement element)
|
|
|
|
|
{
|
|
|
|
|
switch (element.ValueKind)
|
|
|
|
|
{
|
|
|
|
|
case JsonValueKind.Object:
|
|
|
|
|
writer.WriteStartObject();
|
|
|
|
|
foreach (var property in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
writer.WritePropertyName(property.Name);
|
|
|
|
|
WriteCanonicalElement(writer, property.Value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writer.WriteEndObject();
|
|
|
|
|
break;
|
|
|
|
|
case JsonValueKind.Array:
|
|
|
|
|
writer.WriteStartArray();
|
|
|
|
|
foreach (var item in element.EnumerateArray())
|
|
|
|
|
{
|
|
|
|
|
WriteCanonicalElement(writer, item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writer.WriteEndArray();
|
|
|
|
|
break;
|
|
|
|
|
case JsonValueKind.String:
|
|
|
|
|
writer.WriteStringValue(element.GetString());
|
|
|
|
|
break;
|
|
|
|
|
case JsonValueKind.Number:
|
|
|
|
|
writer.WriteRawValue(element.GetRawText(), skipInputValidation: true);
|
|
|
|
|
break;
|
|
|
|
|
case JsonValueKind.True:
|
|
|
|
|
case JsonValueKind.False:
|
|
|
|
|
writer.WriteBooleanValue(element.GetBoolean());
|
|
|
|
|
break;
|
|
|
|
|
case JsonValueKind.Null:
|
|
|
|
|
writer.WriteNullValue();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new InvalidOperationException($"Unsupported JSON token kind '{element.ValueKind}'.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ComputeSha256Hex(byte[] value)
|
|
|
|
|
=> Convert.ToHexString(SHA256.HashData(value)).ToLowerInvariant();
|
|
|
|
|
|
|
|
|
|
private static void CreateTgzBundle(string bundlePath, string manifestJson, object signature, object bundleMetadata)
|
|
|
|
|
{
|
|
|
|
|
var files = new Dictionary<string, byte[]>(StringComparer.Ordinal)
|
|
|
|
|
{
|
|
|
|
|
["manifest.json"] = Encoding.UTF8.GetBytes(manifestJson),
|
|
|
|
|
["signature.json"] = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(signature)),
|
|
|
|
|
["bundle.json"] = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundleMetadata)),
|
|
|
|
|
["checksums.txt"] = Encoding.UTF8.GetBytes($"# checksums\n{new string('f', 64)} sbom/cyclonedx.json\n")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
CreateTgzBundle(bundlePath, files);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void CreateTgzBundle(string bundlePath, IReadOnlyDictionary<string, byte[]> files)
|
|
|
|
|
{
|
|
|
|
|
using var memoryStream = new MemoryStream();
|
|
|
|
|
using (var gzipStream = new GZipStream(memoryStream, CompressionLevel.Optimal, leaveOpen: true))
|
|
|
|
|
using (var tarWriter = new TarWriter(gzipStream))
|
|
|
|
|
{
|
|
|
|
|
AddTarEntry(tarWriter, "manifest.json", manifestJson);
|
|
|
|
|
AddTarEntry(tarWriter, "signature.json", JsonSerializer.Serialize(signature));
|
|
|
|
|
AddTarEntry(tarWriter, "bundle.json", JsonSerializer.Serialize(bundleMetadata));
|
|
|
|
|
AddTarEntry(tarWriter, "checksums.txt", $"# checksums\n{new string('f', 64)} sbom/cyclonedx.json\n");
|
|
|
|
|
foreach (var file in files.OrderBy(f => f.Key, StringComparer.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
AddTarEntry(tarWriter, file.Key, file.Value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memoryStream.Position = 0;
|
|
|
|
|
@@ -301,7 +660,7 @@ public sealed class DevPortalBundleVerifierTests : IDisposable
|
|
|
|
|
memoryStream.CopyTo(fileStream);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void AddTarEntry(TarWriter writer, string name, string content)
|
|
|
|
|
private static void AddTarEntry(TarWriter writer, string name, byte[] content)
|
|
|
|
|
{
|
|
|
|
|
var entry = new PaxTarEntry(TarEntryType.RegularFile, name)
|
|
|
|
|
{
|
|
|
|
|
@@ -309,8 +668,7 @@ public sealed class DevPortalBundleVerifierTests : IDisposable
|
|
|
|
|
ModificationTime = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var bytes = Encoding.UTF8.GetBytes(content);
|
|
|
|
|
entry.DataStream = new MemoryStream(bytes);
|
|
|
|
|
entry.DataStream = new MemoryStream(content);
|
|
|
|
|
writer.WriteEntry(entry);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|