Skip to content

EUDIW / ARF reference tutorial

Configure EUDIW / ARF reference helpers using the SD-JWT .NET ecosystem.

Duration: 25 minutes
Difficulty: Advanced
Prerequisites:

Overview

EUDIW-style ecosystems under eIDAS 2.0 / ARF concepts use:

  • Architecture Reference Framework (ARF) algorithms
  • EU Trust List (LOTL) integration
  • Person Identification Data (PID) handling
  • HAIP Final flow/profile validation for OpenID4VC flows

Simple explanation

The EU Digital Identity Wallet ecosystem under eIDAS 2.0 requires specific credential formats, trust infrastructure, and protocol flows. This tutorial shows how SdJwt.Net.Eudiw provides ARF-aligned reference models for .NET implementations.

Packages used

Package Purpose
SdJwt.Net.Eudiw EUDIW / ARF reference models
SdJwt.Net.HAIP Profile validation
SdJwt.Net.Oid4Vci Issuance protocol
SdJwt.Net.Oid4Vp Presentation protocol

Where this fits

flowchart LR
    A["HAIP\nProfile"] --> B["EUDIW / ARF\nReference"]
    B --> C["PID Issuance"]
    B --> D["Cross-border\nPresentation"]
    style B fill:#2a6478,color:#fff

This tutorial builds an EUDIW-style reference wallet configuration step by step.


Step 1: Install packages

dotnet add package SdJwt.Net.Eudiw
dotnet add package SdJwt.Net.Wallet
dotnet add package SdJwt.Net.HAIP

Step 2: Create the EudiWallet

using SdJwt.Net.Eudiw;
using SdJwt.Net.Wallet.Storage;

// Set up storage and key management
var store = new InMemoryCredentialStore();
var keyManager = new SoftwareKeyManager();

// Configure EUDIW options
var options = new EudiWalletOptions
{
    WalletId = "demo-eudi-wallet",
    DisplayName = "My EU Wallet",
    EnforceArfCompliance = true,
    MinimumHaipLevel = 2, // Legacy local policy setting
    ValidateIssuerTrust = true,
    TrustListCacheHours = 6,
    SupportedCredentialTypes = new[]
    {
        EudiwConstants.Pid.DocType,   // PID
        EudiwConstants.Mdl.DocType    // mDL
    }
};

var wallet = new EudiWallet(store, keyManager, eudiOptions: options);

Console.WriteLine($"Wallet ID: {wallet.Options.WalletId}");
Console.WriteLine($"ARF Enforced: {wallet.IsArfEnforced}");
Console.WriteLine($"Legacy HAIP policy level: {wallet.MinimumHaipLevel}");

Deprecation note: MinimumHaipLevel is a legacy compatibility API. Use HAIP Final flow/profile validation via SdJwt.Net.HAIP for new implementations.

Expected Output:

Wallet ID: demo-eudi-wallet
ARF Enforced: True
Legacy HAIP policy level: 2

Step 3: Validate ARF algorithms

EUDIW only allows ECDSA algorithms per the Architecture Reference Framework:

// Test algorithm compliance
var algorithms = new[]
{
    ("ES256", true),   // Allowed
    ("ES384", true),   // Allowed
    ("ES512", true),   // Allowed
    ("RS256", false),  // Not in ARF
    ("PS256", false),  // Not in ARF
    ("HS256", false)   // Symmetric not allowed
};

foreach (var (alg, expected) in algorithms)
{
    var valid = wallet.ValidateAlgorithm(alg);
    var status = valid == expected ? "OK" : "FAIL";
    Console.WriteLine($"[{status}] {alg}: {valid}");
}

Expected Output:

[OK] ES256: True
[OK] ES384: True
[OK] ES512: True
[OK] RS256: False
[OK] PS256: False
[OK] HS256: False

Step 4: Validate EU member states

Only credentials from EU member states are accepted:

// EU member states
var euCountries = new[] { "DE", "FR", "ES", "IT", "PL", "NL", "BE", "AT" };
foreach (var country in euCountries)
{
    var valid = wallet.ValidateMemberState(country);
    Console.WriteLine($"{country}: {valid}");
}

Console.WriteLine();

// Non-EU countries (rejected)
var nonEuCountries = new[] { "US", "GB", "CH", "NO", "AU" };
foreach (var country in nonEuCountries)
{
    var valid = wallet.ValidateMemberState(country);
    Console.WriteLine($"{country}: {valid} (non-EU)");
}

// Get all supported member states
var allStates = wallet.GetSupportedMemberStates();
Console.WriteLine($"\nTotal EU member states: {allStates.Count}");

