Thanks to visit codestin.com
Credit goes to github.com

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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.14.8" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.14.0" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
<PackageVersion Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageVersion Include="Mono.Cecil" Version="0.11.6" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ find . -type d -name TestResults -exec rm -rf {} \; > /dev/null 2>&1
testtarget="$1"

if [ "$testtarget" = "" ]; then
testtarget="*.sln"
testtarget="ci.slnf"
fi

dotnet build $testtarget --configuration Release
Expand Down
132 changes: 93 additions & 39 deletions src/NuSeal/LicenseValidator.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace NuSeal;

// I was using Microsoft.IdentityModel.JsonWebTokens to parse tokens.
// But, that package has ungodly amount of dependencies.
// We have to pack all dlls in the tasks folder, so having that dependency is not acceptable.
// We'll parse and validate the token manually.
internal class LicenseValidator
{
internal static bool IsValid(PemData pem, string license)
Expand All @@ -23,55 +25,107 @@ internal static bool IsValid(PemData pem, string license)

try
{
// Note: RSA ImportFromPem is available in .NET 5.0 and later
// We'll use BouncyCastle for netstandard2.0
using var rsa = CreateRsaFromPem(pem.PublicKeyPem);
var key = new RsaSecurityKey(rsa);
var parts = license.Split('.');
if (parts.Length != 3)
return false;

var validationParameters = new TokenValidationParameters
{
ValidateLifetime = true,
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = key,
ClockSkew = TimeSpan.FromMinutes(5)
};

var handler = new JsonWebTokenHandler();
var result = handler.ValidateTokenAsync(license, validationParameters).Result;

if (result.IsValid is false)
if (VerifyHeader(parts) is false)
return false;

// Parse the token and check the "product" claim
var jwt = handler.ReadJsonWebToken(license);
var productClaim = jwt.Claims.FirstOrDefault(c => c.Type == "product")?.Value;
if (VerifySignature(pem, parts) is false)
return false;

var payloadBytes = Base64UrlDecode(parts[1]);
var payload = JsonDocument.Parse(payloadBytes).RootElement;

if (VerifyProductName(payload, pem.ProductName) is false)
return false;

if (productClaim is null)
if (VerifyExpiration(payload) is false)
return false;

return productClaim.Equals(pem.ProductName, StringComparison.OrdinalIgnoreCase);
return true;
}
catch
{
return false;
}
}

private static bool VerifyHeader(string[] parts)
{
var headerBytes = Base64UrlDecode(parts[0]);
var header = JsonDocument.Parse(headerBytes).RootElement;

if (!header.TryGetProperty("alg", out var alg))
return false;

return string.Equals(alg.GetString(), "RS256", StringComparison.OrdinalIgnoreCase);
}

// Note: RSA ImportFromPem is available in .NET 5.0 and later
// We'll use BouncyCastle for netstandard2.0
private static bool VerifySignature(PemData pem, string[] parts)
{
var signatureBytes = Base64UrlDecode(parts[2]);
var data = Encoding.UTF8.GetBytes($"{parts[0]}.{parts[1]}");

var publicKey = GetRsaPublicKeyParameters(pem.PublicKeyPem);
var verifier = SignerUtilities.GetSigner("SHA256withRSA");
verifier.Init(false, publicKey);
verifier.BlockUpdate(data, 0, data.Length);

return verifier.VerifySignature(signatureBytes);

static RsaKeyParameters GetRsaPublicKeyParameters(string pemKey)
{
using var reader = new StringReader(pemKey);
var pemReader = new PemReader(reader);
var obj = pemReader.ReadObject();

if (obj is RsaKeyParameters rsaKeyParams && !rsaKeyParams.IsPrivate)
{
return rsaKeyParams;
}
throw new ArgumentException("PEM string does not contain a valid RSA public key.", nameof(pemKey));
}
catch { }
}

