Credential Exchange Protocols - Best Practices

Guidelines and best practices for using credential exchange protocols effectively and securely.

Table of Contents

  1. Security Best Practices
  2. Performance Optimization
  3. Error Handling
  4. Protocol Selection
  5. Design Patterns
  6. Testing

Security Best Practices

1. Always Validate Inputs

❌ Bad:

1
2
val offer = registry.offerCredential("didcomm", request)
// No validation - may fail with cryptic error

✅ Good:

1
2
3
4
5
6
7
8
9
// Validate before operation
if (!isValidDid(request.issuerDid)) {
    throw IllegalArgumentException("Invalid issuer DID")
}
if (request.credentialPreview.attributes.isEmpty()) {
    throw IllegalArgumentException("Preview must have attributes")
}

val offer = registry.offerCredential("didcomm", request)

Why: Early validation provides better error messages and prevents security issues.


2. Use Secure Key Management

❌ Bad:

1
2
// Storing keys in plain text
val keyStore = InMemoryKeyStore()  // Keys in memory only

✅ Good:

1
2
3
4
5
6
7
8
// Use encrypted key storage
val keyStore = EncryptedFileLocalKeyStore(
    filePath = "/secure/keys",
    masterKey = secureMasterKey
)

// Or use cloud KMS
val kms = AwsKmsService(region = "us-east-1")

Why: Secure key management prevents key theft and unauthorized access.


3. Always Encrypt Messages

❌ Bad:

1
2
3
4
5
6
// Disabling encryption
options = mapOf(
    "fromKeyId" to "did:key:issuer#key-1",
    "toKeyId" to "did:key:holder#key-1",
    "encrypt" to false  // ⚠️ Security risk
)

✅ Good:

1
2
3
4
5
6
// Always encrypt (default)
options = mapOf(
    "fromKeyId" to "did:key:issuer#key-1",
    "toKeyId" to "did:key:holder#key-1"
    // encrypt defaults to true
)

Why: Encryption protects message confidentiality and integrity.


4. Verify Credentials Before Use

❌ Bad:

1
2
3
val issue = registry.issueCredential("didcomm", request)
// Use credential without verification
processCredential(issue.credential)

✅ Good:

1
2
3
4
5
6
7
8
9
10
11
12
val issue = registry.issueCredential("didcomm", request)

// Verify before use
val verification = trustLayer.verify {
    credential(issue.credential)
}

if (verification.valid) {
    processCredential(issue.credential)
} else {
    throw IllegalStateException("Credential invalid: ${verification.errors}")
}

Why: Verification ensures credential authenticity and validity.


5. Use Secure DID Resolution

❌ Bad:

1
2
3
4
// Mock resolver in production
val resolveDid: suspend (String) -> DidDocument? = { did ->
    DidDocument(id = did, verificationMethod = emptyList())
}

✅ Good:

1
2
3
4
// Use real DID resolver
val resolveDid: suspend (String) -> DidDocument? = { did ->
    yourDidResolver.resolve(did)
}

Why: Secure DID resolution ensures you’re using correct keys and identities.


6. Implement Proper Error Handling

❌ Bad:

1
2
val offer = registry.offerCredential("didcomm", request)
// No error handling - may expose sensitive information

✅ Good:

1
2
3
4
5
6
7
8
9
10
try {
    val offer = registry.offerCredential("didcomm", request)
} catch (e: IllegalArgumentException) {
    logger.error("Invalid argument", e)
    // Don't expose sensitive information
    throw UserFriendlyException("Failed to create offer")
} catch (e: Exception) {
    logger.error("Unexpected error", e)
    throw UserFriendlyException("An error occurred")
}

Why: Proper error handling prevents information leakage and improves security.


Performance Optimization

1. Reuse Registry Instances

❌ Bad:

1
2
3
4
5
6
// Creating new registry for each operation
fun createOffer(request: CredentialOfferRequest) {
    val registry = CredentialExchangeProtocolRegistry()
    registry.register(DidCommExchangeProtocol(didCommService))
    return registry.offerCredential("didcomm", request)
}

✅ Good:

1
2
3
4
5
6
7
8
9
10
11
12
// Reuse registry instance
class CredentialService {
    private val registry = CredentialExchangeProtocolRegistry()

    init {
        registry.register(DidCommExchangeProtocol(didCommService))
    }

