Skip to main content
Version: v0.13

JOSE and COSE Operations

The IDK provides comprehensive support for 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.

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​

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.component.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")
}
)
)
)

when (result) {
is IdkResult.Success -> {
val jwt = result.value.jwt
println("JWT: $jwt")
// eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im15LXNpZ25pbmcta2V5In0.eyJpc3MiOi...
}
is IdkResult.Failure -> {
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​

For scenarios requiring JSON format or multiple signatures:

// 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")
)
)

when (result) {
is IdkResult.Success -> {
val validation = result.value
if (validation.isValid) {
println("Signature valid")
println("Payload: ${jws.payload.decodeToString()}")
} else {
println("Invalid: ${validation.errorMessages.joinToString()}")
}
}
is IdkResult.Failure -> {
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​

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

Obtaining the Service​

val jweService = session.component.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 and one for content encryption.

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
)
)
)

when (prepareResult) {
is IdkResult.Success -> {
val prepared = prepareResult.value

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

when (jweResult) {
is IdkResult.Success -> {
val jwe = jweResult.value
println("JWE: ${jwe.compact}")
// eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.OKOaw...
}
is IdkResult.Failure -> {
println("Encryption failed: ${jweResult.error.message}")
}
}
}
is IdkResult.Failure -> {
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"
)
)

when (prepareResult) {
is IdkResult.Success -> {
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
}
is IdkResult.Failure -> { /* 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")
)
)

when (decryptResult) {
is IdkResult.Success -> {
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}")
}
is IdkResult.Failure -> {
println("Decryption failed: ${decryptResult.error.message}")
}
}

COSE Operations​

COSE is used for ISO mDoc credentials and other CBOR-based protocols. The IDK provides CoseCryptoService for COSE operations.

Obtaining the Service​

val coseCryptoService = session.component.coseCryptoService

Creating COSE_Sign1​

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

// Build COSE_Sign1 input
val coseSign1Input = CoseSign1Input(
protectedHeader = CoseHeaderCbor(
algorithm = 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​

The IDK provides mapping utilities to convert between COSE and JOSE formats. This is essential when working with systems that use different formats.

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​

Convert algorithm identifiers between COSE and JOSE:

// 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​

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.