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

Implement verifier authentication with x509_san_dns ClientIdScheme #44

Merged
merged 5 commits into from
Feb 23, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System;
using static Microsoft.IdentityModel.Tokens.Base64UrlEncoder;
using static Org.BouncyCastle.Security.SignerUtilities;
using static System.Text.Encoding;

namespace Hyperledger.Aries.Features.OpenID4VC.Vp.Extensions
{
/// <summary>
/// Extension methods for <see cref="JwtSecurityToken" />.
/// </summary>
public static class JwtSecurityTokenExtensions
{
/// <summary>
/// Validates the signature of the JWT token using the provided public key.
/// </summary>
/// <param name="token">The JWT token to validate.</param>
/// <param name="publicKey">The public key to use for validation.</param>
/// <returns>True if the signature is valid, otherwise false.</returns>
public static bool IsSignatureValid(this JwtSecurityToken token, ECPublicKeyParameters publicKey)
{
var signer = GetSigner("SHA-256withECDSA");

signer.Init(false, publicKey);

var encodedHeaderAndPayload = UTF8.GetBytes(token.EncodedHeader + "." + token.EncodedPayload);

signer.BlockUpdate(encodedHeaderAndPayload, 0, encodedHeaderAndPayload.Length);

return signer.VerifySignature(
ConvertRawToDerFormat(
DecodeBytes(token.RawSignature)
)
);
}

private static byte[] ConvertRawToDerFormat(byte[] rawSignature)
{
if (rawSignature.Length != 64)
throw new ArgumentException("Raw signature should be 64 bytes long", nameof(rawSignature));

var r = new BigInteger(1, rawSignature.Take(32).ToArray());
var s = new BigInteger(1, rawSignature.Skip(32).ToArray());

var derSignature = new DerSequence(
new DerInteger(r),
new DerInteger(s)
).GetDerEncoded();

return derSignature;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Linq;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Pkix;
using Org.BouncyCastle.Utilities.Collections;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.X509.Store;

namespace Hyperledger.Aries.Features.OpenID4VC.Vp.Extensions
{
/// <summary>
/// Extension methods for <see cref="X509Certificate" />.
/// </summary>
public static class X509CertificateExtensions
{
/// <summary>
/// Validates the trust chain of the certificate.
/// </summary>
/// <param name="trustChain">The trust chain to validate.</param>
/// <returns>True if the trust chain is valid, otherwise false.</returns>
public static bool IsTrustChainValid(this List<X509Certificate> trustChain)
{
var leafCert = trustChain.First();

var rootCerts =
new HashSet(
trustChain
.Skip(1)
.Where(cert => cert.IssuerDN.Equivalent(cert.SubjectDN))
.Select(cert => new TrustAnchor(cert, null))
.ToList()
);

var intermediateCerts =
new HashSet(
trustChain
.Skip(1)
.Where(cert => !cert.IssuerDN.Equivalent(cert.SubjectDN))
.Append(leafCert)
);

var builderParams =
new PkixBuilderParameters(
rootCerts,
new X509CertStoreSelector
{
Certificate = leafCert
}
)
{
IsRevocationEnabled =
leafCert.GetExtensionValue(X509Extensions.CrlDistributionPoints) is not null
};

builderParams.AddStore(
X509StoreFactory.Create(
"Certificate/Collection",
new X509CollectionStoreParameters(intermediateCerts)
)
);

// This throws if validation fails
new PkixCertPathValidator()
.Validate(
new PkixCertPathBuilder()
.Build(builderParams)
.CertPath,
builderParams
);

return true;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I understand, all those Properties make up the RequestObject. The question is only how it is delivered through the AuthorizationRequest (directly in uri or indirectly via request_uri)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I dont really understand the question. Besides that, the HAIP defines that only request_uri must be supported https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-sd-jwt-vc-1_0.html#section-5-1.5

Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Web;
using Hyperledger.Aries.Features.Pex.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static Hyperledger.Aries.Features.OpenId4Vc.Vp.Models.ClientIdScheme;

namespace Hyperledger.Aries.Features.OpenId4Vc.Vp.Models
{
Expand All @@ -12,156 +11,118 @@ namespace Hyperledger.Aries.Features.OpenId4Vc.Vp.Models
/// </summary>
public class AuthorizationRequest
{
/// <summary>
/// Gets or sets the response type. Determines what the Authorization Response should contain.
/// </summary>
[JsonProperty("response_type")]
public string ResponseType { get; set; } = null!;

/// <summary>
/// Gets or sets the client id. The Identifier of the Verifier
/// </summary>
[JsonProperty("client_id")]
public string ClientId { get; set; } = null!;
private const string DirectPost = "direct_post";

/// <summary>
/// Gets or sets the client id scheme.
/// </summary>
[JsonProperty("client_id_scheme")]
public string? ClientIdScheme { get; set; }

/// <summary>
/// Gets or sets the redirect uri.
/// </summary>
[JsonProperty("redirect_uri")]
public string? RedirectUri { get; set; }

/// <summary>
/// Gets or sets the client metadata. Contains the Verifier metadata
/// </summary>
[JsonProperty("client_metadata")]
public string? ClientMetadata { get; set; }
private const string VpToken = "vp_token";

/// <summary>
/// Gets or sets the client metadata uri. Can be used to retrieve the verifier metadata.
/// Gets the client id scheme.
/// </summary>
[JsonProperty("client_metadata_uri")]
public string? ClientMetadataUri { get; set; }
[JsonProperty("client_id_scheme")]
public ClientIdScheme ClientIdScheme { get; }

/// <summary>
/// The scope of the request.
/// Gets the presentation definition. Contains the claims that the Verifier wants to receive.
/// </summary>
[JsonProperty("scope")]
public string? Scope { get; set; }
[JsonProperty("presentation_definition")]
public PresentationDefinition PresentationDefinition { get; }

/// <summary>
/// Gets or sets the nonce. Random string for session binding.
/// Gets the client id. The Identifier of the Verifier.
/// </summary>
[JsonProperty("nonce")]
public string Nonce { get; set; } = null!;
[JsonProperty("client_id")]
public string ClientId { get; }

/// <summary>
/// Gets or sets the response mode. Determines how to send the Authorization Response.
/// Gets the nonce. Random string for session binding.
/// </summary>
[JsonProperty("response_mode")]
public string? ResponseMode { get; set; }
[JsonProperty("nonce")]
public string Nonce { get; }

/// <summary>
/// Gets or sets the response mode. Determines where to send the Authorization Response to.
/// Gets the response mode. Determines where to send the Authorization Response to.
/// </summary>
[JsonProperty("response_uri")]
public string ResponseUri { get; set; } = null!;
public string ResponseUri { get; }

/// <summary>
/// Gets or sets the state.
/// Gets the client metadata. Contains the Verifier metadata.
/// </summary>
[JsonProperty("state")]
public string? State { get; set; }
[JsonProperty("client_metadata")]
public string? ClientMetadata { get; }

/// <summary>
/// Gets or sets the presentation definition. Contains the claims that the Verifier wants to receive.
/// Gets the client metadata uri. Can be used to retrieve the verifier metadata.
/// </summary>
[JsonIgnore]
public PresentationDefinition PresentationDefinition { get; set; } = null!;
[JsonProperty("client_metadata_uri")]
public string? ClientMetadataUri { get; }

/// <summary>
/// Parses an Authorization Request from a Jwt.
/// The scope of the request.
/// </summary>
/// <param name="jwt"></param>
/// <returns>The AuthorizationRequest</returns>
public static AuthorizationRequest? ParseFromJwt(string jwt)
{
var jwtHandler = new JwtSecurityTokenHandler();
var token = jwtHandler.ReadToken(jwt) as JwtSecurityToken;
if (token == null) return null;

var presentationDefinition = JsonConvert.DeserializeObject<PresentationDefinition>(token.Payload["presentation_definition"].ToString());
var authorizationRequest = JsonConvert.DeserializeObject<AuthorizationRequest>(token.Payload.SerializeToJson());

if (!(authorizationRequest != null && presentationDefinition != null))
return null;

authorizationRequest.PresentationDefinition = presentationDefinition;

return authorizationRequest;
}
[JsonProperty("scope")]
public string? Scope { get; }

/// <summary>
/// Parses an Authorization Request from a Uri.
/// Gets the state.
/// </summary>
/// <param name="uri"></param>
/// <returns>The AuthorizationRequest</returns>
public static AuthorizationRequest? ParseFromUri(Uri uri)
[JsonProperty("state")]
public string? State { get; }

[JsonConstructor]
private AuthorizationRequest(
ClientIdScheme clientIdScheme,
PresentationDefinition presentationDefinition,
string clientId,
string nonce,
string responseUri,
string? clientMetadata,
string? clientMetadataUri,
string? scope,
string? state)
{
var query = HttpUtility.ParseQueryString(uri.Query);
var dict = query.AllKeys.ToDictionary(key => key, key => query[key]);
var json = JsonConvert.SerializeObject(dict);

var presentationDefinition = JsonConvert.DeserializeObject<PresentationDefinition>(query["presentation_definition"]);
var authorizationRequest = JsonConvert.DeserializeObject<AuthorizationRequest>(json);

if (!(authorizationRequest != null && presentationDefinition != null))
return null;

authorizationRequest.PresentationDefinition = presentationDefinition;

return authorizationRequest;
ClientId = clientId;
ClientIdScheme = clientIdScheme;
ClientMetadata = clientMetadata;
ClientMetadataUri = clientMetadataUri;
Nonce = nonce;
PresentationDefinition = presentationDefinition;
ResponseUri = responseUri;
Scope = scope;
State = state;
}
}

/// <summary>
/// Extension methods for the <see cref="AuthorizationRequest"/> class.
/// </summary>
public static class AuthorizationRequestExtension
{
/// <summary>
/// Checks if the Authorization Request is HAIP conform.
/// Creates a new instance of the <see cref="AuthorizationRequest" /> class.
/// </summary>
/// <param name="authorizationRequest"></param>
/// <returns>Returns bool indicating whether the AuthorizationRequest is haip conform</returns>
public static bool IsHaipConform(this AuthorizationRequest authorizationRequest)
/// <param name="authorizationRequestJson">The json representation of the authorization request.</param>
/// <returns>A new instance of the <see cref="AuthorizationRequest" /> class.</returns>
/// <exception cref="InvalidOperationException">Thrown when the request does not match the HAIP.</exception>
public static AuthorizationRequest CreateAuthorizationRequest(string authorizationRequestJson)
=> CreateAuthorizationRequest(JObject.Parse(authorizationRequestJson));

private static AuthorizationRequest CreateAuthorizationRequest(JObject authorizationRequestJson) =>
IsHaipConform(authorizationRequestJson)
? authorizationRequestJson.ToObject<AuthorizationRequest>()
?? throw new InvalidOperationException("Could not parse the Authorization Request")
: throw new InvalidOperationException(
"Invalid Authorization Request. The request does not match the HAIP"
);

private static bool IsHaipConform(JObject authorizationRequestJson)
{
if (authorizationRequest.ResponseType != "vp_token")
return false;

if (authorizationRequest.ResponseMode != "direct_post")
return false;

if (String.IsNullOrEmpty(authorizationRequest.ResponseUri))
return false;

if (!String.IsNullOrEmpty(authorizationRequest.RedirectUri))
return false;

if (authorizationRequest.ClientIdScheme == "redirect_uri"
& authorizationRequest.ClientId != authorizationRequest.ResponseUri)
return false;

//TODO: Not supported yet
//if (!(authorizationRequest.ClientIdScheme == "x509_san_dns" || authorizationRequest.ClientIdScheme == "verifier_attestation"))
//return false;

return true;
var responseType = authorizationRequestJson["response_type"]!.ToString();
var responseUri = authorizationRequestJson["response_uri"]!.ToString();
var responseMode = authorizationRequestJson["response_mode"]!.ToString();
var redirectUri = authorizationRequestJson["redirect_uri"];
var clientIdScheme = authorizationRequestJson["client_id_scheme"];

return
responseType == VpToken
&& responseMode == DirectPost
&& !string.IsNullOrEmpty(responseUri)
&& redirectUri is null
&& clientIdScheme!.ToString() is X509SanDnsScheme or VerifierAttestationScheme;
}
}
}
Loading
Loading