/// <summary> /// Determines whether the signature is valid for the specified message. /// </summary> /// <param name="validator">Used to validate the signature.</param> /// <param name="request">The request message that contains the signature.</param> /// <param name="signature">The signature to validate.</param> /// <returns> /// A <see cref="SignatureValidationResult"/> that represents the result of the validation. /// </returns> public static async Task <SignatureValidationResult> ValidateAsync( this HttpSignatureValidator validator, HttpRequest request, HttpSignature signature) { // Allow to read the request body multiple times request.EnableBuffering(); // First, we try the raw request URL (if available) var requestFeature = request.HttpContext.Features.Get <IHttpRequestFeature>(); if (!string.IsNullOrEmpty(requestFeature.RawTarget)) { var result = await validator.ValidateAsync(signature, request.Method, requestFeature.RawTarget, request.Body).ConfigureAwait(false); // If the signature is OK, we're done. If it's Expired or Duplicate, there's no point // in checking again. if (result != SignatureValidationResult.Invalid) { return(result); } } return(await validator.ValidateAsync(signature, request.Method, request.GetEncodedUrl(), request.Body).ConfigureAwait(false)); }
public void StringCannotBeParsedWithoutNonce() { var serializedString = "keyId=test,created=1,signature=\"OSQPsZ+PegY=\""; Action parse = () => HttpSignature.Parse(serializedString); parse.Should().Throw <FormatException>(); }
public void StringCannotBeParsedWithoutSignature() { var serializedString = "keyId=test,nonce=test,created=1"; Action parse = () => HttpSignature.Parse(serializedString); parse.Should().Throw <FormatException>(); }
public void ExceptionIsThrownForInvalidTimestamp() { var serializedString = "keyId=test,nonce=test,created=\"undefined\",signature=\"OSQPsZ+PegY=\""; Action parse = () => HttpSignature.Parse(serializedString); parse.Should().Throw <FormatException>(); }
public void TimestampCanBeParsedFromUnixTimestamp() { var timestamp = TestClock.TestValue; var serializedString = "keyId=test,nonce=test,created=\"" + timestamp.ToUnixTimeSeconds().ToString() + "\",signature=\"OSQPsZ+PegY=\""; var param = HttpSignature.Parse(serializedString); param.Timestamp.Should().Be(timestamp); }
public void TimestampCanBeParsedFromIso8601String() { var timestamp = TestClock.TestValue; var serializedString = "keyId=test,nonce=test,created=\"" + timestamp.ToString("s", CultureInfo.InvariantCulture) + "\",signature=\"OSQPsZ+PegY=\""; var param = HttpSignature.Parse(serializedString); param.Timestamp.Should().Be(timestamp); }
public void NonceCanBeParsed() { const string nonce = "99e3006e-b846-4fe6-9572-6b5e2031773f"; const string serializedString = "keyId=\"test\",nonce=\"" + nonce + "\",created=1,signature=\"OSQPsZ+PegY=\""; var param = HttpSignature.Parse(serializedString); param.Nonce.Should().Be(nonce); }
public void QuotedKeyIdCanBeParsed() { const string keyId = "te,st"; const string serializedString = "keyId=\"" + keyId + "\",nonce=test,created=1,signature=\"OSQPsZ+PegY=\""; var param = HttpSignature.Parse(serializedString); param.KeyId.Should().Be(keyId); }
public void KeyIdCanBeParsedCaseInsensitive() { const string keyId = "test"; const string serializedString = "KEYID=" + keyId + ",nonce=test,created=1,signature=\"OSQPsZ+PegY=\""; var param = HttpSignature.Parse(serializedString); param.KeyId.Should().Be(keyId); }
/// <summary> /// Creates a new authentication ticket for the specified signature. /// </summary> /// <param name="signature">The signature to authenticate.</param> /// <returns>A new <see cref="AuthenticationTicket"/>.</returns> protected virtual AuthenticationTicket TicketFor(HttpSignature signature) { var identity = new ClaimsIdentity(Options.AuthenticationScheme); identity.AddClaim(new Claim(identity.NameClaimType, signature.KeyId)); var principal = new ClaimsPrincipal(identity); return(new AuthenticationTicket(principal, Options.AuthenticationScheme)); }
public void SignatureCanBeParsed() { var hash = new byte[8] { 57, 36, 15, 177, 159, 143, 122, 6 }; const string serializedString = "keyId=\"test\",nonce=test,created=1,signature=\"OSQPsZ+PegY=\""; var param = HttpSignature.Parse(serializedString); param.Hash.Should().Equal(hash); }
public void SignatureParamsCanBeSerializedAndDeserialized() { var expected = GetRandomParams(); var actual = HttpSignature.Parse(expected.ToString()); actual.KeyId.Should().Be(expected.KeyId); actual.Nonce.Should().Be(expected.Nonce); actual.Timestamp.Should().BeCloseTo(expected.Timestamp, TimeSpan.FromSeconds(1)); actual.Hash.Should().Equal(expected.Hash); }
protected override Task <HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { request.Headers.Authorization.Should().NotBeNull(); request.Headers.Authorization.Parameter.Should().NotBeNull(); var param = HttpSignature.Parse(request.Headers.Authorization.Parameter); param.KeyId.Should().NotBeNull(); param.Nonce.Should().NotBeNull(); param.Hash.Should().NotBeEmpty(); return(Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, RequestMessage = request })); }
/// <summary> /// Determines whether the current request is authenticated. /// </summary> /// <returns> /// A task that returns a value indicating whether the authentication succeeded. /// </returns> protected override async Task <AuthenticateResult> HandleAuthenticateAsync() { if (!Request.Headers.TryGetValue("Authorization", out var value)) { return(AuthenticateResult.NoResult()); } var authValue = value.LastOrDefault(x => x.StartsWith(Options.AuthenticationScheme)); if (authValue == null) { return(AuthenticateResult.NoResult()); } authValue = authValue.Substring(Options.AuthenticationScheme.Length).TrimStart(); var signature = HttpSignature.Parse(authValue); var result = await Validator.ValidateAsync(Request, signature).ConfigureAwait(false); switch (result) { case SignatureValidationResult.OK: return(AuthenticateResult.Success(TicketFor(signature))); case SignatureValidationResult.Invalid: Logger.LogInformation("Invalid signature with key ID {KeyId}", signature.KeyId); break; case SignatureValidationResult.Expired: Logger.LogInformation("Expired signature with key ID {KeyId}, created {Timestamp}", signature.KeyId, signature.Timestamp); break; case SignatureValidationResult.Duplicate: Logger.LogInformation("Duplicate signature with key ID {KeyId}, nonce {Nonce}", signature.KeyId, signature.Nonce); break; default: return(AuthenticateResult.Fail("Invalid validation result: " + result)); } return(AuthenticateResult.NoResult()); }
/// <summary> /// Determines whether the signature is valid for the specified message. /// </summary> /// <param name="signature">The signature to validate.</param> /// <param name="method">The HTTP method of the message.</param> /// <param name="uri">The requested URI of the message.</param> /// <param name="body">The message body.</param> /// <param name="cancellationToken">A token to monitor for cancellation requests.</param> /// <returns>A value indicating the result of the validation.</returns> public virtual async Task <SignatureValidationResult> ValidateAsync( HttpSignature signature, string method, string uri, Stream body, CancellationToken cancellationToken) { var timeDiff = Clock.UtcNow - signature.Timestamp; if (timeDiff.Duration() > Options.ClockSkewMargin) { Logger?.LogInformation("The time difference {TimeDiff} between the signature timestamp {Timestamp} and the current time exceeds {Margin}.", timeDiff, signature.Timestamp, Options.ClockSkewMargin); return(SignatureValidationResult.Expired); } var entry = new NonceCacheEntry(signature.Nonce); if (Cache.TryGetValue(entry, out _)) { Logger?.LogInformation("The nonce '{Nonce}' is not unique and has been used before in the past {Expiration}.", signature.Nonce, Options.NonceExpiration); return(SignatureValidationResult.Duplicate); } var key = await KeyLookup.GetKeyOrDefaultAsync(signature.KeyId).ConfigureAwait(false); if (key == null) { throw KeyNotFoundException.WithId(signature.KeyId); } var algorithm = new HttpSignatureAlgorithm(key, Clock, Logger); var newHash = await algorithm.CalculateHashAsync(method, uri, body, signature.Nonce, signature.Timestamp, cancellationToken).ConfigureAwait(false); if (!newHash.HashEquals(signature.Hash)) { Logger?.LogInformation("The signature for {Method} {Uri} with nonce '{Nonce}' and timestamp {Timestamp} does not match.", method, uri, signature.Nonce, signature.Timestamp); return(SignatureValidationResult.Invalid); } Cache.Set(entry, true, Options.NonceExpiration); return(SignatureValidationResult.OK); }
public async Task SignedRequestContainsValidAuthorizationHeader() { var algorithm = new HttpSignatureAlgorithm(TestKeyConstants.TestKey, new TestClock()); var request = new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = new Uri("http://localhost:5000/api/test/1?value=2011-12-20T12:13:21Z") }; await request.SignAsync(algorithm, TestKeyConstants.ValidKeyId); request.Headers.Authorization.Should().NotBeNull(); request.Headers.Authorization.Parameter.Should().NotBeNull(); var param = HttpSignature.Parse(request.Headers.Authorization.Parameter); param.KeyId.Should().Be(TestKeyConstants.ValidKeyId); param.Nonce.Should().NotBeNull(); param.Timestamp.Should().Be(TestClock.TestValue); param.Hash.Should().NotBeEmpty(); }
/// <summary> /// Validate the signature against the requested payload. /// </summary> /// <param name="signature"></param> /// <param name="key">The public key</param> /// <param name="httpRequest"></param> /// <returns></returns> public static bool Validate(this HttpSignature signature, SecurityKey key, HttpRequest httpRequest) { var headers = httpRequest.Headers.ToDictionary(x => x.Key, x => (string)x.Value, StringComparer.OrdinalIgnoreCase); var options = (HttpSignatureOptions)httpRequest.HttpContext.RequestServices.GetService(typeof(HttpSignatureOptions)); var forwardedPath = httpRequest.Headers[options.ForwardedPathHeaderName]; string rawTarget = null; if (!string.IsNullOrWhiteSpace(forwardedPath)) { rawTarget = forwardedPath; } else { var requestFeature = httpRequest.HttpContext.Features.Get <IHttpRequestFeature>(); rawTarget = $"{requestFeature.Path}{requestFeature.QueryString}"; } headers.Add(HttpRequestTarget.HeaderName, new HttpRequestTarget(httpRequest.Method, rawTarget).ToString()); headers.Add(HeaderFieldNames.Created, httpRequest.Headers[options.RequestCreatedHeaderName]); return(signature.Validate(key, headers)); }
/// <summary> /// Invokes the middleware. /// </summary> /// <param name="httpContext">Encapsulates all HTTP-specific information about an individual HTTP request.</param> /// <param name="logger">A generic interface for logging.</param> public async Task Invoke(HttpContext httpContext, ILogger <HttpSignatureMiddleware> logger) { var headerNames = new List <string>(); var mustValidate = _options.RequestValidation && _options.TryMatch(httpContext, out headerNames); if (mustValidate || httpContext.Request.Headers.ContainsKey(HttpSignature.HTTPHeaderName)) { var rawSignature = httpContext.Request.Headers[HttpSignature.HTTPHeaderName]; Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: Raw Signature: {rawSignature}"); var rawDigest = httpContext.Request.Headers[HttpDigest.HTTPHeaderName]; Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: Raw Digest: {rawDigest}"); var rawCertificate = httpContext.Request.Headers[_options.RequestSignatureCertificateHeaderName]; Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: Raw Certificate: {rawCertificate}"); if (string.IsNullOrWhiteSpace(rawSignature)) { var error = $"Missing httpSignature in HTTP header '{HttpSignature.HTTPHeaderName}'. Cannot validate signature."; await WriteErrorResponse(httpContext, logger, HttpStatusCode.BadRequest, error); return; } if (string.IsNullOrWhiteSpace(rawCertificate)) { var error = $"Missing certificate in HTTP header '{_options.RequestSignatureCertificateHeaderName}'. Cannot validate signature."; await WriteErrorResponse(httpContext, logger, HttpStatusCode.BadRequest, error); return; } X509Certificate2 cert; try { cert = new X509Certificate2(Convert.FromBase64String(rawCertificate)); } catch { var error = $"Signature Certificate not in a valid format. Expected a base64 encoded x509."; await WriteErrorResponse(httpContext, logger, HttpStatusCode.Unauthorized, error); return; } var validationKey = new X509SecurityKey(cert); Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: Validation Key: {validationKey.KeyId}"); var httpSignature = HttpSignature.Parse(rawSignature); Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: HTTP Signature: {httpSignature}"); var requestBody = Array.Empty <byte>(); switch (httpContext.Request.Method) { case "POST": case "PUT": requestBody = await GetRequestBody(httpContext.Request); break; default: break; } // Validate the request. if (httpSignature.Headers.Contains(HttpDigest.HTTPHeaderName)) { if (!string.IsNullOrWhiteSpace(rawSignature) && string.IsNullOrWhiteSpace(rawDigest)) { var error = $"Missing digest in HTTP header '{HttpDigest.HTTPHeaderName}'. Cannot validate signature."; await WriteErrorResponse(httpContext, logger, HttpStatusCode.BadRequest, error); return; } var httpDigest = HttpDigest.Parse(rawDigest); Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: HTTP Digest: {httpDigest}"); var digestIsValid = httpDigest.Validate(requestBody); if (!digestIsValid) { var error = $"Digest validation failed."; await WriteErrorResponse(httpContext, logger, HttpStatusCode.Unauthorized, error); return; } } var signatureIsValid = httpSignature.Validate(validationKey, httpContext.Request); if (!signatureIsValid) { var error = $"Signature validation failed."; await WriteErrorResponse(httpContext, logger, HttpStatusCode.Unauthorized, error); return; } logger.LogInformation("Signature validated successfuly for path: '{0} {1}'", httpContext.Request.Method, httpContext.Request.Path); // Call the next middleware delegate in the pipeline. } if (mustValidate && _options.ResponseSigning == true) { using (var responseMemory = new MemoryStream()) { var originalStream = httpContext.Response.Body; httpContext.Response.Body = responseMemory; await _next.Invoke(httpContext); responseMemory.Seek(0, SeekOrigin.Begin); var content = responseMemory.ToArray(); responseMemory.Seek(0, SeekOrigin.Begin); // Apply logic here for deciding which headers to add. var signingCredentialsStore = httpContext.RequestServices.GetService <IHttpSigningCredentialsStore>(); var validationKeysStore = httpContext.RequestServices.GetService <IHttpValidationKeysStore>(); var signingCredentials = await signingCredentialsStore.GetSigningCredentialsAsync(); var validationKeys = await validationKeysStore.GetValidationKeysAsync(); var validationKey = validationKeys.First() as X509SecurityKey; Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: Validation Key: {validationKey.KeyId}"); var rawTarget = httpContext.GetPathAndQuery(); Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: Raw Target: {rawTarget}"); var extraHeaders = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase) { [HttpRequestTarget.HeaderName] = new HttpRequestTarget(httpContext.Request.Method, rawTarget).ToString(), [HttpDigest.HTTPHeaderName] = new HttpDigest(signingCredentials.Algorithm, content).ToString(), [HeaderFieldNames.Created] = _systemClock.UtcNow.ToString("r"), [_options.ResponseIdHeaderName] = Guid.NewGuid().ToString() }; var includedHeaders = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); foreach (var name in headerNames) { if (httpContext.Response.Headers.ContainsKey(name)) { if (includedHeaders.ContainsKey(name)) { includedHeaders[name] = httpContext.Response.Headers[name]; } else { includedHeaders.Add(name, httpContext.Response.Headers[name]); } } else if (extraHeaders.ContainsKey(name)) { if (name != HttpRequestTarget.HeaderName) { var responseHeaderName = name == HeaderFieldNames.Created ? _options.ResponseCreatedHeaderName : name; httpContext.Response.Headers.Add(responseHeaderName, extraHeaders[name]); } includedHeaders.Add(name, extraHeaders[name]); Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: Added Header {name}: {includedHeaders[name]}"); } } var signature = new HttpSignature(signingCredentials, includedHeaders, null, null); httpContext.Response.Headers.Add(HttpSignature.HTTPHeaderName, signature.ToString()); Debug.WriteLine($"{nameof(HttpSignatureMiddleware)}: {HttpSignature.HTTPHeaderName} Header: {signature}"); httpContext.Response.Headers.Add(_options.ResponseSignatureCertificateHeaderName, Convert.ToBase64String(validationKey.Certificate.Export(X509ContentType.Cert))); // Go on with life. await responseMemory.CopyToAsync(originalStream); httpContext.Response.Body = originalStream; } } else { await _next.Invoke(httpContext); } }
/// <summary> /// Validate the signature against the requested payload. /// </summary> /// <param name="signature">The signature to validate.</param> /// <param name="key">The public key.</param> /// <param name="headers">The headers.</param> public static bool Validate(this HttpSignature signature, SecurityKey key, IDictionary <string, StringValues> headers) => signature.Validate(key, headers.ToDictionary(x => x.Key, x => (string)x.Value, StringComparer.OrdinalIgnoreCase));
private async Task <HttpResponseMessage> SendAsync(TestServer server, string method, string uri, HttpSignature signature = null) { var client = server.CreateClient(); var request = new HttpRequestMessage { Method = new HttpMethod(method), RequestUri = new Uri(uri) }; if (signature != null) { request.Headers.Add("Authorization", "Signature " + signature.ToString()); } return(await client.SendAsync(request)); }
/// <summary> /// Determines whether the signature is valid for the specified message. /// </summary> /// <param name="signature">The signature to validate.</param> /// <param name="method">The HTTP method of the message.</param> /// <param name="uri">The requested URI of the message.</param> /// <param name="body">The message body.</param> /// <returns>A value indicating the result of the validation.</returns> public Task <SignatureValidationResult> ValidateAsync( HttpSignature signature, string method, string uri, Stream body) => ValidateAsync(signature, method, uri, body, default);