Skip to content

Commit

Permalink
Implement updated digest validation for DownloadManifest (Azure#34332)
Browse files Browse the repository at this point in the history
  • Loading branch information
annelo-msft authored Feb 28, 2023
1 parent f6d847c commit 176fd47
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,15 @@ public virtual Response<DownloadManifestResult> DownloadManifest(string tagOrDig
rawResponse.Headers.TryGetValue("Content-Type", out string contentType);

var contentDigest = BlobHelper.ComputeDigest(rawResponse.ContentStream);
ValidateDigest(contentDigest, digest);

if (ReferenceIsDigest(tagOrDigest))
{
ValidateDigest(contentDigest, tagOrDigest, "The digest of the received manifest does not match the requested digest reference.");
}
else
{
ValidateDigest(contentDigest, digest);
}

return Response.FromValue(new DownloadManifestResult(digest, contentType, rawResponse.Content), rawResponse);
}
Expand Down Expand Up @@ -539,7 +547,15 @@ public virtual async Task<Response<DownloadManifestResult>> DownloadManifestAsyn
rawResponse.Headers.TryGetValue("Content-Type", out string contentType);

var contentDigest = BlobHelper.ComputeDigest(rawResponse.ContentStream);
ValidateDigest(contentDigest, digest);

if (ReferenceIsDigest(tagOrDigest))
{
ValidateDigest(contentDigest, tagOrDigest, "The digest of the received manifest does not match the requested digest reference.");
}
else
{
ValidateDigest(contentDigest, digest);
}

return Response.FromValue(new DownloadManifestResult(digest, contentType, rawResponse.Content), rawResponse);
}
Expand Down Expand Up @@ -572,11 +588,18 @@ private static string GetAcceptHeader(ManifestMediaType? mediaType)
return sb.ToString();
}

private static void ValidateDigest(string clientDigest, string serverDigest)
private static bool ReferenceIsDigest(string reference)
{
return reference.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase);
}

