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

Signing and Verification

The IDK supports creating and verifying cryptographic signatures across multiple algorithms. This guide covers raw signatures, the supported algorithms, and best practices.

The IDK distinguishes between raw signatures and higher-level signing formats like JWS or COSE_Sign. Raw signatures give you a byte-level sign(input) -> signature operation with no envelope or header structure. Higher-level formats (covered in the JWS and COSE guides) wrap the payload, algorithm, and key ID into a self-describing token.

Raw Signatures

Raw signatures operate directly on byte arrays without additional structure. They're suitable when you need full control over the data format or when integrating with systems that expect raw signatures.

Use raw signatures when you are building your own wire format, signing content for a protocol that defines its own envelope (like COSE or CMS), or when you need to produce a detached signature over data whose format you control.

Creating Raw Signatures

val keyManager = session.graph.keyManagerService

// Generate or retrieve a signing key
val keyPair = keyManager.generateKeyAsync(
providerId = null,
alias = "signing-key",
alg = SignatureAlgorithm.ECDSA_SHA256
)

// Prepare the data to sign
val data = "Important message to sign".encodeToByteArray()

// Get key info for signing (need private key access)
val keyInfo = keyPair.cborToManagedKeyInfo(visibility = KeyVisibility.PRIVATE)

// Create the signature
val signature = keyManager.createRawSignature(
keyInfo = keyInfo,
input = data,
requireX5Chain = false
)

// The signature is a raw byte array
println("Signature length: ${signature.size} bytes")

Verifying Raw Signatures

Verification checks that the given signature was produced by the private key corresponding to the supplied public key, over exactly the provided input bytes. A false result means either the data was modified after signing, or the signature was produced by a different key. The method does not throw on a mismatch; it returns a boolean so you can handle failures in your own control flow.

// Get public key info for verification
val publicKeyInfo = keyPair.cborToManagedKeyInfo(visibility = KeyVisibility.PUBLIC)

// Verify the signature
val isValid = keyManager.isValidRawSignature(
keyInfo = publicKeyInfo,
input = data,
signature = signature
)

if (isValid) {
println("Signature verified successfully")
} else {
println("Signature verification failed - data may have been tampered with")
}

Signature Algorithms

The IDK supports multiple signature algorithms. Choose based on your security requirements and interoperability needs.

ECDSA (Elliptic Curve Digital Signature Algorithm)

ECDSA is the most common choice for mobile applications due to its compact signatures and good performance. For most new projects, ECDSA_SHA256 (P-256) is the right default: it is widely supported, required by many credential profiles, and backed by hardware secure elements on both iOS and Android. Only step up to P-384 or P-521 if your threat model or compliance framework specifically requires it.

AlgorithmCurveSignature SizeSecurity Level
ECDSA_SHA256P-25664 bytes128-bit
ECDSA_SHA384P-38496 bytes192-bit
ECDSA_SHA512P-521132 bytes256-bit
// ECDSA_SHA256 - recommended for most use cases
val es256Key = keyManager.generateKeyAsync(
alias = "es256-key",
alg = SignatureAlgorithm.ECDSA_SHA256
)

// ECDSA_SHA384 - higher security, larger signatures
val es384Key = keyManager.generateKeyAsync(
alias = "es384-key",
alg = SignatureAlgorithm.ECDSA_SHA384
)

// ECDSA_SHA512 - highest security, largest signatures
val es512Key = keyManager.generateKeyAsync(
alias = "es512-key",
alg = SignatureAlgorithm.ECDSA_SHA512
)

RSA Signatures

RSA signatures are larger but offer broad compatibility with legacy systems. Prefer RSA-PSS (RSA_SSA_PSS_*) over PKCS#1 v1.5 (RSA_SHA*) for new work, as PSS has a tighter security proof. Use PKCS#1 v1.5 only when interoperating with systems that do not support PSS.

AlgorithmDescription
RSA_SHA256RSA PKCS#1 v1.5 with SHA-256
RSA_SHA384RSA PKCS#1 v1.5 with SHA-384
RSA_SHA512RSA PKCS#1 v1.5 with SHA-512
RSA_SSA_PSS_SHA256_MGF1RSA-PSS with SHA-256
RSA_SSA_PSS_SHA384_MGF1RSA-PSS with SHA-384
RSA_SSA_PSS_SHA512_MGF1RSA-PSS with SHA-512
// RSA-PSS with SHA-256 (recommended over PKCS#1 v1.5)
val rsaPssKey = keyManager.generateKeyAsync(
alias = "rsa-pss-key",
alg = SignatureAlgorithm.RSA_SSA_PSS_SHA256_MGF1
)

