/// <summary> /// Build an instance of <see cref="NodeEntitlements"/> from the information supplied on the /// command line by the user /// </summary> /// <returns>Either a usable (and completely valid) <see cref="NodeEntitlements"/> or a set /// of errors.</returns> private Errorable <NodeEntitlements> Build() { var entitlement = new NodeEntitlements(); var errors = new List <string>(); ConfigureOptional(VirtualMachineId, url => entitlement.WithVirtualMachineId(url)); Configure(NotBefore, notBefore => entitlement.FromInstant(notBefore)); Configure(NotAfter, notAfter => entitlement.UntilInstant(notAfter)); Configure(Audience, audience => entitlement.WithAudience(audience)); Configure(Issuer, issuer => entitlement.WithIssuer(issuer)); ConfigureAll(Addresses, address => entitlement.AddIpAddress(address)); ConfigureAll(Applications, app => entitlement.AddApplication(app)); if (errors.Any()) { return(Errorable.Failure <NodeEntitlements>(errors)); } return(Errorable.Success(entitlement)); // <param name="readConfiguration">function to read the configuration value.</param> // <param name="applyConfiguration">function to modify our configuration with the value read.</param> void Configure <V>(Func <Errorable <V> > readConfiguration, Func <V, NodeEntitlements> applyConfiguration) { readConfiguration().Match( whenSuccessful: value => entitlement = applyConfiguration(value), whenFailure: e => errors.AddRange(e)); } // <param name="readConfiguration">function to read the configuration value.</param> // <param name="applyConfiguration">function to modify our configuration with the value read.</param> void ConfigureOptional <V>(Func <Errorable <V> > readConfiguration, Func <V, NodeEntitlements> applyConfiguration) where V : class
private List <Claim> CreateClaims(NodeEntitlements entitlements) { var claims = new List <Claim>(); if (!string.IsNullOrEmpty(entitlements.VirtualMachineId)) { _logger.LogDebug("Virtual machine Id: {VirtualMachineId}", entitlements.VirtualMachineId); claims.Add(new Claim(Claims.VirtualMachineId, entitlements.VirtualMachineId)); } if (!string.IsNullOrEmpty(entitlements.Identifier)) { _logger.LogDebug("Entitlement Id: {EntitlementId}", entitlements.Identifier); claims.Add(new Claim(Claims.EntitlementId, entitlements.Identifier)); } foreach (var ip in entitlements.IpAddresses) { _logger.LogDebug("IP Address: {IP}", ip); claims.Add(new Claim(Claims.IpAddress, ip.ToString())); } foreach (var app in entitlements.Applications) { _logger.LogDebug("Application Id: {ApplicationId}", app); var claim = new Claim(Claims.Application, app); claims.Add(claim); } return(claims); }
/// <summary> /// Generate a token /// </summary> /// <param name="entitlements">Details of the entitlements to encode into the token.</param> /// <param name="signingCert">Certificate to use when signing the token (optional).</param> /// <param name="encryptionCert">Certificate to use when encrypting the token (optional).</param> /// <returns>Generated token, if any; otherwise all related errors.</returns> private static string GenerateToken( NodeEntitlements entitlements, X509Certificate2 signingCert = null, X509Certificate2 encryptionCert = null) { SigningCredentials signingCredentials = null; if (signingCert != null) { var signingKey = new X509SecurityKey(signingCert); signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha512Signature); } EncryptingCredentials encryptingCredentials = null; if (encryptionCert != null) { var encryptionKey = new X509SecurityKey(encryptionCert); encryptingCredentials = new EncryptingCredentials( encryptionKey, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512); } var entitlementWithIdentifier = entitlements.WithIdentifier($"entitlement-{Guid.NewGuid():D}"); var generator = new TokenGenerator(_logger, signingCredentials, encryptingCredentials); return(generator.Generate(entitlementWithIdentifier)); }
/// <summary> /// Generate a token from a software entitlement /// </summary> /// <param name="entitlements">Software entitlements to use when populating the token.</param> /// <returns>The generated token.</returns> public string Generate(NodeEntitlements entitlements) { if (entitlements == null) { throw new ArgumentNullException(nameof(entitlements)); } _logger.LogDebug( "Not Before: {NotBefore}", entitlements.NotBefore.ToString(TimestampParser.ExpectedFormat)); _logger.LogDebug( "Not After: {NotAfter}", entitlements.NotAfter.ToString(TimestampParser.ExpectedFormat)); if (SigningCredentials != null) { _logger.LogDebug( "Signing with {Credentials}", SigningCredentials.Kid); } if (EncryptingCredentials != null) { _logger.LogDebug( "Encrypting with {Algorithm}/{Credentials}", EncryptingCredentials.Alg, EncryptingCredentials.Key.KeyId); } var effectiveIssuer = entitlements.Issuer ?? Claims.DefaultIssuer; _logger.LogDebug("Issued by {Issuer}", effectiveIssuer); var effectiveAudience = entitlements.Audience ?? Claims.DefaultAudience; _logger.LogDebug("Audience is {Audience}", effectiveAudience); var claims = CreateClaims(entitlements); var claimsIdentity = new ClaimsIdentity(claims); var securityTokenDescriptor = new SecurityTokenDescriptor { Subject = claimsIdentity, NotBefore = entitlements.NotBefore.UtcDateTime, Expires = entitlements.NotAfter.UtcDateTime, IssuedAt = DateTimeOffset.Now.UtcDateTime, Issuer = effectiveIssuer, Audience = effectiveAudience, SigningCredentials = SigningCredentials, EncryptingCredentials = EncryptingCredentials }; var handler = new JwtSecurityTokenHandler(); var token = handler.CreateToken(securityTokenDescriptor); _logger.LogDebug("Raw token: {Token}", token); return(handler.WriteToken(token)); }
/// <summary> /// Cloning constructor to initialize a new instance of the <see cref="NodeEntitlements"/> /// class as a (near) copy of an existing one. /// </summary> /// <remarks>Specify any of the optional parameters to modify the clone from the original.</remarks> /// <param name="original">Original entitlement to clone.</param> /// <param name="notBefore">Optionally specify a new value for <see cref="NotBefore"/>.</param> /// <param name="notAfter">Optionally specify a new value for <see cref="NotAfter"/>.</param> /// <param name="virtualMachineId">Optionally specify a new value for <see cref="VirtualMachineId"/>.</param> /// <param name="applications">The set of applications entitled to run.</param> /// <param name="identifier">Identifier to use for this entitlement.</param> /// <param name="addresses">Addresses of the entitled machine.</param> /// <param name="audience">Audience for whom the token is intended.</param> /// <param name="issuer">Issuer identifier for the token.</param> private NodeEntitlements( NodeEntitlements original, DateTimeOffset?notBefore = null, DateTimeOffset?notAfter = null, string virtualMachineId = null, ImmutableHashSet <string> applications = null, string identifier = null, ImmutableHashSet <IPAddress> addresses = null, string audience = null, string issuer = null) { Created = original.Created; NotBefore = notBefore ?? original.NotBefore; NotAfter = notAfter ?? original.NotAfter; VirtualMachineId = virtualMachineId ?? original.VirtualMachineId; Applications = applications ?? original.Applications; Identifier = identifier ?? original.Identifier; IpAddresses = addresses ?? original.IpAddresses; Audience = audience ?? original.Audience; Issuer = issuer ?? original.Issuer; }
/// <summary> /// Verify the provided software entitlement token /// </summary> /// <param name="tokenString">A software entitlement token to verify.</param> /// <param name="expectedAudience">The audience for whom the token should be intended.</param> /// <param name="expectedIssuer">The issuer who should have created the token.</param> /// <param name="application">The specific application id of the application </param> /// <param name="ipAddress">Address of the machine requesting token validation.</param> /// <returns>Either a software entitlement describing the approved entitlement, or errors /// explaining why it wasn't approved.</returns> public Errorable <NodeEntitlements> Verify( string tokenString, string expectedAudience, string expectedIssuer, string application, IPAddress ipAddress) { var validationParameters = new TokenValidationParameters { ValidateAudience = true, ValidAudience = expectedAudience, ValidateIssuer = true, ValidIssuer = expectedIssuer, ValidateLifetime = true, RequireExpirationTime = true, RequireSignedTokens = SigningKey != null, ClockSkew = TimeSpan.FromSeconds(60), IssuerSigningKey = SigningKey, ValidateIssuerSigningKey = true, TokenDecryptionKey = EncryptionKey }; try { var handler = new JwtSecurityTokenHandler(); var principal = handler.ValidateToken(tokenString, validationParameters, out var token); if (!VerifyApplication(principal, application)) { return(ApplicationNotEntitledError(application)); } if (!VerifyIpAddress(principal, ipAddress)) { return(MachineNotEntitledError(ipAddress)); } var entitlementIdClaim = principal.FindFirst(Claims.EntitlementId); if (entitlementIdClaim == null) { return(IdentifierNotPresentError()); } var result = new NodeEntitlements() .FromInstant(new DateTimeOffset(token.ValidFrom)) .UntilInstant(new DateTimeOffset(token.ValidTo)) .AddApplication(application) .WithIdentifier(entitlementIdClaim.Value) .AddIpAddress(ipAddress); var virtualMachineIdClaim = principal.FindFirst(Claims.VirtualMachineId); if (virtualMachineIdClaim != null) { result = result.WithVirtualMachineId(virtualMachineIdClaim.Value); } return(Errorable.Success(result)); } catch (SecurityTokenNotYetValidException exception) { return(TokenNotYetValidError(exception.NotBefore)); } catch (SecurityTokenExpiredException exception) { return(TokenExpiredError(exception.Expires)); } catch (SecurityTokenException exception) { return(InvalidTokenError(exception.Message)); } }