Tutorial: OpenID Federation
Establish trust between issuers, holders, and verifiers using OpenID Federation.
Time: 25 minutes
Level: Advanced
Sample: samples/SdJwt.Net.Samples/03-Advanced/01-OpenIdFederation.cs
What You Will Learn
- Trust chain concept and structure
- Entity statements and metadata
- Resolving and validating trust
The Trust Problem
How does a verifier know to trust an issuer?
- Self-asserted metadata can be spoofed
- Manual trust lists don't scale
- Certificate authorities are complex
Solution: OpenID Federation creates hierarchical trust anchors.
Trust Hierarchy
┌─────────────────┐
│ Trust Anchor │
│ (Root of Trust)│
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Authority │ │ Authority │ │ Authority │
│ (Govt) │ │(Industry) │ │ (Region) │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Issuer │ │ Issuer │ │ Verifier│
└─────────┘ └─────────┘ └─────────┘
Step 1: Trust Anchor Configuration
The trust anchor publishes its entity configuration:
using SdJwt.Net.OidFederation.Models;
var trustAnchorConfig = new EntityConfiguration
{
Issuer = "https://federation.example.gov",
Subject = "https://federation.example.gov",
IssuedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExpiresAt = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeSeconds(),
Jwks = new JsonWebKeySet { Keys = { trustAnchorPublicKey } },
Metadata = new EntityMetadata
{
FederationEntity = new FederationEntityMetadata
{
FederationFetchEndpoint = "https://federation.example.gov/fetch",
FederationListEndpoint = "https://federation.example.gov/list"
}
}
};
Step 2: Subordinate Entity Statement
Trust anchor issues statement about subordinate:
var subordinateStatement = new EntityStatement
{
Issuer = "https://federation.example.gov", // Trust Anchor
Subject = "https://university.example.edu", // Subordinate
IssuedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExpiresAt = DateTimeOffset.UtcNow.AddMonths(6).ToUnixTimeSeconds(),
Jwks = new JsonWebKeySet { Keys = { universityPublicKey } },
MetadataPolicy = new MetadataPolicy
{
// Constrain what the university can claim
CredentialIssuer = new PolicyConstraints
{
AllowedCredentialTypes = new[] { "UniversityDegree", "StudentID" }
}
}
};
// Sign with trust anchor key
var signedStatement = SignEntityStatement(subordinateStatement, trustAnchorKey);
Step 3: Entity Configuration (Leaf)
The issuer publishes its own configuration:
var issuerConfig = new EntityConfiguration
{
Issuer = "https://university.example.edu",
Subject = "https://university.example.edu",
IssuedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeSeconds(),
Jwks = new JsonWebKeySet { Keys = { universityPublicKey } },
AuthorityHints = new[] { "https://federation.example.gov" },
Metadata = new EntityMetadata
{
CredentialIssuer = new CredentialIssuerMetadata
{
CredentialIssuer = "https://university.example.edu",
CredentialEndpoint = "https://university.example.edu/credential"
}
}
};
Step 4: Build Trust Chain
using SdJwt.Net.OidFederation.Services;
var resolver = new TrustChainResolver(httpClient);
// Resolve trust chain from issuer to trust anchor
var trustChain = await resolver.ResolveAsync(
leafEntity: "https://university.example.edu",
trustAnchor: "https://federation.example.gov"
);
// Trust chain contains:
// [0] Leaf entity configuration (self-signed)
// [1] Subordinate statement (signed by intermediate or anchor)
// [2] ... more intermediates if present ...
// [n] Trust anchor configuration (self-signed)
Step 5: Validate Trust Chain
var validator = new TrustChainValidator();
var result = validator.Validate(trustChain, trustAnchorPublicKey);
if (result.IsValid)
{
Console.WriteLine("Trust chain is valid");
Console.WriteLine($"Issuer is trusted for: {string.Join(", ", result.AllowedCredentialTypes)}");
}
else
{
Console.WriteLine($"Trust validation failed: {result.Error}");
}
Step 6: Integrate with Verification
public async Task<SecurityKey> ResolveIssuerKey(string issuer)
{
// 1. Resolve trust chain
var trustChain = await resolver.ResolveAsync(issuer, knownTrustAnchor);
// 2. Validate trust chain
var validationResult = validator.Validate(trustChain, trustAnchorKey);
if (!validationResult.IsValid)
{
throw new SecurityException($"Issuer not trusted: {validationResult.Error}");
}
// 3. Return issuer's key from validated chain
return trustChain.LeafConfiguration.Jwks.Keys.First();
}
// Use in verification
var verifier = new SdVerifier(ResolveIssuerKey);
var result = await verifier.VerifyAsync(presentation, params);
Metadata Policies
Intermediates can constrain subordinates:
var policy = new MetadataPolicy
{
CredentialIssuer = new PolicyConstraints
{
// Restrict allowed credential types
AllowedCredentialTypes = new[] { "DriverLicense" },
// Require certain metadata
RequiredMetadata = new[] { "logo_uri", "policy_uri" },
// Constrain values
AllowedAlgorithms = new[] { "ES256", "ES384" }
}
};
Multiple Trust Anchors
Support multiple federations:
var trustedAnchors = new[]
{
"https://federation.example.gov",
"https://industry-trust.example.org"
};
foreach (var anchor in trustedAnchors)
{
try
{
var chain = await resolver.ResolveAsync(issuer, anchor);
if (validator.Validate(chain, GetAnchorKey(anchor)).IsValid)
{
return chain; // Found valid trust path
}
}
catch { /* Try next anchor */ }
}
throw new Exception("No valid trust path found");
Run the Sample
cd samples/SdJwt.Net.Samples
dotnet run -- 3.1
Next Steps
- HAIP Compliance - Security levels
- Multi-Credential Flow - Complex presentations
Key Takeaways
- OpenID Federation establishes hierarchical trust
- Trust chains link entities to trust anchors
- Metadata policies constrain subordinate capabilities
- Verifiers resolve trust before accepting credentials