Compare commits

..

1 Commits

Author SHA1 Message Date
TingluoHuang
632fcb0d50 unset GTIHUB_ACTION_REPOSITORY and GITHUB_ACTION_REF for non-repo based actions. 2020-11-12 10:59:52 -05:00
23 changed files with 343 additions and 120 deletions

View File

@@ -1,14 +1,11 @@
## Features ## Features
- Add labels in the script that register runner (#844) - N/A
- Add proxy support for container actions (#840)
## Bugs ## Bugs
- Unset GTIHUB_ACTION_REPOSITORY and GITHUB_ACTION_REF for non-repo based actions #804 - N/A
- fix compat issue in timeline record state. #861
## Misc ## Misc
- Crypto cleanup and enable usage of FIPS compliant crypto when required (#806) - Add deprecation date to add-path and set-env runner commands (#796)
- Count actions resolve failures as infra failures (#851)
## Windows x64 ## Windows x64
We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows. We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows.

View File

@@ -1 +1 @@
2.275.1 <Update to ./src/runnerversion when creating release>

View File

@@ -12,13 +12,12 @@ set -e
# #
# Usage: # Usage:
# export RUNNER_CFG_PAT=<yourPAT> # export RUNNER_CFG_PAT=<yourPAT>
# ./create-latest-svc scope [ghe_domain] [name] [user] [labels] # ./create-latest-svc scope [ghe_domain] [name] [user]
# #
# scope required repo (:owner/:repo) or org (:organization) # scope required repo (:owner/:repo) or org (:organization)
# ghe_domain optional the fully qualified domain name of your GitHub Enterprise Server deployment # ghe_domain optional the fully qualified domain name of your GitHub Enterprise Server deployment
# name optional defaults to hostname # name optional defaults to hostname
# user optional user svc will run as. defaults to current # user optional user svc will run as. defaults to current
# labels optional list of labels (split by comma) applied on the runner
# #
# Notes: # Notes:
# PATS over envvars are more secure # PATS over envvars are more secure
@@ -31,7 +30,6 @@ runner_scope=${1}
ghe_hostname=${2} ghe_hostname=${2}
runner_name=${3:-$(hostname)} runner_name=${3:-$(hostname)}
svc_user=${4:-$USER} svc_user=${4:-$USER}
labels=${5}
echo "Configuring runner @ ${runner_scope}" echo "Configuring runner @ ${runner_scope}"
sudo echo sudo echo
@@ -132,8 +130,8 @@ fi
echo echo
echo "Configuring ${runner_name} @ $runner_url" echo "Configuring ${runner_name} @ $runner_url"
echo "./config.sh --unattended --url $runner_url --token *** --name $runner_name --labels $labels" echo "./config.sh --unattended --url $runner_url --token *** --name $runner_name"
sudo -E -u ${svc_user} ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN --name $runner_name --labels $labels sudo -E -u ${svc_user} ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN --name $runner_name
#--------------------------------------- #---------------------------------------
# Configuring as a service # Configuring as a service

View File

@@ -141,6 +141,7 @@ namespace GitHub.Runner.Common
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry"; public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
public static readonly string WorkerCrash = "WORKER_CRASH"; public static readonly string WorkerCrash = "WORKER_CRASH";
public static readonly string UnsupportedCommand = "UNSUPPORTED_COMMAND"; public static readonly string UnsupportedCommand = "UNSUPPORTED_COMMAND";
public static readonly string UnsupportedCommandMessage = "The `{0}` command is deprecated and will be disabled on November 16th. Please upgrade to using Environment Files. For more information see: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/";
public static readonly string UnsupportedCommandMessageDisabled = "The `{0}` command is disabled. Please upgrade to using Environment Files or opt into unsecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`. For more information see: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/"; public static readonly string UnsupportedCommandMessageDisabled = "The `{0}` command is disabled. Please upgrade to using Environment Files or opt into unsecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`. For more information see: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/";
} }

View File

@@ -263,7 +263,6 @@ namespace GitHub.Runner.Listener.Configuration
{ {
{ "clientId", agent.Authorization.ClientId.ToString("D") }, { "clientId", agent.Authorization.ClientId.ToString("D") },
{ "authorizationUrl", agent.Authorization.AuthorizationUrl.AbsoluteUri }, { "authorizationUrl", agent.Authorization.AuthorizationUrl.AbsoluteUri },
{ "requireFipsCryptography", agent.Properties.GetValue("RequireFipsCryptography", false).ToString() }
}, },
}; };