    suspend fun createOffer(request: CredentialOfferRequest) {
        return registry.offerCredential("didcomm", request)
    }
}

Why: Reusing instances reduces overhead and improves performance.


2. Use Connection Pooling for Database Storage

❌ Bad:

1
2
// Creating new connection for each operation
val dataSource = DriverManagerDataSource(url, user, password)

✅ Good:

1
2
3
4
5
6
7
8
// Use connection pooling
val dataSource = HikariDataSource().apply {
    jdbcUrl = url
    username = user
    password = password
    maximumPoolSize = 10
    minimumIdle = 5
}

Why: Connection pooling improves database performance and resource usage.


3. Cache DID Documents

❌ Bad:

1
2
3
4
// Resolving DID every time
val resolveDid: suspend (String) -> DidDocument? = { did ->
    yourDidResolver.resolve(did)  // Network call every time
}

✅ Good:

1
2
3
4
5
6
7
8
// Cache DID documents
val didCache = mutableMapOf<String, DidDocument?>()

val resolveDid: suspend (String) -> DidDocument? = { did ->
    didCache.getOrPut(did) {
        yourDidResolver.resolve(did)
    }
}

Why: Caching reduces network calls and improves performance.


4. Use Async Operations

❌ Bad:

1
2
3
4
// Blocking operations
val offer = runBlocking {
    registry.offerCredential("didcomm", request)
}

✅ Good:

1
2
3
4
5
// Async operations
suspend fun createOffer(request: CredentialOfferRequest) {
    val offer = registry.offerCredential("didcomm", request)
    // Process asynchronously
}

Why: Async operations improve concurrency and responsiveness.


5. Batch Operations When Possible

❌ Bad:

1
2
3
4
5
// Processing one at a time
for (request in requests) {
    val offer = registry.offerCredential("didcomm", request)
    processOffer(offer)
}

✅ Good:

1
2
3
4
5
6
7
8
// Batch processing
val offers = requests.map { request ->
    async {
        registry.offerCredential("didcomm", request)
    }
}.awaitAll()

offers.forEach { processOffer(it) }

Why: Batch processing improves throughput and efficiency.


Error Handling

1. Always Handle Errors

❌ Bad:

1
2
val offer = registry.offerCredential("didcomm", request)
// No error handling

✅ Good:

1
2
3
4
5
6
7
8
9
try {
    val offer = registry.offerCredential("didcomm", request)
} catch (e: IllegalArgumentException) {
    // Handle invalid argument
} catch (e: UnsupportedOperationException) {
    // Handle unsupported operation
} catch (e: Exception) {
    // Handle other errors
}

Why: Error handling prevents crashes and improves reliability.


2. Provide User-Friendly Error Messages

❌ Bad:

1
2
3
catch (e: Exception) {
    throw e  // Technical error message
}

✅ Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
catch (e: IllegalArgumentException) {
    when {
        e.message?.contains("not registered") == true -> {
            throw UserFriendlyException("Protocol not available. Please contact support.")
        }
        e.message?.contains("Missing required option") == true -> {
            throw UserFriendlyException("Missing required configuration. Please check settings.")
        }
        else -> {
            throw UserFriendlyException("Invalid request. Please check your input.")
        }
    }
}

Why: User-friendly messages improve user experience.


3. Log Errors for Debugging

❌ Bad:

1
2
3
4
catch (e: Exception) {
    // No logging
    throw UserFriendlyException("An error occurred")
}

✅ Good:

1
2
3
4
5
6
catch (e: Exception) {
    logger.error("Failed to create offer", e)
    logger.debug("Request: $request")
    logger.debug("Protocol: didcomm")
    throw UserFriendlyException("An error occurred")
}

Why: Logging helps with debugging and troubleshooting.


Protocol Selection

1. Choose Protocol Based on Requirements

Decision Tree:

1
2
3
4
5
6
7
8
9
Need peer-to-peer encryption?
├─ Yes → Use DIDComm
└─ No
    ├─ Web-based OAuth integration?
    │  ├─ Yes → Use OIDC4VCI
    │  └─ No
    │     └─ Browser-based wallet?
    │        ├─ Yes → Use CHAPI
    │        └─ No → Use DIDComm (default)

Why: Choosing the right protocol improves security, performance, and compatibility.


2. Support Multiple Protocols

❌ Bad:

1
2
// Only supporting one protocol
val offer = registry.offerCredential("didcomm", request)

✅ Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Support multiple protocols with fallback
suspend fun offerWithFallback(
    request: CredentialOfferRequest
): CredentialOfferResponse {
    val protocols = listOf("didcomm", "oidc4vci", "chapi")

    for (protocol in protocols) {
        if (registry.isRegistered(protocol)) {
            try {
                return registry.offerCredential(protocol, request)
            } catch (e: Exception) {
                logger.warn("Protocol $protocol failed: ${e.message}")
                continue
            }
        }
    }

    throw IllegalStateException("All protocols failed")
}

Why: Multiple protocols provide redundancy and flexibility.


Design Patterns

1. Use Factory Pattern for Service Creation

❌ Bad:

1
2
// Creating services directly
val didCommService = InMemoryDidCommService(packer, resolveDid)

✅ Good:

1
2
// Using factory
val didCommService = DidCommFactory.createInMemoryService(kms, resolveDid)

Why: Factory pattern provides consistent creation and configuration.


2. Use Registry Pattern for Protocol Management

❌ Bad:

1
2
3
// Managing protocols manually
val protocols = mutableMapOf<String, CredentialExchangeProtocol>()
protocols["didcomm"] = DidCommExchangeProtocol(didCommService)

✅ Good:

1
2
3
// Using registry
val registry = CredentialExchangeProtocolRegistry()
registry.register(DidCommExchangeProtocol(didCommService))

Why: Registry pattern provides centralized management and discovery.


3. Use Strategy Pattern for Protocol Selection

❌ Bad:

1
2
// Hard-coded protocol selection
val offer = registry.offerCredential("didcomm", request)

✅ Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Strategy-based selection
class ProtocolSelector {
    fun selectProtocol(context: ExchangeContext): String {
        return when {
            context.needsEncryption -> "didcomm"
            context.isWebBased -> "oidc4vci"
            context.isBrowser -> "chapi"
            else -> "didcomm"
        }
    }
}

val protocol = protocolSelector.selectProtocol(context)
val offer = registry.offerCredential(protocol, request)

Why: Strategy pattern provides flexible protocol selection.


Testing

1. Use In-Memory Implementations for Testing

❌ Bad:

1
2
3
// Using production services in tests
val kms = AwsKmsService(region = "us-east-1")
val didCommService = DidCommFactory.createDatabaseService(...)

✅ Good:

1
2
3
// Using test implementations
val kms = InMemoryKeyManagementService()
val didCommService = DidCommFactory.createInMemoryService(kms, resolveDid)

Why: In-memory implementations are faster and don’t require external services.


2. Test Error Scenarios

❌ Bad:

1
2
3
4
5
6
// Only testing happy path
@Test
fun testOfferCredential() {
    val offer = registry.offerCredential("didcomm", request)
    assertNotNull(offer)
}

✅ Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Testing error scenarios
@Test
fun testOfferCredential_ProtocolNotRegistered() {
    val registry = CredentialExchangeProtocolRegistry()
    assertThrows<IllegalArgumentException> {
        runBlocking {
            registry.offerCredential("didcomm", request)
        }
    }
}

@Test
fun testOfferCredential_InvalidDID() {
    val request = CredentialOfferRequest(
        issuerDid = "invalid-did",  // Invalid format
        holderDid = "did:key:holder",
        credentialPreview = preview
    )
    assertThrows<IllegalArgumentException> {
        runBlocking {
            registry.offerCredential("didcomm", request)
        }
    }
}

Why: Testing error scenarios improves reliability and robustness.


3. Test Protocol Switching

❌ Bad:

1
2
3
4
5
6
// Only testing one protocol
@Test
fun testOfferCredential() {
    val offer = registry.offerCredential("didcomm", request)
    assertNotNull(offer)
}

✅ Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Testing multiple protocols
@Test
fun testOfferCredential_MultipleProtocols() {
    registry.register(DidCommExchangeProtocol(didCommService))
    registry.register(Oidc4VciExchangeProtocol(oidc4vciService))

    val didCommOffer = runBlocking {
        registry.offerCredential("didcomm", request)
    }
    assertNotNull(didCommOffer)

    val oidcOffer = runBlocking {
        registry.offerCredential("oidc4vci", request)
    }
    assertNotNull(oidcOffer)
}

Why: Testing multiple protocols ensures compatibility and flexibility.