Skip to content
Open
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
5 changes: 2 additions & 3 deletions OpenPayments.Sdk.HttpSignatureUtils/HttpRequestSigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
/// <summary>
/// Signature headers returned by the HttpRequestSigner.
/// </summary>

public class SignatureHeaders
{
/// <summary>
Expand All @@ -31,13 +30,13 @@ private static string BuildSignatureInput(List<string> components, string keyId,
var fields = string.Join(" ", components.Select(h => $"\"{h}\""));
return $"({fields});created={created};keyid=\"{keyId}\";alg=\"ed25519\"";
}

private static string ComputeContentDigest(string body)
{
var hash = SHA512.HashData(Encoding.UTF8.GetBytes(body));
return Convert.ToBase64String(hash);
}

private static async Task<string> TryGetHeaderValueAsync(HttpRequestMessage request, string name)
{
name = name.ToLowerInvariant();
Expand Down
6 changes: 3 additions & 3 deletions OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using System.Text;
using NSec.Cryptography;

using OpenPayments.Sdk.HttpSignatureUtils;

public class HttpSignatureValidator : IHttpSignatureValidator

Check warning on line 5 in OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'HttpSignatureValidator'

Check warning on line 5 in OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'HttpSignatureValidator'
{
private readonly ISignatureInputParser _parser;
private readonly ISignatureInputValidator _validator;
private readonly ISignatureInputBuilder _builder;

public HttpSignatureValidator(

Check warning on line 11 in OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'HttpSignatureValidator.HttpSignatureValidator(ISignatureInputParser, ISignatureInputValidator, ISignatureInputBuilder)'

Check warning on line 11 in OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'HttpSignatureValidator.HttpSignatureValidator(ISignatureInputParser, ISignatureInputValidator, ISignatureInputBuilder)'
ISignatureInputParser parser,
ISignatureInputValidator validator,
ISignatureInputBuilder builder)
Expand All @@ -19,13 +18,13 @@
_builder = builder;
}

public bool AreSignatureHeadersPresent(HttpRequestMessage request)

Check warning on line 21 in OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'HttpSignatureValidator.AreSignatureHeadersPresent(HttpRequestMessage)'

Check warning on line 21 in OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'HttpSignatureValidator.AreSignatureHeadersPresent(HttpRequestMessage)'
{
return TryGetHeader(request, "signature") is not null &&
TryGetHeader(request, "signature-input") is not null;
}

public async Task<bool> ValidateSignatureAsync(HttpRequestMessage request, Jwk clientKey)

Check warning on line 27 in OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'HttpSignatureValidator.ValidateSignatureAsync(HttpRequestMessage, Jwk)'

Check warning on line 27 in OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'HttpSignatureValidator.ValidateSignatureAsync(HttpRequestMessage, Jwk)'
{
var sig = TryGetHeader(request, "signature")!;
var sigInput = TryGetHeader(request, "signature-input")!;
Expand All @@ -38,11 +37,12 @@
return false;

var challenge = await _builder.BuildBaseAsync(components, request, sigInput);
if (challenge is null)
if (challenge is null)
return false;

var signatureBytes = Convert.FromBase64String(sig.Replace("sig1=", "").Replace(":", ""));
var publicKey = PublicKey.Import(SignatureAlgorithm.Ed25519, Base64UrlDecode(clientKey.X), KeyBlobFormat.RawPublicKey);
var publicKey = PublicKey.Import(SignatureAlgorithm.Ed25519, Base64UrlDecode(clientKey.X),
KeyBlobFormat.RawPublicKey);

return SignatureAlgorithm.Ed25519.Verify(publicKey, Encoding.UTF8.GetBytes(challenge), signatureBytes);
}
Expand Down
12 changes: 6 additions & 6 deletions OpenPayments.Sdk.HttpSignatureUtils/KeyUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,19 @@ public static Key LoadPem(string pem)
throw new ArgumentException("Invalid PEM");

// Parse PKCS#8 PrivateKeyInfo
var privInfo = PrivateKeyInfo.GetInstance(Asn1Object.FromByteArray(pemObj.Content));
var pkInfo = PrivateKeyInfo.GetInstance(Asn1Object.FromByteArray(pemObj.Content));

// Ensure Ed25519 OID (1.3.101.112)
var oid = privInfo.PrivateKeyAlgorithm.Algorithm.Id;
var oid = pkInfo.PrivateKeyAlgorithm.Algorithm.Id;
if (oid != "1.3.101.112")
throw new ArgumentException($"Unexpected algorithm OID: {oid}. Expected Ed25519 (1.3.101.112).");

// Extract the inner OCTET STRING (seed). For Ed25519 in PKCS#8, this is 32 bytes.
var privateKeyOctets = Asn1OctetString.GetInstance(privInfo.ParsePrivateKey());
var privateKeyOctets = Asn1OctetString.GetInstance(pkInfo.ParsePrivateKey());
var privateKeyBytes = privateKeyOctets.GetOctets();

// Some toolchains wrap the seed in another OCTET STRING layer:
// If length is not 32, try one more unwrap.
// If lenght is not 32, try one more unwrap.
if (privateKeyBytes.Length != 32)
{
try
Expand All @@ -85,7 +85,7 @@ public static Key LoadPem(string pem)
if (privateKeyBytes.Length != 32)
throw new ArgumentException($"Ed25519 seed must be 32 bytes, got {privateKeyBytes.Length}.");

// Import into NSec as raw private key (seed)
// Import into NSec as a raw private key (seed)
var algorithm = SignatureAlgorithm.Ed25519;
var seed = privateKeyBytes.AsSpan(0, 32);
return Key.Import(algorithm, seed, KeyBlobFormat.RawPrivateKey);
Expand Down Expand Up @@ -214,4 +214,4 @@ public static Key LoadOrGenerateKey(string? keyFilePath = null, GenerateKeyArgs?

return GenerateKey(generateKeyArgs);
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing newline.

45 changes: 40 additions & 5 deletions OpenPayments.Sdk/Clients/AuthenticatedClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ internal sealed class AuthenticatedClient(HttpClient http, Key privateKey, strin
private readonly IAuthClientBase _authClient = new AuthClientBase(http, privateKey, keyId, clientUrl);
private readonly IResourceClientBase _resClient = new ResourceClientBase(http, privateKey, keyId, clientUrl);

/// <inheritdoc/>
public Task<AuthResponse> RequestGrantAsync(RequestArgs requestArgs,
GrantCreateBody body,
CancellationToken cancellationToken = default)
{
return _authClient.RequestGrantAsync(requestArgs, body, cancellationToken);
}

/// <inheritdoc/>
public Task<AuthResponse> ContinueGrantAsync(AuthRequestArgs requestArgs,
GrantContinueBody? body,
CancellationToken cancellationToken = default)
Expand All @@ -32,37 +34,71 @@ public Task<AuthResponse> ContinueGrantAsync(AuthRequestArgs requestArgs,
return _authClient.ContinueGrantAsync(requestArgs, body, cancellationToken);
}

/// <inheritdoc/>
public Task CancelGrantAsync(AuthRequestArgs requestArgs,
CancellationToken cancellationToken = default)
{
return _authClient.CancelGrantAsync(requestArgs, cancellationToken);
}

/// <inheritdoc/>
public Task<RotateTokenResponse> RotateTokenAsync(AuthRequestArgs requestArgs,
CancellationToken cancellationToken = default)
{
return _authClient.RotateTokenAsync(requestArgs, cancellationToken);
}


/// <inheritdoc/>
public Task RevokeTokenAsync(AuthRequestArgs requestArgs,
CancellationToken cancellationToken = default)
{
return _authClient.RevokeTokenAsync(requestArgs, cancellationToken);
}

public Task<IncomingPaymentResponse> CreateIncomingPaymentAsync(AuthRequestArgs requestArgs, IncomingPaymentBody body,
/// <inheritdoc/>
public Task<IncomingPaymentResponse> CreateIncomingPaymentAsync(AuthRequestArgs requestArgs,
IncomingPaymentBody body,
CancellationToken cancellationToken = default)
{
return _resClient.CreateIncomingPaymentAsync(requestArgs, body, cancellationToken);
}

/// <inheritdoc/>
public Task<IncomingPaymentResponse> GetIncomingPaymentAsync(AuthRequestArgs requestArgs,
CancellationToken cancellationToken = default)
{
return _resClient.GetIncomingPaymentAsync(requestArgs, cancellationToken);
}

/// <inheritdoc cref="UnauthenticatedClient.GetIncomingPaymentAsync"/>
public Task<PublicIncomingPayment> GetPublicIncomingPaymentAsync(RequestArgs requestArgs, CancellationToken cancellationToken = default)
{
return base.GetIncomingPaymentAsync(requestArgs.Url.ToString(), cancellationToken);
}

/// <inheritdoc/>
public Task<ListIncomingPaymentsResponse> ListIncomingPaymentsAsync(AuthRequestArgs requestArgs,
ListIncomingPaymentQuery query, CancellationToken cancellationToken = default)
{
return _resClient.ListIncomingPaymentsAsync(requestArgs, query, cancellationToken);
}

/// <inheritdoc/>
public Task<IncomingPaymentResponse> CompleteIncomingPaymentsAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default)
{
return _resClient.CompleteIncomingPaymentAsync(requestArgs, cancellationToken);
}

/// <inheritdoc/>
public Task<QuoteResponse> CreateQuoteAsync(AuthRequestArgs requestArgs, QuoteBody body,
CancellationToken cancellationToken = default)
{
return _resClient.CreateQuoteAsync(requestArgs, body, cancellationToken);
return _resClient.CreateQuoteAsync(requestArgs, body, cancellationToken);
}

public Task<OutgoingPaymentResponse> CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs, OutgoingPaymentBody body,
/// <inheritdoc/>
public Task<OutgoingPaymentResponse> CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs,
OutgoingPaymentBody body,
CancellationToken cancellationToken = default)
{
return _resClient.CreateOutgoingPaymentAsync(requestArgs, body, cancellationToken);
Expand All @@ -78,4 +114,3 @@ public class AuthRequestArgs : RequestArgs
{
public required string AccessToken { get; set; }
}

86 changes: 83 additions & 3 deletions OpenPayments.Sdk/Clients/IAuthenticatedClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,112 @@ namespace OpenPayments.Sdk.Clients;
public interface IAuthenticatedClient : IUnauthenticatedClient
{
/// <summary>
/// Resolve a wallet-address URL (or payment pointer) and return its public metadata.
/// Request Grant
/// </summary>
/// <param name="requestArgs">Auth Server URL Address (e.g. <c>https://auth.wallet.example</c>) and access token.</param>
/// <param name="requestArgs">Grant URL (e.g. <c>https://ilp.com/grant</c>) and access token.</param>
/// <param name="body">Request body</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
public Task<AuthResponse> RequestGrantAsync(RequestArgs requestArgs,
GrantCreateBody body,
CancellationToken cancellationToken = default);

/// <summary>
/// Continue Grant
/// </summary>
/// <param name="requestArgs">Continue Grant URL (e.g. <c>https://ilp.com/continue/1234</c>) and access token.</param>
/// <param name="body"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<AuthResponse> ContinueGrantAsync(AuthRequestArgs requestArgs,
GrantContinueBody? body = null,
CancellationToken cancellationToken = default);

/// <summary>
/// Cancel Grant
/// </summary>
/// <param name="requestArgs">Cancel Grant URL (e.g. <c>https://ilp.com/grant/1234</c>) and access token.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task CancelGrantAsync(AuthRequestArgs requestArgs,
CancellationToken cancellationToken = default);


/// <summary>
///
/// </summary>
/// <param name="requestArgs">Auth Token URL Address (e.g. <c>https://ilp.com/token/1234</c>) and access token.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<RotateTokenResponse> RotateTokenAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default);

/// <summary>
///
/// </summary>
/// <param name="requestArgs">Auth Token URL Address (e.g. <c>https://ilp.com/token/1234</c>) and access token.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task RevokeTokenAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default);

/// <summary>
///
/// </summary>
/// <param name="requestArgs">Resource Server URL Address (e.g. <c>https://res.ilp.com/incoming/</c>) and access token.</param>
/// <param name="body"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<IncomingPaymentResponse> CreateIncomingPaymentAsync(AuthRequestArgs requestArgs,
IncomingPaymentBody body, CancellationToken cancellationToken = default);


/// <summary>
/// Get an Incoming Payment
/// </summary>
/// <param name="requestArgs"></param>
/// <param name="cancellationToken"></param>
/// <returns>IncomingPaymentResponse</returns>
public Task<IncomingPaymentResponse> GetIncomingPaymentAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default);

/// <summary>
/// Get a Public Incoming Payment
/// </summary>
/// <param name="requestArgs"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<PublicIncomingPayment> GetPublicIncomingPaymentAsync(RequestArgs requestArgs, CancellationToken cancellationToken = default);

/// <summary>
/// List Incoming Payments
/// </summary>
/// <param name="requestArgs"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns>ListIncomingPaymentsResponse</returns>
public Task<ListIncomingPaymentsResponse> ListIncomingPaymentsAsync(AuthRequestArgs requestArgs, ListIncomingPaymentQuery query, CancellationToken cancellationToken = default);

/// <summary>
/// Complete Incoming Payment
/// </summary>
/// <param name="requestArgs"></param>
/// <param name="cancellationToken"></param>
/// <returns>ListIncomingPaymentsResponse</returns>
public Task<IncomingPaymentResponse> CompleteIncomingPaymentsAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default);

/// <summary>
///
/// </summary>
/// <param name="requestArgs">Resource Server URL Address (e.g. <c>https://res.ilp.com/quote</c>) and access token.</param>
/// <param name="body"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<QuoteResponse> CreateQuoteAsync(AuthRequestArgs requestArgs, QuoteBody body,
CancellationToken cancellationToken = default);

