NuSeal provides the infrastructure for creating and validating NuGet package licenses. The validation occurs during build time (offline), preventing unauthorized use of your packages. It's designed to be generic while allowing each author to set their own public key and license policies.
Packages:
- NuSeal - Core package that validates licenses during build time (
netstandard2.0
library) - NuSeal.Generator - Helper package for generating RSA key pairs and licenses (
net8.0
library)
- Authors create RSA key pairs in PEM format. They may create them using
NuSeal.Generator
package. - Authors create licenses for their users using
NuSeal.Generator
package. License files are namedYourProductName.lic
. - Authors install the
NuSeal
package in their NuGet package to protect it. - Authors add
NuSealPem
item providing the path to the public key PEM and the name of the product. - End users obtain a license file and place it anywhere in their project directory tree.
Package authors first need to create public/private key pairs. You can use the NuSeal.Generator
package for this.
<ItemGroup>
<PackageReference Include="NuSeal.Generator" Version="0.4.1" />
</ItemGroup>
Then generate the keys.
var keys = NuSeal.RsaKeyGenerator.GeneratePem();
File.WriteAllText("private_key.pem", keys.PrivateKey);
File.WriteAllText("public_key.pem", keys.PublicKey);
Keep the private key secure and confidential, as it will be used to sign licenses.
Once you have the key pair, you can create licenses for your product:
var license = NuSeal.License.Create(new()
{
PrivateKeyPem = keys.PrivateKey,
ProductName = "YourProductName",
SubscriptionId = "00000000-0000-0000-0000-000000000000",
ClientId = "00000000-0000-0000-0000-000000000000",
Edition = "Free",
Issuer = "YourCompany",
Audience = "NuSeal",
StartDate = DateTimeOffset.UtcNow,
ExpirationDate = DateTimeOffset.UtcNow.AddYears(1),
GracePeriodInDays = 30,
AdditionalClaims = []
});
// Save the license to a file
File.WriteAllText("YourProductName.lic", license);
Parameters explained:
- privateKeyPem - Your private RSA key in PEM format
- productName - Unique identifier of your product associated with this license. It might be the package name if this license is intended only for this package; or it might be a bundle name if the license is associated with group of packages. Important: this name is used while defining the
NuSealPem
item and as a license filename. - subscriptionId - Unique identifier for the customer subscription
- clientId - Unique identifier for the customer or user
- edition - Edition of your product (e.g., "Free", "Professional", "Enterprise")
- issuer - Your company or organization name
- audience - Intended audience for the license (e.g., "NuSeal")
- startDate - When the license becomes valid
- expirationDate - When the license expires
- gracePeriodInDays - Number of days after expiration during which the license is still considered valid. It will emit a warning instead of an error during validation.
- additionalClaims - Any additional claims you want to include in the license
To protect your NuGet package, add the NuSeal
package as a dependency:
<ItemGroup>
<PackageReference Include="NuSeal" Version="0.4.1" />
</ItemGroup>
Then, add a NuSealPem
item providing the path to the public key PEM and the name of the product:
<ItemGroup>
<NuSealPem Include="public_key.pem" ProductName="YourProductName" />
</ItemGroup>
It's a common practice that authors provide licenses for a single package or a bundle of packages. In this case, you may add multiple items. You may use the same or a different PEM file per product.
<ItemGroup>
<NuSealPem Include="public_key.pem" ProductName="YourProductName" />
<NuSealPem Include="public_key.pem" ProductName="YourBundleName" />
</ItemGroup>
NuSeal will try to find and validate the license against all specified products. At least one valid license is required to pass the validation.
End users of your protected NuGet package need to:
- Obtain a license file from you (the package author)
- Place the license file in one of these locations:
- In the solution/repository root directory.
- Anywhere in the directory tree.
The license file should be named YourProductName.lic
. Important: Avoid checking the license file into source control to prevent leaks.
The default behavior of NuSeal is as follows.
- The
YourPackageId.props
andYourPackageId.targets
assets are generated in the build output path. - The generated assets are packed into the NuGet package under the
build
folder. - License validation is executed for direct consumers of the protected package.
- If no license is found, the build fails with an error.
- The license is validated against the following criteria:
- The license has valid lifetime
- The license is signed with the private key corresponding to the specified public key in
NuSealPem
- The
product
claim in the license matches the product name specified inNuSealPem
The authors can customize the default behavior and adjust the policies to fit their needs.
It alters the behavior when no valid license is found.
Error
(default): The build fails with an error if no valid license is found.Warning
: The build emits a warning if no valid license is found, but continues.
<PropertyGroup>
<NuSealValidationMode>Warning</NuSealValidationMode>
</PropertyGroup>
Depending on the nature of the library and the business model, authors may want a different strategy where even transitive consumers are required to have a license.
Direct
(default): The assets are packed only tobuild
directory. Only projects that directly consume the protected package will be validated for licenses.Transitive
: The assets are packed tobuildTransitive
andbuild
directories. Thebuild
is necessary to support projects usingpackages.config
. The assets will flow to all consumers, direct and transitive. For this scope, to avoid cluttering the build for large solutions, we're constraining the validation to only executable assemblies.
<PropertyGroup>
<NuSealValidationScope>Transitive</NuSealValidationScope>
</PropertyGroup>
The generated target, depending on the validation scope, may or may not include a condition.
Direct
(default): No condition is applied. All projects that directly consume the protected package will be validated for licenses.Transitive
: The target includes a condition to only validate executable assemblies.Condition="'$(OutputType)' == 'Exe' Or '$(OutputType)' == 'WinExe'"
The authors may alter this behavior and specify their custom condition as follows. If defined, it will be applied regardless of the scope.
<PropertyGroup>
<NuSealCondition>"'#(OutputType)' == 'Exe' Or '#(OutputType)' == 'WinExe'"</NuSealCondition>
</PropertyGroup>
Note that #()
is used instead of $()
. Since we need to preserve the original condition as literal, and avoid variable evaluation; the $
and @
characters should not be used. Use the #
character instead, and we'll do the replacement during asset generation.
- Use
##
for@
- Use
#
for$
By default, the assets YourPackageId.props
and YourPackageId.targets
are generated in the build output path; the value of $(OutputPath)
.
If the authors want to generate the assets in a different location, they can specify an output path as follows.
<PropertyGroup>
<NuSealOutputPath>YOUR_DESIRED_OUTPUT_PATH</NuSealOutputPath>
</PropertyGroup>
By default, once the YourPackageId.props
and YourPackageId.targets
assets are generated, we add items to pack them in the NuGet package. We're packing them in build
directory, and in case of Transitive
scope in buildTransitive
directory as well.
<None Include="$(OutputPath)\$(PackageId).props" Pack="true" PackagePath="build\$(PackageId).props" Visible="false"/>
<None Include="$(OutputPath)\$(PackageId).targets" Pack="true" PackagePath="build\$(PackageId).targets" Visible="false"/>
If the author is already packing assets to their NuGet package, they can provide them to NuSeal. We'll do the merging and pack the merged assets.
<PropertyGroup>
<NuSealIncludePropsFile>build\YourPackageId.props</NuSealIncludePropsFile>
<NuSealIncludeTargetsFile>build\YourPackageId.targets</NuSealIncludeTargetsFile>
</PropertyGroup>
Authors may have a different strategy for generating NuGet packages (e.g. they use nuspec files), have complex workflows or simply want to manually pack the assets. In that case they may disable packing assets altogether. We'll just generate the assets in the output path, and it's up to the authors to pack or further process them.
<PropertyGroup>
<NuSealPackAssets>disable</NuSealPackAssets>
</PropertyGroup>
If you like or are using this project please give it a star. Thanks!