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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
/// <summary>
/// Managed façade for <c>AttestationClientLib.dll</c>. Holds initialization state,
/// does ref-count hygiene on <see cref="SafeNCryptKeyHandle"/>, and returns a JWT.
/// does ref-count hygiene on <see cref="SafeNCryptKeyHandle"/>, and returns a JWT with expiry information.
/// </summary>
internal sealed class AttestationClient : IDisposable
{
Expand Down Expand Up @@ -37,14 +37,14 @@ public AttestationClient()
}

/// <summary>
/// Calls the native <c>AttestKeyGuardImportKey</c> and returns a structured result.
/// Calls the native <c>AttestKeyGuardImportKey</c> and returns a structured result with expiry information.
/// </summary>
public AttestationResult Attest(string endpoint,
SafeNCryptKeyHandle keyHandle,
string clientId)
{
if (!_initialized)
return new(AttestationStatus.NotInitialized, null, -1,
return new(AttestationStatus.NotInitialized, null, null, -1,
"Native library not initialized.");

IntPtr buf = IntPtr.Zero;
Expand All @@ -58,33 +58,38 @@ public AttestationResult Attest(string endpoint,
endpoint, null, null, keyHandle, out buf, clientId);

if (rc != 0)
return new(AttestationStatus.NativeError, null, rc, null);
return new(AttestationStatus.NativeError, null, null, rc, null);

if (buf == IntPtr.Zero)
return new(AttestationStatus.TokenEmpty, null, 0,
return new(AttestationStatus.TokenEmpty, null, null, 0,
"rc==0 but token buffer was null.");

string jwt = Marshal.PtrToStringAnsi(buf)!;
return new(AttestationStatus.Success, jwt, 0, null);

// Extract expiry from JWT payload
JwtClaimExtractor.TryExtractExpirationClaim(jwt, out DateTimeOffset expiresOn);

var token = new AttestationToken(jwt, expiresOn);
return new(AttestationStatus.Success, token, jwt, 0, null);
}
catch (DllNotFoundException ex)
{
return new(AttestationStatus.Exception, null, -1,
return new(AttestationStatus.Exception, null, null, -1,
$"Native DLL not found: {ex.Message}");
}
catch (BadImageFormatException ex)
{
return new(AttestationStatus.Exception, null, -1,
return new(AttestationStatus.Exception, null, null, -1,
$"Architecture mismatch (x86/x64) or corrupted DLL: {ex.Message}");
}
catch (SEHException ex)
{
return new(AttestationStatus.Exception, null, -1,
return new(AttestationStatus.Exception, null, null, -1,
$"Native library raised SEHException: {ex.Message}");
}
catch (Exception ex)
{
return new(AttestationStatus.Exception, null, -1, ex.Message);
return new(AttestationStatus.Exception, null, null, -1, ex.Message);
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,24 @@ namespace Microsoft.Identity.Client.KeyAttestation.Attestation
/// AttestationResult is the result of an attestation operation.
/// </summary>
/// <param name="Status">High-level outcome category.</param>
/// <param name="Jwt">JWT on success; null otherwise (caller may pass null).</param>
/// <param name="Token">Structured attestation token with expiry on success; null otherwise (caller may pass null).</param>
/// <param name="Jwt">JWT on success; null otherwise (caller may pass null). Retained for backward compatibility.</param>
/// <param name="NativeErrorCode">Raw native return code (0 on success).</param>
/// <param name="ErrorMessage">Optional descriptive text for non-success cases.</param>
/// <remarks>
/// This is a positional record. The compiler synthesizes init-only auto-properties:
/// public AttestationStatus Status { get; init; }
/// public string Jwt { get; init; }
/// public int NativeErrorCode { get; init; }
/// public string ErrorMessage { get; init; }
/// public AttestationToken Token { get; init; }
/// public string Jwt { get; init; }
/// public int NativeErrorCode { get; init; }
/// public string ErrorMessage { get; init; }
/// Because they are init-only, values are fixed after construction; to "modify" use a 'with'
/// expression, e.g.: var updated = result with { Jwt = newJwt };
/// The netstandard2.0 target relies on the IsExternalInit shim (see IsExternalInit.cs) to enable 'init'.
/// </remarks>
internal sealed record AttestationResult(
AttestationStatus Status,
AttestationToken Token,
string Jwt,
int NativeErrorCode,
string ErrorMessage);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
/// <summary>
/// Represents a successfully attested token with structured metadata.
/// </summary>
/// <param name="Token">The raw JWT string.</param>
/// <param name="ExpiresOn">The expiration time of the token (UTC).</param>
/// <remarks>
/// Internal use only. This record enables the library to access token expiry
/// without requiring callers to manually decode the JWT.
/// </remarks>
internal sealed record AttestationToken(
string Token,
DateTimeOffset ExpiresOn);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using Microsoft.Identity.Client.Utils;

namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
/// <summary>
/// JWT claim extractor leveraging MSAL's existing helper utilities.
/// </summary>
internal static class JwtClaimExtractor
{
/// <summary>
/// Extracts the 'exp' (expiration) claim from a JWT payload.
/// </summary>
/// <param name="jwt">The JWT string (format: header.payload.signature).</param>
/// <param name="expiresOn">The parsed expiration time in UTC, or DateTimeOffset.MinValue on failure.</param>
/// <returns>True if the exp claim was successfully extracted; false otherwise.</returns>
internal static bool TryExtractExpirationClaim(string jwt, out DateTimeOffset expiresOn)
{
expiresOn = DateTimeOffset.MinValue;

try
{
// Split JWT into parts
var parts = jwt.Split('.');
if (parts.Length < 2)
return false;

// Use MSAL's Base64UrlHelpers to decode the payload (handles padding automatically)
string payloadJson = Base64UrlHelpers.Decode(parts[1]);

if (string.IsNullOrEmpty(payloadJson))
return false;

// Use MSAL's JsonHelper to parse JSON (handles both System.Text.Json and Newtonsoft)
var claims = JsonHelper.DeserializeFromJson<Dictionary<string, object>>(payloadJson);

if (claims == null || !claims.TryGetValue("exp", out object expObj))
return false;

// Parse the exp claim (Unix timestamp in seconds)
if (expObj is long expLong)
{
expiresOn = DateTimeOffset.FromUnixTimeSeconds(expLong);
return true;
}

// Handle case where exp comes as string (defensive)
if (expObj is string expStr && long.TryParse(expStr, out long parsedExp))
{
expiresOn = DateTimeOffset.FromUnixTimeSeconds(parsedExp);
return true;
}
}
catch
{
// Silently fail: malformed JWT or parsing error
}

return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public static Task<AttestationResult> AttestCredentialGuardAsync(
catch (Exception ex)
{
// Map any managed exception to AttestationStatus.Exception for consistency.
return new AttestationResult(AttestationStatus.Exception, string.Empty, -1, ex.Message);
return new AttestationResult(AttestationStatus.Exception, null, string.Empty, -1, ex.Message);
}
}, cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public static Func<string, SafeHandle, string, CancellationToken, Task<Attestati
return (attestationEndpoint, keyHandle, clientId, cancellationToken) =>
{
var fakeJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.attestation.sig";
return Task.FromResult(new AttestationResult(AttestationStatus.Success, fakeJwt, 0, string.Empty));
var token = new AttestationToken(fakeJwt, DateTimeOffset.UtcNow.AddHours(1));
return Task.FromResult(new AttestationResult(AttestationStatus.Success, token, fakeJwt, 0, string.Empty));
};
}

Expand Down