Skip to content

Tutorial: mdoc Credential Issuance

Build production-ready mdoc credentials with namespaces, validity, and custom claims.

Time: 20 minutes
Level: Intermediate
Sample: samples/SdJwt.Net.Samples/02-Intermediate/06-MdocIssuance.cs

What You Will Learn

  • How to create complete mDL credentials with all required elements
  • How to work with custom namespaces
  • How to handle COSE key operations
  • How to serialize and deserialize mdoc documents

Prerequisites

  • Completed Hello mdoc
  • Understanding of cryptographic key management
  • Familiarity with JSON/CBOR concepts

Complete mDL Issuance

Step 1: Configure the Issuer

using System.Security.Cryptography;
using SdJwt.Net.Mdoc.Cose;
using SdJwt.Net.Mdoc.Issuer;
using SdJwt.Net.Mdoc.Namespaces;

// Production issuance requires proper key management
using var issuerKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
using var deviceKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);

var cryptoProvider = new DefaultCoseCryptoProvider();

Step 2: Build Complete mDL

var mdoc = await new MdocIssuerBuilder()
    .WithDocType(MdlNamespace.DocType) // "org.iso.18013.5.1.mDL"
    .WithIssuerKey(CoseKey.FromECDsa(issuerKey))
    .WithDeviceKey(CoseKey.FromECDsa(deviceKey))
    .WithAlgorithm(CoseAlgorithm.ES256)
    // Required mDL elements
    .AddMdlElement(MdlDataElement.FamilyName, "Johnson")
    .AddMdlElement(MdlDataElement.GivenName, "Alice Marie")
    .AddMdlElement(MdlDataElement.BirthDate, "1995-07-22")
    .AddMdlElement(MdlDataElement.IssueDate, "2024-03-01")
    .AddMdlElement(MdlDataElement.ExpiryDate, "2029-03-01")
    .AddMdlElement(MdlDataElement.IssuingCountry, "US")
    .AddMdlElement(MdlDataElement.IssuingAuthority, "California DMV")
    .AddMdlElement(MdlDataElement.DocumentNumber, "D1234567")
    .AddMdlElement(MdlDataElement.UnDistinguishingSign, "USA")
    // Optional personal data
    .AddMdlElement(MdlDataElement.Sex, "F")
    .AddMdlElement(MdlDataElement.Height, 165)
    .AddMdlElement(MdlDataElement.Weight, 58)
    .AddMdlElement(MdlDataElement.EyeColour, "brown")
    .AddMdlElement(MdlDataElement.HairColour, "black")
    // Address
    .AddMdlElement(MdlDataElement.ResidentAddress, "123 Main Street")
    .AddMdlElement(MdlDataElement.ResidentCity, "Los Angeles")
    .AddMdlElement(MdlDataElement.ResidentState, "CA")
    .AddMdlElement(MdlDataElement.ResidentPostalCode, "90001")
    .AddMdlElement(MdlDataElement.ResidentCountry, "US")
    // Age flags (computed from birth date)
    .AddMdlElement(MdlDataElement.AgeOver18, true)
    .AddMdlElement(MdlDataElement.AgeOver21, true)
    // Validity period
    .WithValidity(
        DateTimeOffset.UtcNow,
        DateTimeOffset.UtcNow.AddYears(5))
    .BuildAsync(cryptoProvider);

Working with Custom Namespaces

Adding Custom Claims

For credentials beyond mDL, use generic namespaces:

var credentialDocType = "org.example.employee.badge.1";
var credentialNamespace = "org.example.employee.1";

var employeeBadge = await new MdocIssuerBuilder()
    .WithDocType(credentialDocType)
    .WithIssuerKey(CoseKey.FromECDsa(issuerKey))
    .WithDeviceKey(CoseKey.FromECDsa(deviceKey))
    // Custom namespace claims
    .AddClaim(credentialNamespace, "employee_id", "EMP-2024-001")
    .AddClaim(credentialNamespace, "full_name", "Alice Johnson")
    .AddClaim(credentialNamespace, "department", "Engineering")
    .AddClaim(credentialNamespace, "clearance_level", 3)
    .AddClaim(credentialNamespace, "building_access", new[] { "HQ", "Lab-A", "Lab-B" })
    .AddClaim(credentialNamespace, "hire_date", "2020-06-15")
    // Access control flags
    .AddClaim(credentialNamespace, "is_manager", true)
    .AddClaim(credentialNamespace, "remote_access_allowed", true)
    .WithValidity(
        DateTimeOffset.UtcNow,
        DateTimeOffset.UtcNow.AddYears(1))
    .BuildAsync(cryptoProvider);

Multiple Namespaces

mdoc credentials can contain multiple namespaces:

var credential = await new MdocIssuerBuilder()
    .WithDocType("org.example.multi.credential")
    .WithIssuerKey(CoseKey.FromECDsa(issuerKey))
    .WithDeviceKey(CoseKey.FromECDsa(deviceKey))
    // Primary namespace
    .AddClaim("org.example.identity", "name", "Alice Johnson")
    .AddClaim("org.example.identity", "birth_date", "1995-07-22")
    // Employment namespace
    .AddClaim("org.example.employment", "employer", "TechCorp Inc")
    .AddClaim("org.example.employment", "position", "Senior Engineer")
    // Certification namespace
    .AddClaim("org.example.certs", "iso27001_certified", true)
    .AddClaim("org.example.certs", "security_clearance", "Secret")
    .WithValidity(
        DateTimeOffset.UtcNow,
        DateTimeOffset.UtcNow.AddYears(2))
    .BuildAsync(cryptoProvider);

