-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #854 from walt-id/AWS-SDK-KMS
Aws sdk kms
- Loading branch information
Showing
22 changed files
with
609 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
115
waltid-libraries/crypto/waltid-crypto-aws/build.gradle.kts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} |
224 changes: 224 additions & 0 deletions
224
waltid-libraries/crypto/waltid-crypto-aws/src/main/kotlin/id.walt.crypto.keys.aws/AWSKey.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
|
||
} |
9 changes: 9 additions & 0 deletions
9
...ies/crypto/waltid-crypto-aws/src/main/kotlin/id.walt.crypto.keys.aws/AWSKeyMetadataSDK.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
Oops, something went wrong.