View File

@@ -20,7 +20,7 @@ namespace GitHub.Runner.Listener.Configuration
/// key is returned to the caller. /// key is returned to the caller.
/// </summary> /// </summary>
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the runner</returns> /// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the runner</returns>
RSA CreateKey(); RSACryptoServiceProvider CreateKey();
/// <summary> /// <summary>
/// Deletes the RSA key managed by the key manager. /// Deletes the RSA key managed by the key manager.
@@ -32,7 +32,7 @@ namespace GitHub.Runner.Listener.Configuration
/// </summary> /// </summary>
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the runner</returns> /// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the runner</returns>
/// <exception cref="CryptographicException">No key exists in the store</exception> /// <exception cref="CryptographicException">No key exists in the store</exception>
RSA GetKey(); RSACryptoServiceProvider GetKey();
} }
// Newtonsoft 10 is not working properly with dotnet RSAParameters class // Newtonsoft 10 is not working properly with dotnet RSAParameters class

View File

@@ -36,7 +36,7 @@ namespace GitHub.Runner.Listener.Configuration
// We expect the key to be in the machine store at this point. Configuration should have set all of // We expect the key to be in the machine store at this point. Configuration should have set all of
// this up correctly so we can use the key to generate access tokens. // this up correctly so we can use the key to generate access tokens.
var keyManager = context.GetService<IRSAKeyManager>(); var keyManager = context.GetService<IRSAKeyManager>();
var signingCredentials = VssSigningCredentials.Create(() => keyManager.GetKey(), StringUtil.ConvertToBoolean(CredentialData.Data.GetValueOrDefault("requireFipsCryptography"), false)); var signingCredentials = VssSigningCredentials.Create(() => keyManager.GetKey());
var clientCredential = new VssOAuthJwtBearerClientCredential(clientId, authorizationUrl, signingCredentials); var clientCredential = new VssOAuthJwtBearerClientCredential(clientId, authorizationUrl, signingCredentials);
var agentCredential = new VssOAuthCredential(new Uri(oauthEndpointUrl, UriKind.Absolute), VssOAuthGrant.ClientCredentials, clientCredential); var agentCredential = new VssOAuthCredential(new Uri(oauthEndpointUrl, UriKind.Absolute), VssOAuthGrant.ClientCredentials, clientCredential);

View File

