Skip to content

Commit 7ef1547

Browse files
authored
Merge pull request #7 from KeetaNetwork/feat/add-missing-kyc-endpoints
Feature: Sharable References
2 parents 4629093 + fb49e8b commit 7ef1547

28 files changed

Lines changed: 1131 additions & 122 deletions

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,25 +60,25 @@ builder.Services.AddKeetaNetAnchor();
6060
// Any service: inject the singleton and create what you need per use.
6161
public sealed class Onboarding(WasmRuntime runtime)
6262
{
63-
public string NewSignerAddress()
63+
public string NewSignerPublicKeyString()
6464
{
6565
string seed = runtime.Accounts.GenerateRandomSeed();
6666
using Account signer = runtime.Accounts.FromSeed(seed, index: 0, algorithm: "ed25519");
67-
return signer.Address;
67+
return signer.PublicKeyString;
6868
}
6969
}
7070
```
7171

7272
### Accounts
7373

74-
An `Account` is a signer derived from a seed, private key, or BIP39 passphrase, or a read-only account built from an address or public key. Key material never leaves the wasm core. Supported algorithms: `ed25519`, `ecdsa_secp256k1`, `ecdsa_secp256r1`.
74+
An `Account` is a signer derived from a seed, private key, or BIP39 passphrase, or a read-only account built from a public-key string or raw public key. Key material never leaves the wasm core. Supported algorithms: `ed25519`, `ecdsa_secp256k1`, `ecdsa_secp256r1`.
7575

7676
```csharp
7777
// Derive a signer from a fresh seed.
7878
string seed = runtime.Accounts.GenerateRandomSeed();
7979
using Account signer = runtime.Accounts.FromSeed(seed, index: 0, algorithm: "ed25519");
8080

81-
Console.WriteLine(signer.Address); // keeta_...
81+
Console.WriteLine(signer.PublicKeyString); // keeta_...
8282
Console.WriteLine(signer.PublicKeyAndType); // type-prefixed hex
8383
8484
// A watch-only view of the same identity, from the transport key.
@@ -94,7 +94,7 @@ byte[] ciphertext = signer.Encrypt("for your eyes"u8.ToArray());
9494
byte[] plaintext = signer.Decrypt(ciphertext);
9595

9696
// A read-only account verifies and encrypts but cannot sign or decrypt.
97-
using Account watcher = runtime.Accounts.FromAccount(signer.Address);
97+
using Account watcher = runtime.Accounts.FromPublicKeyString(signer.PublicKeyString);
9898
```
9999

100100
### KYC Verification
@@ -163,7 +163,7 @@ A holder can attest to one sensitive attribute without revealing the private key
163163
AttributeProof proof = leaf.GetProof("email", subject);
164164

165165
// Verifier: a read-only subject account suffices.
166-
using Account subjectPublic = runtime.Accounts.FromAccount(subjectAddress);
166+
using Account subjectPublic = runtime.Accounts.FromPublicKeyString(subjectPublicKeyString);
167167
bool attested = leaf.ValidateProof("email", subjectPublic, proof);
168168
```
169169

scripts/pins.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
# The wasm core (scripts/build-wasm.sh).
55
ANCHOR_WASI_CRATE="keetanetwork-anchor-client-wasi"
6-
ANCHOR_WASI_VERSION="0.2.0"
7-
ANCHOR_WASI_SHA256="19c51c46f5efa58bcf4a37e324090bb91644ede5fcb0ae916983214e9092ff42"
6+
ANCHOR_WASI_VERSION="0.2.1"
7+
ANCHOR_WASI_SHA256="1eeffcc3bff2fa78caf8df7b3774a7bb183c18e52ccaabc969d7c1bcee6e3380"
88

99
# The canonical node OpenAPI spec (scripts/generate-node-api.sh).
1010
NODE_CLIENT_CRATE="keetanetwork-client"