Expected Output:

DE: True
FR: True
ES: True
IT: True
PL: True
NL: True
BE: True
AT: True

US: False (non-EU)
GB: False (non-EU)
CH: False (non-EU)
NO: False (non-EU)
AU: False (non-EU)

Total EU member states: 27

Step 5: Validate credential types

Only PID, mDL, and qualified attestation types are allowed:

// Test credential types
var credTypes = new[]
{
    EudiwConstants.Pid.DocType,     // PID
    EudiwConstants.Mdl.DocType,     // mDL
    "eu.europa.ec.eudi.health.1",   // Qualified attestation
    "custom.unknown.credential"     // Unknown
};

foreach (var docType in credTypes)
{
    var result = wallet.ValidateCredentialType(docType);
    Console.WriteLine($"{docType}:");
    Console.WriteLine($"  Valid: {result.IsValid}");
    Console.WriteLine($"  Type: {result.CredentialType}");
}

Expected Output:

eu.europa.ec.eudi.pid.1:
  Valid: True
  Type: Pid
org.iso.18013.5.1.mDL:
  Valid: True
  Type: Mdl
eu.europa.ec.eudi.health.1:
  Valid: True
  Type: Qeaa
custom.unknown.credential:
  Valid: False
  Type: Unknown

Step 6: Validate PID claims

Person Identification Data must contain mandatory claims:

// Valid PID with all mandatory claims
var validPidClaims = new Dictionary<string, object>
{
    ["family_name"] = "Mueller",
    ["given_name"] = "Anna",
    ["birth_date"] = "1985-03-20",
    ["issuance_date"] = "2025-01-01",
    ["expiry_date"] = "2030-01-01",
    ["issuing_authority"] = "Bundesdruckerei",
    ["issuing_country"] = "DE"
};

var validResult = wallet.ValidatePidClaims(validPidClaims);
Console.WriteLine($"Valid PID: {validResult.IsValid}");
Console.WriteLine($"Missing claims: {validResult.MissingClaims.Count}");

// Invalid PID (missing mandatory claims)
var invalidPidClaims = new Dictionary<string, object>
{
    ["family_name"] = "Mueller",
    ["given_name"] = "Anna"
    // Missing: birth_date, issuance_date, expiry_date, etc.
};

var invalidResult = wallet.ValidatePidClaims(invalidPidClaims);
Console.WriteLine($"\nIncomplete PID: {invalidResult.IsValid}");
Console.WriteLine($"Missing: {string.Join(", ", invalidResult.MissingClaims)}");

Expected Output:

Valid PID: True
Missing claims: 0

Incomplete PID: False
Missing: birth_date, issuance_date, expiry_date, issuing_authority, issuing_country

Step 7: Extract PID credential

Convert claims to a typed PID credential:

if (validResult.IsValid)
{
    var pid = wallet.ExtractPidCredential(validPidClaims);

    Console.WriteLine("Extracted PID:");
    Console.WriteLine($"  Name: {pid.GivenName} {pid.FamilyName}");
    Console.WriteLine($"  Birth Date: {pid.BirthDate}");
    Console.WriteLine($"  Issuer: {pid.IssuingAuthority}");
    Console.WriteLine($"  Country: {pid.IssuingCountry}");
    Console.WriteLine($"  Valid Until: {pid.ExpiryDate}");
}

Expected Output:

Extracted PID:
  Name: Anna Mueller
  Birth Date: 1985-03-20
  Issuer: Bundesdruckerei
  Country: DE
  Valid Until: 2030-01-01

Step 8: Validate issuer trust

Validate that credential issuers are in EU Trust Lists:

// German PID provider (in EU Trust Lists)
var trustedResult = await wallet.ValidateIssuerTrustAsync(
    "https://pid-provider.bundesdruckerei.de");

Console.WriteLine("Trusted Issuer:");
Console.WriteLine($"  Trusted: {trustedResult.IsTrusted}");
Console.WriteLine($"  Member State: {trustedResult.MemberState}");
Console.WriteLine($"  Service Type: {trustedResult.ServiceType}");

// Non-EU issuer (not trusted)
var untrustedResult = await wallet.ValidateIssuerTrustAsync(
    "https://issuer.example.com");

Console.WriteLine("\nUnknown Issuer:");
Console.WriteLine($"  Trusted: {untrustedResult.IsTrusted}");
if (!untrustedResult.IsTrusted)
{
    Console.WriteLine($"  Errors: {string.Join(", ", untrustedResult.Errors)}");
}

