Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(did): implemented VerifyRepsonse and ed25519 crypto #163

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Basis.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasisPrometheus", "Basis Se
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiteNetLib", "Basis Server\LiteNetLib\LiteNetLib.csproj", "{BE757547-04D5-425A-A06C-747E0DBB095F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crypto", "Contrib\Crypto\Crypto.csproj", "{F91684EE-DFD2-491E-B78A-67D7C50C8281}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Contrib\Crypto.Tests\Tests.csproj", "{8B22E796-63A3-4129-935D-F8C5D02FA198}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -66,6 +70,14 @@ Global
{BE757547-04D5-425A-A06C-747E0DBB095F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE757547-04D5-425A-A06C-747E0DBB095F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE757547-04D5-425A-A06C-747E0DBB095F}.Release|Any CPU.Build.0 = Release|Any CPU
{F91684EE-DFD2-491E-B78A-67D7C50C8281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F91684EE-DFD2-491E-B78A-67D7C50C8281}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F91684EE-DFD2-491E-B78A-67D7C50C8281}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F91684EE-DFD2-491E-B78A-67D7C50C8281}.Release|Any CPU.Build.0 = Release|Any CPU
{8B22E796-63A3-4129-935D-F8C5D02FA198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B22E796-63A3-4129-935D-F8C5D02FA198}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B22E796-63A3-4129-935D-F8C5D02FA198}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B22E796-63A3-4129-935D-F8C5D02FA198}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7315D5F3-E752-4246-A876-6FC975262B3B} = {2736B04F-892B-4412-8A6F-90EDD68DFE94}
Expand All @@ -77,5 +89,7 @@ Global
{4266CEAF-4D05-4677-B775-BF2DC94E3477} = {7315D5F3-E752-4246-A876-6FC975262B3B}
{70939FBF-C02E-488D-8C6E-04B04EFEF326} = {153BDCB8-6E62-4BB4-A8FD-DD4B6A8FB0AE}
{BE757547-04D5-425A-A06C-747E0DBB095F} = {153BDCB8-6E62-4BB4-A8FD-DD4B6A8FB0AE}
{F91684EE-DFD2-491E-B78A-67D7C50C8281} = {2736B04F-892B-4412-8A6F-90EDD68DFE94}
{8B22E796-63A3-4129-935D-F8C5D02FA198} = {2736B04F-892B-4412-8A6F-90EDD68DFE94}
EndGlobalSection
EndGlobal
Binary file modified Basis/Packages/org.basisvr.bouncycastle-2.5.0.tgz
Binary file not shown.
2 changes: 1 addition & 1 deletion Contrib/Auth/Did/Base64UrlSafe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Basis.Contrib.Auth.DecentralizedIds
{
/// Base64 url-safe encode and decode.
public class Base64UrlSafe
public sealed class Base64UrlSafe
{
public static string Encode(byte[] bytes)
{
Expand Down
7 changes: 4 additions & 3 deletions Contrib/Auth/Did/Did.asmdef
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
{
"name": "Basis.Contrib.Auth.DecentralizedIds",
"rootNamespace": "",
"references": [],
"references": [
"GUID:b02b12758fb9942cda32715ba18e00b0"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"Base128.dll",
"BouncyCastle.Cryptography.dll",
"SimpleBase.dll",
"Newtonsoft.Json.dll"
],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": true
}
}
2 changes: 1 addition & 1 deletion Contrib/Auth/Did/Did.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<!--Third-party dependencies-->
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.0" />
<ProjectReference Include="..\..\Crypto\Crypto.csproj" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SimpleBase" Version="4.0.2" />
<PackageReference Include="VarInt" Version="1.2.2" />
Expand Down
200 changes: 174 additions & 26 deletions Contrib/Auth/Did/DidAuth.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,87 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Basis.Contrib.Auth.DecentralizedIds.Newtypes;
using Basis.Contrib.Crypto;
using CryptoRng = System.Security.Cryptography.RandomNumberGenerator;
using Empty = System.ValueTuple;

namespace Basis.Contrib.Auth.DecentralizedIds
{
/// Configuration for [`DidAuthentication`].
public record Config
public sealed record Config
{
public CryptoRng Rng { get; init; } = CryptoRng.Create();
public IDictionary<DidMethodKind, IDidMethod> Resolvers { get; init; } =
new Dictionary<DidMethodKind, IDidMethod>()
{
// We will add more did methods in the future, like did:web
{ DidMethodKind.Key, new DidKeyResolver() },
};
}

public readonly struct DidResolveErr : IDidVerifyErr
{
public readonly E e;

public DidResolveErr(E e) => this.e = e;

public static implicit operator DidResolveErr(E e) => new(e);

public enum E
{
/// Another generic error happened during DID document resolution.
Other,

/// Did method is not supported.
UnsupportedMethod,

/// Did had an invalid prefix.
InvalidPrefix,
}
}

public readonly struct DidFragmentErr : IDidVerifyErr
{
public readonly E e;

public DidFragmentErr(E e) => this.e = e;

public static implicit operator DidFragmentErr(E e) => new(e);

public enum E
{
/// No such fragment was present in the DID document.
AmbiguousFragment,

/// The given fragment was ambiguous.
NoSuchFragment,
}
}

public readonly struct DidSignatureErr : IDidVerifyErr
{
public readonly E e;

public DidSignatureErr(E e) => this.e = e;

public static implicit operator DidSignatureErr(E e) => new(e);

public enum E
{
InvalidSignature,
UnsupportedSignatureAlgorithm,
}
}

public interface IDidVerifyErr { }

// TODO(@thebutlah): Create and implement an `IChallengeResponseAuth`
// interface. This interface should live in basis core.
public class DidAuthentication
public sealed class DidAuthentication
{
/// Number of bytes in a nonce. This is currently 256 bits.
// TODO(@thebutlah): Decide if its too performance intensive to use 256
Expand All @@ -24,16 +91,20 @@ public class DidAuthentication
// We store the rng to make deterministic testing and seeding possible.
readonly CryptoRng Rng;

// We support possibly multiple did resolvers.
readonly IDictionary<DidMethodKind, IDidMethod> Resolvers;

public DidAuthentication(Config cfg)
{
Rng = cfg.Rng;
Resolvers = cfg.Resolvers;
}

public Challenge MakeChallenge(Did identity)
{
var nonce = new byte[NONCE_LEN];
Rng.GetBytes(nonce);
return new Challenge(Identity: identity, Nonce: nonce);
return new Challenge(Identity: identity, Nonce: new Nonce(nonce));
}

/// Compares the response against the original challenge.
Expand All @@ -45,12 +116,107 @@ public Challenge MakeChallenge(Did identity)
///
/// It is the caller's responsibility to keep track of which challenges
/// should be held for which responses.
public VerifyResponseResult VerifyResponse(
public async Task<Result<Empty, IDidVerifyErr>> VerifyResponse(
Response response,
Challenge challenge
)
{
throw new NotImplementedException("todo");
var resolveResult = await ResolveDid(challenge.Identity);
if (resolveResult.IsErr)
{
return resolveResult.GetErr;
}
DidDocument document = resolveResult.GetOk;

var pubkeyResult = RetrieveKey(document, response.DidUrlFragment);
if (pubkeyResult.IsErr)
{
return pubkeyResult.GetErr;
}
var pubkey = pubkeyResult.GetOk;

var sigResult = VerifySignature(
pubkey,
challenge.Nonce,
response.Signature
);
if (sigResult.IsErr)
{
return sigResult.GetErr;
}
return new Empty();
}

private Result<Empty, DidSignatureErr> VerifySignature(
JsonWebKey pubkey,
Nonce nonce,
Signature signature
)
{
switch (pubkey.GetAlgorithm())
{
case SigningAlgorithm.Ed25519:
if (
Ed25519.VerifySignature(
pubkey: pubkey.DecodePubkey(),
sig: signature,
payload: new Payload(nonce.V)
)
)
{
return new Empty();
}
else
{
return new DidSignatureErr(DidSignatureErr.E.InvalidSignature);
}
default:
return new DidSignatureErr(
DidSignatureErr.E.UnsupportedSignatureAlgorithm
);
}
}

private static Result<JsonWebKey, DidFragmentErr> RetrieveKey(
DidDocument document,
DidUrlFragment keyId
)
{
if (document.Pubkeys.Count == 1)
{
return document.Pubkeys.First().Value;
}
if (!document.Pubkeys.TryGetValue(keyId, out JsonWebKey pubkey))
{
return new DidFragmentErr(DidFragmentErr.E.NoSuchFragment);
}
return pubkey;
}

private async Task<Result<DidDocument, DidResolveErr>> ResolveDid(Did identity)
{
var segments = identity.V.Split(
separator: ":",
count: 3,
StringSplitOptions.None
);
if (segments.Length != 3 || segments[0] != "did")
{
return new DidResolveErr(DidResolveErr.E.InvalidPrefix);
}
DidMethodKind? methodOrNull = segments[1] switch
{
"key" => DidMethodKind.Key,
_ => null,
};
if (methodOrNull is null)
{
return new DidResolveErr(DidResolveErr.E.UnsupportedMethod);
}
var method = methodOrNull.Value;

var resolver = Resolvers[method];
return await resolver.ResolveDocument(identity);
}
}

Expand All @@ -62,12 +228,11 @@ Challenge challenge
/// Challenges also track the identity of the party that the challenge was
/// sent to, so that later the signature's public key can be compared to
/// the identity's public key.
public record Challenge(Did Identity, byte[] Nonce);
public record Challenge(Did Identity, Nonce Nonce);

public record Response(
/// A JSON Web Signature, in "compact serialization" form. The payload
/// of the JWS are the bytes of the corresponding challenge's nonce.
JwsCompact Jws,
/// The raw bytes of the signature. For ed25519 this is 64 bytes long.
Signature Signature,
/// The particular key in the user's did document. If the empty string,
/// it is implied that there is only one key in the document and that
/// this single key should be what is used as the pub key.
Expand All @@ -78,21 +243,4 @@ public record Response(
/// * `"z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"`
DidUrlFragment DidUrlFragment
);

/// Possible return values VerifyResponse method.
public enum VerifyResponseResult
{
/// The verification was successful
Success,

/// The fragment in the response didn't exist in the DID Document resolved
/// from the challenge's identity.
NoSuchFragment,

/// The JWS Payload did not match the challenge nonce
MismatchedNonce,

/// The JWS verification failed due to an invalid signature
InvalidSig,
}
}
4 changes: 3 additions & 1 deletion Contrib/Auth/Did/DidDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ 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 record DidDocument(ReadOnlyDictionary<DidUrlFragment, JsonWebKey> Pubkeys);
public sealed record DidDocument(
ReadOnlyDictionary<DidUrlFragment, JsonWebKey> Pubkeys
);
}
12 changes: 7 additions & 5 deletions Contrib/Auth/Did/DidKeyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
using Debug = System.Diagnostics.Debug;
using Did = Basis.Contrib.Auth.DecentralizedIds.Newtypes.Did;
using DidUrlFragment = Basis.Contrib.Auth.DecentralizedIds.Newtypes.DidUrlFragment;
using Ed25519 = Org.BouncyCastle.Math.EC.Rfc8032.Ed25519;
using Ed25519 = Basis.Contrib.Crypto.Ed25519;
using StringSplitOptions = System.StringSplitOptions;

namespace Basis.Contrib.Auth.DecentralizedIds
{
/// Implements resolution of a did:key to the various information stored in it
public class DidKeyResolver : IDidMethod
public sealed class DidKeyResolver : IDidMethod
{
public const string PREFIX = "did:key:";

Expand All @@ -24,6 +24,8 @@ public class DidKeyResolver : IDidMethod
/// https://datatracker.ietf.org/doc/html/draft-multiformats-multibase#appendix-D.1
const char BASE58_BTC_MULTIBASE_CODE = 'z';

public DidMethodKind Kind => DidMethodKind.Key;

public Task<DidDocument> ResolveDocument(Did did)
{
// Task is immediately complete, we don't need any io.
Expand Down Expand Up @@ -67,7 +69,7 @@ out int prefixLen
);
}
var pubkeyBytes = multicodecPrefixed[prefixLen..];
if (pubkeyBytes.Length != Ed25519.PublicKeySize)
if (pubkeyBytes.Length != Ed25519.PubkeySize)
{
throw new DidKeyDecodeException(DidKeyDecodeError.WrongPubkeyLen);
}
Expand All @@ -84,7 +86,7 @@ out int prefixLen
/// See
private static JsonWebKey CreateEd25519Jwk(byte[] pubkeyBytes)
{
Debug.Assert(pubkeyBytes.Length == Ed25519.PublicKeySize);
Debug.Assert(pubkeyBytes.Length == Ed25519.PubkeySize);
var key = new JsonWebKey
{
Kty = "OKP",
Expand Down Expand Up @@ -112,7 +114,7 @@ public enum DidKeyDecodeError
WrongPubkeyLen,
}

public class DidKeyDecodeException : System.Exception
public sealed class DidKeyDecodeException : System.Exception
{
public DidKeyDecodeError Error { get; }

Expand Down
Loading
Loading