Skip to main content
Version: v0.25.0 (Latest)

JOSE and COSE Operations

The IDK supports two major cryptographic message formats: JOSE (JSON Object Signing and Encryption) and COSE (CBOR Object Signing and Encryption). JOSE is the primary format used in OpenID protocols (OID4VP, OID4VCI, SD-JWT), while COSE is used for ISO mDoc credentials.

Which format you use is typically dictated by the protocol, not personal preference. If you are issuing or verifying SD-JWT credentials through OID4VP/OID4VCI, you will use JOSE. If you are working with ISO 18013-5 mobile driving licenses (mDoc), you will use COSE. Many real-world deployments need both, which is why the IDK provides conversion utilities between them.

Format Overview

AspectJOSECOSE
EncodingText (JSON)Binary (CBOR)
SizeLargerCompact
Primary UseOAuth, OpenID, Web APIsmDoc, IoT
Key FormatJWKCOSE_Key
SignatureJWSCOSE_Sign1
EncryptionJWECOSE_Encrypt

JwtService - JWS and JWT Operations

JWS (JSON Web Signature) provides integrity protection for a payload. A JWT is just a JWS whose payload happens to be a JSON claims set. Compact serialization (header.payload.signature) is what you will use most of the time, but the JSON serialization formats exist for cases where you need multiple signatures or want to keep the structure as regular JSON.

The JwtService is the primary service for creating and verifying JSON Web Signatures (JWS) and JSON Web Tokens (JWT). It provides a command-based API for all JWS operations.

Obtaining the Service

val jwtService = session.graph.jwtService

// Access individual commands
val createJwsCompact = jwtService.commands.createJwsCompact
val createJwsJsonFlattened = jwtService.commands.createJwsJsonFlattened
val createJwsJsonGeneral = jwtService.commands.createJwsJsonGeneral
val verifyJws = jwtService.commands.verifyJws

Creating JWS with Existing Keys

The most common use case is signing with a key that already exists in your KMS. Reference keys by alias, key ID, or pass the key directly:

// Reference key by alias (most common for KMS-managed keys)
val byAlias = ManagedOptsAlias(identifier = "my-signing-key")

// Reference key by key ID
val byKid = ManagedOptsKid(identifier = "key-123")

// Reference key by KeyInfo (with additional metadata)
val byKeyInfo = ManagedOptsKeyInfo(
identifier = KeyInfo(
alias = "my-signing-key",
providerId = "software",
signatureAlgorithm = SignatureAlgorithm.ECDSA_SHA256
)
)

// Pass an existing JWK directly
val byJwk = ManagedOptsJwk(identifier = existingJwkDto)

Creating Compact JWS

Compact serialization produces the familiar header.payload.signature format:

// Create payload
val payload = mapOf(
"iss" to "https://issuer.example.com",
"sub" to "user-123",
"aud" to "https://verifier.example.com",
"iat" to System.currentTimeMillis() / 1000,
"exp" to (System.currentTimeMillis() / 1000) + 3600,
"name" to "Alice Smith"
)

// Create compact JWS using key alias
val result = jwtService.createJwsCompact(
CreateJwsArgs(
issuer = ManagedOptsAlias("my-signing-key"),
payload = payload,
mode = JwsIdentifierMode.KID, // Include key ID in header
opts = CreateJwsOpts(
protectedHeader = buildJsonObject {
put("typ", "JWT")
}
)
)
)

if (result.isOk) {
val jwt = result.value.jwt
println("JWT: $jwt")
// eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im15LXNpZ25pbmcta2V5In0.eyJpc3MiOi...
} else {
println("Error: ${result.error.message}")
}

JWS Identifier Modes

Control how the signing key is identified in the JWS header:

ModeHeader FieldUse Case
KIDkidKey ID reference - verifier looks up key
JWKjwkEmbed public key in header
X5Cx5cEmbed X.509 certificate chain
DIDkid (DID URL)Decentralized identifier reference
AUTOAutomaticIDK selects based on key metadata
// Embed the public key in the header
val jwsWithEmbeddedKey = jwtService.createJwsCompact(
CreateJwsArgs(
issuer = ManagedOptsAlias("my-signing-key"),
payload = payload,
mode = JwsIdentifierMode.JWK // Embeds public JWK in header
)
)

// Include X.509 certificate chain
val jwsWithCertChain = jwtService.createJwsCompact(
CreateJwsArgs(
issuer = ManagedOptsKeyInfo(
identifier = KeyInfo(
alias = "my-signing-key",
x5c = arrayOf(base64EncodedCert)
)
),
payload = payload,
mode = JwsIdentifierMode.X5C
)
)

Creating JSON Serialized JWS

JWS JSON serialization comes in two flavors. Flattened is a single-signature JSON object, useful when you need JSON structure but only one signer. General supports an array of signatures, which lets multiple parties sign the same payload independently.

