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.