@@ -13,14 +13,14 @@ namespace GitHub.Runner.Listener.Configuration
private string _keyFile; private string _keyFile;
private IHostContext _context; private IHostContext _context;
public RSA CreateKey() public RSACryptoServiceProvider CreateKey()
{ {
RSA rsa = null; RSACryptoServiceProvider rsa = null;
if (!File.Exists(_keyFile)) if (!File.Exists(_keyFile))
{ {
Trace.Info("Creating new RSA key using 2048-bit key length"); Trace.Info("Creating new RSA key using 2048-bit key length");
rsa = RSA.Create(2048); rsa = new RSACryptoServiceProvider(2048);
// Now write the parameters to disk // Now write the parameters to disk
SaveParameters(rsa.ExportParameters(true)); SaveParameters(rsa.ExportParameters(true));
@@ -30,7 +30,7 @@ namespace GitHub.Runner.Listener.Configuration
{ {
Trace.Info("Found existing RSA key parameters file {0}", _keyFile); Trace.Info("Found existing RSA key parameters file {0}", _keyFile);
rsa = RSA.Create(); rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(LoadParameters()); rsa.ImportParameters(LoadParameters());
} }
@@ -46,7 +46,7 @@ namespace GitHub.Runner.Listener.Configuration
} }
} }
public RSA GetKey() public RSACryptoServiceProvider GetKey()
{ {
if (!File.Exists(_keyFile)) if (!File.Exists(_keyFile))
{ {
@@ -55,7 +55,7 @@ namespace GitHub.Runner.Listener.Configuration
Trace.Info("Loading RSA key parameters from file {0}", _keyFile); Trace.Info("Loading RSA key parameters from file {0}", _keyFile);
var rsa = RSA.Create(); var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(LoadParameters()); rsa.ImportParameters(LoadParameters());
return rsa; return rsa;
} }

View File

@@ -14,14 +14,14 @@ namespace GitHub.Runner.Listener.Configuration
private string _keyFile; private string _keyFile;
private IHostContext _context; private IHostContext _context;
public RSA CreateKey() public RSACryptoServiceProvider CreateKey()
{ {
RSA rsa = null; RSACryptoServiceProvider rsa = null;
if (!File.Exists(_keyFile)) if (!File.Exists(_keyFile))
{ {
Trace.Info("Creating new RSA key using 2048-bit key length"); Trace.Info("Creating new RSA key using 2048-bit key length");
rsa = RSA.Create(2048); rsa = new RSACryptoServiceProvider(2048);
// Now write the parameters to disk // Now write the parameters to disk
IOUtil.SaveObject(new RSAParametersSerializable(rsa.ExportParameters(true)), _keyFile); IOUtil.SaveObject(new RSAParametersSerializable(rsa.ExportParameters(true)), _keyFile);
@@ -54,7 +54,7 @@ namespace GitHub.Runner.Listener.Configuration
{ {
Trace.Info("Found existing RSA key parameters file {0}", _keyFile); Trace.Info("Found existing RSA key parameters file {0}", _keyFile);
rsa = RSA.Create(); rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(IOUtil.LoadObject<RSAParametersSerializable>(_keyFile).RSAParameters); rsa.ImportParameters(IOUtil.LoadObject<RSAParametersSerializable>(_keyFile).RSAParameters);
} }
@@ -70,7 +70,7 @@ namespace GitHub.Runner.Listener.Configuration
} }
} }
public RSA GetKey() public RSACryptoServiceProvider GetKey()
{ {
if (!File.Exists(_keyFile)) if (!File.Exists(_keyFile))
{ {
@@ -80,7 +80,7 @@ namespace GitHub.Runner.Listener.Configuration
Trace.Info("Loading RSA key parameters from file {0}", _keyFile); Trace.Info("Loading RSA key parameters from file {0}", _keyFile);
var parameters = IOUtil.LoadObject<RSAParametersSerializable>(_keyFile).RSAParameters; var parameters = IOUtil.LoadObject<RSAParametersSerializable>(_keyFile).RSAParameters;
var rsa = RSA.Create(); var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(parameters); rsa.ImportParameters(parameters);
return rsa; return rsa;
} }

View File

@@ -319,8 +319,7 @@ namespace GitHub.Runner.Listener
var keyManager = HostContext.GetService<IRSAKeyManager>(); var keyManager = HostContext.GetService<IRSAKeyManager>();
using (var rsa = keyManager.GetKey()) using (var rsa = keyManager.GetKey())
{ {
var padding = _session.UseFipsEncryption ? RSAEncryptionPadding.OaepSHA256 : RSAEncryptionPadding.OaepSHA1; return aes.CreateDecryptor(rsa.Decrypt(_session.EncryptionKey.Value, RSAEncryptionPadding.OaepSHA1), message.IV);
return aes.CreateDecryptor(rsa.Decrypt(_session.EncryptionKey.Value, padding), message.IV);
} }
} }
else else

View File

@@ -184,6 +184,9 @@ namespace GitHub.Runner.Worker
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container) public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{ {
var configurationStore = HostContext.GetService<IConfigurationStore>();
var isHostedServer = configurationStore.GetSettings().IsHostedServer;
var allowUnsecureCommands = false; var allowUnsecureCommands = false;
bool.TryParse(Environment.GetEnvironmentVariable(Constants.Variables.Actions.AllowUnsupportedCommands), out allowUnsecureCommands); bool.TryParse(Environment.GetEnvironmentVariable(Constants.Variables.Actions.AllowUnsupportedCommands), out allowUnsecureCommands);
@@ -198,10 +201,22 @@ namespace GitHub.Runner.Worker
bool.TryParse(envContext[Constants.Variables.Actions.AllowUnsupportedCommands].ToString(), out allowUnsecureCommands); bool.TryParse(envContext[Constants.Variables.Actions.AllowUnsupportedCommands].ToString(), out allowUnsecureCommands);
} }
if (!allowUnsecureCommands) // TODO: Eventually remove isHostedServer and apply this to dotcom customers as well
if (!isHostedServer && !allowUnsecureCommands)
{ {
throw new Exception(String.Format(Constants.Runner.UnsupportedCommandMessageDisabled, this.Command)); throw new Exception(String.Format(Constants.Runner.UnsupportedCommandMessageDisabled, this.Command));
} }
else if (!allowUnsecureCommands)
{
// Log Telemetry and let user know they shouldn't do this
var issue = new Issue()
{
Type = IssueType.Error,
Message = String.Format(Constants.Runner.UnsupportedCommandMessage, this.Command)
};
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.UnsupportedCommand;
context.AddIssue(issue);
}
if (!command.Properties.TryGetValue(SetEnvCommandProperties.Name, out string envName) || string.IsNullOrEmpty(envName)) if (!command.Properties.TryGetValue(SetEnvCommandProperties.Name, out string envName) || string.IsNullOrEmpty(envName))
{ {
@@ -324,7 +339,10 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension); public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container) public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{ {
var configurationStore = HostContext.GetService<IConfigurationStore>();
var isHostedServer = configurationStore.GetSettings().IsHostedServer;
var allowUnsecureCommands = false; var allowUnsecureCommands = false;
bool.TryParse(Environment.GetEnvironmentVariable(Constants.Variables.Actions.AllowUnsupportedCommands), out allowUnsecureCommands); bool.TryParse(Environment.GetEnvironmentVariable(Constants.Variables.Actions.AllowUnsupportedCommands), out allowUnsecureCommands);
@@ -339,10 +357,22 @@ namespace GitHub.Runner.Worker
bool.TryParse(envContext[Constants.Variables.Actions.AllowUnsupportedCommands].ToString(), out allowUnsecureCommands); bool.TryParse(envContext[Constants.Variables.Actions.AllowUnsupportedCommands].ToString(), out allowUnsecureCommands);
} }
if (!allowUnsecureCommands) // TODO: Eventually remove isHostedServer and apply this to dotcom customers as well
if (!isHostedServer && !allowUnsecureCommands)
{ {
throw new Exception(String.Format(Constants.Runner.UnsupportedCommandMessageDisabled, this.Command)); throw new Exception(String.Format(Constants.Runner.UnsupportedCommandMessageDisabled, this.Command));
} }
else if (!allowUnsecureCommands)
{
// Log Telemetry and let user know they shouldn't do this
var issue = new Issue()
{
Type = IssueType.Error,
Message = String.Format(Constants.Runner.UnsupportedCommandMessage, this.Command)
};
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.UnsupportedCommand;
context.AddIssue(issue);
}
ArgUtil.NotNullOrEmpty(command.Data, "path"); ArgUtil.NotNullOrEmpty(command.Data, "path");
context.Global.PrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture)); context.Global.PrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));

