Skip to content

Commit

Permalink
Merge pull request #169 from TheButlah/thebutlah/implement-sig-signing
Browse files Browse the repository at this point in the history
feat(Crypto,Did): implement rest of did:key
  • Loading branch information
dooly123 authored Mar 8, 2025
2 parents 35580f2 + 7347b52 commit 4124a05
Show file tree
Hide file tree
Showing 25 changed files with 463 additions and 61 deletions.
2 changes: 2 additions & 0 deletions Basis/Packages/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"com.unity.profiling.core": "1.0.2",
"com.unity.render-pipelines.universal": "17.0.4",
"com.unity.timeline": "1.8.7",
"com.unity.toolchain.macos-arm64-linux-x86_64": "2.0.4",
"com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10",
"com.unity.visualeffectgraph": "17.0.4",
"com.unity.xr.management": "4.5.0",
Expand All @@ -21,6 +22,7 @@
"org.basisvr.bouncycastle": "file:org.basisvr.bouncycastle-2.5.0.tgz",
"org.basisvr.contrib.auth.did": "file:../../Contrib/Auth/Did",
"org.basisvr.contrib.crypto": "file:../../Contrib/Crypto",
"org.basisvr.generator.equals": "file:org.basisvr.generator.equals-3.2.0.tgz",
"org.basisvr.newtonsoft.json": "file:org.basisvr.newtonsoft.json-13.0.3.tgz",
"org.basisvr.simplebase": "file:org.basisvr.simplebase-4.0.2.tgz",
"com.unity.modules.accessibility": "1.0.0",
Expand Down
Binary file not shown.
21 changes: 20 additions & 1 deletion Basis/Packages/packages-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,16 @@
},
"url": "https://packages.unity.com"
},
"com.unity.toolchain.macos-arm64-linux-x86_64": {
"version": "2.0.4",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.sysroot": "2.0.10",
"com.unity.sysroot.linux-x86_64": "2.0.9"
},
"url": "https://packages.unity.com"
},
"com.unity.toolchain.win-x86_64-linux-x86_64": {
"version": "2.0.10",
"depth": 0,
Expand Down Expand Up @@ -484,6 +494,8 @@
"source": "local",
"dependencies": {
"org.basisvr.base128": "1.2.2",
"org.basisvr.contrib.crypto": "0.0.0",
"org.basisvr.generator.equals": "3.2.0",
"org.basisvr.newtonsoft.json": "13.0.3",
"org.basisvr.simplebase": "4.0.2"
}
Expand All @@ -493,9 +505,16 @@
"depth": 0,
"source": "local",
"dependencies": {
"org.basisvr.bouncycastle": "2.5.0"
"org.basisvr.bouncycastle": "2.5.0",
"org.basisvr.generator.equals": "3.2.0"
}
},
"org.basisvr.generator.equals": {
"version": "file:org.basisvr.generator.equals-3.2.0.tgz",
"depth": 0,
"source": "local-tarball",
"dependencies": {}
},
"org.basisvr.newtonsoft.json": {
"version": "file:org.basisvr.newtonsoft.json-13.0.3.tgz",
"depth": 0,
Expand Down
34 changes: 29 additions & 5 deletions Contrib/Auth/Did.Tests/DidKeyTests.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Threading.Tasks;
using Basis.Contrib.Auth.DecentralizedIds.Newtypes;
using Basis.Contrib.Crypto;
using Xunit;

namespace Basis.Contrib.Auth.DecentralizedIds
{
public class DidKeyTests
{
[Fact]
public async Task DidKeyTestVectors()
// See https://w3c-ccg.github.io/did-method-key/#ed25519-x25519
static List<(string, JsonWebKey)> TestVectors()
{
// See https://w3c-ccg.github.io/did-method-key/#ed25519-x25519
var examples = new List<(string, JsonWebKey)>
return new List<(string, JsonWebKey)>
{
(
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
Expand All @@ -26,9 +28,13 @@ public async Task DidKeyTestVectors()
}
),
};
}

[Fact]
public async Task DidKeyTestVectors()
{
var resolver = new DidKeyResolver();
foreach (var (inputDid, expectedJwk) in examples)
foreach (var (inputDid, expectedJwk) in TestVectors())
{
var expectedFragment = new DidUrlFragment(
inputDid.Split(
Expand All @@ -47,5 +53,23 @@ public async Task DidKeyTestVectors()
);
}
}

[Fact]
public void DidKeyTestEncode()
{
foreach (var (expectedDid, jwkInput) in TestVectors())
{
var pubkeyBytes = Base64UrlSafe.Decode(
jwkInput.X ?? throw new Exception("the examples are not null")
);
var encodedDid = DidKeyResolver.EncodePubkeyAsDid(
new PubKey(pubkeyBytes)
);
Debug.Assert(
expectedDid.Equals(encodedDid.V),
$"encoded was {encodedDid}, expected {expectedDid}"
);
}
}
}
}
158 changes: 158 additions & 0 deletions Contrib/Auth/Did.Tests/ServerExample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Basis.Contrib.Auth.DecentralizedIds;
using Basis.Contrib.Auth.DecentralizedIds.Newtypes;
using Basis.Contrib.Crypto;
using Xunit;
using CryptoRng = System.Security.Cryptography.RandomNumberGenerator;

namespace Basis.Contrib.Auth.DecentralizedIds
{
class ConnectionState
{
readonly Server Server;
Did? Did;
Challenge? Challenge = null;

public ConnectionState(Server server)
{
Server = server;
}

public Did? Player => Did;

/// Returns false if connection should be terminated
public bool RecvDid(Did playerDid)
{
Did = playerDid;
return !Server.BannedDids.Contains(playerDid);
}

public Challenge SendChallenge()
{
Challenge = Server.DidAuth.MakeChallenge(
Did ?? throw new Exception("call RecvDid first")
);
return Challenge;
}

/// Returns false if connection should be terminated
public async Task<bool> RecvChallengeResponse(Response response)
{
if (!response.DidUrlFragment.V.Equals(string.Empty))
{
throw new Exception("multiple pubkeys not yet supported");
}
var challenge =
Challenge ?? throw new Exception("call SendChallenge first");
var result = await Server.DidAuth.VerifyResponse(response, challenge);
return result.IsOk;
}
}

class Server
{
internal readonly DidAuthentication DidAuth;
internal readonly HashSet<Did> BannedDids = new();
internal readonly HashSet<IPAddress> BannedIps = new();
readonly Dictionary<IPAddress, ConnectionState> Connections = new();

public Server(DidAuthentication didAuth)
{
DidAuth = didAuth;
}

public void Ban(IPAddress playerIp)
{
BannedIps.Add(playerIp);
if (!Connections.TryGetValue(playerIp, out ConnectionState? conn))
{
// No such connection
return;
}
Connections.Remove(playerIp);
var connDid = conn.Player;
if (connDid is not null)
{
BannedDids.Add(connDid);
}
}

/// Returns a challenge that is sent to player, or else null if player is banned.
public ConnectionState OnConnection(IPAddress remoteAddr)
{
var connectionState = new ConnectionState(this);
Connections.Add(remoteAddr, connectionState);
return connectionState;
}
}

public class ServerExample
{
static (PubKey, PrivKey) RandomKeyPair(CryptoRng rng)
{
var privKeyBytes = new byte[Ed25519.PrivkeySize];
rng.GetBytes(privKeyBytes);
var privKey = new PrivKey(privKeyBytes);
var pubKey =
Ed25519.ConvertPrivkeyToPubkey(privKey)
?? throw new Exception("privkey was invalid");
return (pubKey, privKey);
}

[Fact]
static async Task Main()

Check warning on line 111 in Contrib/Auth/Did.Tests/ServerExample.cs

View workflow job for this annotation

GitHub Actions / Run `dotnet test`

Method 'ServerExample.Main()' will not be used as an entry point because a synchronous entry point 'AutoGeneratedProgram.Main(string[])' was found.

Check warning on line 111 in Contrib/Auth/Did.Tests/ServerExample.cs

View workflow job for this annotation

GitHub Actions / Run `dotnet build`

Method 'ServerExample.Main()' will not be used as an entry point because a synchronous entry point 'AutoGeneratedProgram.Main(string[])' was found.
{
// Client
var rng = CryptoRng.Create();
var (pubKey, privKey) = RandomKeyPair(rng);
var playerDid = DidKeyResolver.EncodePubkeyAsDid(pubKey);
var playerIp = IPAddress.Loopback;

// Server
var cfg = new Config { Rng = rng };
Server server = new(didAuth: new DidAuthentication(cfg));
ConnectionState conn = server.OnConnection(playerIp);
Debug.Assert(conn.RecvDid(playerDid));
Challenge challenge = conn.SendChallenge();

// Client
var payloadToSign = new Payload(challenge.Nonce.V);
Debug.Assert(
Ed25519.Sign(privKey, payloadToSign, out Signature? sig),
"signing with a valid privkey should always succeed"
);
Debug.Assert(
sig is not null,
"signing with a valid privkey should always succeed"
);
Debug.Assert(
Ed25519.Verify(pubKey, sig, payloadToSign),
"sanity check: verifying sig"
);
// for simplicity, use an empty fragment since the client only has one pubkey
var response = new Response(sig, new DidUrlFragment(string.Empty));

// Server
var isAuthenticated = await conn.RecvChallengeResponse(response);
Debug.Assert(isAuthenticated, "the response should have been valid");

// Next we ban the player
server.Ban(playerIp);

// Client tries to connect again, but from a different IP
var bannedConn = server.OnConnection(
new IPAddress(new byte[] { 192, 168, 1, 1 })
);
// Connection terminated when DID matches ban list
Debug.Assert(!bannedConn.RecvDid(playerDid));
}
}
}
1 change: 1 addition & 0 deletions Contrib/Auth/Did.Tests/Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<!--First-party dependencies-->
<ItemGroup>
<ProjectReference Include="..\Did\Did.csproj" />
<ProjectReference Include="..\..\Crypto\Crypto.csproj" />
</ItemGroup>

</Project>
3 changes: 2 additions & 1 deletion Contrib/Auth/Did/Did.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"precompiledReferences": [
"Base128.dll",
"SimpleBase.dll",
"Newtonsoft.Json.dll"
"Newtonsoft.Json.dll",
"Generator.Equals.Runtime.dll"
],
"autoReferenced": true,
"defineConstraints": [],
Expand Down
2 changes: 1 addition & 1 deletion Contrib/Auth/Did/DidAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Signature signature
{
case SigningAlgorithm.Ed25519:
if (
Ed25519.VerifySignature(
Ed25519.Verify(
pubkey: pubkey.DecodePubkey(),
sig: signature,
payload: new Payload(nonce.V)
Expand Down
7 changes: 5 additions & 2 deletions Contrib/Auth/Did/DidDocument.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#nullable enable

using System.Collections.ObjectModel;
using Generator.Equals;
using DidUrlFragment = Basis.Contrib.Auth.DecentralizedIds.Newtypes.DidUrlFragment;

namespace Basis.Contrib.Auth.DecentralizedIds
{
/// Contains the info that we care about in the DID Document.
/// A DID Document is what a DID is resolved into. See
/// https://www.w3.org/TR/did-core/#did-resolution
public sealed record DidDocument(
ReadOnlyDictionary<DidUrlFragment, JsonWebKey> Pubkeys
[Equatable]
public sealed partial record DidDocument(
[property: UnorderedEquality]
ReadOnlyDictionary<DidUrlFragment, JsonWebKey> Pubkeys
);
}
29 changes: 28 additions & 1 deletion Contrib/Auth/Did/DidKeyResolver.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Basis.Contrib.Crypto;
using Base128 = WojciechMikołajewicz.Base128;
using Base58 = SimpleBase.Base58;
using Debug = System.Diagnostics.Debug;
Expand All @@ -19,7 +23,7 @@ public sealed class DidKeyResolver : IDidMethod
public const string PREFIX = "did:key:";

/// https://github.com/multiformats/multicodec/blob/master/table.csv#L98
const ushort ED25519_MULTIFORMAT_CODE = 0xED;
const byte ED25519_MULTIFORMAT_CODE = 0xED;

/// https://datatracker.ietf.org/doc/html/draft-multiformats-multibase#appendix-D.1
const char BASE58_BTC_MULTIBASE_CODE = 'z';
Expand Down Expand Up @@ -83,6 +87,29 @@ out int prefixLen
);
}

public static Did EncodePubkeyAsDid(PubKey pubKey)
{
var nBytesForMultiformat = Base128.GetRequiredBytesUInt32(
ED25519_MULTIFORMAT_CODE
);
byte[] withMultiformatCode = new byte[
pubKey.V.Length + nBytesForMultiformat
];
Base128.WriteUInt32(
withMultiformatCode.AsSpan()[..nBytesForMultiformat],
ED25519_MULTIFORMAT_CODE,
out int _
);
pubKey.V.CopyTo(withMultiformatCode.AsSpan()[nBytesForMultiformat..]);

var base58Encoded = Base58.Bitcoin.Encode(withMultiformatCode);

var s = new StringBuilder(PREFIX);
s.Append(BASE58_BTC_MULTIBASE_CODE);
s.Append(base58Encoded);
return new Did(s.ToString());
}

/// See
private static JsonWebKey CreateEd25519Jwk(byte[] pubkeyBytes)
{
Expand Down
4 changes: 4 additions & 0 deletions Contrib/Auth/Did/IDidMethod.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Basis.Contrib.Auth.DecentralizedIds.Newtypes;
using Basis.Contrib.Crypto;
using Did = Basis.Contrib.Auth.DecentralizedIds.Newtypes.Did;

namespace Basis.Contrib.Auth.DecentralizedIds
Expand Down
Loading

0 comments on commit 4124a05

Please sign in to comment.