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
Simple explanation¶
OpenID Federation establishes trust between organizations by publishing cryptographic statements in a chain. Think of it as a hierarchy of accreditation: a top authority vouches for intermediate bodies, which vouch for individual issuers and verifiers.
Trust chain is not business policy. A valid trust chain proves that an entity is cryptographically vouched for. It does not prove the entity is authorized to issue a specific credential type. Business authorization checks are separate.
Packages used¶
| Package | Purpose |
|---|---|
SdJwt.Net.OidFederation |
Trust chain resolution and validation |
Where this fits¶
flowchart TB
A["Trust Anchor"] --> B["Intermediate"]
B --> C["Issuer / Verifier"]
C --> D["Entity Statement"]
style A fill:#2a6478,color:#fff
style B fill:#2a6478,color:#fff
style C fill:#2a6478,color:#fff
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
OpenID Federation addresses this by creating hierarchical trust anchors.
Trust hierarchy¶
graph TD
TA[Trust Anchor<br/>Root of Trust]
TA --> AG[Authority - Govt]
TA --> AI[Authority - Industry]
TA --> AR[Authority - Region]
AG --> I1[Issuer]
AI --> I2[Issuer]
AR --> V1[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¶
The trust anchor issues a statement about a 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.Logic;
// Configure trust anchors (public keys you trust)
var trustAnchors = new Dictionary<string, SecurityKey>
{
["https://federation.example.gov"] = trustAnchorPublicKey
};
var resolver = new TrustChainResolver(
httpClient,
trustAnchors,
options: new TrustChainResolverOptions
{
MaxPathLength = 10,
EnableCaching = true,
CacheDurationMinutes = 60
});
// Resolve trust chain from issuer to trust anchor
var trustChainResult = await resolver.ResolveAsync(
"https://university.example.edu");
// 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: Use resolved trust chain¶
if (trustChainResult.IsValid)
{
Console.WriteLine("Trust chain is valid");
// Access the resolved metadata and keys from the trust chain
}
else
{
Console.WriteLine($"Trust validation failed: {string.Join(", ", trustChainResult.Errors)}");
}
Step 6: Integrate with verification¶
public async Task<SecurityKey> ResolveIssuerKey(string issuer)
{
// 1. Resolve trust chain
var trustChainResult = await resolver.ResolveAsync(issuer);
// 2. Check result
if (!trustChainResult.IsValid)
{
throw new SecurityException(
$"Issuer not trusted: {string.Join(", ", trustChainResult.Errors)}");
}
// 3. Return issuer's key from validated chain
return trustChainResult.ResolvedKeys.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 Profile Validation - HAIP Final flows and credential profiles
- Multi-Credential Flow - Complex presentations
Expected output¶
Trust anchor configured
Entity statement created for: https://issuer.example.com
Trust chain resolved: 3 statements
Chain validation: valid
Demo vs production¶
Trust chains must be fetched over HTTPS from real .well-known/openid-federation endpoints in production. This tutorial simulates the chain locally.
Common mistakes¶
- Confusing trust chain validation (cryptographic) with business policy (the chain proves identity, not that the issuer is authorized for a particular credential type)
- Setting trust chain cache TTL too high (stale chains may contain revoked entities)
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