Skip to content

Commit

Permalink
Merge pull request #854 from walt-id/AWS-SDK-KMS
Browse files Browse the repository at this point in the history
Aws sdk kms
  • Loading branch information
philpotisk authored Dec 19, 2024
2 parents daa5a48 + 65722fc commit 251980b
Show file tree
Hide file tree
Showing 22 changed files with 609 additions and 28 deletions.
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ val modules = listOf(
* "$libraries:crypto".group(
"waltid-crypto",
"waltid-crypto-oci",
"waltid-crypto-aws",
"waltid-crypto-android" whenEnabled enableAndroidBuild,
"waltid-crypto-ios" whenEnabled enableIosBuild,
"waltid-target-ios" whenEnabled enableIosBuild,
Expand Down
33 changes: 33 additions & 0 deletions waltid-libraries/crypto/waltid-crypto-aws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# AWS SDK Extension for walt.id Crypto

A Kotlin-based extension that enhances walt.id crypto with native AWS key management capabilities.

## Overview

This extension introduces `AwsKey`, a robust implementation leveraging the AWS SDK for Kotlin to manage cryptographic keys. It serves as a more integrated alternative to the platform-agnostic `AWSKeyRestAPI` found in the base walt.id crypto library.

## Key Features

- Native AWS SDK integration for optimal performance
- Kotlin-specific implementation
- Seamless key management through AWS KMS
- Direct SDK access instead of REST API calls

## Authentication

The extension utilizes AWS SDK's default credential provider chain for authentication, automatically detecting credentials from multiple sources including:

- Environment variables
- AWS credentials file
- IAM roles for EC2
- Container credentials
- SSO credentials

## Comparison to Base Implementation

While the base `AWSKeyRestAPI` offers cross-platform compatibility through REST endpoints, this extension provides:

- Improved performance through direct SDK calls
- Enhanced error handling
- Native integration with AWS services

115 changes: 115 additions & 0 deletions waltid-libraries/crypto/waltid-crypto-aws/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
plugins {
kotlin("jvm") version "2.0.21"
kotlin("plugin.serialization") version "2.0.21"
id("com.github.ben-manes.versions")
id("maven-publish")
}

group = "id.walt.crypto"

repositories {
mavenCentral()
maven("https://jitpack.io")
}

dependencies {
testImplementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")

// walt.id
api(project(":waltid-libraries:crypto:waltid-crypto"))

// JSON
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")

// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

// AWS
implementation("aws.sdk.kotlin:kms:1.3.91")

// JOSE
implementation("com.nimbusds:nimbus-jose-jwt:9.41.1")
}

java {
sourceCompatibility = JavaVersion.VERSION_15
targetCompatibility = JavaVersion.VERSION_15
withJavadocJar()
withSourcesJar()
}

tasks.test {
useJUnitPlatform()
}

tasks.withType<Test> {
enabled = false
}


kotlin {
jvmToolchain(15)
sourceSets {
all {
languageSettings.enableLanguageFeature("InlineClasses")
}
}
}

publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])

pom {
name.set("Walt.id Crypto AWS")
description.set("Walt.id Crypto AWS Integration")
url.set("https://walt.id")

licenses {
license {
name.set("Apache License 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0")
}
}

developers {
developer {
id.set("walt.id")
name.set("walt.id")
email.set("[email protected]")
}
}
}
}
}