// JSON Flattened - single signature in JSON format
val flattenedResult = jwtService.createJwsJsonFlattened(
CreateJwsJsonArgs(
issuer = ManagedOptsAlias("my-signing-key"),
payload = payload,
mode = JwsIdentifierMode.KID
)
)

// Result structure:
// {
// "payload": "eyJpc3MiOi...",
// "protected": "eyJhbGciOi...",
// "header": { "kid": "my-signing-key" },
// "signature": "DtEhU3lj..."
// }

// JSON General - supports multiple signatures
val generalResult = jwtService.createJwsJsonGeneral(
CreateJwsJsonArgs(
issuer = ManagedOptsAlias("primary-key"),
payload = payload,
mode = JwsIdentifierMode.KID,
existingSignatures = listOf(existingSignature) // Add to existing signatures
)
)

// Result structure:
// {
// "payload": "eyJpc3MiOi...",
// "signatures": [
// { "protected": "...", "header": {...}, "signature": "..." },
// { "protected": "...", "header": {...}, "signature": "..." }
// ]
// }

Verifying JWS

Verify signatures using the verifier's key or by resolving the key from the JWS header:

// Parse the JWS
val jws = Jws.fromCompact(jwtString)

// Verify with explicit key
val result = jwtService.verifyJws(
VerifyJwsArgs(
jws = jws,
identifier = ManagedOptsAlias("issuer-public-key")
)
)

if (result.isOk) {
val validation = result.value
if (validation.isValid) {
println("Signature valid")
println("Payload: ${jws.payload.decodeToString()}")
} else {
println("Invalid: ${validation.errorMessages.joinToString()}")
}
} else {
println("Verification error: ${result.error.message}")
}

// Verify using embedded JWK from header (if present)
val autoResult = jwtService.verifyJws(
VerifyJwsArgs(
jws = jws,
identifier = null // Key resolved from header
)
)

JweService - JWE Encryption Operations

While JWS protects integrity (anyone can read the payload, but nobody can tamper with it), JWE protects confidentiality. JWE encrypts the payload so only the intended recipient can read it. The two-step process (prepare, then create) separates key agreement from serialization, which matters when encrypting to multiple recipients with different key types.

The JweService handles JSON Web Encryption for encrypting content to one or more recipients.

Obtaining the Service

val jweService = session.graph.jweService

// Access individual commands
val prepareJwe = jweService.commands.prepareJwe
val createJweCompact = jweService.commands.createJweCompact
val createJweJsonFlattened = jweService.commands.createJweJsonFlattened
val createJweJsonGeneral = jweService.commands.createJweJsonGeneral
val decryptJwe = jweService.commands.decryptJwe

JWE Algorithm Selection

JWE uses two algorithms: one for key encryption (wrapping the content encryption key so the recipient can unwrap it) and one for content encryption (the actual AES encryption of the payload). For most use cases, ECDH-ES+A256KW with A256GCM is a good default for EC keys, and RSA-OAEP-256 with A256GCM for RSA keys.

Key Encryption Algorithms:

AlgorithmDescriptionKey Type
RSA-OAEPRSA with OAEP paddingRSA
RSA-OAEP-256RSA-OAEP with SHA-256RSA
ECDH-ESDirect ECDH key agreementEC
ECDH-ES+A128KWECDH with AES key wrapEC
ECDH-ES+A256KWECDH with AES-256 key wrapEC
A128KWAES key wrap (symmetric)Symmetric
A256KWAES-256 key wrap (symmetric)Symmetric
dirDirect encryption (no key wrap)Symmetric

Content Encryption Algorithms:

AlgorithmDescription
A128GCMAES-128 in GCM mode
A192GCMAES-192 in GCM mode
A256GCMAES-256 in GCM mode
A128CBC-HS256AES-128-CBC with HMAC-SHA-256
A256CBC-HS512AES-256-CBC with HMAC-SHA-512

Creating Compact JWE

val plaintext = "Sensitive data to encrypt".encodeToByteArray()

// Step 1: Prepare the JWE (generates CEK, builds headers)
val prepareResult = jweService.prepareJwe(
PrepareJweArgs(
plaintext = plaintext,
recipient = ManagedOptsAlias("recipient-public-key"),
keyEncryptionAlg = "RSA-OAEP-256",
contentEncryptionAlg = "A256GCM",
opts = CreateJweOpts(
compress = false
)
)
)

if (prepareResult.isOk) {
val prepared = prepareResult.value

// Step 2: Create the compact JWE
val jweResult = jweService.createJweCompact(
CreateJweCompactArgs(
preparedJwe = prepared,
aad = null // Optional additional authenticated data
)
)

if (jweResult.isOk) {
val jwe = jweResult.value
println("JWE: ${jwe.compact}")
// eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.OKOaw...
} else {
println("Encryption failed: ${jweResult.error.message}")
}
} else {
println("Preparation failed: ${prepareResult.error.message}")
}

