Due to various business and technical circumstances, I have to create an internet-facing .NET 4.5 WCF web service which should be called only by a certain party while not using native authentication schemes (mainly because I don't have an SSL certificate available, which precludes X.509 and user/password authentication).
What I thought is:
- A shared secret is shared between the server and the client.
- The client gets a cryptographic nonce from the server.
- The client computes an HMAC of the payload, the secret and the nonce.
- The client sends the payload, the nonce and the HMAC to the server.
- The server checks if the nonce and the HMAC are valid.
Assuming the payloads are not sensitive, I have a couple of questions regarding this implementation:
- Is there a native authentication scheme I've overlooked (remember that everything will be plaintext).
- Does this implementation prevent third parties from using the service?
- Does this implementation prevent replay attacks?
- Is there something security-wise I'm missing?
How the shared secret is generated:
var buffer = new byte[64];
using (var random = new RNGCryptoServiceProvider())
{
random.GetBytes(buffer);
}
var secret = Convert.ToBase64String(buffer);
How the server issues and maintains tokens:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
public class TokenManager
{
private readonly List<Token> issuedTokens = new List<Token>();
private readonly RandomNumberGenerator random = new RNGCryptoServiceProvider();
private readonly static Lazy<TokenManager> Instance =
new Lazy<TokenManager>(() => new TokenManager());
private TokenManager()
{
}
public static TokenManager Default => Instance.Value;
public string IssueToken()
{
var now = DateTimeOffset.UtcNow;
var value = this.GenerateTokenValue();
lock (Instance)
{
this.issuedTokens.RemoveAll(a => a.ExpireMoment < now);
this.issuedTokens.Add(new Token(value, now.AddSeconds(30)));
}
return value;
}
public bool IsValidToken(string value)
{
Token issuedToken;
lock (Instance)
{
issuedToken = this.issuedTokens.SingleOrDefault(a => a.Value == value);
this.issuedTokens.Remove(issuedToken);
}
return issuedToken != null && issuedToken.ExpireMoment > DateTimeOffset.UtcNow;
}
private string GenerateTokenValue()
{
var buffer = new byte[64];
this.random.GetBytes(buffer);
return Convert.ToBase64String(buffer);
}
private class Token
{
private readonly string value;
private readonly DateTimeOffset expireMoment;
public Token(string value, DateTimeOffset expireMoment)
{
this.expireMoment = expireMoment;
this.value = value;
}
public DateTimeOffset ExpireMoment => this.expireMoment;
public string Value => this.value;
}
}
How the client and the server compute the payload hash:
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Security.Cryptography;
public static class Hashing
{
public static string ComputeHash<TGraph>(TGraph graph, string secret, string token)
{
using (var stream = new MemoryStream())
{
new DataContractSerializer(typeof(TGraph)).WriteObject(stream, graph);
using (var hasher = new HMACSHA512(Convert.FromBase64String(secret).Concat(Convert.FromBase64String(token)).ToArray()))
{
return Convert.ToBase64String(hasher.ComputeHash(stream.ToArray()));
}
}
}
}
How the web service is structured:
using System;
using System.Collections.Generic;
using System.Configuration;
using System.ServiceModel;
[ServiceContract]
public interface ISomething
{
[OperationContract]
string GetToken();
[OperationContract]
void Do(Payload payload, string token, string hash);
}
public class Something : ISomething
{
public string GetToken()
{
return TokenManager.Default.IssueToken();
}
public void Do(Payload payload, string token, string hash)
{
if (!TokenManager.Default.IsValidToken(token))
{
throw new ArgumentException("token");
}
var computedHash = Hashing.ComputeHash(payload, "secret", token);
if (hash != computedHash)
{
throw new ArgumentException("hash");
}
// DO THINGS
}
}
(The previous snippets are meant to be MCVE, various optimizations — like reading the secret from the configuration — have been omitted.)