Secure Remote Password (SRP) in C# and .NET Core

PAKE

Password Authenticated Key Exchange (PAKE) is one of those odd protocols that sounds like a great idea, but one that no one seems to be using. Even then, it seems no one can agree upon a good implementation. Secure Remote Password (SRP) is the most common implementation, found in use by Apple and 1Password; however, it is far from perfect.

I’m underqualified to explain any of those sweeping statements, so I’m going to leave it to cryptographer Matthew Green, who has two excellent articles on both PAKE and SRP. I highly recommend reading at least the first one before implementing PAKE in your application.

At its core, PAKE is a technology that prevents you from sending passwords across the wire through the use of cryptographic key exchange. The client application still receives the password from the user, but instead of sending the password to the server, it will instead send the result of a cryptographic function that proves that the user supplied the correct password.

Whilst there are better PAKE implementations than SRP out there (for instance, Matthew Green recommends OPAQUE) the availability of PAKE SDKs seems to be patchy across the board. SRP is by far the most popular implementation, however even then choosing a client library is hard due to many being either abandonware or having around 2 stars on GitHub. Even the SRP website’s demo is broken on modern browsers with its listed browser support serving as a time capsule of the pre-Google internet. The best C# implementation I could find was Bouncy Castle. The jury is still out for a JavaScript SRP client. If you know a good one, please let me know in the comments.

In this article, I am going to walk through how to implement SRP manually in C#. A future post will deal with how to use SRP in ASP.NET Core using popular libraries such as ASP.NET Identity and Bouncy Castle.

What PAKE Solves

  • Plaintext passwords being sent across the wire
  • Plaintext passwords being accidentally logged (most recently Facebook & Google)
  • Plaintext being revealed on 3rd party TLS terminators (think Cloudflare or a load balancer)

What PAKE Does Not Solve

  • Passwords being cracked in the event of a breach
  • Cross-Site Scripting (XSS)
  • Any other attack on your authentication system

Implementing Client and Server in C#

For the rest of this article, I’m going to talk through some C# code that implements both the client and server parts of SRP version 6a. In this scenario, the client is who collects the password and the server is who creates a session. In some examples you’ll see client referred to as user, and server as host.

As always, this is in no way production ready code, but created for the purpose of understanding the protocol (much like my Cryptopals solutions, of which SRP is actually featured in Challenge Set 5).

In most web scenarios I imagine we would want the client part in JavaScript. Maybe with Blazor the client could be in C#, but I can’t say I know anything about Blazor.

The demo values I’ll be using throughout this example are the test vectors from Appendix B of RFC 5054. I really enjoy hacking together crypto/specs using known inputs like this, which led to the creation of this article.

Integer Arithmetic

SRP has a heavy use of integer arithmetic, as a result we’ll be using C#’s BigInteger an awful lot. Whilst the test vectors we’ll be using are stored as hex strings, according to RFC 2945 the strings were created with a constraint that the first byte must be non-zero (unless otherwise stated), with the most significant digit first. Because of this I’ll be using the following extension methods throughout the rest of this article:

public static class Helpers
{
    public static byte[] ToBytes(this string hex)
    {
        var hexAsBytes = new byte[hex.Length / 2];

for (var i = 0; i < hex.Length; i += 2) { hexAsBytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); }
return hexAsBytes; }
// both unsigned and big endian public static BigInteger ToSrpBigInt(this byte[] bytes) { return new BigInteger(bytes, true, true); }
// Add padding character back to hex before parsing public static BigInteger ToSrpBigInt(this string hex) { return BigInteger.Parse("0" + hex, NumberStyles.HexNumber); } }

RFC 5054 Test Vectors

The test vectors from RFC 5054 will also need to be available to our code. Most of these are BigIntegers parsed from hex strings, however we also have a hashing function of type Func<byte[], byte[]>.

Show test vectors.