COSE Key Operations

Creating Keys from Raw Materials

using SdJwt.Net.Mdoc.Cose;

// From existing ECDsa parameters
var parameters = ecdsa.ExportParameters(includePrivateParameters: true);

var coseKey = new CoseKey
{
    KeyType = CoseKeyTypes.Ec2,
    Curve = CoseCurves.P256,
    X = parameters.Q.X!,
    Y = parameters.Q.Y!,
    D = parameters.D // Private key component
};

// Get public key only (for sharing)
var publicCoseKey = coseKey.GetPublicKey();

Key Serialization

// Serialize to CBOR bytes
byte[] keyBytes = coseKey.ToCbor();

// Deserialize from CBOR
var restoredKey = CoseKey.FromCbor(keyBytes);

// Convert back to .NET ECDsa
using var restored = restoredKey.ToECDsa();

Algorithm Selection

// ES256 (P-256) - Broad compatibility, recommended default
var es256Builder = new MdocIssuerBuilder()
    .WithAlgorithm(CoseAlgorithm.ES256);

// ES384 (P-384) - Higher security
var es384Builder = new MdocIssuerBuilder()
    .WithAlgorithm(CoseAlgorithm.ES384);

// ES512 (P-521) - Maximum security
var es512Builder = new MdocIssuerBuilder()
    .WithAlgorithm(CoseAlgorithm.ES512);

Document Serialization

Serialize Complete Document

// Serialize document to CBOR
byte[] documentBytes = mdoc.ToCbor();
Console.WriteLine($"Document size: {documentBytes.Length} bytes");

// Deserialize document
using SdJwt.Net.Mdoc.Models;
var restored = Document.FromCbor(documentBytes);

Access Document Components

// Get document type
Console.WriteLine($"DocType: {mdoc.DocType}");

// Access issuer-signed data
var issuerSigned = mdoc.IssuerSigned;

// List namespaces
foreach (var ns in issuerSigned.NameSpaces)
{
    Console.WriteLine($"Namespace: {ns.Key}");
    foreach (var item in ns.Value)
    {
        Console.WriteLine($"  {item.ElementIdentifier}: {item.ElementValue}");
    }
}

Complete Example: DMV Issuer Service

using System.Security.Cryptography;
using SdJwt.Net.Mdoc.Cose;
using SdJwt.Net.Mdoc.Issuer;
using SdJwt.Net.Mdoc.Namespaces;

public class DmvMdlService
{
    private readonly ECDsa _issuerKey;
    private readonly ICoseCryptoProvider _crypto;

    public DmvMdlService(ECDsa issuerKey)
    {
        _issuerKey = issuerKey;
        _crypto = new DefaultCoseCryptoProvider();
    }

    public async Task<byte[]> IssueMdlAsync(
        string familyName,
        string givenName,
        DateTime birthDate,
        string documentNumber,
        byte[] devicePublicKey)
    {
        // Parse device key from wallet
        var deviceKey = CoseKey.FromCbor(devicePublicKey);

        // Calculate age flags
        var age = DateTime.UtcNow.Year - birthDate.Year;
        if (birthDate.Date > DateTime.UtcNow.AddYears(-age)) age--;

        // Issue credential
        var mdoc = await new MdocIssuerBuilder()
            .WithDocType(MdlNamespace.DocType)
            .WithIssuerKey(CoseKey.FromECDsa(_issuerKey))
            .WithDeviceKey(deviceKey)
            .WithAlgorithm(CoseAlgorithm.ES256)
            .AddMdlElement(MdlDataElement.FamilyName, familyName)
            .AddMdlElement(MdlDataElement.GivenName, givenName)
            .AddMdlElement(MdlDataElement.BirthDate, birthDate.ToString("yyyy-MM-dd"))
            .AddMdlElement(MdlDataElement.IssueDate, DateTime.UtcNow.ToString("yyyy-MM-dd"))
            .AddMdlElement(MdlDataElement.ExpiryDate, DateTime.UtcNow.AddYears(5).ToString("yyyy-MM-dd"))
            .AddMdlElement(MdlDataElement.IssuingCountry, "US")
            .AddMdlElement(MdlDataElement.IssuingAuthority, "State DMV")
            .AddMdlElement(MdlDataElement.DocumentNumber, documentNumber)
            .AddMdlElement(MdlDataElement.AgeOver18, age >= 18)
            .AddMdlElement(MdlDataElement.AgeOver21, age >= 21)
            .WithValidity(
                DateTimeOffset.UtcNow,
                DateTimeOffset.UtcNow.AddYears(5))
            .BuildAsync(_crypto);

        return mdoc.ToCbor();
    }
}

Run the Sample

cd samples/SdJwt.Net.Samples
dotnet run -- 2.6

Next Steps

Key Concepts

Concept Description
Namespace Grouping of related claims in mdoc
DocType Unique identifier for credential type
IssuerAuth COSE_Sign1 signature over MSO
Validity Signed validity period in MSO
Device Binding Holder's public key in credential