diff --git a/src/NuGet.Core/NuGet.Protocol/Converters/FingerprintsStjConverter.cs b/src/NuGet.Core/NuGet.Protocol/Converters/FingerprintsStjConverter.cs new file mode 100644 index 00000000000..67a624634c1 --- /dev/null +++ b/src/NuGet.Core/NuGet.Protocol/Converters/FingerprintsStjConverter.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using NuGet.Packaging.Core; + +namespace NuGet.Protocol.Converters +{ + internal sealed class FingerprintsStjConverter : JsonConverter + { + public override Fingerprints Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + var dict = new Dictionary(); + while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName) + { + string key = reader.GetString()!; + reader.Read(); + dict[key] = reader.GetString() ?? string.Empty; + } + + return new Fingerprints(dict); + } + + public override void Write(Utf8JsonWriter writer, Fingerprints value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach (KeyValuePair kvp in value) + { + writer.WriteString(kvp.Key, kvp.Value); + } + writer.WriteEndObject(); + } + } +} diff --git a/src/NuGet.Core/NuGet.Protocol/Model/RepositoryCertificateInfo.cs b/src/NuGet.Core/NuGet.Protocol/Model/RepositoryCertificateInfo.cs index a6025f90b4c..fee1b68bb4b 100644 --- a/src/NuGet.Core/NuGet.Protocol/Model/RepositoryCertificateInfo.cs +++ b/src/NuGet.Core/NuGet.Protocol/Model/RepositoryCertificateInfo.cs @@ -4,27 +4,53 @@ using System; using Newtonsoft.Json; using NuGet.Packaging.Core; +using System.Text.Json.Serialization; namespace NuGet.Protocol { public class RepositoryCertificateInfo : IRepositoryCertificateInfo { [JsonProperty(PropertyName = JsonProperties.Fingerprints, Required = Required.Always)] + [JsonPropertyName(JsonProperties.Fingerprints)] public Fingerprints Fingerprints { get; private set; } = null!; [JsonProperty(PropertyName = JsonProperties.Subject, Required = Required.Always)] + [JsonPropertyName(JsonProperties.Subject)] public string Subject { get; private set; } = null!; [JsonProperty(PropertyName = JsonProperties.Issuer, Required = Required.Always)] + [JsonPropertyName(JsonProperties.Issuer)] public string Issuer { get; private set; } = null!; [JsonProperty(PropertyName = JsonProperties.NotBefore, Required = Required.Always)] + [JsonPropertyName(JsonProperties.NotBefore)] public DateTimeOffset NotBefore { get; private set; } [JsonProperty(PropertyName = JsonProperties.NotAfter, Required = Required.Always)] + [JsonPropertyName(JsonProperties.NotAfter)] public DateTimeOffset NotAfter { get; private set; } [JsonProperty(PropertyName = JsonProperties.ContentUrl, Required = Required.Always)] + [JsonPropertyName(JsonProperties.ContentUrl)] public string ContentUrl { get; private set; } = null!; + + public RepositoryCertificateInfo() { } + + [System.Text.Json.Serialization.JsonConstructor] + internal RepositoryCertificateInfo( + Fingerprints fingerprints, + string subject, + string issuer, + DateTimeOffset notBefore, + DateTimeOffset notAfter, + string contentUrl) + { + Fingerprints = fingerprints; + Subject = subject; + Issuer = issuer; + NotBefore = notBefore; + NotAfter = notAfter; + ContentUrl = contentUrl; + } } } diff --git a/src/NuGet.Core/NuGet.Protocol/Model/RepositorySignatureModel.cs b/src/NuGet.Core/NuGet.Protocol/Model/RepositorySignatureModel.cs new file mode 100644 index 00000000000..409bf1ff948 --- /dev/null +++ b/src/NuGet.Core/NuGet.Protocol/Model/RepositorySignatureModel.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text.Json.Serialization; + +namespace NuGet.Protocol.Model +{ + internal sealed class RepositorySignatureModel + { + [JsonPropertyName(JsonProperties.AllRepositorySigned)] + public bool? AllRepositorySigned { get; set; } + + [JsonPropertyName(JsonProperties.SigningCertificates)] + public RepositoryCertificateInfo[]? SigningCertificates { get; set; } + } +} diff --git a/src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj b/src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj index 5ed856305a9..4e873de17c2 100644 --- a/src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj +++ b/src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj @@ -37,6 +37,7 @@ + diff --git a/src/NuGet.Core/NuGet.Protocol/Providers/RepositorySignatureResourceProvider.cs b/src/NuGet.Core/NuGet.Protocol/Providers/RepositorySignatureResourceProvider.cs index 719c4aae806..6c2f0829230 100644 --- a/src/NuGet.Core/NuGet.Protocol/Providers/RepositorySignatureResourceProvider.cs +++ b/src/NuGet.Core/NuGet.Protocol/Providers/RepositorySignatureResourceProvider.cs @@ -6,10 +6,13 @@ using System; using System.Globalization; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using NuGet.Common; using NuGet.Protocol.Core.Types; +using NuGet.Protocol.Model; +using NuGet.Protocol.Utility; namespace NuGet.Protocol { @@ -80,9 +83,12 @@ private async Task GetRepositorySignatureResourceAs }, async httpSourceResult => { - var json = await httpSourceResult.Stream.AsJObjectAsync(token); + RepositorySignatureModel model = await JsonSerializer.DeserializeAsync( + httpSourceResult.Stream, + JsonContext.Default.RepositorySignatureModel, + token); - return new RepositorySignatureResource(json, source); + return new RepositorySignatureResource(model, source); }, log, token); diff --git a/src/NuGet.Core/NuGet.Protocol/Resources/RepositorySignatureResource.cs b/src/NuGet.Core/NuGet.Protocol/Resources/RepositorySignatureResource.cs index 85740ba0666..0cbe6142187 100644 --- a/src/NuGet.Core/NuGet.Protocol/Resources/RepositorySignatureResource.cs +++ b/src/NuGet.Core/NuGet.Protocol/Resources/RepositorySignatureResource.cs @@ -11,6 +11,7 @@ using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.Protocol.Core.Types; +using NuGet.Protocol.Model; namespace NuGet.Protocol { @@ -44,6 +45,27 @@ public RepositorySignatureResource(JObject repoSignInformationContent, SourceRep Source = source.PackageSource.Source; } + internal RepositorySignatureResource(RepositorySignatureModel model, SourceRepository source) + { + AllRepositorySigned = model.AllRepositorySigned ?? + throw new FatalProtocolException(string.Format(CultureInfo.CurrentCulture, Strings.Log_FailedToParseRepoSignInfor, JsonProperties.AllRepositorySigned, source.PackageSource.Source)); + + RepositoryCertificateInfo[] certs = model.SigningCertificates ?? + throw new FatalProtocolException(string.Format(CultureInfo.CurrentCulture, Strings.Log_FailedToParseRepoSignInfor, JsonProperties.SigningCertificates, source.PackageSource.Source)); + + foreach (RepositoryCertificateInfo cert in certs) + { + if (!Uri.TryCreate(cert.ContentUrl, UriKind.Absolute, out Uri contentUrl) + || !string.Equals(contentUrl.Scheme, "https", StringComparison.OrdinalIgnoreCase)) + { + throw new FatalProtocolException(Strings.RepositoryContentUrlMustBeHttps); + } + } + + RepositoryCertificateInfos = certs; + Source = source.PackageSource.Source; + } + // Test only. public RepositorySignatureResource(bool allRepositorySigned, IEnumerable repositoryCertInfos) { diff --git a/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs b/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs index 2a181357a86..4155f3ba1d6 100644 --- a/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs +++ b/src/NuGet.Core/NuGet.Protocol/Utility/JsonContext.cs @@ -12,11 +12,12 @@ namespace NuGet.Protocol.Utility [JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, GenerationMode = JsonSourceGenerationMode.Metadata, - Converters = [typeof(VersionRangeStjConverter)])] + Converters = [typeof(VersionRangeStjConverter), typeof(FingerprintsStjConverter)])] #pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant [JsonSerializable(typeof(HttpFileSystemBasedFindPackageByIdResource.FlatContainerVersionList))] [JsonSerializable(typeof(IReadOnlyList), TypeInfoPropertyName = "VulnerabilityIndex")] [JsonSerializable(typeof(CaseInsensitiveDictionary>), TypeInfoPropertyName = "VulnerabilityPage")] + [JsonSerializable(typeof(RepositorySignatureModel))] internal partial class JsonContext : JsonSerializerContext { }