Expected Output:

Trusted Issuer:
  Trusted: True
  Member State: DE
  Service Type: QualifiedAttestation

Unknown Issuer:
  Trusted: False
  Errors: Issuer not found in EU Trust Lists

Step 9: Store with ARF enforcement

Credentials are validated against ARF requirements during storage:

try
{
    // Store credential (validation happens automatically)
    var stored = await wallet.StoreCredentialAsync(parsedCredential);
    Console.WriteLine($"Stored successfully: {stored.Id}");
}
catch (ArfComplianceException ex)
{
    Console.WriteLine("ARF Compliance Failure:");
    foreach (var violation in ex.Violations)
    {
        Console.WriteLine($"  - {violation}");
    }
}
catch (EudiTrustException ex)
{
    Console.WriteLine($"Trust Failure: {ex.Message}");
}

Step 10: Create presentations

Create presentations with ARF-enforced credentials:

try
{
    var presentation = await wallet.CreatePresentationAsync(
        credentialId: storedCredential.Id,
        disclosurePaths: new[] { "family_name", "birth_date", "age_over_18" },
        audience: "https://verifier.example.eu",
        nonce: "unique-nonce-123"
    );

    Console.WriteLine($"Presentation created: {presentation.Length} bytes");
}
catch (ArfComplianceException ex)
{
    Console.WriteLine($"Cannot present: {string.Join(", ", ex.Violations)}");
}

Complete example

using SdJwt.Net.Eudiw;
using SdJwt.Net.Wallet.Storage;

public class EudiwComplianceDemo
{
    public static async Task RunAsync()
    {
        // 1. Create wallet
        var store = new InMemoryCredentialStore();
        var keyManager = new SoftwareKeyManager();

        var options = new EudiWalletOptions
        {
            WalletId = "demo-wallet",
            EnforceArfCompliance = true,
            MinimumHaipLevel = 2, // Legacy local policy setting
            ValidateIssuerTrust = true
        };

> **Deprecation note:** `MinimumHaipLevel` is a legacy compatibility API. Use HAIP Final flow/profile validation via `SdJwt.Net.HAIP` for new implementations.

        var wallet = new EudiWallet(store, keyManager, eudiOptions: options);

        // 2. Validate algorithm
        var isEs256Valid = wallet.ValidateAlgorithm("ES256");
        Console.WriteLine($"ES256 valid: {isEs256Valid}");

        // 3. Validate member state
        var isDeValid = wallet.ValidateMemberState("DE");
        Console.WriteLine($"Germany valid: {isDeValid}");

        // 4. Validate PID claims
        var pidClaims = new Dictionary<string, object>
        {
            ["family_name"] = "Mueller",
            ["given_name"] = "Anna",
            ["birth_date"] = "1985-03-20",
            ["issuance_date"] = "2025-01-01",
            ["expiry_date"] = "2030-01-01",
            ["issuing_authority"] = "Bundesdruckerei",
            ["issuing_country"] = "DE"
        };

        var pidResult = wallet.ValidatePidClaims(pidClaims);
        Console.WriteLine($"PID valid: {pidResult.IsValid}");

        // 5. Extract typed credential
        if (pidResult.IsValid)
        {
            var pid = wallet.ExtractPidCredential(pidClaims);
            Console.WriteLine($"Citizen: {pid.GivenName} {pid.FamilyName}");
        }

        // 6. Validate issuer trust
        var trustResult = await wallet.ValidateIssuerTrustAsync(
            "https://pid-provider.bundesdruckerei.de");
        Console.WriteLine($"Issuer trusted: {trustResult.IsTrusted}");

        Console.WriteLine("\nEUDIW / ARF reference demo complete!");
    }
}

Expected output

EUDIW configuration loaded
PID credential format: dc+sd-jwt
Trust list resolver configured
ARF validation: profile requirements met
Cross-border presentation: success

Demo vs production

This package provides reference models for experimentation. Production EUDIW deployments require certified wallets, accredited issuers, and member state trust infrastructure that is outside the scope of this library.

Common mistakes

  • Treating reference models as certified EUDIW components (they are implementation building blocks, not certified products)
  • Using legacy MinimumHaipLevel validation (use HAIP Final flow/profile validation instead)

Key takeaways

  1. The reference ARF-oriented policy allows ES256/ES384/ES512
  2. Only 27 EU member states are accepted
  3. PID requires seven mandatory claims
  4. Issuers must be in EU Trust Lists
  5. ARF validation runs at storage time
  6. ARF-enforced presentations protect holders

Next steps