src/KeetaNet.Anchor.Extensions.DependencyInjection/KeetaNetAnchorServiceCollectionExtensions.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@ public static class KeetaNetAnchorServiceCollectionExtensions
1010
{
1111
/// <summary>
1212
/// Register the shared <see cref="WasmRuntime"/> as a singleton, loading the
13-
/// embedded wasm core on first resolve. The container disposes it on
14-
/// shutdown. The runtime is thread-safe.
13+
/// embedded wasm core on first resolve, along with its factory surfaces so
14+
/// consumers can inject a factory directly instead of the runtime. The
15+
/// container disposes the runtime on shutdown. The runtime is thread-safe.
1516
/// </summary>
1617
/// <returns>The same collection, for chaining.</returns>
1718
public static IServiceCollection AddKeetaNetAnchor(this IServiceCollection services)
1819
{
1920
services.TryAddSingleton(static _ => WasmRuntime.Load());
21+
services.TryAddSingleton(static provider => provider.GetRequiredService<WasmRuntime>().Accounts);
22+
services.TryAddSingleton(static provider => provider.GetRequiredService<WasmRuntime>().Certificates);
23+
services.TryAddSingleton(static provider => provider.GetRequiredService<WasmRuntime>().KycCertificates);
24+
services.TryAddSingleton(static provider => provider.GetRequiredService<WasmRuntime>().Containers);
25+
services.TryAddSingleton(static provider => provider.GetRequiredService<WasmRuntime>().Sharables);
26+
2027
return services;
2128
}
2229

src/KeetaNet.Anchor/Crypto/Account.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace KeetaNet.Anchor.Crypto;
22

33
/// <summary>
44
/// A Keeta account: a signer derived from a seed, or a read-only account parsed
5-
/// from an address. The key material lives inside the wasm core.
5+
/// from a public-key string. The key material lives inside the wasm core.
66
/// </summary>
77
public sealed class Account : WasmObject
88
{
@@ -11,8 +11,8 @@ internal Account(WasmRuntime runtime, int handle)
1111
{
1212
}
1313

14-
/// <summary>The account's textual <c>keeta_...</c> address.</summary>
15-
public string Address => Runtime.AccountAddress(Handle);
14+
/// <summary>The account's textual <c>keeta_...</c> public-key string.</summary>
15+
public string PublicKeyString => Runtime.AccountAddress(Handle);
1616

1717
/// <summary>The account's algorithm name.</summary>
1818
public string Algorithm => Runtime.AccountAlgorithm(Handle);

src/KeetaNet.Anchor/Crypto/AccountFactory.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ public Account FromSeed(string seed, uint index, string algorithm)
1919
return new(_runtime, handle);
2020
}
2121

22-
/// <summary>Build a read-only account from its textual <c>keeta_...</c> account string.</summary>
23-
public Account FromAccount(string account)
22+
/// <summary>Build a read-only account from its textual <c>keeta_...</c> public-key string.</summary>
23+
public Account FromPublicKeyString(string publicKeyString)
2424
{
25-
int handle = _runtime.AccountFromAddress(account);
25+
int handle = _runtime.AccountFromAddress(publicKeyString);
2626
return new(_runtime, handle);
2727
}
2828

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
using System.Net;
2+
using System.Text;
3+
using System.Text.Json;
4+
5+
namespace KeetaNet.Anchor.Crypto;
6+
7+
/// <summary>
8+
/// Networked convenience for sharable-attribute external references. The core
9+
/// ingests external blobs but never performs I/O. This fetches each discovered
10+
/// reference's stored bytes, decoding <c>data:</c> URLs inline.
11+
/// </summary>
12+
public static class ExternalReferences
13+
{
14+
/// <summary>
15+
/// Fetch every reference's blob, keyed by the reference's digest id: a
16+
/// <c>data:</c> URL decodes inline, an http(s) URL is fetched with
17+
/// <paramref name="httpClient"/>, and a JSON response in the
18+
/// storage-service <c>{data, mimeType}</c> convention is unwrapped.
19+
/// </summary>
20+
/// <exception cref="KeetaException">
21+
/// Code <c>REFERENCE_FETCH</c> when a URL cannot be decoded or the server
22+
/// does not answer 200.
23+
/// </exception>
24+
public static async Task<IReadOnlyDictionary<string, byte[]>> FetchBlobs(
25+
HttpClient httpClient,
26+
IEnumerable<AttributeReference> references,
27+
CancellationToken cancellationToken = default)
28+
{
29+
var blobs = new Dictionary<string, byte[]>();
30+
foreach (AttributeReference reference in references)
31+
{
32+
blobs[reference.Id] = await FetchReference(httpClient, reference, cancellationToken).ConfigureAwait(false);
33+
}
34+
35+
return blobs;
36+
}
37+
38+
/// <summary>Fetch one reference's raw stored bytes from its URL.</summary>
39+
private static async Task<byte[]> FetchReference(
40+
HttpClient httpClient,
41+
AttributeReference reference,
42+
CancellationToken cancellationToken)
43+
{
44+
string url = reference.Url;
45+
if (url.StartsWith("data:", StringComparison.Ordinal))
46+
{
47+
return DecodeDataUrl(url);
48+
}
49+
50+
using HttpResponseMessage response = await Get(httpClient, url, cancellationToken).ConfigureAwait(false);
51+
if (response.StatusCode != HttpStatusCode.OK)
52+
{
53+
throw new KeetaException(
54+
"REFERENCE_FETCH",
55+
$"the server answered {(int)response.StatusCode} for `{url}`");
56+
}
57+
58+
byte[] body = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
59+
return UnwrapContainerPayload(url, body);
60+
}
61+
62+
/// <summary>Issue the GET, surfacing a transport failure as the stable typed error.</summary>
63+
private static async Task<HttpResponseMessage> Get(
64+
HttpClient httpClient,
65+
string url,
66+
CancellationToken cancellationToken)
67+
{
68+
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri)
69+
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
70+
{
71+
throw new KeetaException("REFERENCE_FETCH", $"the reference URL `{url}` is not a fetchable http(s) URL");
72+
}
73+
74+
try
75+
{
76+
return await httpClient.GetAsync(uri, cancellationToken).ConfigureAwait(false);
77+
}
78+
catch (HttpRequestException error)
79+
{
80+
throw new KeetaException("REFERENCE_FETCH", $"the request to `{url}` failed", error);
81+
}
82+
}
83+
84+
/// <summary>Decode a <c>data:&lt;media type&gt;[;base64],&lt;data&gt;</c> URL body.</summary>
85+
private static byte[] DecodeDataUrl(string url)
86+
{
87+
string rest = url["data:".Length..];
88+
int separator = rest.IndexOf(',', StringComparison.Ordinal);
89+
if (separator < 0)
90+
{
91+
throw new KeetaException("REFERENCE_FETCH", $"the data URL `{url}` has no payload");
92+
}
93+
94+
string header = rest[..separator];
95+
string data = rest[(separator + 1)..];
96+
if (!header.EndsWith(";base64", StringComparison.Ordinal))
97+
{
98+
return Encoding.UTF8.GetBytes(data);
99+
}
100+
101+
return DecodeBase64(data, $"the data URL `{url}`");
102+
}
103+
104+
/// <summary>
105+
/// Unwrap the storage-service container-payload convention: a JSON body of
106+
/// exactly <c>{data, mimeType}</c> (both strings) carries the base64 stored
107+
/// bytes. Anything else is the stored bytes themselves.
108+
/// </summary>
109+
/// <exception cref="KeetaException">
110+
/// Code <c>REFERENCE_FETCH</c> when the wrapper shape is detected but its
111+
/// base64 payload does not decode, so a corrupted wrapper surfaces here
112+
/// instead of as a later decryption failure.
113+
/// </exception>
114+
private static byte[] UnwrapContainerPayload(string url, byte[] body)
115+
{
116+
string? data = WrappedPayload(body);
117+
if (data is null)
118+
{
119+
return body;
120+
}
121+
122+
return DecodeBase64(data, $"the wrapped payload from `{url}`");
123+
}
124+
125+
/// <summary>Decode base64 <paramref name="data"/>, naming <paramref name="source"/> on failure.</summary>
126+
private static byte[] DecodeBase64(string data, string source)
127+
{
128+
try
129+
{
130+
return Convert.FromBase64String(data);
131+
}
132+
catch (FormatException error)
133+
{
134+
throw new KeetaException("REFERENCE_FETCH", $"{source} is not valid base64", error);
135+
}
136+
}
137+
138+
/// <summary>
139+
/// The base64 payload of an exact <c>{data, mimeType}</c> wrapper, or
140+
/// <c>null</c> when the body is anything else.
141+
/// </summary>
142+
private static string? WrappedPayload(byte[] body)
143+
{
144+
try
145+
{
146+
using JsonDocument parsed = JsonDocument.Parse(body);
147+
JsonElement root = parsed.RootElement;
148+
if (root.ValueKind != JsonValueKind.Object)
149+
{
150+
return null;
151+
}
152+
153+
string? data = null;
154+
bool hasMimeType = false;
155+
int properties = 0;
156+
using JsonElement.ObjectEnumerator enumerated = root.EnumerateObject();
157+
foreach (JsonProperty property in enumerated)
158+
{
159+
properties++;
160+
if (property.Name == "data" && property.Value.ValueKind == JsonValueKind.String)
161+
{
162+
data = property.Value.GetString();
163+
}
164+
else if (property.Name == "mimeType" && property.Value.ValueKind == JsonValueKind.String)
165+
{
166+
hasMimeType = true;
167+
}
168+
}
169+
170+
if (properties != 2 || !hasMimeType)
171+
{
172+
return null;
173+
}
174+
175+
return data;
176+
}
177+
catch (JsonException)
178+
{
179+
// A non-JSON body is the stored bytes themselves (e.g. raw DER).
180+
return null;
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)