repositories {
maven {
val releasesRepoUrl = uri("https://maven.waltid.dev/releases")
val snapshotsRepoUrl = uri("https://maven.waltid.dev/snapshots")
url = uri(
if (version.toString().endsWith("SNAPSHOT")
) snapshotsRepoUrl else releasesRepoUrl
)

val envUsername = System.getenv("MAVEN_USERNAME")
val envPassword = System.getenv("MAVEN_PASSWORD")

val usernameFile = File("$rootDir/secret_maven_username.txt")
val passwordFile = File("$rootDir/secret_maven_password.txt")

val secretMavenUsername = envUsername ?: usernameFile.let {
if (it.isFile) it.readLines().first() else ""
}
val secretMavenPassword = envPassword ?: passwordFile.let {
if (it.isFile) it.readLines().first() else ""
}
credentials {
username = secretMavenUsername
password = secretMavenPassword
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package id.walt.crypto.keys.aws

import aws.sdk.kotlin.services.kms.KmsClient
import aws.sdk.kotlin.services.kms.model.*
import id.walt.crypto.exceptions.KeyTypeNotSupportedException
import id.walt.crypto.keys.EccUtils
import id.walt.crypto.keys.Key
import id.walt.crypto.keys.KeyMeta
import id.walt.crypto.keys.KeyType
import id.walt.crypto.keys.jwk.JWKKey
import id.walt.crypto.utils.Base64Utils.decodeFromBase64
import id.walt.crypto.utils.Base64Utils.encodeToBase64
import id.walt.crypto.utils.Base64Utils.encodeToBase64Url
import id.walt.crypto.utils.JsonUtils.toJsonElement
import id.walt.crypto.utils.jwsSigningAlgorithm
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject


@Serializable
@SerialName("aws")
class AWSKey(
val config: AWSKeyMetadataSDK,
val id: String,
private var _publicKey: String? = null,
private var _keyType: KeyType? = null,

) : Key() {

@Transient
override var keyType: KeyType
get() = _keyType!!
set(value) {
_keyType = value
}

override val hasPrivateKey: Boolean
get() = false


override fun toString(): String = "[AWS ${keyType.name} key @AWS ${config.region} - $id]"

override suspend fun getKeyId(): String = getPublicKey().getKeyId()

override suspend fun getThumbprint(): String {
TODO("Not yet implemented")
}

override suspend fun exportJWK(): String = throw NotImplementedError("JWK export is not available for remote keys.")

override suspend fun exportJWKObject(): JsonObject = Json.parseToJsonElement(_publicKey!!).jsonObject

override suspend fun exportPEM(): String = throw NotImplementedError("PEM export is not available for remote keys.")

private val awsSigningAlgorithm by lazy {
when (keyType) {
KeyType.secp256r1, KeyType.secp256k1 -> "ECDSA_SHA_256"
KeyType.RSA -> "RSASSA_PKCS1_V1_5_SHA_256"
else -> throw KeyTypeNotSupportedException(keyType.name)
}
}

override suspend fun signRaw(plaintext: ByteArray): ByteArray {
val signRequest = SignRequest {
this.keyId = id
signingAlgorithm = SigningAlgorithmSpec.fromValue(awsSigningAlgorithm)
message = plaintext
messageType = MessageType.Raw
}

return KmsClient { region = config.region }.use { kmsClient ->
kmsClient.sign(signRequest).signature ?: throw IllegalStateException("Signature not returned")
}
}


override suspend fun signJws(
plaintext: ByteArray,
headers: Map<String, JsonElement>
): String {
val appendedHeader = HashMap(headers).apply {
put("alg", jwsSigningAlgorithm(keyType).toJsonElement())
}

val header = Json.encodeToString(appendedHeader).encodeToByteArray().encodeToBase64Url()
val payload = plaintext.encodeToBase64Url()

var rawSignature = signRaw("$header.$payload".encodeToByteArray())

if (keyType in listOf(KeyType.secp256r1, KeyType.secp256k1)) { // TODO: Add RSA support
rawSignature = EccUtils.convertDERtoIEEEP1363(rawSignature)
}

val encodedSignature = rawSignature.encodeToBase64Url()
val jws = "$header.$payload.$encodedSignature"

return jws
}

override suspend fun verifyRaw(
signed: ByteArray,
detachedPlaintext: ByteArray?
): Result<ByteArray> {
val verifyRequest = VerifyRequest {
this.keyId = id
signingAlgorithm = SigningAlgorithmSpec.fromValue(awsSigningAlgorithm)
message = detachedPlaintext
this.signature = signed
messageType = MessageType.Raw
}

return KmsClient { region = config.region }.use { kmsClient ->
val response = kmsClient.verify(verifyRequest)
Result.success(response.signatureValid.toString().decodeFromBase64())
}
}


override suspend fun verifyJws(signedJws: String): Result<JsonElement> {
val publicKey = getPublicKey()
val verification = publicKey.verifyJws(signedJws)
return verification
}

@Transient
private var backedKey: Key? = null

override suspend fun getPublicKey(): Key = backedKey ?: when {
_publicKey != null -> _publicKey!!.let { JWKKey.importJWK(it).getOrThrow() }
else -> retrievePublicKey()
}.also { newBackedKey -> backedKey = newBackedKey }


private suspend fun retrievePublicKey(): Key {
val publicKey = getAwsPublicKey(config, id)
_publicKey = publicKey.exportJWK()
return publicKey
}

override suspend fun getPublicKeyRepresentation(): ByteArray {
TODO("Not yet implemented")
}

override suspend fun getMeta(): KeyMeta {
TODO("Not yet implemented")
}

override suspend fun deleteKey(): Boolean {
val request = ScheduleKeyDeletionRequest {
this.keyId = id
pendingWindowInDays = 7
}

val delete = KmsClient { region = config.region }.use { kmsClient ->
kmsClient.scheduleKeyDeletion(request)
}
return delete.keyState?.value == "PendingDeletion"
}


companion object {


suspend fun generateKey(keyType: KeyType, config: AWSKeyMetadataSDK): AWSKey {
val request = CreateKeyRequest {
this.description = description
keySpec = KeySpec.fromValue(keyTypeToAwsKeyMapping(keyType))
keyUsage = KeyUsageType.SignVerify
}
val response = KmsClient { region = config.region }.use { kmsClient ->
kmsClient.createKey(request)
}

val keyid = response.keyMetadata?.keyId ?: throw IllegalStateException("Key ID not returned")
val publicKey = getAwsPublicKey(config, keyid)
val keyType = response.keyMetadata?.keySpec?.value
return AWSKey(
config = config,
id = keyid,
_publicKey = publicKey.exportJWK(),
_keyType = awsKeyToKeyTypeMapping(keyType.toString())
)
}


suspend fun getAwsPublicKey(config: AWSKeyMetadataSDK, keyId: String): Key {
KmsClient { region = config.region }.use { kmsClient ->
val pk = kmsClient.getPublicKey(GetPublicKeyRequest {
this.keyId = keyId
}).publicKey ?: throw IllegalStateException("Public key not returned")
val encodedPk = pk.encodeToBase64()

val pemKey = """
-----BEGIN PUBLIC KEY-----
$encodedPk
-----END PUBLIC KEY-----
""".trimIndent()
val keyJWK = JWKKey.importPEM(pemKey)
return keyJWK.getOrThrow()
}
}

private fun keyTypeToAwsKeyMapping(type: KeyType) = when (type) {
KeyType.secp256r1 -> "ECC_NIST_P256"
KeyType.secp256k1 -> "ECC_SECG_P256K1"
KeyType.RSA -> "RSA_2048"
else -> throw KeyTypeNotSupportedException(type.name)
}

private fun awsKeyToKeyTypeMapping(type: String) = when (type) {
"ECC_NIST_P256" -> KeyType.secp256r1
"ECC_SECG_P256K1" -> KeyType.secp256k1
"RSA_2048" -> KeyType.RSA
else -> throw KeyTypeNotSupportedException(type)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package id.walt.crypto.keys.aws

import kotlinx.serialization.Serializable


@Serializable
data class AWSKeyMetadataSDK (
val region: String
)
Loading

0 comments on commit 251980b

Please sign in to comment.