/// <summary>
///
/// </summary>
/// <param name="requestArgs">Resource Server URL Address (e.g. <c>https://res.ilp.com/outgoing</c>) and access token.</param>
/// <param name="body"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<OutgoingPaymentResponse> CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs, OutgoingPaymentBody body,
CancellationToken cancellationToken = default);
}
43 changes: 39 additions & 4 deletions OpenPayments.Sdk/Clients/ResourceClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,48 @@ public async Task<IncomingPaymentResponse> CreateIncomingPaymentAsync(AuthReques
{
_client.BaseUrl = requestArgs.Url.ToString();

return await _client.PostIncomingPaymentAsync(body, requestArgs.AccessToken!, cancellationToken);
return await _client.PostIncomingPaymentAsync(body, requestArgs.AccessToken, cancellationToken);
}

public async Task<IncomingPaymentResponse> GetIncomingPaymentAsync(AuthRequestArgs requestArgs,
CancellationToken cancellationToken = default)
{
_client.BaseUrl = requestArgs.Url.ToString();

return await _client.GetIncomingPaymentAsync(requestArgs.AccessToken, cancellationToken);
}

public async Task<IncomingPaymentResponse> CompleteIncomingPaymentAsync(AuthRequestArgs requestArgs,
CancellationToken cancellationToken = default)
{
_client.BaseUrl = requestArgs.Url.ToString();

return await _client.CompleteIncomingPaymentAsync(requestArgs.AccessToken, cancellationToken);
}

public async Task<ListIncomingPaymentsResponse> ListIncomingPaymentsAsync(AuthRequestArgs requestArgs,
ListIncomingPaymentQuery query, CancellationToken cancellationToken = default)
{
_client.BaseUrl = requestArgs.Url.ToString();

return await _client.ListIncomingPaymentsAsync(requestArgs.AccessToken, query.WalletAddress, query.Cursor,
query.First, query.Last, cancellationToken);
}

public async Task<QuoteResponse> CreateQuoteAsync(AuthRequestArgs requestArgs, QuoteBody body,
CancellationToken cancellationToken = default)
{
_client.BaseUrl = requestArgs.Url.ToString();

return await _client.PostQuoteAsync(body, requestArgs.AccessToken!, cancellationToken);
return await _client.PostQuoteAsync(body, requestArgs.AccessToken, cancellationToken);
}

public async Task<OutgoingPaymentResponse> CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs,
OutgoingPaymentBody body, CancellationToken cancellationToken = default)
{
_client.BaseUrl = requestArgs.Url.ToString();

return await _client.PostOutgoingPaymentAsync(body, requestArgs.AccessToken!, cancellationToken);
return await _client.PostOutgoingPaymentAsync(body, requestArgs.AccessToken, cancellationToken);
}
}