View File

@@ -594,22 +594,15 @@ namespace GitHub.Runner.Worker
actionDownloadInfos = await jobServer.ResolveActionDownloadInfoAsync(executionContext.Global.Plan.ScopeIdentifier, executionContext.Global.Plan.PlanType, executionContext.Global.Plan.PlanId, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken); actionDownloadInfos = await jobServer.ResolveActionDownloadInfoAsync(executionContext.Global.Plan.ScopeIdentifier, executionContext.Global.Plan.PlanType, executionContext.Global.Plan.PlanId, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
break; break;
} }
catch (Exception ex) catch (Exception ex) when (attempt < 3)
{ {
if (attempt < 3) executionContext.Output($"Failed to resolve action download info. Error: {ex.Message}");
executionContext.Debug(ex.ToString());
if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF")))
{ {
executionContext.Output($"Failed to resolve action download info. Error: {ex.Message}"); var backoff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
executionContext.Debug(ex.ToString()); executionContext.Output($"Retrying in {backoff.TotalSeconds} seconds");
if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF"))) await Task.Delay(backoff);
{
var backoff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
executionContext.Output($"Retrying in {backoff.TotalSeconds} seconds");
await Task.Delay(backoff);
}
}
else
{
throw new WebApi.FailedToResolveActionDownloadInfoException("Failed to resolve action download info.", ex);
} }
} }
} }

View File