public static class TestVectors
{
    public const string I = "alice"; // I - user's username
    public const string P = "password123"; // P - user's password
    public static readonly byte[] s = "BEB25379D1A8581EB5A727673A2441EE".ToBytes(); // s - user's salt (from server)

private static readonly HashAlgorithm hasher = SHA1.Create(); public static readonly Func<byte[], byte[]> H = i => hasher.ComputeHash(i); // H - hash function
public const int g = 2; // g - generator, modulo N (defined in RFC 5054) public static readonly BigInteger N = // N - a large, safe prime (defined in RFC 5054) "EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8EF4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA9AFD5138FE8376435B9FC61D2FC0EB06E3" .ToSrpBigInt();
public static readonly BigInteger a = "60975527035CF2AD1989806F0407210BC81EDC04E2762A56AFD529DDDA2D4393".ToSrpBigInt(); public static readonly BigInteger b = "E487CB59D31AC550471E81F00F6928E01DDA08E974A004F49E61F5D105284D20".ToSrpBigInt();
public static BigInteger expected_v = "7E273DE8696FFC4F4E337D05B4B375BEB0DDE1569E8FA00A9886D8129BADA1F1822223CA1A605B530E379BA4729FDC59F105B4787E5186F5C671085A1447B52A48CF1970B4FB6F8400BBF4CEBFBB168152E08AB5EA53D15C1AFF87B2B9DA6E04E058AD51CC72BFC9033B564E26480D78E955A5E29E7AB245DB2BE315E2099AFB" .ToSrpBigInt();
public static BigInteger expected_A = "61D5E490F6F1B79547B0704C436F523DD0E560F0C64115BB72557EC44352E8903211C04692272D8B2D1A5358A2CF1B6E0BFCF99F921530EC8E39356179EAE45E42BA92AEACED825171E1E8B9AF6D9C03E1327F44BE087EF06530E69F66615261EEF54073CA11CF5858F0EDFDFE15EFEAB349EF5D76988A3672FAC47B0769447B" .ToSrpBigInt();
public static BigInteger expected_B = "BD0C61512C692C0CB6D041FA01BB152D4916A1E77AF46AE105393011BAF38964DC46A0670DD125B95A981652236F99D9B681CBF87837EC996C6DA04453728610D0C6DDB58B318885D7D82C7F8DEB75CE7BD4FBAA37089E6F9C6059F388838E7A00030B331EB76840910440B1B27AAEAEEB4012B7D7665238A8E3FB004B117B58" .ToSrpBigInt();
public static BigInteger expected_S = "B0DC82BABCF30674AE450C0287745E7990A3381F63B387AAF271A10D233861E359B48220F7C4693C9AE12B0A6F67809F0876E2D013800D6C41BB59B6D5979B5C00A172B4A2A5903A0BDCAF8A709585EB2AFAFA8F3499B200210DCC1F10EB33943CD67FC88A2F39A4BE5BEC4EC0A3212DC346D7E474B29EDE8A469FFECA686E5A" .ToSrpBigInt(); }

Registration

The first step is for the user to register themselves with our application. This is the same as any other site, with the user choosing their username and password, and the server creating a user record.

The SRP process for registration involves the server generating a salt for the user and the client using the salt alongside the user’s credentials to create a password verifier. The server will only be storing the user’s username, salt, and verifier.

The server should generate the salt and return the same salt for a username no matter if that username is associated with an account or not. I’ll go into this in more detail in an upcoming ASP.NET Core implementation example.

To generate the verifier, we first must calculate a private key, x, from the user’s username (I), password (P), and salt (s), where x is:

x = H(s | H(I | ":" | P))

H is a hashing function of our choice. The test data from RFC 5054 that I’m using in this example used SHA1. For modern solutions we would want to use a more appropriate password hashing algorithm such as scrypt or Argon2. The hash function is pre-agreed by the client and server.

The use of the user’s username here was a point of confusion for me. It’s present in both RFC 2945 and 5054, but not in the SRP design document, which defines x as H(s, P). I guess that as long as the private key, x, is always the same value for a user, then the client can create it however it wants assuming information from both the client and server are used.

We can then use x to compute the password verifier, v, where v is:

v = g^x

g is read from the server and is defined as “a generator of modulo N”. If that doesn’t mean anything to you, don’t worry g and N are constants defined by the protocol (Appendix A of RFC 5054). The test vectors we are using use the 1024-bit Group where g is 2, and N is a defined 128-byte prime number. Which values to use for g and N are pre-agreed by the client and server.

Putting this all together, and utilising BigInteger’s ModPow method, we can put together a class and method like this:

public class SrpClient
{
    private readonly Func<byte[], byte[]> H;
    private readonly int g;
    private readonly BigInteger N;

public SrpClient(Func<byte[], byte[]> H, int g, BigInteger N) { this.H = H; this.g = g; this.N = N; }
public BigInteger GenerateVerifier(string I, string P, byte[] s) { // x = H(s | H(I | ":" | P)) var x = GeneratePrivateKey(I, P, s); // v = g^x var v = BigInteger.ModPow(g, x, N); return v; }
private BigInteger GeneratePrivateKey(string I, string P, byte[] s) { // x = H(s | H(I | ":" | P)) var x = H(s.Concat(H(Encoding.UTF8.GetBytes(I + ":" + P))).ToArray()); return x.ToSrpBigInt(); } }

The generated verifier would then be sent to the server to be stored alongside the user’s username, and salt. The verifier will never leave the server and in effect, replaces a hashed password.

Login

Now that the user has registered, they can login using their username and password.

Once the client application knows the user’s username, it needs to find out the salt associated with that username from the server. This would typically be done with an HTTP request, however for now, let’s continue to work with the test vector salt that we have from registration.

Other than the server receiving the username, I, and the client receiving salt, s, the two also need to generate two secret ephemeral values, a & b, and two public ephemeral values, A & B. a & b are random values, whilst A & B are defined as:

A = g^a
B = kv + g^b

And k is defined as:

k = H(N, g)

In this example, the test vectors already define the a and b values to use, but otherwise these would be generated manually. The Bouncy Castle library has a utility method in SRP6Utilities.GeneratePrivateValue, that is worth checking out.

So, in the client, let’s generate a & A:

private BigInteger A;
private BigInteger a;

public BigInteger GenerateAValues() { // a = random() a = TestVectors.a;
// A = g^a A = BigInteger.ModPow(g, a, N);
return A; }

To generate k we can create another helper method. This will be used by both the server and the client.

public static BigInteger Computek(int g, BigInteger N ,Func<byte[], byte[]> H)
{
    // k = H(N, g)
    var NBytes = N.ToByteArray(true, true);
    var gBytes = PadBytes(BitConverter.GetBytes(g).Reverse().ToArray(), NBytes.Length);

var k = H(NBytes.Concat(gBytes).ToArray());
return new BigInteger(k, isBigEndian: true); }
public static byte[] PadBytes(byte[] bytes, int length) { var paddedBytes = new byte[length]; Array.Copy(bytes, 0, paddedBytes, length - bytes.Length, bytes.Length);
return paddedBytes; }

And now a server class that can generate b & B:

public class SrpServer
{
    private readonly Func<byte[], byte[]> H;
    private readonly int g;
    private readonly BigInteger N;

private BigInteger B; private BigInteger b;
public SrpServer(Func<byte[], byte[]> H, int g, BigInteger N) { this.H = H; this.g = g; this.N = N; }
public BigInteger GenerateBValues(BigInteger v) { // b = random() b = TestVectors.b;
var k = Helpers.Computek(g, N, H);
// kv % N var left = (k * v) % N;
// g^b % N var right = BigInteger.ModPow(g, b, N);
// B = kv + g^b B = (left + right) % N;
return B; } }

With our two public keys generated, the server and client then exchange them with on another. So, the values the two parties should have are:

  • Client: I, P, s, a, A, B
  • Server: I, b, A, B

Now that both parties have everything they need, they must both generate a “random scrambling parameter”, u, using A & B. We can do this with another helper method (which assumes A and B are the same length):

public static BigInteger Computeu(Func<byte[], byte[]> H, BigInteger A, BigInteger B) 
{
    return H(A.ToByteArray(true, true)
            .Concat(B.ToByteArray(true, true))
            .ToArray())
            .ToSrpBigInt();
}

And now with all of the above, both parties should be able to compute the session key, S. In RFC 5054, you’ll see this session key referred to as the premaster secret.

Client Session Key

The client computes the session key, S, as the following:

S = (B - kg^x) ^ (a + ux)

To do this in C#, the following method can be added to our client:

public BigInteger ComputeSessionKey(string I, string P, byte[] s, BigInteger B)
{
    var u = Helpers.Computeu(H, A, B);
    var x = GeneratePrivateKey(I, P, s);
    var k = Helpers.Computek(g, N, H);

    // (a + ux)
    var exp = a + u * x;

    // (B - kg ^ x)
    var val = mod(B - (BigInteger.ModPow(g, x, N) * k % N), N);

    // S = (B - kg ^ x) ^ (a + ux)
    return BigInteger.ModPow(val, exp, N);
}

Server Session Key

The server computes the session key, S, as the following:

S = (Av^u) ^ b

To do this in C#, the following method can be added to our server:

public BigInteger ComputeSessionKey(BigInteger v, BigInteger A)
{
    var u = Helpers.Computeu(H, A, B);

    // (Av^u)
    var left = A * BigInteger.ModPow(v, u, N) % N;
    
    // S = (Av^u) ^ b
    return BigInteger.ModPow(left, b, N);
}

Completing Authentication

Now that both parties have generated the same session key, they need to prove to one another that their keys match. The SRP design document and RFC 2945 suggest the following process:

K = H(S)
Client -> Server:  M = H(H(N) XOR H(g) | H(I) | s | A | B | K)
Server -> Client:  H(A | M | K)

Here, the client generates their proof, M, and sends it to the server. The server should then generate the same proof and compare. If everything checks out, the server can send its own proof back to the client.

The client must show their proof first. If the server cannot validate the proof, then they must fail validation and not show their own proof.

However, the bouncy castle implementation uses:

Client -> Server: M1 = H(A | B | S)
Server -> Client: M2 = H(A | M1 | S)

From what I can tell, these are the evidence steps found in the original 1997 specification. Based on other implementations I found on GitHub, this approach seems to be the most common. So, this is what we will implement!

To do this, I used the following helper methods and verified their output against the Bouncy Castle implementation, since unfortunately the RFC test vectors do not cover the proof steps. You can call these helpers from client and server methods such as GenerateClientProof in the client, and ValidateClientProof in the server. Corresponding methods should also be added for the servers proof.

public static BigInteger ComputeClientProof(
    BigInteger N,
    Func<byte[], byte[]> H,
    BigInteger A,
    BigInteger B,
    BigInteger S)
{
    var padLength = N.ToByteArray(true, true).Length;

    // M1 = H( A | B | S )
    return H((PadBytes(A.ToByteArray(true, true), padLength))
            .Concat(PadBytes(B.ToByteArray(true, true), padLength))
            .Concat(PadBytes(S.ToByteArray(true, true), padLength))
            .ToArray())
        .ToSrpBigInt();
}

public static BigInteger ComputeServerProof(BigInteger N, Func<byte[], byte[]> H, BigInteger A, BigInteger M1, BigInteger S)
{
    var padLength = N.ToByteArray(true, true).Length;

    // M2 = H( A | M1 | S )
    return H((PadBytes(A.ToByteArray(true, true), padLength))
            .Concat(PadBytes(M1.ToByteArray(true, true), padLength))
            .Concat(PadBytes(S.ToByteArray(true, true), padLength))
            .ToArray())
        .ToSrpBigInt();
}

SRP is an odd protocol and I feel it doesn’t give much explanation about what its doing or why - but it works.

Further Reading

Do check out those two articles by Matthew Green.

Source Code and Test Runner

You can find the full source from this article on GitHub. It’s a console app that you should be able to run and see working.