How to Issue a Verifiable Credential
Purpose
This guide shows you how to issue a Verifiable Credential (VC) using TrustWeave. You’ll create a cryptographically signed credential that can be verified by anyone without contacting the issuer. By the end, you’ll have issued a credential with a digital proof that demonstrates authenticity and integrity.
What you’ll accomplish:
- Configure TrustWeave with key management and DID methods
- Create an issuer identity (DID)
- Build and issue a credential with a cryptographic proof
- Verify the credential to confirm it was issued correctly
Why this matters: Verifiable Credentials enable trust without intermediaries. They’re tamper-proof, privacy-preserving, and instantly verifiable—essential for identity systems, academic credentials, professional certifications, and more.
Prerequisites
- Kotlin: 2.2.21+ or higher
- Java: 21 or higher
- TrustWeave SDK: Latest version
- Dependencies:
distribution-allandtestkit(for in-memory KMS)
Required imports:
1
2
3
4
5
6
7
8
9
10
11
12
import org.trustweave.credential.model.ProofType
import org.trustweave.credential.results.IssuanceResult
import org.trustweave.credential.results.VerificationResult
import org.trustweave.did.resolver.DidResolutionResult
import org.trustweave.did.resolver.errorMessage
import org.trustweave.trust.TrustWeave
import org.trustweave.trust.types.DidCreationResult
import org.trustweave.trust.types.getOrThrow
import org.trustweave.credential.results.getOrThrow
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.days
Configuration needed:
- Key Management Service (KMS) provider
- DID method (e.g.,
did:key) - Signing key for the issuer
Before You Begin
A Verifiable Credential is a tamper-proof document that proves something about a subject. TrustWeave handles the cryptographic complexity—you focus on the credential content.
When to use this:
- Issuing academic degrees, professional certifications, or identity documents
- Creating attestations that need to be independently verifiable
- Building systems where trust must be established without a central authority
How it fits in a workflow:
1
2
3
4
5
6
7
8
9
10
11
// 1. Configure TrustWeave
val trustWeave = TrustWeave.build { ... }
// 2. Create issuer DID
val (issuerDid, issuerDoc) = trustWeave.createDid { ... }.getOrThrow()
// 3. Issue credential (IssuanceResult)
val credential = trustWeave.issue { ... }.getOrThrow()
// 4. Verify credential (VerificationResult)
val result = trustWeave.verify { ... }
Step-by-Step Guide
Step 1: Configure TrustWeave
Set up TrustWeave with a Key Management Service and DID method. For development, use the in-memory KMS.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.trustweave.credential.model.ProofType
import org.trustweave.trust.TrustWeave
import org.trustweave.trust.dsl.credential.DidMethods.KEY
import org.trustweave.trust.dsl.credential.KeyAlgorithms.ED25519
import org.trustweave.trust.dsl.credential.KmsProviders.IN_MEMORY
val trustWeave = TrustWeave.build {
// KMS and DID methods auto-discovered via SPI
keys {
provider(IN_MEMORY)
algorithm(ED25519)
}
did {
method(KEY) {
algorithm(ED25519)
}
}
credentials {
defaultProofType(ProofType.Ed25519Signature2020)
}
}
What this does:
keys { }configures the Key Management Service for signingdid { }registers the DID method (e.g.,did:key) for creating issuer identitiescredentials { }sets the default proof type for cryptographic signatures
Note: For production, replace
"inMemory"with a secure KMS provider (AWS KMS, CyberArk, etc.). See the KMS plugins documentation for options.
Step 2: Create an Issuer Identity
Create a DID (Decentralized Identifier) for the credential issuer. This identity will sign the credential.
1
2
3
// Create issuer DID (uses default method from config)
val (issuerDid, issuerDoc) = trustWeave.createDid().getOrThrow()
println("Issuer DID: ${issuerDid.value}")
What this does:
- Creates a new DID using the
did:keymethod - Generates an Ed25519 key pair for signing
- Returns a
Didobject containing the issuer’s identifier
The DID document includes verification methods that prove ownership of the signing key.
Step 3: Get the Signing Key
Retrieve the key ID from the issuer’s DID document. This key will be used to sign the credential.
1
2
3
4
5
6
import org.trustweave.did.identifiers.extractKeyId
// Extract key ID from DID document (already available from createDid().getOrThrow())
val keyId = issuerDoc.verificationMethod.firstOrNull()?.extractKeyId()
?: throw IllegalStateException("No verification method found")
println("Signing key ID: $keyId")
What this does:
- Gets the verification method (public key) that will be used for signing
- Extracts the key ID fragment using the type-safe
extractKeyId()extension function - Returns the key ID string (e.g., “key-1”) which references the private key stored in the KMS
Tip: In production, you might store the key ID separately or derive it from your key management workflow.
Step 4: Build the Credential
Define the credential content using the DSL builder. Specify the subject, types, and metadata.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.trustweave.credential.results.IssuanceResult
import org.trustweave.did.identifiers.Did
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.days
val credential = trustWeave.issue {
credential {
id("https://example.edu/credentials/degree-123")
type("DegreeCredential", "BachelorDegreeCredential")
issuer(issuerDid)
subject {
id(Did("did:key:student-456"))
"degree" {
"type" to "BachelorDegree"
"name" to "Bachelor of Science in Computer Science"
"university" to "Example University"
"graduationDate" to "2023-05-15"
"gpa" to "3.8"
}
}
issued(Clock.System.now())
expires((365 * 10).days) // Valid for 10 years
}
signedBy(issuerDid)
}.getOrThrow()
What this does:
credential { }builds the credential structureid()sets a unique credential identifiertype()specifies credential types (VerifiableCredential is added automatically)issuer()sets the issuer DIDsubject { }defines the credential claims about the subjectissued()andexpires()set validity period
by()specifies the issuer DID and key ID for signing
Key concepts:
- Subject: The entity the credential is about (e.g., a student)
- Claims: Properties asserted about the subject (e.g., degree, GPA)
- Types: Semantic meaning of the credential (e.g., DegreeCredential)
Step 5: Issue the Credential
The issue { } block automatically generates a cryptographic proof and attaches it to the credential.
1
2
3
4
5
6
7
val issuedCredential = trustWeave.issue {
credential {
// ... credential definition from Step 4
}
signedBy(issuerDid)
withProof(ProofSuiteId.VC_LD)
}.getOrThrow()
What happens internally:
- TrustWeave canonicalizes the credential JSON (JCS)
- Computes a cryptographic digest
- Signs the digest using the issuer’s private key (via KMS)
- Creates a proof object with the signature
- Attaches the proof to the credential
Result:
The credential now contains a proof property that anyone can verify without contacting the issuer.
Step 6: Verify the Credential
Verify the credential to confirm the proof is valid and the credential hasn’t been tampered with.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.trustweave.credential.results.VerificationResult
import org.trustweave.trust.types.issuerValid
import org.trustweave.trust.types.proofValid
val verificationResult = trustWeave.verify {
credential(issuedCredential)
checkExpiration()
}
when (verificationResult) {
is VerificationResult.Valid -> {
println("✅ Credential is valid!")
println(" - Proof valid: ${verificationResult.proofValid}")
println(" - Issuer resolved: ${verificationResult.issuerValid}")
}
is VerificationResult.Invalid -> {
println("❌ Verification failed: ${verificationResult.allErrors.joinToString("; ")}")
}
}
What this does:
- Resolves the issuer DID to get the public key
- Verifies the cryptographic signature
- Checks expiration (if enabled)
- Returns a sealed result type for exhaustive handling
Complete Example
Here’s a complete, runnable example that brings all steps together:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import org.trustweave.trust.TrustWeave
import org.trustweave.credential.model.ProofType
import org.trustweave.credential.results.VerificationResult
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.days
import org.trustweave.trust.types.getOrThrowDid
import org.trustweave.did.resolver.DidResolutionResult
import org.trustweave.did.identifiers.extractKeyId
import org.trustweave.trust.dsl.credential.DidMethods.KEY
import org.trustweave.trust.dsl.credential.KeyAlgorithms.ED25519
import org.trustweave.trust.dsl.credential.KmsProviders.IN_MEMORY
import org.trustweave.credential.results.getOrThrow
fun main() = runBlocking {
// Step 1: Configure TrustWeave
val trustWeave = TrustWeave.build {
keys {
provider(IN_MEMORY)
algorithm(ED25519)
}
did {
method(KEY) {
algorithm(ED25519)
}
}
credentials {
defaultProofType(ProofType.Ed25519Signature2020)
}
}
// Step 2: Create issuer DID
val issuerDid = trustWeave.createDid {
method(KEY)
}.getOrThrowDid()
println("Issuer DID: ${issuerDid.value}")
// Step 3: Get signing key
val issuerDocument = when (val res = trustWeave.resolveDid(issuerDid)) {
is DidResolutionResult.Success -> res.document
else -> throw IllegalStateException(res.errorMessage ?: "Failed to resolve DID")
}
val verificationMethod = issuerDocument.verificationMethod.firstOrNull()
?: throw IllegalStateException("No verification method found")
val keyId = verificationMethod.extractKeyId()
?: throw IllegalStateException("Failed to extract key ID")
// Step 4 & 5: Issue credential
val issuedCredential = trustWeave.issue {
credential {
id("https://example.edu/credentials/degree-123")
type("DegreeCredential")
issuer(issuerDid)
subject {
id("did:key:student-456")
"degree" {
"name" to "Bachelor of Science"
"university" to "Example University"
}
}
issued(Clock.System.now())
expires((365 * 10).days)
}
signedBy(issuerDid = issuerDid, keyId = keyId)
}.getOrThrow()
println("Credential issued:")
println(" - ID: ${issuedCredential.id}")
println(" - Issuer: ${issuedCredential.issuer}")
println(" - Has proof: ${issuedCredential.proof != null}")
// Step 6: Verify credential
val result = trustWeave.verify {
credential(issuedCredential)
checkExpiration()
}
when (result) {
is VerificationResult.Valid -> println("✅ Credential verified successfully")
else -> println("[FAIL] Verification failed: ${result.allErrors.joinToString()}")
}
}
Visual Flow Diagram
flowchart TD
A[Step 1: Configure TrustWeave<br/>- KMS Provider<br/>- DID Method<br/>- Proof Type] --> B[Step 2: Create Issuer Identity<br/>- Generate key pair<br/>- Create DID document<br/>- Store keys in KMS]
B --> C[Step 3: Get Signing Key<br/>- Resolve DID document<br/>- Get verification method<br/>- Reference private key]
C --> D[Step 4: Build Credential<br/>- Define subject & claims<br/>- Set types & metadata<br/>- Specify issuer & validity]
D --> E[Step 5: Sign & Issue<br/>- Canonicalize JSON (JCS)<br/>- Compute digest<br/>- Sign with private key<br/>- Attach proof]
E --> F[Step 6: Verify Credential<br/>- Resolve issuer DID<br/>- Get public key<br/>- Verify signature<br/>- Check expiration]
F --> G{Valid?}
G -->|Yes| H["[OK] Valid"]
G -->|No| I["[FAIL] Invalid"]
style A fill:#1976d2,stroke:#0d47a1,stroke-width:2px,color:#fff
style B fill:#388e3c,stroke:#1b5e20,stroke-width:2px,color:#fff
style C fill:#f57c00,stroke:#e65100,stroke-width:2px,color:#fff
style D fill:#7b1fa2,stroke:#4a148c,stroke-width:2px,color:#fff
style E fill:#c2185b,stroke:#880e4f,stroke-width:2px,color:#fff
style F fill:#00796b,stroke:#004d40,stroke-width:2px,color:#fff
style H fill:#4caf50,stroke:#2e7d32,stroke-width:2px,color:#fff
style I fill:#f44336,stroke:#c62828,stroke-width:2px,color:#fff
Verification Step
After issuing, verify the credential programmatically:
1
2
3
4
5
6
7
8
9
10
// Quick verification check
val isValid = when (trustWeave.verify {
credential(issuedCredential)
checkExpiration()
}) {
is VerificationResult.Valid -> true
else -> false
}
println("Credential is ${if (isValid) "valid" else "invalid"}")
Expected output:
1
2
3
4
5
6
Issuer DID: did:key:z6Mk...
Credential issued:
- ID: https://example.edu/credentials/degree-123
- Issuer: did:key:z6Mk...
- Has proof: true
✅ Credential verified successfully
What to check:
- Credential has a
proofproperty - Proof type matches (e.g.,
Ed25519Signature2020) - Verification returns
VerificationResult.Valid - No errors in verification result
Common Errors & Troubleshooting
Error: “Issuer identity is required”
Problem: The by() method wasn’t called or the issuer DID/key ID is missing.
Solution:
1
2
3
4
5
6
7
8
9
10
11
// ⌠Missing issuer identity
trustWeave.issue {
credential { ... }
// Missing: signedBy(issuerDid = ..., keyId = ...)
}
// ✅ Correct
trustWeave.issue {
credential { ... }
signedBy(issuerDid = issuerDid, keyId = keyId)
}
Error: “KMS is not configured”
Problem: TrustWeave wasn’t configured with a Key Management Service.
Solution:
1
2
3
4
5
6
7
8
9
10
11
import org.trustweave.trust.dsl.credential.KmsProviders.IN_MEMORY
import org.trustweave.trust.dsl.credential.KeyAlgorithms.ED25519
// ✅ Ensure KMS is configured
val trustWeave = TrustWeave.build {
// KMS and DID methods auto-discovered via SPI
keys {
provider(IN_MEMORY) // or your KMS provider
algorithm(ED25519)
}
// ... rest of config
}
Error: “DID method ‘key’ not registered”
Problem: The DID method wasn’t registered in the configuration.
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
import org.trustweave.trust.dsl.credential.DidMethods.KEY
import org.trustweave.trust.dsl.credential.KeyAlgorithms.ED25519
import org.trustweave.trust.dsl.credential.KmsProviders.IN_MEMORY
// ✅ Register DID method
val trustWeave = TrustWeave.build {
keys { provider(IN_MEMORY); algorithm(ED25519) }
did {
method(KEY) {
algorithm(ED25519)
}
}
}
Error: “No verification method found”
Problem: The issuer DID document doesn’t have a verification method, or the DID wasn’t resolved correctly.
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.trustweave.did.resolver.DidResolutionResult
// ✅ Prefer createDid: you get the document immediately (no extra resolve)
import org.trustweave.did.identifiers.extractKeyId
import org.trustweave.trust.types.getOrThrow
import org.trustweave.credential.results.getOrThrow
val (issuerDid, issuerDoc) = trustWeave.createDid { /* method/algorithm if needed */ }.getOrThrow()
val keyId = issuerDoc.verificationMethod.firstOrNull()?.extractKeyId()
?: throw IllegalStateException("No verification method found")
// If you must resolve later, use the sealed result (not a nullable document)
val issuerDocument = when (val res = trustWeave.resolveDid(issuerDid)) {
is DidResolutionResult.Success -> res.document
else -> throw IllegalStateException("DID not found or not resolvable")
}
val keyIdFromResolve = issuerDocument.verificationMethod.firstOrNull()?.extractKeyId()
?: throw IllegalStateException("No verification method")
Error: “Invalid proof” during verification
Problem: The credential was modified after issuance, or the issuer DID can’t be resolved.
Solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ Ensure issuer DID is resolvable
val result = trustWeave.verify {
credential(issuedCredential)
checkExpiration()
}
// Check specific error
when (result) {
is VerificationResult.Invalid.InvalidProof -> {
println("Proof invalid: ${result.reason}")
// Common causes:
// - Credential was modified after issuance
// - Issuer DID can't be resolved
// - Public key doesn't match signing key
}
// ... other cases
}
Warning: Credential expires soon
Problem: The credential expiration date is in the past or very near.
Solution:
1
2
3
4
5
6
7
8
// ✅ Set appropriate expiration
credential {
// ...
issued(Clock.System.now())
expires((365 * 10).days) // 10 years
// or
expires(Clock.System.now().plus(365.days)) // 1 year from now
}
Next Steps
Now that you can issue credentials, here are ways to extend your implementation:
1. Add Revocation Support
Enable credential revocation using status lists:
1
2
3
4
5
val credential = trustWeave.issue {
credential { ... }
signedBy(issuerDid = issuerDid, keyId = keyId)
withRevocation() // Auto-creates status list
}
See: How to Revoke Credentials
2. Store Credentials in a Wallet
Let credential holders store and manage their credentials:
1
2
3
4
5
6
7
8
import org.trustweave.trust.types.getOrThrow
val wallet = trustWeave.wallet {
holder(holderDid.value)
enableOrganization()
}.getOrThrow()
val stored = issuedCredential.storeIn(wallet)
See: How to Manage Credentials in Wallets
3. Create Verifiable Presentations
Allow holders to present credentials selectively:
1
2
3
4
5
6
7
8
9
import org.trustweave.trust.dsl.wallet.presentationFromWalletResult
import org.trustweave.trust.types.getOrThrow
import org.trustweave.credential.results.getOrThrow
val presentation = trustWeave.presentationFromWalletResult(wallet) {
fromWallet(credentialId)
holder(holderDid.value)
challenge("job-application-123")
}.getOrThrow()
See: How to Create Verifiable Presentations
4. Configure Trust Registry
Establish trust anchors for credential verification:
1
2
3
4
5
6
trustWeave.trust {
addAnchor(issuerDid.value) {
credentialTypes("DegreeCredential")
description("Trusted university")
}
}
See: How to Configure Trust Registries
Summary
You’ve learned how to issue Verifiable Credentials with TrustWeave:
✅ Configured TrustWeave with KMS and DID methods
✅ Created an issuer identity using DIDs
✅ Built and issued a credential with cryptographic proof
✅ Verified the credential to confirm authenticity
Key takeaways:
- TrustWeave handles cryptographic complexity—you focus on credential content
- The DSL builder makes credential creation type-safe and ergonomic
- Sealed result types (
VerificationResult) enable exhaustive error handling - Credentials are tamper-proof and independently verifiable
What’s next:
- Explore revocation for credential lifecycle management
- Set up wallets for credential storage and organization
- Configure trust registries for production deployments
- Integrate with blockchain anchoring for additional immutability
For more examples, see the scenarios documentation.