@@ -21,11 +21,6 @@ namespace GitHub.Runner.Worker.Container
{ {
} }
public ContainerInfo(IHostContext hostContext)
{
UpdateWebProxyEnv(hostContext.WebProxy);
}
public ContainerInfo(IHostContext hostContext, Pipelines.JobContainer container, bool isJobContainer = true, string networkAlias = null) public ContainerInfo(IHostContext hostContext, Pipelines.JobContainer container, bool isJobContainer = true, string networkAlias = null)
{ {
this.ContainerName = container.Alias; this.ContainerName = container.Alias;

View File

@@ -918,12 +918,6 @@ namespace GitHub.Runner.Worker
context.AddIssue(new Issue() { Type = IssueType.Error, Message = message }); context.AddIssue(new Issue() { Type = IssueType.Error, Message = message });
} }
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void InfrastructureError(this IExecutionContext context, string message)
{
context.AddIssue(new Issue() { Type = IssueType.Error, Message = message, IsInfrastructureIssue = true});
}
// Do not add a format string overload. See comment on ExecutionContext.Write(). // Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Warning(this IExecutionContext context, string message) public static void Warning(this IExecutionContext context, string message)
{ {

View File

@@ -70,7 +70,7 @@ namespace GitHub.Runner.Worker.Handlers
} }
// run container // run container
var container = new ContainerInfo(HostContext) var container = new ContainerInfo()
{ {
ContainerImage = Data.Image, ContainerImage = Data.Image,
ContainerName = ExecutionContext.Id.ToString("N"), ContainerName = ExecutionContext.Id.ToString("N"),

View File

@@ -335,14 +335,6 @@ namespace GitHub.Runner.Worker
context.Result = TaskResult.Canceled; context.Result = TaskResult.Canceled;
throw; throw;
} }
catch (FailedToResolveActionDownloadInfoException ex)
{
// Log the error and fail the JobExtension Initialization.
Trace.Error($"Caught exception from JobExtenion Initialization: {ex}");
context.InfrastructureError(ex.Message);
context.Result = TaskResult.Failed;
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
// Log the error and fail the JobExtension Initialization. // Log the error and fail the JobExtension Initialization.

View File

@@ -2458,23 +2458,4 @@ namespace GitHub.DistributedTask.WebApi
{ {
} }
} }
[Serializable]
public sealed class FailedToResolveActionDownloadInfoException : DistributedTaskException
{
public FailedToResolveActionDownloadInfoException(String message)
: base(message)
{
}
public FailedToResolveActionDownloadInfoException(String message, Exception innerException)
: base(message, innerException)
{
}
private FailedToResolveActionDownloadInfoException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
} }

View File

@@ -17,7 +17,6 @@ namespace GitHub.DistributedTask.WebApi
this.Type = issueToBeCloned.Type; this.Type = issueToBeCloned.Type;
this.Category = issueToBeCloned.Category; this.Category = issueToBeCloned.Category;
this.Message = issueToBeCloned.Message; this.Message = issueToBeCloned.Message;
this.IsInfrastructureIssue = issueToBeCloned.IsInfrastructureIssue;
if (issueToBeCloned.m_data != null) if (issueToBeCloned.m_data != null)
{ {
@@ -49,13 +48,6 @@ namespace GitHub.DistributedTask.WebApi
set; set;
} }
[DataMember(Order = 4)]
public bool? IsInfrastructureIssue
{
get;
set;
}
public IDictionary<String, String> Data public IDictionary<String, String> Data
{ {
get get

View File

@@ -65,15 +65,5 @@ namespace GitHub.DistributedTask.WebApi
get; get;
set; set;
} }
/// <summary>
/// Gets or sets whether to use FIPS compliant encryption scheme for job message key
/// </summary>
[DataMember]
public bool UseFipsEncryption
{
get;
set;
}
} }
} }

View File

@@ -13,8 +13,5 @@ namespace GitHub.DistributedTask.WebApi
[EnumMember] [EnumMember]
Completed, Completed,
[EnumMember]
Delayed,
} }
} }

View File