Creating Multi-Recipient JWE

JSON General serialization supports encrypting to multiple recipients:

// Prepare for primary recipient
val prepareResult = jweService.prepareJwe(
PrepareJweArgs(
plaintext = plaintext,
recipient = ManagedOptsAlias("recipient-1-key"),
keyEncryptionAlg = "ECDH-ES+A256KW",
contentEncryptionAlg = "A256GCM"
)
)

if (prepareResult.isOk) {
val prepared = prepareResult.value

// Create multi-recipient JWE
val generalResult = jweService.createJweJsonGeneral(
CreateJweJsonGeneralArgs(
preparedJwe = prepared,
additionalRecipients = listOf(
AdditionalRecipient(
recipient = ManagedOptsAlias("recipient-2-key"),
keyEncryptionAlg = "ECDH-ES+A256KW"
),
AdditionalRecipient(
recipient = ManagedOptsAlias("recipient-3-key"),
keyEncryptionAlg = "RSA-OAEP-256"
)
)
)
)

// Result: JweJsonGeneral with recipients array
// Each recipient can decrypt using their own key
} else {
// handle error
}

Decrypting JWE

// Parse the JWE (handles compact, flattened, and general formats)
val jwe = Jwe.parse(jweString)

// Decrypt using recipient's private key
val decryptResult = jweService.decryptJwe(
DecryptJweArgs(
jwe = jwe,
decryptor = ManagedOptsAlias("my-private-key")
)
)

if (decryptResult.isOk) {
val decrypted = decryptResult.value
val plaintext = decrypted.plaintext
println("Decrypted: ${plaintext.decodeToString()}")

// Access header information
val header = decrypted.header
println("Algorithm: ${header.alg}")
println("Encryption: ${header.enc}")
} else {
println("Decryption failed: ${decryptResult.error.message}")
}

COSE Operations

COSE is the binary counterpart to JOSE, encoding everything in CBOR instead of JSON. The main structure you will work with is COSE_Sign1, which is a single-signer signature. This is the format ISO 18013-5 uses for mDoc issuer-signed data. If you are not working with mDoc or CBOR protocols, you probably want the JOSE/JWS section above instead.

The IDK provides CoseCryptoService for COSE operations.

Obtaining the Service

val coseCryptoService = session.graph.coseCryptoService

Creating COSE_Sign1

COSE_Sign1 is the single-signer signature format used in mDoc:

// Build COSE_Sign1 input
val coseSign1Input = CoseSign1Input(
protectedHeader = CoseHeaderCbor(
alg = SignatureAlgorithm.ECDSA_SHA256.cose
),
unprotectedHeader = null,
payload = CborByteString("Payload data".encodeToByteArray())
)

// Get key info for signing
val keyInfo = KeyInfo<CoseKeyType>(
alias = "my-signing-key",
providerId = "mobile",
keyVisibility = KeyVisibility.PRIVATE,
signatureAlgorithm = SignatureAlgorithm.ECDSA_SHA256
)

// Sign
val signResult = coseCryptoService.sign1<ByteArray>(
input = coseSign1Input,
keyInfo = keyInfo,
requireX5Chain = false
)

val coseSign1 = signResult.coseSign1
val cborBytes = coseSign1.toCbor()

Verifying COSE_Sign1

// Parse COSE_Sign1 from CBOR bytes
val coseSign1 = CoseSign1.fromCbor<ByteArray>(cborBytes)

// Get public key for verification
val publicKeyInfo = KeyInfo<CoseKeyType>(
alias = "issuer-public-key",
providerId = "software",
keyVisibility = KeyVisibility.PUBLIC
)

// Verify
val verificationResult = coseCryptoService.verify1(
input = coseSign1,
keyInfo = publicKeyInfo,
requireX5Chain = false
)

if (verificationResult.isValid) {
val payload = coseSign1.payload?.value
println("Verified payload: ${payload?.decodeToString()}")
} else {
println("Verification failed: ${verificationResult.error}")
}

Key Format Conversion

In practice, you often have a key in one format but need it in the other. For example, your KMS stores keys as JWKs, but you need a COSE_Key to sign an mDoc. The IDK provides bidirectional conversion between JWK and COSE_Key that preserves key material, key IDs, and algorithm metadata.

Converting Existing Keys

Use KeyInfo or ResolvedKeyInfo to work with existing keys and convert between formats:

// Start with an existing JWK
val existingJwk: Jwk = loadJwkFromSomewhere()

// Create KeyInfo for JOSE operations
val joseKeyInfo = ResolvedKeyInfo(
key = existingJwk,
kid = existingJwk.kid,
keyVisibility = KeyVisibility.PUBLIC,
signatureAlgorithm = SignatureAlgorithm.ECDSA_SHA256,
keyEncoding = KeyEncoding.JOSE
)

