From 7fc6982fa849958adf3760ab4bff57ffe8b29c03 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 7 Aug 2024 00:39:18 +0200 Subject: [PATCH] feat: persist generated keys --- .../server/admin/controllers/KeyController.kt | 55 ++-- .../com/sphereon/oid/fed/openapi/openapi.yaml | 258 ++++++++++++++++-- .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 4 +- .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 18 +- .../oid/fed/common/jwt/JoseJwt.jvm.kt | 32 ++- .../persistence/repositories/KeyRepository.kt | 52 +++- .../commonMain/resources/db/migration/1.sql | 26 +- .../commonMain/resources/db/migration/2.sql | 12 - .../sphereon/oid/fed/persistence/models/1.sqm | 1 - .../sphereon/oid/fed/persistence/models/2.sqm | 31 ++- .../oid/fed/persistence/models/Key.sq | 24 +- .../sphereon/oid/fed/services/KeyService.kt | 56 +++- .../fed/services/extensions/KeyExtensions.kt | 15 +- 13 files changed, 495 insertions(+), 89 deletions(-) delete mode 100644 modules/persistence/src/commonMain/resources/db/migration/2.sql diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index 82cda1d1..a4a76ed9 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -1,24 +1,33 @@ -package com.sphereon.oid.fed.server.admin.controllers - -import com.sphereon.oid.fed.openapi.models.JwkDto -import com.sphereon.oid.fed.persistence.models.Jwk -import com.sphereon.oid.fed.services.KeyService -import org.springframework.web.bind.annotation.* - -@RestController -@RequestMapping("/accounts/{accountUsername}/keys") -class KeyController { - private val keyService = KeyService() - - @PostMapping - fun create(@PathVariable accountUsername: String): Int { - val key = keyService.create(accountUsername) - return key.id - } - - @GetMapping - fun getKeys(@PathVariable accountUsername: String): List { - val keys = keyService.getKeys(accountUsername) - return keys - } +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.services.KeyService +import com.sphereon.oid.fed.services.extensions.toJwkDTO +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts/{accountUsername}/keys") +class KeyController { + private val keyService = KeyService() + + @PostMapping + fun create(@PathVariable accountUsername: String): JwkDto { + val key = keyService.create(accountUsername) + return key.toJwkDTO() + } + + @GetMapping + fun getKeys(@PathVariable accountUsername: String): List { + val keys = keyService.getKeys(accountUsername) + return keys + } + + @DeleteMapping("/{keyId}") + fun revokeKey( + @PathVariable accountUsername: String, + @PathVariable keyId: Int, + @RequestParam reason: String? + ): JwkDto { + return keyService.revokeKey(accountUsername, keyId, reason) + } } \ No newline at end of file diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index bf3c787c..a69353f0 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -25,7 +25,7 @@ tags: servers: - description: SwaggerHub API Auto Mocking - url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d35 + url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d36 paths: /status: @@ -1856,46 +1856,183 @@ components: properties: kty: type: string - description: The "kty" (key type) parameter identifies the cryptographic algorithm family used with the key, such as "RSA" or "EC". + description: The key type (e.g., EC, RSA). example: RSA + crv: + type: string + description: The elliptic curve used (only for EC keys). + example: P-256 + nullable: true + kid: + type: string + description: The key ID (optional). + example: 12345 + nullable: true + x: + type: string + description: The X coordinate for EC keys (optional). + example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 + nullable: true + y: + type: string + description: The Y coordinate for EC keys (optional). + example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 + nullable: true + n: + type: string + description: The modulus for RSA keys. + example: modulus_value + nullable: true + e: + type: string + description: The exponent for RSA keys. + example: AQAB + nullable: true + alg: + type: string + description: The algorithm associated with the key. + example: ES256 + nullable: true use: type: string - description: The "use" (public key use) parameter identifies the intended use of the public key. + description: The intended use of the key (e.g., sig, enc). example: sig - key_ops: + nullable: true + x5u: type: string - description: The "key_ops" (key operations) parameter identifies the operation(s) for which the key is intended to be used. - example: encrypt - alg: + format: uri + description: A URL that points to an X.509 public key certificate or certificate chain. + example: https://example.com/cert.pem + nullable: true + x5c: + type: array + items: + type: string + description: A base64-encoded string representing an X.509 certificate. + example: MIICoTCCAYkCAQ... + description: The X.509 certificate chain. + nullable: true + x5t: + type: string + description: The SHA-1 thumbprint of the X.509 certificate. + example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA + nullable: true + x5tS256: # Renamed to comply with OpenAPI restrictions type: string - description: The "alg" (algorithm) parameter identifies the algorithm intended for use with the key. - example: RS256 + description: The SHA-256 thumbprint of the X.509 certificate. + example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... + nullable: true + revoked: + $ref: '#/components/schemas/JWTRevoked' + + JwtWithPrivateKey: + type: object + x-tags: + - federation + required: + - kty + properties: + kty: + type: string + description: The key type (e.g., EC, RSA). + example: RSA + crv: + type: string + description: The elliptic curve used (only for EC keys). + example: P-256 + nullable: true kid: type: string - description: The "kid" (key ID) parameter is used to match a specific key. - example: 1 + description: The key ID (optional). + example: 12345 + nullable: true + x: + type: string + description: The X coordinate for EC keys (optional). + example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 + nullable: true + y: + type: string + description: The Y coordinate for EC keys (optional). + example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 + nullable: true + n: + type: string + description: The modulus for RSA keys. + example: modulus_value + nullable: true + e: + type: string + description: The exponent for RSA keys. + example: AQAB + nullable: true + alg: + type: string + description: The algorithm associated with the key. + example: ES256 + use: + type: string + description: The intended use of the key (e.g., sig, enc). + example: sig + nullable: true x5u: type: string - description: The "x5u" (X.509 URL) parameter is a URI that refers to a resource for an X.509 public key certificate or certificate chain. + format: uri + description: A URL that points to an X.509 public key certificate or certificate chain. example: https://example.com/cert.pem + nullable: true x5c: type: array - description: The "x5c" (X.509 certificate chain) parameter contains a chain of one or more PKIX certificates. items: type: string - example: - - MIIDQzCCA...+3whvMF1XEt0K2bA8wpPmSTPgQ== + description: A base64-encoded string representing an X.509 certificate. + example: MIICoTCCAYkCAQ... + description: The X.509 certificate chain. + nullable: true x5t: type: string - description: The "x5t" (X.509 certificate SHA-1 thumbprint) parameter is a base64url-encoded SHA-1 thumbprint of the DER encoding of an X.509 certificate. - example: 0fVuYF8jJ3onI+9Zk2/Iy+Oh5ZpE - x5t#S256: + description: The SHA-1 thumbprint of the X.509 certificate. + example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA + nullable: true + x5tS256: + type: string + description: The SHA-256 thumbprint of the X.509 certificate. + example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... + nullable: true + d: + type: string + description: The private key value (for RSA and EC keys). + example: base64url_encoded_private_key + nullable: true + p: + type: string + description: The first prime factor (for RSA private key). + example: base64url_encoded_p + nullable: true + q: + type: string + description: The second prime factor (for RSA private key). + example: base64url_encoded_q + nullable: true + dp: type: string - description: The "x5t#S256" (X.509 certificate SHA-256 thumbprint) parameter is a base64url-encoded SHA-256 thumbprint of the DER encoding of an X.509 certificate. - example: 1MvI4/VhnEzTz7Jo/0Q/d/jI3rE7IMoMT34wvAjyLvs + description: The first factor CRT exponent (for RSA private key). + example: base64url_encoded_dp + nullable: true + dq: + type: string + description: The second factor CRT exponent (for RSA private key). + example: base64url_encoded_dq + nullable: true + qi: + type: string + description: The first CRT coefficient (for RSA private key). + example: base64url_encoded_qi + nullable: true revoked: $ref: '#/components/schemas/JWTRevoked' + JwkDto: type: object x-tags: @@ -1914,20 +2051,91 @@ components: type: integer description: The ID of the account associated with this JWK. example: 100 - created_at: + kty: type: string - format: date-time - description: The timestamp when the JWK was created. - example: 2024-08-06T12:34:56Z + description: The key type (e.g., EC, RSA). + example: RSA + crv: + type: string + description: The elliptic curve used (only for EC keys). + example: P-256 + nullable: true + kid: + type: string + description: The key ID (optional). + example: 12345 + nullable: true + x: + type: string + description: The X coordinate for EC keys (optional). + example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 + nullable: true + y: + type: string + description: The Y coordinate for EC keys (optional). + example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 + nullable: true + n: + type: string + description: The modulus for RSA keys. + example: modulus_value + nullable: true + e: + type: string + description: The exponent for RSA keys. + example: AQAB + nullable: true + alg: + type: string + description: The algorithm associated with the key. + example: ES256 + nullable: true + use: + type: string + description: The intended use of the key (e.g., sig, enc). + example: sig + nullable: true + x5u: + type: string + format: uri + description: A URL that points to an X.509 public key certificate or certificate chain. + example: https://example.com/cert.pem + nullable: true + x5c: + type: array + items: + type: string + description: A base64-encoded string representing an X.509 certificate. + example: MIICoTCCAYkCAQ... + description: The X.509 certificate chain. + nullable: true + x5t: + type: string + description: The SHA-1 thumbprint of the X.509 certificate. + example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA + nullable: true + x5t#S256: + type: string + description: The SHA-256 thumbprint of the X.509 certificate. + example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... + nullable: true revoked_at: type: string format: date-time description: The timestamp when the JWK was revoked, if applicable. example: 2024-09-01T12:34:56Z + nullable: true revoked_reason: type: string description: The reason for revoking the JWK, if applicable. example: Key compromise + nullable: true + created_at: + type: string + format: date-time + description: The timestamp when the JWK was created. + example: 2024-08-06T12:34:56Z + nullable: true JWTRevoked: @@ -3778,4 +3986,4 @@ components: enum: - LOCAL description: Enum for KMS integrations. - example: LOCAL + example: LOCAL \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index 17b10f3c..10e36873 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -1,8 +1,10 @@ package com.sphereon.oid.fed.common.jwt +import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey + expect class JwtHeader expect class JwtPayload expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String expect fun verify(jwt: String, key: Any, opts: Map): Boolean -expect fun generateKeyPair(): String \ No newline at end of file +expect fun generateKeyPair(): JwtWithPrivateKey \ No newline at end of file diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 541ccaff..cab1eaa6 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -2,6 +2,7 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.openapi.models.EntityStatement import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -12,13 +13,16 @@ external object Jose { constructor(payload: dynamic) { definedExternally } + fun setProtectedHeader(protectedHeader: dynamic): SignJWT { definedExternally } + fun sign(key: Any?, signOptions: Any?): String { definedExternally } } + fun generateKeyPair(alg: String, options: dynamic = definedExternally): dynamic fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic } @@ -50,6 +54,16 @@ actual fun verify( return Jose.jwtVerify(jwt, key, opts) } -actual fun generateKeyPair(): String { - return Jose.generateKeyPair("EC").toString() +actual fun generateKeyPair(): JwtWithPrivateKey { + val key = Jose.generateKeyPair("EC") + return JwtWithPrivateKey( + d = key.d, + alg = key.alg, + crv = key.crv, + x = key.x, + y = key.y, + kid = key.kid, + kty = key.kty as? String, + use = key.use, + ) } diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index 0a45a249..72da1b8f 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.common.jwt +import com.nimbusds.jose.Algorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.JWSSigner import com.nimbusds.jose.JWSVerifier @@ -11,6 +12,9 @@ import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT +import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey + +import java.util.* actual typealias JwtPayload = JWTClaimsSet actual typealias JwtHeader = JWSHeader @@ -21,7 +25,7 @@ actual fun sign( opts: Map ): String { val rsaJWK = opts["key"] as RSAKey? ?: throw IllegalArgumentException("The RSA key pair is required") - + val signer: JWSSigner = RSASSASigner(rsaJWK) val signedJWT = SignedJWT( @@ -49,14 +53,28 @@ actual fun verify( } } -actual fun generateKeyPair(): String { +actual fun generateKeyPair(): JwtWithPrivateKey { try { + val ecKey: ECKey = ECKeyGenerator(Curve.P_256) + .keyIDFromThumbprint(true) + .algorithm(Algorithm("EC")) + .issueTime(Date()) + .expirationTime(Calendar.getInstance().apply { + time = Date() + add(Calendar.YEAR, 1) + }.time) + .generate() - val key: ECKey = ECKeyGenerator(Curve.P_256) - .keyID("123") - .generate() - - return key.toJSONString() + return JwtWithPrivateKey( + d = ecKey.d.toString(), + alg = ecKey.algorithm.name, + crv = ecKey.curve.name, + kid = ecKey.keyID, + kty = ecKey.keyType.value, + use = ecKey.keyUse?.value ?: "sig", + x = ecKey.x.toString(), + y = ecKey.y.toString() + ) } catch (e: Exception) { throw Exception("Couldn't generate the EC Key Pair: ${e.message}", e) diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt index 17e63912..0105e22d 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt @@ -10,11 +10,59 @@ class KeyRepository(keyQueries: KeyQueries) { return keyQueries.findById(id).executeAsOneOrNull() } - fun create(accountId: Int, jwk: String): Jwk { - return keyQueries.create(accountId, jwk).executeAsOne() + fun create( + accountId: Int, + kty: String, + e: String? = null, + n: String? = null, + x: String? = null, + y: String? = null, + alg: String? = null, + crv: String? = null, + kid: String? = null, + use: String? = null, + x5c: List? = null, + x5t: String? = null, + x5u: String? = null, + d: String? = null, + p: String? = null, + q: String? = null, + dp: String? = null, + dq: String? = null, + qi: String? = null, + x5ts256: String? = null + ): Jwk { + val createdKey = keyQueries.create( + accountId, + kty = kty, + e = e, + n = n, + x = x, + y = y, + alg = alg, + crv = crv, + kid = kid, + use = use, + x5c = x5c as Array?, + x5t = x5t, + x5u = x5u, + d = d, + p = p, + q = q, + dp = dp, + dq = dq, + qi = qi, + x5t_s256 = x5ts256 + ) + + return createdKey.executeAsOne() } fun findByAccountId(accountId: Int): List { return keyQueries.findByAccountId(accountId).executeAsList() } + + fun revokeKey(id: Int, reason: String? = null) { + return keyQueries.revoke(reason, id) + } } diff --git a/modules/persistence/src/commonMain/resources/db/migration/1.sql b/modules/persistence/src/commonMain/resources/db/migration/1.sql index 225c2fd5..aa92896a 100644 --- a/modules/persistence/src/commonMain/resources/db/migration/1.sql +++ b/modules/persistence/src/commonMain/resources/db/migration/1.sql @@ -1,4 +1,4 @@ -CREATE TABLE accounts ( +CREATE TABLE account ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -6,6 +6,26 @@ CREATE TABLE accounts ( deleted_at TIMESTAMP ); -CREATE INDEX account_username_index ON accounts (username); +CREATE INDEX account_username_index ON account (username); -INSERT INTO accounts (username) VALUES ('root'); \ No newline at end of file +INSERT INTO account (username) VALUES ('root'); + +CREATE TABLE jwk ( + id SERIAL PRIMARY KEY, + uuid UUID NOT NULL DEFAULT gen_random_uuid(), + account_id INT NOT NULL, + kty VARCHAR(10) NOT NULL, -- Key Type + crv VARCHAR(10), -- Curve (used for EC keys) + kid VARCHAR(255) UNIQUE, -- Key ID + x TEXT, -- X coordinate (for EC keys) + y TEXT, -- Y coordinate (for EC keys) + d TEXT, -- Private key (should be secured) + n TEXT, -- Modulus (for RSA keys) + e TEXT, -- Exponent (for RSA keys) + alg VARCHAR(10), -- Algorithm + use VARCHAR(10), -- Key Use (sig, enc, etc.) + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) -- Foreign key constraint moved here +); + +CREATE INDEX jwk_account_id_index ON jwk (account_id); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/resources/db/migration/2.sql b/modules/persistence/src/commonMain/resources/db/migration/2.sql deleted file mode 100644 index cf2c480c..00000000 --- a/modules/persistence/src/commonMain/resources/db/migration/2.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE jwk ( - id SERIAL PRIMARY KEY, - uuid UUID NOT NULL DEFAULT gen_random_uuid(), - account_id INTEGER NOT NULL, - key JSONB NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - revoked_at TIMESTAMP, - revoked_reason TEXT, - FOREIGN KEY (account_id) REFERENCES account (id) -); - -CREATE INDEX jwks_account_id_index ON jwk (account_id); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm index 5cfa2c48..0c59f113 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm @@ -9,4 +9,3 @@ CREATE TABLE account ( CREATE INDEX account_username_index ON account (username); INSERT INTO account (username) VALUES ('root'); - diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm index fca08ea3..7b42cf9e 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm @@ -1,13 +1,30 @@ CREATE TABLE jwk ( id SERIAL PRIMARY KEY, - uuid UUID NOT NULL DEFAULT gen_random_uuid(), - account_id INTEGER NOT NULL, - key JSONB NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + uuid UUID DEFAULT gen_random_uuid(), + account_id INT NOT NULL, + kty VARCHAR(10) NOT NULL, + crv VARCHAR(10), + kid VARCHAR(255) UNIQUE, + x TEXT, + y TEXT, + d TEXT, + n TEXT, + e TEXT, + p TEXT, + q TEXT, + dp TEXT, + dq TEXT, + qi TEXT, + x5u TEXT, + x5c TEXT[], + x5t TEXT, + x5t_s256 TEXT, + alg VARCHAR(10), + use VARCHAR(10) NULL, revoked_at TIMESTAMP, revoked_reason TEXT, - FOREIGN KEY (account_id) REFERENCES account (id) + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) ); -CREATE INDEX jwks_account_id_index ON jwk (account_id); - +CREATE INDEX jwk_account_id_index ON jwk (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq index 27259f0c..04ff78c0 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq @@ -1,5 +1,27 @@ create: -INSERT INTO jwk (account_id, key) VALUES (?, ?) RETURNING *; +INSERT INTO jwk ( + account_id, + kty, + crv, + kid, + x, + y, + d, + n, + e, + p, + q, + dp, + dq, + qi, + x5u, + x5c, + x5t, + x5t_s256, + alg, + use +) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; revoke: UPDATE jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt index aaa898f4..e8f239bd 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,8 +1,8 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.common.jwt.generateKeyPair import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Jwk import com.sphereon.oid.fed.services.extensions.toJwkDTO @@ -11,15 +11,63 @@ class KeyService { private val keyRepository = Persistence.keyRepository fun create(accountUsername: String): Jwk { - val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val account = + accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id val key = generateKeyPair() - return keyRepository.create(accountId, key) + + val createdKey = keyRepository.create( + accountId, + kty = key.kty, + e = key.e, + n = key.n, + x = key.x, + y = key.y, + d = key.d, + dq = key.dq, + dp = key.dp, + qi = key.qi, + p = key.p, + q = key.q, + x5c = key.x5c, + x5t = key.x5t, + x5u = key.x5u, + x5ts256 = key.x5tS256, + alg = key.alg, + crv = key.crv, + kid = key.kid, + use = key.use, + ) + + return createdKey } fun getKeys(accountUsername: String): List { - val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val account = + accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id return keyRepository.findByAccountId(accountId).map { it.toJwkDTO() } } + + fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkDto { + val account = + accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val accountId = account.id + + var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException("Key not found") + + if (key.account_id != accountId) { + throw IllegalArgumentException("Key does not belong to account") + } + + if (key.revoked_at != null) { + throw IllegalArgumentException("Key already revoked") + } + + keyRepository.revokeKey(keyId, reason) + + key = keyRepository.findById(keyId) ?: throw IllegalArgumentException("Key not found") + + return key.toJwkDTO() + } } diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt index 4b24274b..5db13011 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt @@ -8,8 +8,21 @@ fun Jwk.toJwkDTO(): JwkDto { id = this.id, accountId = this.account_id, uuid = this.uuid.toString(), + e = this.e, + n = this.n, + x = this.x, + y = this.y, + alg = this.alg, + crv = this.crv, + kid = this.kid, + kty = this.kty, + use = this.use, + x5c = this.x5c as List? ?: null, + x5t = this.x5t, + x5u = this.x5u, + x5tHashS256 = this.x5t_s256, createdAt = this.created_at.toString(), revokedAt = this.revoked_at.toString(), - revokedReason = this.revoked_reason, + revokedReason = this.revoked_reason ) }