@@ -130,6 +130,55 @@ namespace GitHub.Services.WebApi.Jwt
return credentials.SignatureAlgorithm; return credentials.SignatureAlgorithm;
} }
public static ClaimsPrincipal ValidateToken(this JsonWebToken token, JsonWebTokenValidationParameters parameters)
{
ArgumentUtility.CheckForNull(token, nameof(token));
ArgumentUtility.CheckForNull(parameters, nameof(parameters));
ClaimsIdentity actorIdentity = ValidateActor(token, parameters);
ValidateLifetime(token, parameters);
ValidateAudience(token, parameters);
ValidateSignature(token, parameters);
ValidateIssuer(token, parameters);
ClaimsIdentity identity = new ClaimsIdentity("Federation", parameters.IdentityNameClaimType, ClaimTypes.Role);
if (actorIdentity != null)
{
identity.Actor = actorIdentity;
}
IEnumerable<Claim> claims = token.ExtractClaims();
foreach (Claim claim in claims)
{
identity.AddClaim(new Claim(claim.Type, claim.Value, claim.ValueType, token.Issuer));
}
return new ClaimsPrincipal(identity);
}
private static ClaimsIdentity ValidateActor(JsonWebToken token, JsonWebTokenValidationParameters parameters)
{
ArgumentUtility.CheckForNull(token, nameof(token));
ArgumentUtility.CheckForNull(parameters, nameof(parameters));
if (!parameters.ValidateActor)
{
return null;
}
//this recursive call with check the parameters
ClaimsPrincipal principal = token.Actor.ValidateToken(parameters.ActorValidationParameters);
if (!(principal?.Identity is ClaimsIdentity))
{
throw new ActorValidationException();
}
return (ClaimsIdentity)principal.Identity;
}
private static void ValidateLifetime(JsonWebToken token, JsonWebTokenValidationParameters parameters) private static void ValidateLifetime(JsonWebToken token, JsonWebTokenValidationParameters parameters)
{ {
ArgumentUtility.CheckForNull(token, nameof(token)); ArgumentUtility.CheckForNull(token, nameof(token));
@@ -192,6 +241,59 @@ namespace GitHub.Services.WebApi.Jwt
throw new InvalidAudienceException(); //validation exception; throw new InvalidAudienceException(); //validation exception;
} }
private static void ValidateSignature(JsonWebToken token, JsonWebTokenValidationParameters parameters)
{
ArgumentUtility.CheckForNull(token, nameof(token));
ArgumentUtility.CheckForNull(parameters, nameof(parameters));
if (!parameters.ValidateSignature)
{
return;
}
string encodedData = token.EncodedToken;
string[] parts = encodedData.Split('.');
if (parts.Length != 3)
{
throw new InvalidTokenException(JwtResources.EncodedTokenDataMalformed()); //validation exception
}
if (string.IsNullOrEmpty(parts[2]))
{
throw new InvalidTokenException(JwtResources.SignatureNotFound()); //validation exception
}
if (token.Algorithm == JWTAlgorithm.None)
{
throw new InvalidTokenException(JwtResources.InvalidSignatureAlgorithm()); //validation exception
}
ArgumentUtility.CheckForNull(parameters.SigningCredentials, nameof(parameters.SigningCredentials));
//ArgumentUtility.CheckEnumerableForNullOrEmpty(parameters.SigningToken.SecurityKeys, nameof(parameters.SigningToken.SecurityKeys));
byte[] sourceInput = Encoding.UTF8.GetBytes(string.Format("{0}.{1}", parts[0], parts[1]));
byte[] sourceSignature = parts[2].FromBase64StringNoPadding();
try
{
if (parameters.SigningCredentials.VerifySignature(sourceInput, sourceSignature))
{
return;
}
}
catch (Exception)
{
//swallow exceptions here, we'll throw if nothing works...
}
throw new SignatureValidationException(); //valiation exception
}
private static void ValidateIssuer(JsonWebToken token, JsonWebTokenValidationParameters parameters) private static void ValidateIssuer(JsonWebToken token, JsonWebTokenValidationParameters parameters)
{ {
ArgumentUtility.CheckForNull(token, nameof(token)); ArgumentUtility.CheckForNull(token, nameof(token));

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using GitHub.Services.Common; using GitHub.Services.Common;
using GitHub.Services.WebApi.Jwt; using GitHub.Services.WebApi.Jwt;
@@ -74,6 +75,7 @@ namespace GitHub.Services.WebApi
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
return GetSignature(input); return GetSignature(input);
} }
@@ -84,13 +86,48 @@ namespace GitHub.Services.WebApi
/// <returns>A blob of data representing the signature of the input data</returns> /// <returns>A blob of data representing the signature of the input data</returns>
protected abstract Byte[] GetSignature(Byte[] input); protected abstract Byte[] GetSignature(Byte[] input);
/// <summary>
/// Verifies the signature of the input data, returning true if the signature is valid.
/// </summary>
/// <param name="input">The data which should be signed</param>
/// <param name="signature">The signature which should be verified</param>
/// <returns>True if the provided signature matches the current signing token; otherwise, false</returns>
public abstract Boolean VerifySignature(Byte[] input, Byte[] signature);
/// <summary>
/// Creates a new <c>VssSigningCredentials</c> instance using the specified <paramref name="certificate"/> instance
/// as the signing key.
/// </summary>
/// <param name="certificate">The certificate which contains the key used for signing and verification</param>
/// <returns>A new <c>VssSigningCredentials</c> instance which uses the specified certificate for signing</returns>
public static VssSigningCredentials Create(X509Certificate2 certificate)
{
ArgumentUtility.CheckForNull(certificate, nameof(certificate));
if (certificate.HasPrivateKey)
{
var rsa = certificate.GetRSAPrivateKey();
if (rsa == null)
{
throw new SignatureAlgorithmUnsupportedException(certificate.SignatureAlgorithm.FriendlyName);
}
if (rsa.KeySize < c_minKeySize)
{
throw new InvalidCredentialsException(JwtResources.SigningTokenKeyTooSmall());
}
}
return new X509Certificate2SigningToken(certificate);
}
/// <summary> /// <summary>
/// Creates a new <c>VssSigningCredentials</c> instance using the specified <paramref name="factory"/> /// Creates a new <c>VssSigningCredentials</c> instance using the specified <paramref name="factory"/>
/// callback function to retrieve the signing key. /// callback function to retrieve the signing key.
/// </summary> /// </summary>
/// <param name="factory">The factory which creates <c>RSA</c> keys used for signing and verification</param> /// <param name="factory">The factory which creates <c>RSA</c> keys used for signing and verification</param>
/// <returns>A new <c>VssSigningCredentials</c> instance which uses the specified provider for signing</returns> /// <returns>A new <c>VssSigningCredentials</c> instance which uses the specified provider for signing</returns>
public static VssSigningCredentials Create(Func<RSA> factory, bool requireFipsCryptography) public static VssSigningCredentials Create(Func<RSA> factory)
{ {
ArgumentUtility.CheckForNull(factory, nameof(factory)); ArgumentUtility.CheckForNull(factory, nameof(factory));
@@ -106,19 +143,80 @@ namespace GitHub.Services.WebApi
throw new InvalidCredentialsException(JwtResources.SigningTokenKeyTooSmall()); throw new InvalidCredentialsException(JwtResources.SigningTokenKeyTooSmall());
} }
if (requireFipsCryptography) return new RSASigningToken(factory, rsa.KeySize);
{
return new RSASigningToken(factory, rsa.KeySize, RSASignaturePadding.Pss);
}
return new RSASigningToken(factory, rsa.KeySize, RSASignaturePadding.Pkcs1);
} }
} }
/// <summary>
/// Creates a new <c>VssSigningCredentials</c> instance using the specified <paramref name="key"/> as the signing
/// key. The returned signing token performs symmetric key signing and verification.
/// </summary>
/// <param name="rsa">The key used for signing and verification</param>
/// <returns>A new <c>VssSigningCredentials</c> instance which uses the specified key for signing</returns>
public static VssSigningCredentials Create(Byte[] key)
{
ArgumentUtility.CheckForNull(key, nameof(key));
// Probably should have validation here, but there was none previously
return new SymmetricKeySigningToken(key);
}
private const Int32 c_minKeySize = 2048; private const Int32 c_minKeySize = 2048;
private readonly DateTime m_effectiveDate; private readonly DateTime m_effectiveDate;
#region Concrete Implementations #region Concrete Implementations
private class SymmetricKeySigningToken : VssSigningCredentials
{
public SymmetricKeySigningToken(Byte[] key)
{
m_key = new Byte[key.Length];
Buffer.BlockCopy(key, 0, m_key, 0, m_key.Length);
}
public override Boolean CanSignData
{
get
{
return true;
}
}
public override Int32 KeySize
{
get
{
return m_key.Length * 8;
}
}
public override JWTAlgorithm SignatureAlgorithm
{
get
{
return JWTAlgorithm.HS256;
}
}
protected override Byte[] GetSignature(Byte[] input)
{
using (var hash = new HMACSHA256(m_key))
{
return hash.ComputeHash(input);
}
}
public override Boolean VerifySignature(
Byte[] input,
Byte[] signature)
{
var computedSignature = SignData(input);
return SecureCompare.TimeInvariantEquals(computedSignature, signature);
}
private readonly Byte[] m_key;
}
private abstract class AsymmetricKeySigningToken : VssSigningCredentials private abstract class AsymmetricKeySigningToken : VssSigningCredentials
{ {
protected abstract Boolean HasPrivateKey(); protected abstract Boolean HasPrivateKey();
@@ -146,14 +244,70 @@ namespace GitHub.Services.WebApi
private Boolean? m_hasPrivateKey; private Boolean? m_hasPrivateKey;
} }
private class X509Certificate2SigningToken : AsymmetricKeySigningToken, IJsonWebTokenHeaderProvider
{
public X509Certificate2SigningToken(X509Certificate2 certificate)
{
m_certificate = certificate;
}
public override Int32 KeySize
{
get
{
return m_certificate.GetRSAPublicKey().KeySize;
}
}
public override DateTime ValidFrom
{
get
{
return m_certificate.NotBefore;
}
}
public override DateTime ValidTo
{
get
{
return m_certificate.NotAfter;
}
}
public override Boolean VerifySignature(
Byte[] input,
Byte[] signature)
{
var rsa = m_certificate.GetRSAPublicKey();
return rsa.VerifyData(input, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
protected override Byte[] GetSignature(Byte[] input)
{
var rsa = m_certificate.GetRSAPrivateKey();
return rsa.SignData(input, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
protected override Boolean HasPrivateKey()
{
return m_certificate.HasPrivateKey;
}
void IJsonWebTokenHeaderProvider.SetHeaders(IDictionary<String, Object> headers)
{
headers[JsonWebTokenHeaderParameters.X509CertificateThumbprint] = m_certificate.GetCertHash().ToBase64StringNoPadding();
}
private readonly X509Certificate2 m_certificate;
}
private class RSASigningToken : AsymmetricKeySigningToken private class RSASigningToken : AsymmetricKeySigningToken
{ {
public RSASigningToken( public RSASigningToken(
Func<RSA> factory, Func<RSA> factory,
Int32 keySize, Int32 keySize)
RSASignaturePadding signaturePadding)
{ {
m_signaturePadding = signaturePadding;
m_keySize = keySize; m_keySize = keySize;
m_factory = factory; m_factory = factory;
} }
@@ -170,7 +324,7 @@ namespace GitHub.Services.WebApi
{ {
using (var rsa = m_factory()) using (var rsa = m_factory())
{ {
return rsa.SignData(input, HashAlgorithmName.SHA256, m_signaturePadding); return rsa.SignData(input, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
} }
} }
@@ -181,7 +335,7 @@ namespace GitHub.Services.WebApi
// As unfortunate as this is, there is no way to tell from an RSA implementation, based on querying // As unfortunate as this is, there is no way to tell from an RSA implementation, based on querying
// properties alone, if it supports signature operations or has a private key. This is a one-time // properties alone, if it supports signature operations or has a private key. This is a one-time
// hit for the signing credentials implementation, so it shouldn't be a huge deal. // hit for the signing credentials implementation, so it shouldn't be a huge deal.
GetSignature(new Byte[1] { 1 }); GetSignature(new Byte[1] { 1 });
return true; return true;
} }
catch (CryptographicException) catch (CryptographicException)
@@ -190,9 +344,18 @@ namespace GitHub.Services.WebApi
} }
} }
public override Boolean VerifySignature(
Byte[] input,
Byte[] signature)
{
using (var rsa = m_factory())
{
return rsa.VerifyData(input, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
private readonly Int32 m_keySize; private readonly Int32 m_keySize;
private readonly Func<RSA> m_factory; private readonly Func<RSA> m_factory;
private readonly RSASignaturePadding m_signaturePadding;
} }
#endregion #endregion

View File

@@ -1 +1 @@
2.275.1 2.274.1