// Convert JWK to COSE Key
val coseKey = CoseKey.fromJwk(existingJwk)

// Create KeyInfo for COSE operations
val coseKeyInfo = ResolvedKeyInfo(
key = coseKey,
kid = coseKey.kid?.value,
keyVisibility = KeyVisibility.PUBLIC,
signatureAlgorithm = SignatureAlgorithm.ECDSA_SHA256,
keyEncoding = KeyEncoding.COSE
)

Direct Key Conversion

Convert keys directly between formats:

// JWK to COSE Key
val jwk: Jwk = ...
val coseKey = CoseKey.fromJwk(jwk)

// COSE Key to JWK
val coseKey: CoseKey = ...
val jwk = coseKey.toJwk()

// Both directions preserve key material and metadata
println("Original kid: ${jwk.kid}")
println("Converted kid: ${coseKey.kid?.value}")

Algorithm Mapping

COSE identifies algorithms by integer IDs (e.g., -7 for ES256) while JOSE uses string names (e.g., "ES256"). The SignatureAlgorithm enum is the IDK's unified representation, and you can convert to either format from it.

// From SignatureAlgorithm to both formats
val algorithm = SignatureAlgorithm.ECDSA_SHA256
val coseAlg = algorithm.cose // CoseAlgorithm.ES256 (-7)
val joseAlg = algorithm.jose // JwaAlgorithm.ES256 ("ES256")

// Convert COSE algorithm to JOSE
val joseFromCose = SignatureAlgorithm.toJose(CoseAlgorithm.ES256)
// Result: JwaAlgorithm.ES256

// Convert JOSE algorithm to COSE
val coseFromJose = SignatureAlgorithm.toCose(JwaAlgorithm.RS256)
// Result: CoseAlgorithm.RS256

// Flexible conversion from any format
val alg = SignatureAlgorithm.toJoseAlg(-7) // Works with Int (COSE ID)
val alg2 = SignatureAlgorithm.toCoseAlg("ES256") // Works with String (JOSE name)

Key Type and Curve Mapping

// Key type mapping
val keyType = KeyTypeMapping.EC
val joseKty = keyType.jose // JwaKeyType.EC
val coseKty = keyType.cose // CoseKeyTypeEnum.EC2

// Convert from COSE to JOSE
val joseKeyType = KeyTypeMapping.toJose(CoseKeyTypeEnum.EC2)
// Result: JwaKeyType.EC

// Curve mapping
val curve = Curve.P_256
val joseCrv = curve.jose // JwaCurve.P_256
val coseCrv = curve.cose // CoseCurve.P_256

// Key operations mapping
val ops = KeyOperations.SIGN
val joseOps = ops.jose // JoseKeyOperations.SIGN
val coseOps = ops.cose // CoseKeyOperations.SIGN

Algorithm Reference

The tables below list all supported algorithms with their COSE and JOSE identifiers. For new deployments, ES256 (P-256) is the most widely supported choice across both JOSE and COSE ecosystems. Use Ed25519 if you want smaller signatures and do not need NIST curve compatibility. RSA algorithms are mainly relevant for legacy systems.

Signature Algorithms

AlgorithmCOSE IDJOSE NameCurve/Key
ED25519-8EdDSAEd25519
ECDSA_SHA256-7ES256P-256
ECDSA_SHA384-35ES384P-384
ECDSA_SHA512-36ES512P-521
RSA_SHA256-257RS256RSA
RSA_SHA384-258RS384RSA
RSA_SHA512-259RS512RSA
RSA_SSA_PSS_SHA256_MGF1-37PS256RSA
RSA_SSA_PSS_SHA384_MGF1-38PS384RSA
RSA_SSA_PSS_SHA512_MGF1-39PS512RSA

Key Types

Key TypeCOSEJOSE
Elliptic CurveEC2 (2)EC
RSARSA (3)RSA
Octet Key PairOKP (1)OKP

Curves

CurveCOSEJOSE
P-256P-256 (1)P-256
P-384P-384 (2)P-384
P-521P-521 (3)P-521
Ed25519Ed25519 (6)Ed25519
X25519X25519 (4)X25519
secp256k1secp256k1 (8)secp256k1

When to Use JOSE vs COSE

Use JOSE when:

  • Working with OAuth 2.0 or OpenID Connect
  • Implementing OID4VP or OID4VCI protocols
  • Working with SD-JWT credentials
  • Building web APIs
  • Human readability aids debugging

Use COSE when:

  • Working with ISO mDoc credentials (ISO 18013-5)
  • Size constraints are critical
  • Interoperating with CBOR-based systems
  • Building IoT or constrained device applications

Many identity solutions use both formats. The IDK makes it straightforward to work with both and convert between them as needed.