// RSA PKCS#1 v1.5 with SHA-256 (for legacy compatibility)
val rsaKey = keyManager.generateKeyAsync(
alias = "rsa-key",
alg = SignatureAlgorithm.RSA_SHA256
)

Including X.509 Certificates

When signing for external parties, you may need to include the X.509 certificate chain that vouches for your signing key. Setting requireX5Chain = true tells the service that the signing key must have an associated certificate chain. If no chain is available for the key, the operation fails rather than silently producing an unsigned or unattested signature.

// Sign with certificate chain included
val signature = keyManager.createRawSignature(
keyInfo = keyInfo,
input = data,
requireX5Chain = true // Include X.509 certificate chain
)

// The key info must have an associated certificate for this to work
// If no certificate is available and requireX5Chain is true,
// the operation will fail

Encryption and Decryption

The KeyManagerService also supports content encryption operations. The encrypt/decrypt methods use authenticated encryption (AES-GCM), which protects both confidentiality and integrity. You can optionally supply Additional Authenticated Data (AAD) that is integrity-protected but not encrypted, useful for binding context like a request ID to the ciphertext.

// Encrypt data
val encryptionResult = keyManager.encrypt(
keyInfo = keyInfo,
plaintext = "Secret message".encodeToByteArray(),
algorithm = ContentEncryptionAlgorithm.A256GCM,
additionalAuthenticatedData = "context".encodeToByteArray() // Optional AAD
)

// Access encrypted components
val ciphertext = encryptionResult.ciphertext
val iv = encryptionResult.iv
val authTag = encryptionResult.authTag

// Decrypt data
val plaintext = keyManager.decrypt(
keyInfo = keyInfo,
ciphertext = ciphertext,
algorithm = ContentEncryptionAlgorithm.A256GCM,
iv = iv,
authTag = authTag,
additionalAuthenticatedData = "context".encodeToByteArray()
)

Content Encryption Algorithms

AlgorithmDescription
A128GCMAES-128-GCM
A192GCMAES-192-GCM
A256GCMAES-256-GCM

Key Wrapping

Key wrapping lets you encrypt one key with another so it can be safely transported or stored outside the KMS. This is common in key distribution protocols where a content encryption key (CEK) is wrapped with a recipient's public key.

Wrap and unwrap keys for secure key transport:

// Wrap a key for transport
val wrappedKey = keyManager.wrapKey(
wrappingKeyInfo = wrappingKeyInfo,
keyToWrap = secretKey,
algorithm = KeyWrapAlgorithm.RSA_OAEP_256
)

// Unwrap a received key
val unwrappedKey = keyManager.unwrapKey(
unwrappingKeyInfo = unwrappingKeyInfo,
wrappedKey = wrappedKey,
algorithm = KeyWrapAlgorithm.RSA_OAEP_256
)

Key Wrap Algorithms

AlgorithmDescription
RSA_OAEPRSA-OAEP with SHA-1
RSA_OAEP_256RSA-OAEP with SHA-256
RSA_OAEP_512RSA-OAEP with SHA-512

Key Agreement

ECDH key agreement derives a shared secret from your private key and the other party's public key. The resulting bytes can be used as a symmetric encryption key or as input to a KDF. Both sides arrive at the same secret without ever transmitting it.

Perform ECDH key agreement to establish shared secrets:

// Perform ECDH key agreement
val sharedSecret = keyManager.performKeyAgreement(
privateKeyInfo = myPrivateKeyInfo,
publicKeyInfo = theirPublicKeyInfo,
algorithm = KeyAgreementAlgorithm.ECDH_ES_HKDF_256,
keyDataLen = 32 // Desired output length in bytes
)

// Use the shared secret for symmetric encryption

Key Agreement Algorithms

AlgorithmDescription
ECDH_ES_HKDF_256ECDH-ES with HKDF-SHA256
ECDH_ES_HKDF_512ECDH-ES with HKDF-SHA512
ECDH_SS_HKDF_256ECDH-SS with HKDF-SHA256
ECDH_SS_HKDF_512ECDH-SS with HKDF-SHA512

Best Practices

When implementing signing and verification:

Always verify signatures before trusting data. Never assume data is authentic without cryptographic verification.

Use appropriate algorithms for your security requirements. ECDSA_SHA256 provides good security for most applications. Use ECDSA_SHA384 or ECDSA_SHA512 for higher security requirements.

Protect signing keys. Use the mobile KMS provider for hardware-backed protection on mobile devices.

Consider replay attacks. Signatures alone don't prevent replay attacks. Include timestamps, nonces, or sequence numbers in signed data.

Log signature failures. Failed signature verifications may indicate attacks or data corruption and should be logged for security monitoring.

Handle errors appropriately. Signature operations can fail for various reasons including key not found, hardware errors, or missing certificates.