diff --git a/build.gradle.kts b/build.gradle.kts index af9d60f7..6c4c1d48 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -143,7 +143,6 @@ dependencies { implementation("org.drewcarlson:kjob-core:0.6.0") implementation("org.drewcarlson:kjob-mongo:0.6.0") - testImplementation("org.slf4j:slf4j-simple:2.0.7") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.1") } @@ -152,10 +151,19 @@ detekt { parallel = true config = files("detekt.yml") buildUponDefaultConfig = true - basePath = "${rootDir.absolutePath}/src/" + basePath = "${rootDir.absolutePath}/src/main/kotlin" autoCorrect = true } +tasks.withType() { + exclude("**/generated/**") + doFirst { + + } + setSource("src/main/kotlin") + exclude("build/") +} + tasks.withType().configureEach { exclude("**/org/koin/ksp/generated/**", "**/generated/**") } diff --git a/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt index 5e6c3016..e9786a1f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt @@ -7,6 +7,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import java.time.format.DateTimeFormatter +import java.util.* @Configuration class ActivityPubConfig { @@ -20,4 +22,8 @@ class ActivityPubConfig { .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) return objectMapper } + + @Bean + @Qualifier("http") + fun dateTimeFormatter(): DateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) } diff --git a/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt index f373805c..3332c81d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt @@ -1,7 +1,6 @@ package dev.usbharu.hideout.config import dev.usbharu.hideout.plugins.KtorKeyMap -import dev.usbharu.hideout.plugins.httpSignaturePlugin import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.core.Transaction import io.ktor.client.* @@ -16,9 +15,6 @@ import tech.barbero.http.message.signing.KeyMap class HttpClientConfig { @Bean fun httpClient(keyMap: KeyMap): HttpClient = HttpClient(CIO).config { - install(httpSignaturePlugin) { - this.keyMap = keyMap - } install(Logging) { logger = Logger.DEFAULT level = LogLevel.INFO diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt index 01ed970e..6b235333 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt @@ -1,16 +1,11 @@ package dev.usbharu.hideout.plugins -import com.fasterxml.jackson.databind.ObjectMapper import dev.usbharu.hideout.config.ApplicationConfig -import dev.usbharu.hideout.domain.model.ap.JsonLd import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.user.UserAuthServiceImpl -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.client.* import io.ktor.client.plugins.api.* import io.ktor.client.request.* -import io.ktor.client.statement.* import io.ktor.http.* import kotlinx.coroutines.runBlocking import tech.barbero.http.message.signing.HttpMessage @@ -27,31 +22,6 @@ import java.text.SimpleDateFormat import java.util.* import javax.crypto.SecretKey -suspend fun HttpClient.postAp( - urlString: String, - username: String, - jsonLd: JsonLd, - objectMapper: ObjectMapper -): HttpResponse { - jsonLd.context += "https://www.w3.org/ns/activitystreams" - return this.post(urlString) { - header("Accept", ContentType.Application.Activity) - header("Content-Type", ContentType.Application.Activity) - header("Signature", "keyId=\"$username\",algorithm=\"rsa-sha256\",headers=\"(request-target) digest date\"") - val text = objectMapper.writeValueAsString(jsonLd) - setBody(text) - } -} - -suspend fun HttpClient.getAp(urlString: String, username: String?): HttpResponse { - return this.get(urlString) { - header("Accept", ContentType.Application.Activity) - username?.let { - header("Signature", "keyId=\"$username\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\"") - } - } -} - class HttpSignaturePluginConfig { lateinit var keyMap: KeyMap } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt index 41303839..a989af78 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt @@ -12,19 +12,19 @@ import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.exception.ap.FailedToGetActivityPubResourceException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException -import dev.usbharu.hideout.plugins.getAp -import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.query.FollowerQueryService import dev.usbharu.hideout.query.MediaQueryService import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.repository.PostRepository +import dev.usbharu.hideout.service.ap.resource.APResourceResolveService +import dev.usbharu.hideout.service.ap.resource.resolve +import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.post.PostCreateInterceptor import dev.usbharu.hideout.service.post.PostService import io.ktor.client.* import io.ktor.client.plugins.* -import io.ktor.client.statement.* import kjob.core.job.JobProps import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -68,7 +68,10 @@ class APNoteServiceImpl( private val mediaQueryService: MediaQueryService, @Qualifier("activitypub") private val objectMapper: ObjectMapper, private val applicationConfig: ApplicationConfig, - private val postService: PostService + private val postService: PostService, + private val apResourceResolveService: APResourceResolveService, + private val apRequestService: APRequestService, + private val transaction: Transaction ) : APNoteService, PostCreateInterceptor { @@ -120,17 +123,20 @@ class APNoteServiceImpl( ) val inbox = props[DeliverPostJob.inbox] logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox) - httpClient.postAp( - urlString = inbox, - username = "$actor#pubkey", - jsonLd = Create( - name = "Create Note", - `object` = note, - actor = note.attributedTo, - id = "${applicationConfig.url}/create/note/${postEntity.id}" - ), - objectMapper - ) + + transaction.transaction { + val signer = userQueryService.findByUrl(actor) + apRequestService.apPost( + inbox, + Create( + name = "Create Note", + `object` = note, + actor = note.attributedTo, + id = "${applicationConfig.url}/create/note/${postEntity.id}" + ), + signer + ) + } } override suspend fun fetchNote(url: String, targetActor: String?): Note { @@ -143,11 +149,8 @@ class APNoteServiceImpl( } logger.info("AP GET url: {}", url) - val response = try { - httpClient.getAp( - url, - targetActor?.let { "$targetActor#pubkey" } - ) + val note = try { + apResourceResolveService.resolve(url, null as Long?) } catch (e: ClientRequestException) { logger.warn( "FAILED Failed to retrieve ActivityPub resource. HTTP Status Code: {} url: {}", @@ -156,7 +159,6 @@ class APNoteServiceImpl( ) throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e) } - val note = objectMapper.readValue(response.bodyAsText()) val savedNote = saveIfMissing(note, targetActor, url) logger.debug("SUCCESS Fetch Note url: {}", url) return savedNote diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt index d4c1dfd1..5a8cb788 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt @@ -8,12 +8,10 @@ import dev.usbharu.hideout.domain.model.ap.Undo import dev.usbharu.hideout.domain.model.hideout.entity.Reaction import dev.usbharu.hideout.domain.model.job.DeliverReactionJob import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob -import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.query.FollowerQueryService import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.job.JobQueueParentService -import io.ktor.client.* import kjob.core.job.JobProps import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Service @@ -29,13 +27,12 @@ interface APReactionService { @Service class APReactionServiceImpl( private val jobQueueParentService: JobQueueParentService, - private val httpClient: HttpClient, private val userQueryService: UserQueryService, private val followerQueryService: FollowerQueryService, private val postQueryService: PostQueryService, @Qualifier("activitypub") private val objectMapper: ObjectMapper, - private val applicationConfig: ApplicationConfig - + private val applicationConfig: ApplicationConfig, + private val apRequestService: APRequestService ) : APReactionService { override suspend fun reaction(like: Reaction) { val followers = followerQueryService.findFollowersById(like.userId) @@ -74,17 +71,19 @@ class APReactionServiceImpl( val postUrl = props[DeliverReactionJob.postUrl] val id = props[DeliverReactionJob.id] val content = props[DeliverReactionJob.reaction] - httpClient.postAp( - urlString = inbox, - username = "$actor#pubkey", - jsonLd = Like( + + val signer = userQueryService.findByUrl(actor) + + apRequestService.apPost( + inbox, + Like( name = "Like", actor = actor, `object` = postUrl, id = "${applicationConfig.url}/like/note/$id", content = content ), - objectMapper + signer ) } @@ -92,17 +91,19 @@ class APReactionServiceImpl( val inbox = props[DeliverRemoveReactionJob.inbox] val actor = props[DeliverRemoveReactionJob.actor] val like = objectMapper.readValue(props[DeliverRemoveReactionJob.like]) - httpClient.postAp( - urlString = inbox, - username = "$actor#pubkey", - jsonLd = Undo( + + val signer = userQueryService.findByUrl(actor) + + apRequestService.apPost( + inbox, + Undo( name = "Undo Reaction", actor = actor, `object` = like, id = "${applicationConfig.url}/undo/note/${like.id}", published = Instant.now() ), - objectMapper + signer ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt index 13b95b7d..38af4dd2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt @@ -7,12 +7,10 @@ import dev.usbharu.hideout.domain.model.ActivityPubStringResponse import dev.usbharu.hideout.domain.model.ap.Accept import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob -import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.user.UserService -import io.ktor.client.* import io.ktor.http.* import kjob.core.job.JobProps import org.springframework.beans.factory.annotation.Qualifier @@ -28,10 +26,10 @@ class APReceiveFollowServiceImpl( private val jobQueueParentService: JobQueueParentService, private val apUserService: APUserService, private val userService: UserService, - private val httpClient: HttpClient, private val userQueryService: UserQueryService, private val transaction: Transaction, - @Qualifier("activitypub") private val objectMapper: ObjectMapper + @Qualifier("activitypub") private val objectMapper: ObjectMapper, + private val apRequestService: APRequestService ) : APReceiveFollowService { override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { // TODO: Verify HTTP Signature @@ -50,15 +48,19 @@ class APReceiveFollowServiceImpl( val targetActor = props[ReceiveFollowJob.targetActor] val person = apUserService.fetchPerson(actor, targetActor) val follow = objectMapper.readValue(props[ReceiveFollowJob.follow]) - httpClient.postAp( - urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"), - username = "$targetActor#pubkey", - jsonLd = Accept( + + val signer = userQueryService.findByUrl(actor) + + val urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found") + + apRequestService.apPost( + urlString, + Accept( name = "Follow", `object` = follow, actor = targetActor ), - objectMapper + signer ) val targetEntity = userQueryService.findByUrl(targetActor) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt new file mode 100644 index 00000000..be5c5796 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt @@ -0,0 +1,25 @@ +package dev.usbharu.hideout.service.ap + +import dev.usbharu.hideout.domain.model.ap.Object +import dev.usbharu.hideout.domain.model.hideout.entity.User + +interface APRequestService { + suspend fun apGet(url: String, signer: User? = null, responseClass: Class): R + suspend fun apPost( + url: String, + body: T? = null, + signer: User? = null, + responseClass: Class + ): R + + suspend fun apPost(url: String, body: T? = null, signer: User? = null): String +} + +suspend inline fun APRequestService.apGet(url: String, signer: User? = null): R = + apGet(url, signer, R::class.java) + +suspend inline fun APRequestService.apPost( + url: String, + body: T? = null, + signer: User? = null +): R = apPost(url, body, signer, R::class.java) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt new file mode 100644 index 00000000..4d8e227d --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt @@ -0,0 +1,139 @@ +package dev.usbharu.hideout.service.ap + +import com.fasterxml.jackson.databind.ObjectMapper +import dev.usbharu.hideout.domain.model.ap.Object +import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.service.signature.HttpSignatureSigner +import dev.usbharu.hideout.service.signature.Key +import dev.usbharu.hideout.util.Base64Util +import dev.usbharu.hideout.util.HttpUtil.Activity +import dev.usbharu.hideout.util.RsaUtil +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Service +import java.net.URL +import java.security.MessageDigest +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +@Service +class APRequestServiceImpl( + private val httpClient: HttpClient, + @Qualifier("activitypub") private val objectMapper: ObjectMapper, + private val httpSignatureSigner: HttpSignatureSigner, + @Qualifier("http") private val dateTimeFormatter: DateTimeFormatter, +) : APRequestService { + + override suspend fun apGet(url: String, signer: User?, responseClass: Class): R { + val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT"))) + val u = URL(url) + if (signer?.privateKey == null) { + val bodyAsText = httpClient.get(url) { + header("Accept", ContentType.Application.Activity) + header("Date", date) + }.bodyAsText() + return objectMapper.readValue(bodyAsText, responseClass) + } + + val headers = headers { + append("Accept", ContentType.Application.Activity) + append("Date", date) + append("Host", u.host) + } + + val sign = httpSignatureSigner.sign( + url = url, + method = HttpMethod.Get, + headers = headers, + requestBody = "", + keyPair = Key( + keyId = "${signer.url}#pubkey", + privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey), + publicKey = RsaUtil.decodeRsaPublicKeyPem(signer.publicKey) + ), + signHeaders = listOf("(request-target)", "date", "host", "accept") + ) + + val bodyAsText = httpClient.get(url) { + headers { + headers { + appendAll(sign.headers) + remove("Host") + } + } + contentType(ContentType.Application.Activity) + }.bodyAsText() + return objectMapper.readValue(bodyAsText, responseClass) + } + + override suspend fun apPost( + url: String, + body: T?, + signer: User?, + responseClass: Class + ): R { + val bodyAsText = apPost(url, body, signer) + return objectMapper.readValue(bodyAsText, responseClass) + } + + override suspend fun apPost(url: String, body: T?, signer: User?): String { + if (body != null) { + val mutableListOf = mutableListOf() + mutableListOf.add("https://www.w3.org/ns/activitystreams") + mutableListOf.addAll(body.context) + body.context = mutableListOf + } + + val requestBody = objectMapper.writeValueAsString(body) + + val sha256 = MessageDigest.getInstance("SHA-256") + + val digest = Base64Util.encode(sha256.digest(requestBody.toByteArray())) + + val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT"))) + val u = URL(url) + if (signer?.privateKey == null) { + return httpClient.post(url) { + header("Accept", ContentType.Application.Activity) + header("Date", date) + header("Digest", "sha-256=$digest") + setBody(requestBody) + contentType(ContentType.Application.Activity) + }.bodyAsText() + } + + val headers = headers { + append("Accept", ContentType.Application.Activity) + append("Date", date) + append("Host", u.host) + append("Digest", "sha-256=$digest") + } + + val sign = httpSignatureSigner.sign( + url = url, + method = HttpMethod.Post, + headers = headers, + requestBody = "", + keyPair = Key( + keyId = "${signer.url}#pubkey", + privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey), + publicKey = RsaUtil.decodeRsaPublicKeyPem(signer.publicKey) + ), + signHeaders = listOf("(request-target)", "date", "host", "digest") + ) + + return httpClient.post(url) { + headers { + headers { + appendAll(sign.headers) + } + } + setBody(requestBody) + contentType(ContentType.Application.Activity) + }.bodyAsText() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt index 97d50c5d..0bc74d60 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt @@ -1,11 +1,7 @@ package dev.usbharu.hideout.service.ap -import com.fasterxml.jackson.databind.ObjectMapper import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.hideout.dto.SendFollowDto -import dev.usbharu.hideout.plugins.postAp -import io.ktor.client.* -import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Service interface APSendFollowService { @@ -14,8 +10,7 @@ interface APSendFollowService { @Service class APSendFollowServiceImpl( - private val httpClient: HttpClient, - @Qualifier("activitypub") private val objectMapper: ObjectMapper, + private val apRequestService: APRequestService, ) : APSendFollowService { override suspend fun sendFollow(sendFollowDto: SendFollowDto) { val follow = Follow( @@ -23,11 +18,7 @@ class APSendFollowServiceImpl( `object` = sendFollowDto.followTargetUserId.url, actor = sendFollowDto.userId.url ) - httpClient.postAp( - urlString = sendFollowDto.followTargetUserId.inbox, - username = sendFollowDto.userId.url, - jsonLd = follow, - objectMapper - ) + + apRequestService.apPost(sendFollowDto.followTargetUserId.inbox, follow, sendFollowDto.userId) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt index 49056e21..5909d3f5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt @@ -1,7 +1,5 @@ package dev.usbharu.hideout.service.ap -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue import dev.usbharu.hideout.config.ApplicationConfig import dev.usbharu.hideout.domain.model.ap.Image import dev.usbharu.hideout.domain.model.ap.Key @@ -10,16 +8,11 @@ import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException -import dev.usbharu.hideout.plugins.getAp import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.ap.resource.APResourceResolveService +import dev.usbharu.hideout.service.ap.resource.resolve import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.user.UserService -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Service interface APUserService { @@ -40,11 +33,10 @@ interface APUserService { @Service class APUserServiceImpl( private val userService: UserService, - private val httpClient: HttpClient, private val userQueryService: UserQueryService, private val transaction: Transaction, private val applicationConfig: ApplicationConfig, - @Qualifier("activitypub") private val objectMapper: ObjectMapper + private val apResourceResolveService: APResourceResolveService ) : APUserService { @@ -111,14 +103,7 @@ class APUserServiceImpl( endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox") ) to userEntity } catch (ignore: FailedToGetResourcesException) { - val httpResponse = if (targetActor != null) { - httpClient.getAp(url, "$targetActor#pubkey") - } else { - httpClient.get(url) { - accept(ContentType.Application.Activity) - } - } - val person = objectMapper.readValue(httpResponse.bodyAsText()) + val person = apResourceResolveService.resolve(url, null as Long?) person to userService.createRemoteUser( RemoteUserCreateDto( diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveService.kt new file mode 100644 index 00000000..7815f088 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveService.kt @@ -0,0 +1,15 @@ +package dev.usbharu.hideout.service.ap.resource + +import dev.usbharu.hideout.domain.model.ap.Object +import dev.usbharu.hideout.domain.model.hideout.entity.User + +interface APResourceResolveService { + suspend fun resolve(url: String, clazz: Class, singer: User?): T + suspend fun resolve(url: String, clazz: Class, singerId: Long?): T +} + +suspend inline fun APResourceResolveService.resolve(url: String, singer: User?): T = + resolve(url, T::class.java, singer) + +suspend inline fun APResourceResolveService.resolve(url: String, singerId: Long?): T = + resolve(url, T::class.java, singerId) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImpl.kt new file mode 100644 index 00000000..962f4563 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImpl.kt @@ -0,0 +1,49 @@ +package dev.usbharu.hideout.service.ap.resource + +import dev.usbharu.hideout.domain.model.ap.Object +import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.repository.UserRepository +import dev.usbharu.hideout.service.ap.APRequestService +import org.springframework.stereotype.Service + +@Service +class APResourceResolveServiceImpl( + private val apRequestService: APRequestService, + private val userRepository: UserRepository, + private val cacheManager: CacheManager +) : + APResourceResolveService { + + override suspend fun resolve(url: String, clazz: Class, singerId: Long?): T = + internalResolve(url, singerId, clazz) + + override suspend fun resolve(url: String, clazz: Class, singer: User?): T = + internalResolve(url, singer, clazz) + + private suspend fun internalResolve(url: String, singerId: Long?, clazz: Class): T { + val key = genCacheKey(url, singerId) + + cacheManager.putCache(key) { + runResolve(url, singerId?.let { userRepository.findById(it) }, clazz) + } + return cacheManager.getOrWait(key) as T + } + + private suspend fun internalResolve(url: String, singer: User?, clazz: Class): T { + val key = genCacheKey(url, singer?.id) + cacheManager.putCache(key) { + runResolve(url, singer, clazz) + } + return cacheManager.getOrWait(key) as T + } + + private suspend fun runResolve(url: String, singer: User?, clazz: Class): Object = + apRequestService.apGet(url, singer, clazz) + + private fun genCacheKey(url: String, singerId: Long?): String { + if (singerId != null) { + return "$url-$singerId" + } + return url + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/CacheManager.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/CacheManager.kt new file mode 100644 index 00000000..909e7c62 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/CacheManager.kt @@ -0,0 +1,9 @@ +package dev.usbharu.hideout.service.ap.resource + +import dev.usbharu.hideout.domain.model.ap.Object + +interface CacheManager { + + suspend fun putCache(key: String, block: suspend () -> Object) + suspend fun getOrWait(key: String): Object +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/InMemoryCacheManager.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/InMemoryCacheManager.kt new file mode 100644 index 00000000..e68692bd --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/InMemoryCacheManager.kt @@ -0,0 +1,50 @@ +package dev.usbharu.hideout.service.ap.resource + +import dev.usbharu.hideout.domain.model.ap.Object +import dev.usbharu.hideout.util.LruCache +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.springframework.stereotype.Service +import java.time.Instant + +@Service +class InMemoryCacheManager : CacheManager { + private val cacheKey = LruCache(15) + private val valueStore = mutableMapOf() + private val keyMutex = Mutex() + + override suspend fun putCache(key: String, block: suspend () -> Object) { + val needRunBlock: Boolean + keyMutex.withLock { + cacheKey.filter { Instant.ofEpochMilli(it.value).plusSeconds(300) <= Instant.now() } + + val cached = cacheKey.get(key) + if (cached == null) { + needRunBlock = true + cacheKey[key] = Instant.now().toEpochMilli() + + valueStore.remove(key) + } else { + needRunBlock = false + } + } + if (needRunBlock) { + val processed = block() + + if (cacheKey.containsKey(key)) { + valueStore[key] = processed + } + } + } + + override suspend fun getOrWait(key: String): Object { + while (valueStore.contains(key).not()) { + if (cacheKey.containsKey(key).not()) { + throw IllegalStateException("Invalid cache key.") + } + delay(1) + } + return valueStore.getValue(key) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSigner.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSigner.kt new file mode 100644 index 00000000..f920f048 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSigner.kt @@ -0,0 +1,17 @@ +package dev.usbharu.hideout.service.signature + +import io.ktor.http.* + +interface HttpSignatureSigner { + @Suppress("LongParameterList") + suspend fun sign( + url: String, + method: HttpMethod, + headers: Headers, + requestBody: String, + keyPair: Key, + signHeaders: List + ): SignedRequest + + suspend fun signRaw(signString: String, keyPair: Key, signHeaders: List): Sign +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImpl.kt new file mode 100644 index 00000000..193658ba --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImpl.kt @@ -0,0 +1,84 @@ +package dev.usbharu.hideout.service.signature + +import dev.usbharu.hideout.util.Base64Util +import io.ktor.http.* +import io.ktor.util.* +import org.springframework.stereotype.Component +import java.net.URL +import java.security.Signature + +@Component +class HttpSignatureSignerImpl : HttpSignatureSigner { + override suspend fun sign( + url: String, + method: HttpMethod, + headers: Headers, + requestBody: String, + keyPair: Key, + signHeaders: List + ): SignedRequest { + val sign = signRaw( + signString = buildSignString( + url = URL(url), + method = method, + headers = headers, + signHeaders = signHeaders + ), + keyPair = keyPair, + signHeaders = signHeaders + ) + val signedHeaders = headers { + appendAll(headers) + set("Signature", sign.signatureHeader) + } + return SignedRequest( + url = url, + method = method, + headers = signedHeaders, + requestBody = requestBody, + sign = sign + ) + } + + override suspend fun signRaw(signString: String, keyPair: Key, signHeaders: List): Sign { + val signer = Signature.getInstance("SHA256withRSA") + signer.initSign(keyPair.privateKey) + signer.update(signString.toByteArray()) + val sign = signer.sign() + val signature = Base64Util.encode(sign) + return Sign( + signature, + """keyId="${keyPair.keyId}",algorithm="rsa-sha256",headers="${ + signHeaders.joinToString( + " " + ) + }",signature="$signature"""" + ) + } + + private fun buildSignString( + url: URL, + method: HttpMethod, + headers: Headers, + signHeaders: List + ): String { + headers.toMap().map { it.key.lowercase() to it.value }.toMap() + val result = signHeaders.joinToString("\n") { + if (it.startsWith("(")) { + specialHeader(it, url, method) + } else { + generalHeader(it, headers.get(it)!!) + } + } + return result + } + + private fun specialHeader(fieldName: String, url: URL, method: HttpMethod): String { + if (fieldName != "(request-target)") { + throw IllegalArgumentException(fieldName + "is unsupported type") + } + return "(request-target): ${method.value.lowercase()} ${url.path}" + } + + private fun generalHeader(fieldName: String, value: String): String = "$fieldName: $value" +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/Key.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/Key.kt new file mode 100644 index 00000000..0eb5171f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/Key.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.service.signature + +import java.security.PrivateKey +import java.security.PublicKey + +data class Key( + val keyId: String, + val privateKey: PrivateKey, + val publicKey: PublicKey +) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/Sign.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/Sign.kt new file mode 100644 index 00000000..75711759 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/Sign.kt @@ -0,0 +1,6 @@ +package dev.usbharu.hideout.service.signature + +data class Sign( + val signature: String, + val signatureHeader: String +) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/SignedRequest.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/SignedRequest.kt new file mode 100644 index 00000000..346dca87 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/SignedRequest.kt @@ -0,0 +1,23 @@ +package dev.usbharu.hideout.service.signature + +import io.ktor.client.request.* +import io.ktor.http.* + +data class SignedRequest( + val url: String, + val method: HttpMethod, + val headers: Headers, + val requestBody: String, + val sign: Sign +) { + fun toRequestBuilder(): HttpRequestBuilder { + val httpRequestBuilder = HttpRequestBuilder() + httpRequestBuilder.url(this.url) + httpRequestBuilder.method = this.method + httpRequestBuilder.headers { + this.appendAll(headers) + } + httpRequestBuilder.setBody(requestBody) + return httpRequestBuilder + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/LruCache.kt b/src/main/kotlin/dev/usbharu/hideout/util/LruCache.kt new file mode 100644 index 00000000..3f65175a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/LruCache.kt @@ -0,0 +1,13 @@ +package dev.usbharu.hideout.util + +import java.io.Serial + +class LruCache(private val maxSize: Int) : LinkedHashMap(15, 0.75f, true) { + + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = size > maxSize + + companion object { + @Serial + private const val serialVersionUID: Long = -6446947260925053191L + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt index e0ebbfc8..278019e3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt @@ -7,6 +7,8 @@ import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec object RsaUtil { + private val replaceHeaderAndFooterRegex = Regex("-----.*?-----") + fun decodeRsaPublicKey(byteArray: ByteArray): RSAPublicKey { val x509EncodedKeySpec = X509EncodedKeySpec(byteArray) return KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey @@ -14,10 +16,22 @@ object RsaUtil { fun decodeRsaPublicKey(encoded: String): RSAPublicKey = decodeRsaPublicKey(Base64Util.decode(encoded)) + fun decodeRsaPublicKeyPem(pem: String): RSAPublicKey { + val replace = pem.replace(replaceHeaderAndFooterRegex, "") + .replace("\n", "") + return decodeRsaPublicKey(replace) + } + fun decodeRsaPrivateKey(byteArray: ByteArray): RSAPrivateKey { val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(byteArray) return KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey } fun decodeRsaPrivateKey(encoded: String): RSAPrivateKey = decodeRsaPrivateKey(Base64Util.decode(encoded)) + + fun decodeRsaPrivateKeyPem(pem: String): RSAPrivateKey { + val replace = pem.replace(replaceHeaderAndFooterRegex, "") + .replace("\n", "") + return decodeRsaPrivateKey(replace) + } } diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt deleted file mode 100644 index 52ac0b6b..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package dev.usbharu.hideout.plugins - -import dev.usbharu.hideout.domain.model.ap.JsonLd -import dev.usbharu.hideout.domain.model.hideout.entity.User -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.user.toPem -import io.ktor.client.* -import io.ktor.client.engine.mock.* -import io.ktor.client.plugins.logging.* -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.mock -import utils.JsonObjectMapper.objectMapper -import utils.TestApplicationConfig.testApplicationConfig -import utils.TestTransaction -import java.security.KeyPairGenerator -import java.time.Instant - -class ActivityPubKtTest { - @Test - fun HttpSignTest() { - val userQueryService = mock { - onBlocking { findByNameAndDomain(any(), any()) } doAnswer { - val keyPairGenerator = KeyPairGenerator.getInstance("RSA") - keyPairGenerator.initialize(1024) - val generateKeyPair = keyPairGenerator.generateKeyPair() - User.of( - id = 1, - name = "test", - domain = "localhost", - screenName = "test", - description = "", - password = "", - inbox = "https://example.com/inbox", - outbox = "https://example.com/outbox", - url = "https://example.com", - publicKey = "", - privateKey = generateKeyPair.private.toPem(), - createdAt = Instant.now() - ) - } - } - runBlocking { - val ktorKeyMap = KtorKeyMap(userQueryService, TestTransaction, testApplicationConfig) - - val httpClient = HttpClient( - MockEngine { httpRequestData -> - respondOk() - } - ) { - install(httpSignaturePlugin) { - keyMap = ktorKeyMap - } - install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.ALL - } - } - - httpClient.postAp("https://localhost", "test", JsonLd(emptyList()), objectMapper) - } - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt index 4a1e06bc..9d6975f7 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt @@ -96,7 +96,10 @@ class APNoteServiceImplTest { objectMapper = objectMapper, applicationConfig = testApplicationConfig, postService = mock(), - mediaQueryService = mediaQueryService + mediaQueryService = mediaQueryService, + apResourceResolveService = mock(), + apRequestService = mock(), + transaction = mock() ) val postEntity = Post.of( 1L, @@ -136,7 +139,10 @@ class APNoteServiceImplTest { objectMapper = objectMapper, applicationConfig = testApplicationConfig, postService = mock(), - mediaQueryService = mediaQueryService + mediaQueryService = mediaQueryService, + apResourceResolveService = mock(), + transaction = mock(), + apRequestService = mock() ) activityPubNoteService.createNoteJob( JobProps( diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt index 9aebf1ee..d16fcc2e 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt @@ -3,17 +3,17 @@ package dev.usbharu.hideout.service.ap -import com.fasterxml.jackson.module.kotlin.readValue import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.ConfigData -import dev.usbharu.hideout.domain.model.ap.* +import dev.usbharu.hideout.domain.model.ap.Follow +import dev.usbharu.hideout.domain.model.ap.Image +import dev.usbharu.hideout.domain.model.ap.Key +import dev.usbharu.hideout.domain.model.ap.Person import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.user.UserService -import io.ktor.client.* -import io.ktor.client.engine.mock.* import kjob.core.dsl.ScheduleContext import kjob.core.job.JobProps import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -40,9 +40,9 @@ class APReceiveFollowServiceImplTest { mock(), mock(), mock(), - mock(), TestTransaction, - objectMapper + objectMapper, + mock() ) activityPubFollowService.receiveFollow( Follow( @@ -145,35 +145,10 @@ class APReceiveFollowServiceImplTest { mock(), apUserService, userService, - HttpClient( - MockEngine { httpRequestData -> - assertEquals(person.inbox, httpRequestData.url.toString()) - val accept = Accept( - type = emptyList(), - name = "Follow", - `object` = Follow( - type = emptyList(), - name = "Follow", - `object` = "https://example.com", - actor = "https://follower.example.com" - ), - actor = "https://example.com" - ) - accept.context += "https://www.w3.org/ns/activitystreams" - val content = httpRequestData.body.toByteArray().decodeToString() - println(content) - assertEquals( - accept, - Config.configData.objectMapper.readValue( - content - ) - ) - respondOk() - } - ), userQueryService, TestTransaction, - objectMapper + objectMapper, + mock() ) activityPubFollowService.receiveFollowJob( JobProps( diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImplTest.kt new file mode 100644 index 00000000..fa244184 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImplTest.kt @@ -0,0 +1,187 @@ +package dev.usbharu.hideout.service.ap.resource + +import dev.usbharu.hideout.domain.model.ap.Object +import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.repository.UserRepository +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.time.Instant +import kotlin.test.assertEquals + +@ExtendWith(MockitoExtension::class) +@Disabled +class APResourceResolveServiceImplTest { + + @Test + fun `単純な一回のリクエスト`() = runTest { + + var count = 0 + + val httpClient = HttpClient(MockEngine { request -> + count++ + respondOk("{}") + }) + + val userRepository = mock() + + whenever(userRepository.findById(any())).doReturn( + User.of( + 2L, + "follower", + "follower.example.com", + "followerUser", + "test follower user", + "https://follower.example.com/inbox", + "https://follower.example.com/outbox", + "https://follower.example.com", + "https://follower.example.com", + publicKey = "", + createdAt = Instant.now() + ) + ) + + val apResourceResolveService = + APResourceResolveServiceImpl(mock(), userRepository, InMemoryCacheManager()) + + apResourceResolveService.resolve("https", 0) + + assertEquals(1, count) + } + + @Test + fun 複数回の同じリクエストが重複して発行されない() = runTest { + var count = 0 + + val httpClient = HttpClient(MockEngine { request -> + count++ + respondOk("{}") + }) + + val userRepository = mock() + + whenever(userRepository.findById(any())).doReturn( + User.of( + 2L, + "follower", + "follower.example.com", + "followerUser", + "test follower user", + "https://follower.example.com/inbox", + "https://follower.example.com/outbox", + "https://follower.example.com", + "https://follower.example.com", + publicKey = "", + createdAt = Instant.now() + ) + ) + + val apResourceResolveService = + APResourceResolveServiceImpl(mock(), userRepository, InMemoryCacheManager()) + + apResourceResolveService.resolve("https", 0) + apResourceResolveService.resolve("https", 0) + apResourceResolveService.resolve("https", 0) + apResourceResolveService.resolve("https", 0) + + assertEquals(1, count) + } + + @Test + fun 複数回の同じリクエストが同時に発行されても重複して発行されない() = runTest { + var count = 0 + + val httpClient = HttpClient(MockEngine { request -> + count++ + respondOk("{}") + }) + + val userRepository = mock() + + whenever(userRepository.findById(any())).doReturn( + User.of( + 2L, + "follower", + "follower.example.com", + "followerUser", + "test follower user", + "https://follower.example.com/inbox", + "https://follower.example.com/outbox", + "https://follower.example.com", + "https://follower.example.com", + publicKey = "", + createdAt = Instant.now() + ) + ) + + val apResourceResolveService = + APResourceResolveServiceImpl(mock(), userRepository, InMemoryCacheManager()) + + repeat(10) { + awaitAll( + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + async { apResourceResolveService.resolve("https", 0) }, + ) + } + + assertEquals(1, count) + } + + @Test + fun 関係のないリクエストは発行する() = runTest { + var count = 0 + + val httpClient = HttpClient(MockEngine { request -> + count++ + respondOk("{}") + }) + + val userRepository = mock() + + whenever(userRepository.findById(any())).doReturn( + User.of( + 2L, + "follower", + "follower.example.com", + "followerUser", + "test follower user", + "https://follower.example.com/inbox", + "https://follower.example.com/outbox", + "https://follower.example.com", + "https://follower.example.com", + publicKey = "", + createdAt = Instant.now() + ) + ) + + val apResourceResolveService = + APResourceResolveServiceImpl(mock(), userRepository, InMemoryCacheManager()) + + apResourceResolveService.resolve("abcd", 0) + apResourceResolveService.resolve("1234", 0) + apResourceResolveService.resolve("aaaa", 0) + + assertEquals(3, count) + } + + +} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImplTest.kt new file mode 100644 index 00000000..09c006db --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImplTest.kt @@ -0,0 +1,340 @@ +package dev.usbharu.hideout.service.signature + +import dev.usbharu.hideout.util.Base64Util +import dev.usbharu.hideout.util.RsaUtil +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import tech.barbero.http.message.signing.HttpMessage +import tech.barbero.http.message.signing.HttpRequest +import tech.barbero.http.message.signing.KeyMap +import tech.barbero.http.message.signing.SignatureHeaderVerifier +import java.net.URI +import java.net.URL +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.PublicKey +import java.text.SimpleDateFormat +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.* +import javax.crypto.SecretKey +import kotlin.test.assertFalse + +class HttpSignatureSignerImplTest { + @Test + fun `HTTP Signatureの署名を作成できる`() = runTest { + + val publicKey = RsaUtil.decodeRsaPublicKey( + """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv6tEMdAw9xk3Pt5YMxJ2t+1QZeb9p+PKpS1lVbkL5oWj6aL2Q3nRVQQabcILOb5YNUpWQVQWRjW4jkrBDuiAgvlmu126OPs4E1cVVWEqylJ5VOkOIeXpldOu/SvHM/sHPNHXYlovaHDIqT+3zp2xUmXQx2kum0b/o8Vp+wh45iIoflb62/0dQ5YZyZEp283XKne+u813BzCOa1IAsywbUvX9kUv1SaUDn3oxnjdjWgSqsJcJVU1lyiN0OrpnEg5TMVjDqN3vimoR4uqNn5Zm8rrif/o8w+/FlnWticbty5MQun0gFaCfLsR8ODm1/0DwT6WI/bRpy6zye1n4iQn/nwIDAQAB""" + ) + val privateKey = RsaUtil.decodeRsaPrivateKey( + """MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/q0Qx0DD3GTc+3lgzEna37VBl5v2n48qlLWVVuQvmhaPpovZDedFVBBptwgs5vlg1SlZBVBZGNbiOSsEO6ICC+Wa7Xbo4+zgTVxVVYSrKUnlU6Q4h5emV0679K8cz+wc80ddiWi9ocMipP7fOnbFSZdDHaS6bRv+jxWn7CHjmIih+Vvrb/R1DlhnJkSnbzdcqd767zXcHMI5rUgCzLBtS9f2RS/VJpQOfejGeN2NaBKqwlwlVTWXKI3Q6umcSDlMxWMOo3e+KahHi6o2flmbyuuJ/+jzD78WWda2Jxu3LkxC6fSAVoJ8uxHw4ObX/QPBPpYj9tGnLrPJ7WfiJCf+fAgMBAAECggEAIkL4LrtbdWAxivBt7bs4M4qdW4nd/9vtRneF7LvmT6/F7CawRMGK1Nql6sbMAOdwlx4Rqx3f2W8S7YSZXBPdnQv9/DI17qehj3t6mceDwaTagX4jg5W4moq7dhAUTMtrsMiF6tPaM54tkGuObMWtg+AlYPABX8piOiE436HVErXrOaWsrQ6ReoHodTyibfO8aByzLkIb2k3nt1j8HotjjFe6ZqFVkXiGVWOUwdLpsqE+8BV6g1IF480SyKF4HnUfr/AxDnpKtTFspGCKu/w7BA6yOaaONeal0/EUA8vlfLsKdaRY2TRmCFCQzUwluBTr6ssjQyilJzgJ6VbDFpVSSQKBgQDgpt5kB7TDXN5ucD0alN0umI/rLD5TTg0rbpLo2wzfh2IAPYSiCgNOVr1Mi6JRxqSLa4KeEOCYATLu9wrFU8y+i/ffrDAMo/b2z3TORV3p3m1fPx6CnqBZMvxrHl2CCbij+6O1qmq+8AW8+lQuilq3u6dRBkYpt+mRHWsqvMeNqwKBgQDaair8CIEcoCtxlw8lDRJNn7bC9DRiaJLxPYuOHop7lolUy1amd2srREgoEB7FRwC5bki+BsSUffFyix2kUsf4I2dLHYmbf4Aci2GpqdRW4AnO2tWnvHGsAnkmsRQ2ZuoF7+8Phd1pnXY9DHImAxmpUgqhKDqbP4Hi1W2w5s0Z3QKBgQCTlUxYTq+0AFioGNgrlExSBivWBXTUaVxBghzFGNK2Lkx1d/SgNw/A8T7fAIScUHFcnj5q9Q93DKKXVnge9lR1gaJPsODIDRd7QQKtV+jAcT1M6zxx9x/EObiV7pbjjNtd7zy3ZcNGuIwsgA+5m27JcWAT3JlPYuDwUnFK3EYEjQKBgCHCm1ZNsjdMgqqSIOMnPBcHguZrfNVhOKVVUAbtrZYg1KVosMIWX1hWu5iFtVvk97Wx2EiXHzecp/9+hVxq90HhpwuzSxvf/1tqJ/RjrdCn3Jw+sxu0QxXFZBiY8njeO3ojdh4+INU8Y5RYIiTCAetsJPx4DWcFz/vR5ZyccEN5AoGAHgP5ZeUvn/NR5GvX7NIVbYReO6+YeilNE8mGa57Ew4GJotrS5P4nevDyZWZCs63f4ZQ/I/lJnrGRtQDfQC7wUGhMf7VjZfagFHcSO44uCVKsSO7ToTyuObTpdEC9dUeVaJt96ZP5eX4vWZ6MNgYstlmXKVLg9LHsLJlXKNHufg0=""" + ) + + val httpSignatureSignerImpl = HttpSignatureSignerImpl() + + val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + format.timeZone = TimeZone.getTimeZone("GMT") + + //language=JSON + val requestBody = """{ + "hoge": "fuga" +}""" + + val sha256 = MessageDigest.getInstance("SHA-256") + + val encode = Base64Util.encode(sha256.digest(requestBody.toByteArray())) + + val url = "https://example.com/" + httpSignatureSignerImpl.sign( + url, + HttpMethod.Post, + Headers.build { + append("Date", "Fri, 13 Oct 2023 07:14:50 GMT") + append("Host", URL(url).host) + append("Digest", "SHA-256=$encode") + }, + requestBody, + Key("https://example.com", privateKey, publicKey), + listOf("(request-target)", "date", "host", "digest") + ) + } + + @Test + fun `HTTP Signatureの署名が検証に成功する`() = runTest { + val publicKey = RsaUtil.decodeRsaPublicKey( + """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJVqbb17nCo8aBZYF+vDgnFANaFDNuvHKMT39qQGnetItYZ8DBtRZzvYE6njn1vH7gixPhGnjt6qLWJJzeoSSv1FgQp9yUq719QFC9BQ87RughpkrP1Nq0ZHuTLMH0U13g2oziRp04FZXElq6b3aHLK+Y78mX20l9HCqIh4GdBRjgiAjcZr/XOZl1cKa7ai3z4yO4euOb8LiJavMHz7/ISefUGtikrhnIqNwwQ1prxT1bZduTotjSi8bitdzsvGh5ftTiFxJC+Pe1yJn3ALW/L3SBm72x60S14osQv1gMaDLaA6YNXCYm34xKndF+UxWTUwLUpNM/GRDoNa8Yq7HBwIDAQAB""" + ) + val privateKey = RsaUtil.decodeRsaPrivateKey( + """MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4lWptvXucKjxoFlgX68OCcUA1oUM268coxPf2pAad60i1hnwMG1FnO9gTqeOfW8fuCLE+EaeO3qotYknN6hJK/UWBCn3JSrvX1AUL0FDztG6CGmSs/U2rRke5MswfRTXeDajOJGnTgVlcSWrpvdocsr5jvyZfbSX0cKoiHgZ0FGOCICNxmv9c5mXVwprtqLfPjI7h645vwuIlq8wfPv8hJ59Qa2KSuGcio3DBDWmvFPVtl25Oi2NKLxuK13Oy8aHl+1OIXEkL497XImfcAtb8vdIGbvbHrRLXiixC/WAxoMtoDpg1cJibfjEqd0X5TFZNTAtSk0z8ZEOg1rxirscHAgMBAAECggEAU5VRQs09Rpt3jBimHnrjptM6pK5X/ewpXKRItpZS6rqqy4xQ6riKFYmrUEgrazOH5ploDTe4XMEmZXOvAP/f9bYXfZXvHLHrOpHnERDtP1XyfpaOBSmUvJyQCORgOz6/ZERiLqqdgyl8+gXC1IJkXH9yKD/cE/UcbUKBP/7BpFj7lPMyNCApiS1Z2RinvOSsx2TCBfVLpEE1dTLdHg3g3vfkmnn+KQ/SU4z3ksXJa0ODZY9lsUGWUrGmnhd/tviSuNUJG3wx7h1er4LBjuA4OZD8qJA+sXcEY2Kn7XQHAOBWUfAOR7nzAl3mPYycIZs4sDrq2awwX12ML9qR/40swQKBgQDtBhIML+Xt32fLw4/wtSDmDJo4szyu0c3Gangl4eMjOc1WEXl/bL8uryNS9b+1he8b+VgEBFH2nhl3u1eman0/xpk9hqj9hd/IDazMqUr7mKq+b9WXWd24LFZNew+35RUELW01FdEDSr+KZsCIjFilAeWfpJORoj3oZFU5C/5mQQKBgQDHXI7NqHy2ATqDiQI3aG72B8n3TbR9B8G01Anfn3ZKcXIFWnDHoB9y/ITYzGrjrbbEOD2BsAacOy7bOWHlX1RIcD10ZWJIBdjqc+zfpahb36mXbcEQkb7col5s992KGVZHu8OBwfGJMVHYprIxOmygj1CAF9pEZyMy3alHChOrRwKBgQCYeyxHHNVNh0huBLxn/Q5SEM9yJJSoXp6Dw+DRdhU6hyf687j26c3ASblu2Fvhem1N0MX3p5PXFPSLW0FS9PTof2n789JpbqN9Ppbo/wwW+ar2YlnFSXHi1tsac020XzJ7AoJcAVH6TS8V6W55KdipJqRDZIvux7IN++X7kiSyQQKBgQCweIIAEhCym0vMe0729P6j0ik5PBN0SZVyF+/VfzYal2kyy+fhDSBJjLWbovdLKs4Jyy7GyaZQTSMg8x5xB3130cLUcZoZ3vMwNgWLwvvQt59LZ9/qZtjoPOIQ2yfDwsHZJZ/eEGtZ4cptWMGLSgg16CZ9/J88xX8m24eoVocqqQKBgCEj/FK26bBLnPtRlQ+5mTQ/CjcjD5/KoaHLawULvXq03qIiZfDZg+sm7JUmlaC48sERGLJnjNYk/1pjw5N8txyAk2UHxqi+dayRkTCRSfBm0PUWyVWiperHNEuByHnyh+qX00sE3SCz2qDSDLb1x7kV+2BhEL+XfgD7evqrvrNq""" + ) + + val httpSignatureSignerImpl = HttpSignatureSignerImpl() + + val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + format.timeZone = TimeZone.getTimeZone("GMT") + + //language=JSON + val requestBody = """{ + "hoge": "fuga" +}""" + + val sha256 = MessageDigest.getInstance("SHA-256") + + val encode = Base64Util.encode(sha256.digest(requestBody.toByteArray())) + + val url = "https://example.com/" + val headers = Headers.build { + append("Date", "Fri, 13 Oct 2023 07:14:50 GMT") + append("Host", URL(url).host) + append("Digest", "SHA-256=$encode") + } + val sign = httpSignatureSignerImpl.sign( + url, + HttpMethod.Post, + headers, + requestBody, + Key("https://example.com", privateKey, publicKey), + listOf("(request-target)", "date", "host", "digest") + ) + + val keyMap = object : KeyMap { + override fun getPublicKey(keyId: String?): PublicKey { + return publicKey + } + + override fun getPrivateKey(keyId: String?): PrivateKey { + return privateKey + } + + override fun getSecretKey(keyId: String?): SecretKey { + TODO("Not yet implemented") + } + + } + val verifier = SignatureHeaderVerifier.builder().keyMap(keyMap).build() + + val headers1 = headers { + appendAll(headers) + append("Signature", sign.sign.signatureHeader) + } + + val httpMessage = object : HttpMessage, HttpRequest { + override fun headerValues(name: String?): MutableList { + return name?.let { headers1.getAll(it) }.orEmpty().toMutableList() + } + + override fun addHeader(name: String?, value: String?) { + TODO("Not yet implemented") + } + + override fun method(): String { + return "POST" + } + + override fun uri(): URI { + return URI(url) + } + } + val verify = verifier.verify(httpMessage) + assertTrue(verify) + } + + @Test + fun `HTTP Signatureの署名が検証に成功する2`() = runTest { + val publicKey = RsaUtil.decodeRsaPublicKeyPem( + """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3YdxpopDvAIp+Ciplvx +SfY8tV3GquYIfxSfTPAqiusgf8zXxYz0ilxY+nHjzIpdOA8rDHcDVhBXI/5lP1Vl +sgeY5cgJRuG9g9ZWaQV/8oKYoillgTkNuyNB0OGa84BAeKo+VMG1NNtlVCn2DrvA +8FLXAc2e4wPcOozKV5JYHZ0RDcSIS1bPb5ArxhhF8zAjn9+s/plsDz+mgHD0Ce5z +UUv1uHQF8nj53WL4cCcrl5TSvqaK6Krcmb7i1YVSlk52p0AYg79pXpPQLhe3TnvJ +Gy+KPvKPq1cho5jM1vJktK6eGlnUPEgD0bCSXl7FrtE7mPMCsaQCRj+up4t+NBWu +gwIDAQAB +-----END PUBLIC KEY-----""" + ) + val privateKey = RsaUtil.decodeRsaPrivateKeyPem( + """-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrdh3GmikO8Ain +4KKmW/FJ9jy1Xcaq5gh/FJ9M8CqK6yB/zNfFjPSKXFj6cePMil04DysMdwNWEFcj +/mU/VWWyB5jlyAlG4b2D1lZpBX/ygpiiKWWBOQ27I0HQ4ZrzgEB4qj5UwbU022VU +KfYOu8DwUtcBzZ7jA9w6jMpXklgdnRENxIhLVs9vkCvGGEXzMCOf36z+mWwPP6aA +cPQJ7nNRS/W4dAXyePndYvhwJyuXlNK+poroqtyZvuLVhVKWTnanQBiDv2lek9Au +F7dOe8kbL4o+8o+rVyGjmMzW8mS0rp4aWdQ8SAPRsJJeXsWu0TuY8wKxpAJGP66n +i340Fa6DAgMBAAECggEAUsE0h9l5/aKumtAZ0K9JmwgErwiuzWcvLJ64cDruXZQ0 +YFpuvgNVN75wl5gGeX9ClL8FaQO8EXrbhBzRoyrFZZKzIhxVFef4PzxhAllMMrED +mCjgu+jcjrjqmDV7QxFgjJymbuP7YKKPmnqSLvRBn/xrl4w1pp4DWiL/uhqA+vE8 +ZOgfzJ6LzU3CUFjCEi73gfZzTyykzpw+H3Lf8WPYCRQteng7zGxFDpPM3uDt0AKV +nTReopN6HKVOqobBuJLbD2kORfFzfzfLKrkAELivO/yOdosbG5GIf8nxZ0h86QIo +knav6boRgF9LqZTzC+QWBjGXEng58gEYEuAaovup8QKBgQDeR9onVIj67FZ/J1k4 +VBTfxRZ4r2oFHyhh3O2Y1xmVM0ejlvtnQL989d6HCieT6wd9CcfTOnTidgXCW+1a +wW3Q6eqtaPanRsU8aCcG2Pa19hbEkdsAvu/8eS8SWegnyqk0lKZjRP6KXDto99dd +CWs8KMcTXTqpFfNr83AeuR1ViwKBgQDFeLms7hvnLVF0oS6LIh73WVd1YfhcCsxo +MfjLmsivCfvyo/RAWmWjHTvh9ofYm3a/1gU4ACm33tI++uWz1juHxJFy+ryjjz7z +MHimmohaWkeax9wyUn66hG52JYUHQFoi85cL/YLMMX3WZXa5LQyyXPgirF4L9+c9 +MTZNrKDZ6QKBgEhDX77NksLQtsYbyruvSiH9dvLBRFxp5rz6EBxSQbTpuO6MFSta +N2auoCuSt481J3gVB+u542oEKJcpP57zp3n1sh+yMg3ryg97ZMSrIHnDiV9ac7Jo +YKjZ1N3IcNsO3beEZBt9wKrGlWHowRE0ELK8Jww6kOmLg1mjCN5UHB9FAoGAVewl +vl0MvxY07y6C9f8uwimZqHWsf0AjmOLFgrIiyCbr/bPhP28V8ldyCuweR929WdNi +Ce/oNx05FjZNZGa/GGAreYAoPHLDzUU1+igbVFUb+vkjkrHaeoXNGpNQwsr5bWPY +QVtZYkfWnUcg1YoIkENrpIqjkUmY0ENtgXavtqECgYEA2F+FJPPpm39gD2mnbnAH +goM9c+h9hh/o3kW3CUNgPKeYT4ptd3AG0k9C9De+eWb3GGqH1/KUGvUbyXm7f1Wi +y+SBT1Uk6/85ZZ3nCz2Yj8eGokhcfKhXd8K3HV2wgoUWMJT1Qvedrqc2R5S9wdY8 +wADggCG8df/amNR+dyQOOuQ= +-----END PRIVATE KEY-----""" + ) + + val httpSignatureSignerImpl = HttpSignatureSignerImpl() + + val format = DateTimeFormatter.RFC_1123_DATE_TIME + + //language=JSON + val requestBody = """{ + "hoge": "fuga" +}""" + + val sha256 = MessageDigest.getInstance("SHA-256") + + val encode = Base64Util.encode(sha256.digest(requestBody.toByteArray())) + + val url = "https://test-hideout.usbharu.dev/users/97ws8y3rj6/inbox" + val headers = Headers.build { + append("Date", format.format(ZonedDateTime.now(ZoneId.of("GMT")))) + append("Host", URL(url).host) + append("Digest", "sha-256=$encode") + } + val sign = httpSignatureSignerImpl.sign( + url, + HttpMethod.Post, + headers, + requestBody, + Key("https://test-hideout.usbharu.dev/users/c#pubkey", privateKey, publicKey), + listOf("(request-target)", "date", "host", "digest") + ) + + val keyMap = object : KeyMap { + override fun getPublicKey(keyId: String?): PublicKey { + return publicKey + } + + override fun getPrivateKey(keyId: String?): PrivateKey { + return privateKey + } + + override fun getSecretKey(keyId: String?): SecretKey { + TODO("Not yet implemented") + } + + } + val verifier = SignatureHeaderVerifier.builder().keyMap(keyMap).build() + + val headers1 = headers { + appendAll(headers) + append("Signature", sign.sign.signatureHeader) + } + + val httpMessage = object : HttpMessage, HttpRequest { + override fun headerValues(name: String?): MutableList { + return name?.let { headers1.getAll(it) }.orEmpty().toMutableList() + } + + override fun addHeader(name: String?, value: String?) { + TODO("Not yet implemented") + } + + override fun method(): String { + return "POST" + } + + override fun uri(): URI { + return URI(url) + } + } + val verify = verifier.verify(httpMessage) + assertTrue(verify) + } + + @Test + fun `HTTP Signatureで署名した後、改ざんされた場合検証に失敗する`() = runTest { + val publicKey = RsaUtil.decodeRsaPublicKey( + """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJVqbb17nCo8aBZYF+vDgnFANaFDNuvHKMT39qQGnetItYZ8DBtRZzvYE6njn1vH7gixPhGnjt6qLWJJzeoSSv1FgQp9yUq719QFC9BQ87RughpkrP1Nq0ZHuTLMH0U13g2oziRp04FZXElq6b3aHLK+Y78mX20l9HCqIh4GdBRjgiAjcZr/XOZl1cKa7ai3z4yO4euOb8LiJavMHz7/ISefUGtikrhnIqNwwQ1prxT1bZduTotjSi8bitdzsvGh5ftTiFxJC+Pe1yJn3ALW/L3SBm72x60S14osQv1gMaDLaA6YNXCYm34xKndF+UxWTUwLUpNM/GRDoNa8Yq7HBwIDAQAB""" + ) + val privateKey = RsaUtil.decodeRsaPrivateKey( + """MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4lWptvXucKjxoFlgX68OCcUA1oUM268coxPf2pAad60i1hnwMG1FnO9gTqeOfW8fuCLE+EaeO3qotYknN6hJK/UWBCn3JSrvX1AUL0FDztG6CGmSs/U2rRke5MswfRTXeDajOJGnTgVlcSWrpvdocsr5jvyZfbSX0cKoiHgZ0FGOCICNxmv9c5mXVwprtqLfPjI7h645vwuIlq8wfPv8hJ59Qa2KSuGcio3DBDWmvFPVtl25Oi2NKLxuK13Oy8aHl+1OIXEkL497XImfcAtb8vdIGbvbHrRLXiixC/WAxoMtoDpg1cJibfjEqd0X5TFZNTAtSk0z8ZEOg1rxirscHAgMBAAECggEAU5VRQs09Rpt3jBimHnrjptM6pK5X/ewpXKRItpZS6rqqy4xQ6riKFYmrUEgrazOH5ploDTe4XMEmZXOvAP/f9bYXfZXvHLHrOpHnERDtP1XyfpaOBSmUvJyQCORgOz6/ZERiLqqdgyl8+gXC1IJkXH9yKD/cE/UcbUKBP/7BpFj7lPMyNCApiS1Z2RinvOSsx2TCBfVLpEE1dTLdHg3g3vfkmnn+KQ/SU4z3ksXJa0ODZY9lsUGWUrGmnhd/tviSuNUJG3wx7h1er4LBjuA4OZD8qJA+sXcEY2Kn7XQHAOBWUfAOR7nzAl3mPYycIZs4sDrq2awwX12ML9qR/40swQKBgQDtBhIML+Xt32fLw4/wtSDmDJo4szyu0c3Gangl4eMjOc1WEXl/bL8uryNS9b+1he8b+VgEBFH2nhl3u1eman0/xpk9hqj9hd/IDazMqUr7mKq+b9WXWd24LFZNew+35RUELW01FdEDSr+KZsCIjFilAeWfpJORoj3oZFU5C/5mQQKBgQDHXI7NqHy2ATqDiQI3aG72B8n3TbR9B8G01Anfn3ZKcXIFWnDHoB9y/ITYzGrjrbbEOD2BsAacOy7bOWHlX1RIcD10ZWJIBdjqc+zfpahb36mXbcEQkb7col5s992KGVZHu8OBwfGJMVHYprIxOmygj1CAF9pEZyMy3alHChOrRwKBgQCYeyxHHNVNh0huBLxn/Q5SEM9yJJSoXp6Dw+DRdhU6hyf687j26c3ASblu2Fvhem1N0MX3p5PXFPSLW0FS9PTof2n789JpbqN9Ppbo/wwW+ar2YlnFSXHi1tsac020XzJ7AoJcAVH6TS8V6W55KdipJqRDZIvux7IN++X7kiSyQQKBgQCweIIAEhCym0vMe0729P6j0ik5PBN0SZVyF+/VfzYal2kyy+fhDSBJjLWbovdLKs4Jyy7GyaZQTSMg8x5xB3130cLUcZoZ3vMwNgWLwvvQt59LZ9/qZtjoPOIQ2yfDwsHZJZ/eEGtZ4cptWMGLSgg16CZ9/J88xX8m24eoVocqqQKBgCEj/FK26bBLnPtRlQ+5mTQ/CjcjD5/KoaHLawULvXq03qIiZfDZg+sm7JUmlaC48sERGLJnjNYk/1pjw5N8txyAk2UHxqi+dayRkTCRSfBm0PUWyVWiperHNEuByHnyh+qX00sE3SCz2qDSDLb1x7kV+2BhEL+XfgD7evqrvrNq""" + ) + + val httpSignatureSignerImpl = HttpSignatureSignerImpl() + + val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + format.timeZone = TimeZone.getTimeZone("GMT") + + //language=JSON + val requestBody = """{ + "hoge": "fuga" +}""" + + val sha256 = MessageDigest.getInstance("SHA-256") + + val encode = Base64Util.encode(sha256.digest(requestBody.toByteArray())) + + val url = "https://example.com/" + val headers = Headers.build { + append("Date", "Fri, 13 Oct 2023 07:14:50 GMT") + append("Host", URL(url).host) + append("Digest", "SHA-256=$encode") + } + val sign = httpSignatureSignerImpl.sign( + url, + HttpMethod.Post, + headers, + requestBody, + Key("https://example.com", privateKey, publicKey), + listOf("(request-target)", "date", "host", "digest") + ) + + val keyMap = object : KeyMap { + override fun getPublicKey(keyId: String?): PublicKey { + return publicKey + } + + override fun getPrivateKey(keyId: String?): PrivateKey { + return privateKey + } + + override fun getSecretKey(keyId: String?): SecretKey { + TODO("Not yet implemented") + } + + } + val verifier = SignatureHeaderVerifier.builder().keyMap(keyMap).build() + + val headers1 = headers { + appendAll(headers) + append("Signature", sign.sign.signatureHeader) + set("Digest", "aaaaaaaaaaaaaaaaafsadasfgafaaaaaaaaaaa") + } + + val httpMessage = object : HttpMessage, HttpRequest { + override fun headerValues(name: String?): MutableList { + return name?.let { headers1.getAll(it) }.orEmpty().toMutableList() + } + + override fun addHeader(name: String?, value: String?) { + TODO("Not yet implemented") + } + + override fun method(): String { + return "POST" + } + + override fun uri(): URI { + return URI(url) + } + } + val verify = verifier.verify(httpMessage) + assertFalse(verify) + } +}