private static void ValidateDigest(string clientDigest, string serverDigest, string message = default)
{
message ??= "The server-computed digest does not match the client-computed digest.";

if (!clientDigest.Equals(serverDigest, StringComparison.OrdinalIgnoreCase))
{
throw new RequestFailedException("The server-computed digest does not match the client-computed digest.");
throw new RequestFailedException(message);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,6 @@ public async Task Setup()
await SetProxyOptionsAsync(new ProxyOptions { Transport = new ProxyOptionsTransport { AllowAutoRedirect = false } });
}

/// <summary>
/// Create an OciManifest type that matches the contents of the manifest.json test data file.
/// </summary>
/// <returns></returns>
private static OciManifest CreateManifest()
{
OciManifest manifest = new OciManifest()
{
SchemaVersion = 2,
Config = new OciBlobDescriptor()
{
MediaType = "application/vnd.acme.rocket.config",
Digest = "sha256:d25b42d3dbad5361ed2d909624d899e7254a822c9a632b582ebd3a44f9b0dbc8",
Size = 171
}
};
manifest.Layers.Add(new OciBlobDescriptor()
{
MediaType = "application/vnd.oci.image.layer.v1.tar",
Digest = "sha256:654b93f61054e4ce90ed203bb8d556a6200d5f906cf3eca0620738d6dc18cbed",
Size = 28,
Annotations = new OciAnnotations()
{
Name = "artifact.txt"
}
});

return manifest;
}

[RecordedTest]
public async Task CanUploadOciManifest()
{
Expand All @@ -72,7 +42,7 @@ public async Task CanUploadOciManifest()
await UploadManifestPrerequisites(client);

// Act
var manifest = CreateManifest();
var manifest = ContainerRegistryTestDataHelpers.CreateManifest();
var uploadResult = await client.UploadManifestAsync(manifest);
string digest = uploadResult.Value.Digest;

Expand Down Expand Up @@ -181,7 +151,7 @@ public async Task CanUploadOciManifestWithTag()
await UploadManifestPrerequisites(client);

// Act
var manifest = CreateManifest();
var manifest = ContainerRegistryTestDataHelpers.CreateManifest();
var uploadResult = await client.UploadManifestAsync(manifest, tag);
var digest = uploadResult.Value.Digest;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;
using Azure.Containers.ContainerRegistry.Specialized;
using Azure.Core;
using Azure.Core.TestFramework;
Expand Down Expand Up @@ -60,5 +61,54 @@ public void ServiceMethodsValidateArguments()
Assert.That(async () => await client.UploadManifestAsync(manifestStream: null), Throws.InstanceOf<ArgumentNullException>(), "The method should validate that `manifest stream` is not null.");
Assert.That(async () => await client.UploadBlobAsync(null), Throws.InstanceOf<ArgumentNullException>(), "The method should validate that `stream` is not null.");
}

/// <summary>
/// Validates digest checks for DownloadManifest.
/// </summary>
[Test]
public async Task DownloadManifestValidatesDigest()
{
// Arrange
Uri endpoint = new("https://example.acr.io");
string repository = "TestRepository";
string tagName = "v1";
BinaryData manifest = BinaryData.FromObjectAsJson(ContainerRegistryTestDataHelpers.CreateManifest());
string manifestContent = manifest.ToString();
string digest = BlobHelper.ComputeDigest(manifest.ToStream());

ContainerRegistryClientOptions options = new()
{
Transport = new MockTransport(
new MockResponse(200).SetContent(manifestContent).AddHeader("Docker-Content-Digest", digest),
new MockResponse(200).SetContent(manifestContent).AddHeader("Docker-Content-Digest", digest),
new MockResponse(200).SetContent(manifestContent).AddHeader("Docker-Content-Digest", digest),
new MockResponse(200).SetContent(manifestContent).AddHeader("Docker-Content-Digest", "Invalid server digest")),
Audience = ContainerRegistryAudience.AzureResourceManagerPublicCloud
};

ContainerRegistryBlobClient client = new(endpoint, new MockCredential(), repository, options);

// Act

// Request with digest
DownloadManifestResult result = await client.DownloadManifestAsync(digest);
Assert.AreEqual(manifestContent, result.Content.ToString());

// Request with tag
result = await client.DownloadManifestAsync(tagName);
Assert.AreEqual(manifestContent, result.Content.ToString());

// Request with digest that doesn't match the content
Assert.ThrowsAsync<RequestFailedException>(async () =>
{
await client.DownloadManifestAsync(digest.Replace('0', '1'));
});

// Request with tag, getting a response with invalid digest header.
Assert.ThrowsAsync<RequestFailedException>(async () =>
{
await client.DownloadManifestAsync(tagName);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Azure.Containers.ContainerRegistry.Specialized;

namespace Azure.Containers.ContainerRegistry.Tests
{
internal static class ContainerRegistryTestDataHelpers
{
/// <summary>
/// Create an OciManifest type that matches the contents of the manifest.json test data file.
/// </summary>
/// <returns></returns>
internal static OciManifest CreateManifest()
{
OciManifest manifest = new OciManifest()
{
SchemaVersion = 2,
Config = new OciBlobDescriptor()
{
MediaType = "application/vnd.acme.rocket.config",
Digest = "sha256:d25b42d3dbad5361ed2d909624d899e7254a822c9a632b582ebd3a44f9b0dbc8",
Size = 171
}
};
manifest.Layers.Add(new OciBlobDescriptor()
{
MediaType = "application/vnd.oci.image.layer.v1.tar",
Digest = "sha256:654b93f61054e4ce90ed203bb8d556a6200d5f906cf3eca0620738d6dc18cbed",
Size = 28,
Annotations = new OciAnnotations()
{
Name = "artifact.txt"
}
});

return manifest;
}
}
}

0 comments on commit 176fd47

Please sign in to comment.