Expand All @@ -46,9 +71,19 @@ public interface IResourceClientBase
public Task<IncomingPaymentResponse> CreateIncomingPaymentAsync(AuthRequestArgs requestArgs, Body body,
CancellationToken cancellationToken = default);

public Task<IncomingPaymentResponse> GetIncomingPaymentAsync(AuthRequestArgs requestArgs,
CancellationToken cancellationToken = default);

public Task<IncomingPaymentResponse> CompleteIncomingPaymentAsync(AuthRequestArgs requestArgs,
CancellationToken cancellationToken = default);

public Task<ListIncomingPaymentsResponse> ListIncomingPaymentsAsync(AuthRequestArgs requestArgs,
ListIncomingPaymentQuery query, CancellationToken cancellationToken = default);

public Task<QuoteResponse> CreateQuoteAsync(AuthRequestArgs requestArgs, QuoteBody body,
CancellationToken cancellationToken = default);

public Task<OutgoingPaymentResponse> CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs, OutgoingPaymentBody body,
public Task<OutgoingPaymentResponse> CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs,
OutgoingPaymentBody body,
CancellationToken cancellationToken = default);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing newline.

7 changes: 3 additions & 4 deletions OpenPayments.Sdk/Clients/UnauthenticatedClient.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Net.Http.Headers;
using Newtonsoft.Json;
using OpenPayments.Sdk.Generated.Wallet;
using OpenPayments.Sdk.Generated.Resource;
Expand Down Expand Up @@ -34,10 +35,8 @@ public async Task<PublicIncomingPayment> GetIncomingPaymentAsync(string incoming
if (string.IsNullOrWhiteSpace(incomingPaymentUrl))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(incomingPaymentUrl));

using var request = new HttpRequestMessage(HttpMethod.Get, incomingPaymentUrl)
{
Headers = { Accept = { new("application/json") } }
};
using var request = new HttpRequestMessage(HttpMethod.Get, incomingPaymentUrl);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
Expand Down
Loading
Loading