private static bool VerifyProductName(JsonElement payload, string productName)
{
if (!payload.TryGetProperty("product", out var productClaim))
return false;

return false;
return string.Equals(productClaim.GetString(), productName, StringComparison.OrdinalIgnoreCase);
}

private static RSA CreateRsaFromPem(string pem)
private static bool VerifyExpiration(JsonElement payload)
{
using var reader = new StringReader(pem);
var pemReader = new PemReader(reader);
var obj = pemReader.ReadObject();
var clockSkewInMinutes = 5;

if (obj is RsaKeyParameters rsaKeyParams)
if (payload.TryGetProperty("nbf", out var nbf)
&& nbf.GetInt64() > DateTimeOffset.UtcNow.AddMinutes(-1 * clockSkewInMinutes).ToUnixTimeSeconds())
{
return DotNetUtilities.ToRSA(rsaKeyParams);
return false;
}

if (payload.TryGetProperty("exp", out var exp)
&& exp.GetInt64() < DateTimeOffset.UtcNow.AddMinutes(clockSkewInMinutes).ToUnixTimeSeconds())
{
return false;
}
else

return true;
}

private static byte[] Base64UrlDecode(string input)
{
string padded = input.Replace('-', '+').Replace('_', '/');
switch (padded.Length % 4)
{
throw new ArgumentException("PEM string does not contain a valid RSA public key.", nameof(pem));
case 2: padded += "=="; break;
case 3: padded += "="; break;
}
return Convert.FromBase64String(padded);
}
}
2 changes: 1 addition & 1 deletion src/NuSeal/NuSeal.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core" PrivateAssets="all" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" PrivateAssets="all" />
<PackageReference Include="System.Text.Json" PrivateAssets="all" />
<PackageReference Include="Portable.BouncyCastle" PrivateAssets="all" />
<PackageReference Include="Mono.Cecil" PrivateAssets="all" />
</ItemGroup>
Expand Down
20 changes: 18 additions & 2 deletions src/NuSeal/PrepareAssetsForConsumer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,22 @@ public static bool Execute(
{
var content = File.ReadAllText(consumerPropsFile);
content = RemoveProjectTags(content, consumerPropsFile);
props = props.Replace("</Project>", $"{content}{Environment.NewLine}</Project>");
if (!string.IsNullOrEmpty(content))
{
var linedEnding = DetectLineEnding(content);
props = props.Replace("</Project>", $"{content}{linedEnding}</Project>");
}
}

if (!string.IsNullOrEmpty(consumerTargetsFile) && File.Exists(consumerTargetsFile))
{
var content = File.ReadAllText(consumerTargetsFile);
content = RemoveProjectTags(content, consumerTargetsFile);
targets = targets.Replace("</Project>", $"{content}{Environment.NewLine}</Project>");
if (!string.IsNullOrEmpty(content))
{
var linedEnding = DetectLineEnding(content);
targets = targets.Replace("</Project>", $"{content}{linedEnding}</Project>");
}
}

File.WriteAllText(propsOutputFile, props);
Expand All @@ -64,4 +72,12 @@ private static string RemoveProjectTags(string content, string fileName)
string projectTag = content.Substring(startIndex, endIndex - startIndex + 1);
return content.Replace(projectTag, "").Replace("</Project>", "");
}

private static string DetectLineEnding(string content)
{
var index = content.IndexOf('\n');
if (index > 0 && content[index - 1] == '\r')
return "\r\n";
return "\n";
}
}
2 changes: 0 additions & 2 deletions tests/NuSeal.Tests/PrepareAssetsForConsumerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,13 @@ public void HandlesEmptyConsumerFiles_GivenEmptyConsumerFiles()
<PropertyGroup>
<NuSealVersion>1.0.0</NuSealVersion>
</PropertyGroup>

</Project>
""";
var expectedTargetsContent = """
<Project>
<Target Name="NuSealCheck">
<Message Text="NuSeal is working" />
</Target>

</Project>
""";

Expand Down