diff --git a/build.gradle.kts b/build.gradle.kts index 289e4815..0269b222 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -73,6 +73,18 @@ tasks.create("openApiGenerateMastodonCompatibleApi", GenerateTask: repositories { mavenCentral() + maven { + url = uri("https://git.usbharu.dev/api/packages/usbharu/maven") + } + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/usbharu/http-signature") + credentials { + + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } } kotlin { @@ -125,7 +137,7 @@ dependencies { implementation("software.amazon.awssdk:s3:2.20.157") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3") - + implementation("dev.usbharu:http-signature:1.0.0") implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") @@ -136,7 +148,6 @@ dependencies { implementation("io.ktor:ktor-client-cio:$ktor_version") implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") testImplementation("io.ktor:ktor-client-mock:$ktor_version") - implementation("tech.barbero.http-messages-signing:http-messages-signing-core:1.0.0") testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") diff --git a/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt index e9786a1f..8d83c5dd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.httpsignature.sign.HttpSignatureSigner +import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -26,4 +28,7 @@ class ActivityPubConfig { @Bean @Qualifier("http") fun dateTimeFormatter(): DateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + + @Bean + fun httpSignatureSigner(): HttpSignatureSigner = RsaSha256HttpSignatureSigner() } diff --git a/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt index 3332c81d..0daa4aa0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt @@ -1,20 +1,16 @@ package dev.usbharu.hideout.config -import dev.usbharu.hideout.plugins.KtorKeyMap -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.core.Transaction import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.logging.* import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import tech.barbero.http.message.signing.KeyMap @Configuration class HttpClientConfig { @Bean - fun httpClient(keyMap: KeyMap): HttpClient = HttpClient(CIO).config { + fun httpClient(): HttpClient = HttpClient(CIO).config { install(Logging) { logger = Logger.DEFAULT level = LogLevel.INFO @@ -23,17 +19,4 @@ class HttpClientConfig { } expectSuccess = true } - - @Bean - fun keyMap( - userQueryService: UserQueryService, - transaction: Transaction, - applicationConfig: ApplicationConfig - ): KtorKeyMap { - return KtorKeyMap( - userQueryService, - transaction, - applicationConfig - ) - } } diff --git a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt index 56bafa6c..841abf53 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt @@ -7,7 +7,16 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.proc.SecurityContext import dev.usbharu.hideout.domain.model.UserDetailsImpl +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.core.Transaction +import dev.usbharu.hideout.service.signature.HttpSignatureFilter +import dev.usbharu.hideout.service.signature.HttpSignatureUserDetailsService +import dev.usbharu.hideout.service.signature.HttpSignatureVerifierComposite import dev.usbharu.hideout.util.RsaUtil +import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner +import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser +import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer import org.springframework.boot.autoconfigure.security.servlet.PathRequest @@ -19,9 +28,13 @@ import org.springframework.core.annotation.Order import org.springframework.http.HttpMethod import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.security.authentication.AccountStatusUserDetailsChecker +import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.Customizer +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.core.Authentication import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder @@ -33,6 +46,7 @@ import org.springframework.security.oauth2.server.authorization.token.JwtEncodin import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher import org.springframework.web.servlet.handler.HandlerMappingIntrospector import java.security.KeyPairGenerator @@ -42,11 +56,64 @@ import java.util.* @EnableWebSecurity(debug = false) @Configuration -@Suppress("FunctionMaxLength ") +@Suppress("FunctionMaxLength", "TooManyFunctions") class SecurityConfig { + @Autowired + private lateinit var userQueryService: UserQueryService + + @Bean + fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager? = + authenticationConfiguration.authenticationManager + @Bean @Order(1) + fun httpSignatureFilterChain(http: HttpSecurity, httpSignatureFilter: HttpSignatureFilter): SecurityFilterChain { + http.securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox") + .addFilter(httpSignatureFilter) + .authorizeHttpRequests { + it.anyRequest().permitAll() + } + .csrf { + it.disable() + } + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + return http.build() + } + + @Bean + fun getHttpSignatureFilter(authenticationManager: AuthenticationManager): HttpSignatureFilter { + val httpSignatureFilter = HttpSignatureFilter(DefaultSignatureHeaderParser()) + httpSignatureFilter.setAuthenticationManager(authenticationManager) + return httpSignatureFilter + } + + @Bean + fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider { + val provider = PreAuthenticatedAuthenticationProvider() + provider.setPreAuthenticatedUserDetailsService( + HttpSignatureUserDetailsService( + userQueryService, + HttpSignatureVerifierComposite( + mapOf( + "rsa-sha256" to RsaSha256HttpSignatureVerifier( + DefaultSignatureHeaderParser(), + RsaSha256HttpSignatureSigner() + ) + ), + DefaultSignatureHeaderParser() + ), + transaction + ) + ) + provider.setUserDetailsChecker(AccountStatusUserDetailsChecker()) + return provider + } + + @Bean + @Order(2) fun oauth2SecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain { val builder = MvcRequestMatcher.Builder(introspector) @@ -64,7 +131,7 @@ class SecurityConfig { } @Bean - @Order(2) + @Order(4) fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain { val builder = MvcRequestMatcher.Builder(introspector) diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt index 796894ae..02737c71 100644 --- a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt +++ b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt @@ -15,7 +15,6 @@ class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiSe override suspend fun apiV1StatusesPost( devUsbharuHideoutDomainModelMastodonStatusesRequest: StatusesRequest ): ResponseEntity { - val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt return ResponseEntity( @@ -25,6 +24,5 @@ class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiSe ), HttpStatus.OK ) - } } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Person.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Person.kt index 5c7f1176..e7625b44 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Person.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Person.kt @@ -9,6 +9,8 @@ open class Person : Object { private var icon: Image? = null var publicKey: Key? = null var endpoints: Map = emptyMap() + var following: String? = null + var followers: String? = null protected constructor() : super() @@ -24,7 +26,9 @@ open class Person : Object { url: String?, icon: Image?, publicKey: Key?, - endpoints: Map = emptyMap() + endpoints: Map = emptyMap(), + followers: String?, + following: String? ) : super(add(type, "Person"), name, id = id) { this.preferredUsername = preferredUsername this.summary = summary @@ -34,6 +38,8 @@ open class Person : Object { this.icon = icon this.publicKey = publicKey this.endpoints = endpoints + this.followers = followers + this.following = following } override fun equals(other: Any?): Boolean { diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/RemoteUserCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/RemoteUserCreateDto.kt index f36eed2a..2a6a34fb 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/RemoteUserCreateDto.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/RemoteUserCreateDto.kt @@ -9,4 +9,7 @@ data class RemoteUserCreateDto( val outbox: String, val url: String, val publicKey: String, + val keyId: String, + val followers: String?, + val following: String? ) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/User.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/User.kt index 9a2c4e4a..d79d617b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/User.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/User.kt @@ -16,18 +16,22 @@ data class User private constructor( val url: String, val publicKey: String, val privateKey: String? = null, - val createdAt: Instant + val createdAt: Instant, + val keyId: String, + val followers: String? = null, + val following: String? = null ) { - override fun toString(): String { - return "User(id=$id, name='$name', domain='$domain', screenName='$screenName', description='$description'," + - " password=****, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," + - " privateKey=****, createdAt=$createdAt)" - } + override fun toString(): String = + "User(id=$id, name='$name', domain='$domain', screenName='$screenName', description='$description'," + + " password=$password, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," + + " privateKey=$privateKey, createdAt=$createdAt, keyId='$keyId', followers=$followers," + + " following=$following)" companion object { + private val logger = LoggerFactory.getLogger(User::class.java) - @Suppress("LongParameterList", "FunctionMinLength") + @Suppress("LongParameterList", "FunctionMinLength", "LongMethod") fun of( id: Long, name: String, @@ -40,7 +44,10 @@ data class User private constructor( url: String, publicKey: String, privateKey: String? = null, - createdAt: Instant + createdAt: Instant, + keyId: String, + following: String? = null, + followers: String? = null ): User { val characterLimit = Config.configData.characterLimit @@ -115,6 +122,10 @@ data class User private constructor( "outbox must not exceed ${characterLimit.general.url} characters." } + require(keyId.isNotBlank()) { + "keyId must contain non-blank characters." + } + return User( id = id, name = limitedName, @@ -127,7 +138,10 @@ data class User private constructor( url = url, publicKey = publicKey, privateKey = privateKey, - createdAt = createdAt + createdAt = createdAt, + keyId = keyId, + followers = followers, + following = following ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt deleted file mode 100644 index 6b235333..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt +++ /dev/null @@ -1,183 +0,0 @@ -package dev.usbharu.hideout.plugins - -import dev.usbharu.hideout.config.ApplicationConfig -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.core.Transaction -import dev.usbharu.hideout.service.user.UserAuthServiceImpl -import io.ktor.client.plugins.api.* -import io.ktor.client.request.* -import io.ktor.http.* -import kotlinx.coroutines.runBlocking -import tech.barbero.http.message.signing.HttpMessage -import tech.barbero.http.message.signing.HttpMessageSigner -import tech.barbero.http.message.signing.HttpRequest -import tech.barbero.http.message.signing.KeyMap -import java.net.URI -import java.security.KeyFactory -import java.security.PrivateKey -import java.security.PublicKey -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import java.text.SimpleDateFormat -import java.util.* -import javax.crypto.SecretKey - -class HttpSignaturePluginConfig { - lateinit var keyMap: KeyMap -} - -val httpSignaturePlugin: ClientPlugin = createClientPlugin( - "HttpSign", - ::HttpSignaturePluginConfig -) { - val keyMap = pluginConfig.keyMap - val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) - format.timeZone = TimeZone.getTimeZone("GMT") - onRequest { request, body -> - - request.header("Date", format.format(Date())) - request.header("Host", request.url.host) - if (request.bodyType?.type == String::class) { - body as String - -// UserAuthService.sha256.reset() - val digest = - Base64.getEncoder().encodeToString(UserAuthServiceImpl.sha256.digest(body.toByteArray(Charsets.UTF_8))) - request.headers.append("Digest", "sha-256=$digest") - } - - if (request.headers.contains("Signature")) { - val all = request.headers.getAll("Signature").orEmpty() - val parameters = mutableListOf() - for (s in all) { - s.split(",").forEach { parameters.add(it) } - } - - val keyId = parameters.find { it.startsWith("keyId") } - .orEmpty() - .split("=")[1] - .replace("\"", "") - val algorithm = - parameters.find { it.startsWith("algorithm") } - .orEmpty() - .split("=")[1] - .replace("\"", "") - val headers = parameters.find { it.startsWith("headers") } - .orEmpty() - .split("=")[1] - .replace("\"", "") - .split(" ") - .toMutableList() - - val algorithmType = when (algorithm) { - "rsa-sha256" -> { - HttpMessageSigner.Algorithm.RSA_SHA256 - } - - else -> { - TODO() - } - } - - headers.map { - when (it) { - "(request-target)" -> { - HttpMessageSigner.REQUEST_TARGET - } - - "digest" -> { - "Digest" - } - - "date" -> { - "Date" - } - - "host" -> { - "Host" - } - - else -> { - it - } - } - } - - val builder = HttpMessageSigner.builder().algorithm(algorithmType).keyId(keyId).keyMap(keyMap) - var tmp = builder - headers.forEach { - tmp = tmp.addHeaderToSign(it) - } - val signer = tmp.build() - - request.headers.remove("Signature") - - (signer ?: return@onRequest).sign(object : HttpMessage, HttpRequest { - override fun headerValues(name: String?): MutableList = - name?.let { request.headers.getAll(it) }?.toMutableList() ?: mutableListOf() - - override fun addHeader(name: String?, value: String?) { - val split = value?.split("=").orEmpty() - name?.let { request.header(it, split[0] + "=\"" + split[1].trim('"') + "\"") } - } - - override fun method(): String = request.method.value - - override fun uri(): URI = request.url.build().toURI() - }) - - val signatureHeader = request.headers.getAll("Signature").orEmpty() - request.headers.remove("Signature") - signatureHeader.joinToString(",") { it.replace("; ", ",").replace(";", ",") } - .let { request.header("Signature", it) } - } - } -} - -class KtorKeyMap( - private val userQueryService: UserQueryService, - private val transaction: Transaction, - private val applicationConfig: ApplicationConfig -) : KeyMap { - override fun getPublicKey(keyId: String?): PublicKey = runBlocking { - val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey") - .substringAfterLast("/") - val publicBytes = Base64.getDecoder().decode( - transaction.transaction { - userQueryService.findByNameAndDomain( - username, - applicationConfig.url.host - ).run { - publicKey - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - .replace("\n", "") - } - } - ) - val x509EncodedKeySpec = X509EncodedKeySpec(publicBytes) - return@runBlocking KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) - } - - override fun getPrivateKey(keyId: String?): PrivateKey = runBlocking { - val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey") - .substringAfterLast("/") - val publicBytes = Base64.getDecoder().decode( - transaction.transaction { - userQueryService.findByNameAndDomain( - username, - applicationConfig.url.host - ).privateKey?.run { - replace("-----BEGIN PRIVATE KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - .replace("\n", "") - } - } - ) - val x509EncodedKeySpec = PKCS8EncodedKeySpec(publicBytes) - return@runBlocking KeyFactory.getInstance("RSA").generatePrivate(x509EncodedKeySpec) - } - - @Suppress("NotImplementedDeclaration") - override fun getSecretKey(keyId: String?): SecretKey = TODO("Not yet implemented") -} diff --git a/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryServiceImpl.kt index bb4f56fc..10be4068 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryServiceImpl.kt @@ -34,7 +34,10 @@ class FollowerQueryServiceImpl : FollowerQueryService { followers[Users.url], followers[Users.publicKey], followers[Users.privateKey], - followers[Users.createdAt] + followers[Users.createdAt], + followers[Users.keyId], + followers[Users.following], + followers[Users.followers] ) .select { Users.id eq id } .map { @@ -50,7 +53,10 @@ class FollowerQueryServiceImpl : FollowerQueryService { url = it[followers[Users.url]], publicKey = it[followers[Users.publicKey]], privateKey = it[followers[Users.privateKey]], - createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]) + createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), + keyId = it[followers[Users.keyId]], + followers = it[followers[Users.followers]], + following = it[followers[Users.following]] ) } } @@ -79,7 +85,10 @@ class FollowerQueryServiceImpl : FollowerQueryService { followers[Users.url], followers[Users.publicKey], followers[Users.privateKey], - followers[Users.createdAt] + followers[Users.createdAt], + followers[Users.keyId], + followers[Users.following], + followers[Users.followers] ) .select { Users.name eq name and (Users.domain eq domain) } .map { @@ -95,7 +104,10 @@ class FollowerQueryServiceImpl : FollowerQueryService { url = it[followers[Users.url]], publicKey = it[followers[Users.publicKey]], privateKey = it[followers[Users.privateKey]], - createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]) + createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), + keyId = it[followers[Users.keyId]], + followers = it[followers[Users.followers]], + following = it[followers[Users.following]] ) } } @@ -124,7 +136,10 @@ class FollowerQueryServiceImpl : FollowerQueryService { followers[Users.url], followers[Users.publicKey], followers[Users.privateKey], - followers[Users.createdAt] + followers[Users.createdAt], + followers[Users.keyId], + followers[Users.following], + followers[Users.followers] ) .select { followers[Users.id] eq id } .map { @@ -140,7 +155,10 @@ class FollowerQueryServiceImpl : FollowerQueryService { url = it[followers[Users.url]], publicKey = it[followers[Users.publicKey]], privateKey = it[followers[Users.privateKey]], - createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]) + createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), + keyId = it[followers[Users.keyId]], + followers = it[followers[Users.followers]], + following = it[followers[Users.following]] ) } } @@ -169,7 +187,10 @@ class FollowerQueryServiceImpl : FollowerQueryService { followers[Users.url], followers[Users.publicKey], followers[Users.privateKey], - followers[Users.createdAt] + followers[Users.createdAt], + followers[Users.keyId], + followers[Users.following], + followers[Users.followers] ) .select { followers[Users.name] eq name and (followers[Users.domain] eq domain) } .map { @@ -185,7 +206,10 @@ class FollowerQueryServiceImpl : FollowerQueryService { url = it[followers[Users.url]], publicKey = it[followers[Users.publicKey]], privateKey = it[followers[Users.privateKey]], - createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]) + createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), + keyId = it[followers[Users.keyId]], + followers = it[followers[Users.followers]], + following = it[followers[Users.following]] ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/query/UserQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/query/UserQueryService.kt index 09e5972a..0bab8304 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/UserQueryService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/UserQueryService.kt @@ -12,4 +12,5 @@ interface UserQueryService { suspend fun findByUrl(url: String): User suspend fun findByIds(ids: List): List suspend fun existByNameAndDomain(name: String, domain: String): Boolean + suspend fun findByKeyId(keyId: String): User } diff --git a/src/main/kotlin/dev/usbharu/hideout/query/UserQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/UserQueryServiceImpl.kt index ef2e8cd6..351a8303 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/UserQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/UserQueryServiceImpl.kt @@ -44,4 +44,10 @@ class UserQueryServiceImpl : UserQueryService { override suspend fun existByNameAndDomain(name: String, domain: String): Boolean = Users.select { Users.name eq name and (Users.domain eq domain) }.empty().not() + + override suspend fun findByKeyId(keyId: String): User { + return Users.select { Users.keyId eq keyId } + .singleOr { FailedToGetResourcesException("keyId: $keyId is duplicate or does not exist.", it) } + .toUser() + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepositoryImpl.kt index 10fb3a2e..a6955a5d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepositoryImpl.kt @@ -29,6 +29,9 @@ class UserRepositoryImpl(private val idGenerateService: IdGenerateService) : it[createdAt] = user.createdAt.toEpochMilli() it[publicKey] = user.publicKey it[privateKey] = user.privateKey + it[keyId] = user.keyId + it[following] = user.following + it[followers] = user.followers } } else { Users.update({ Users.id eq user.id }) { @@ -43,6 +46,9 @@ class UserRepositoryImpl(private val idGenerateService: IdGenerateService) : it[createdAt] = user.createdAt.toEpochMilli() it[publicKey] = user.publicKey it[privateKey] = user.privateKey + it[keyId] = user.keyId + it[following] = user.following + it[followers] = user.followers } } return user @@ -89,6 +95,9 @@ object Users : Table("users") { length = Config.configData.characterLimit.general.privateKey ).nullable() val createdAt: Column = long("created_at") + val keyId = varchar("key_id", length = Config.configData.characterLimit.general.url) + val following = varchar("following", length = Config.configData.characterLimit.general.url).nullable() + val followers = varchar("followers", length = Config.configData.characterLimit.general.url).nullable() override val primaryKey: PrimaryKey = PrimaryKey(id) @@ -110,7 +119,10 @@ fun ResultRow.toUser(): User { url = this[Users.url], publicKey = this[Users.publicKey], privateKey = this[Users.privateKey], - createdAt = Instant.ofEpochMilli((this[Users.createdAt])) + createdAt = Instant.ofEpochMilli((this[Users.createdAt])), + keyId = this[Users.keyId], + followers = this[Users.followers], + following = this[Users.following] ) } 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 a989af78..6fbe6583 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt @@ -23,7 +23,6 @@ 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 kjob.core.job.JobProps import kotlinx.coroutines.CoroutineScope @@ -58,7 +57,6 @@ interface APNoteService { @Service @Suppress("LongParameterList") class APNoteServiceImpl( - private val httpClient: HttpClient, private val jobQueueParentService: JobQueueParentService, private val postRepository: PostRepository, private val apUserService: APUserService, 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 38af4dd2..6d9321cd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt @@ -49,18 +49,18 @@ class APReceiveFollowServiceImpl( val person = apUserService.fetchPerson(actor, targetActor) val follow = objectMapper.readValue(props[ReceiveFollowJob.follow]) - val signer = userQueryService.findByUrl(actor) + val signer = userQueryService.findByUrl(targetActor) val urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found") apRequestService.apPost( - urlString, - Accept( + url = urlString, + body = Accept( name = "Follow", `object` = follow, actor = targetActor ), - signer + signer = signer ) val targetEntity = userQueryService.findByUrl(targetActor) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt index 4d8e227d..3106f131 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt @@ -3,15 +3,20 @@ 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 dev.usbharu.httpsignature.common.HttpHeaders +import dev.usbharu.httpsignature.common.HttpMethod +import dev.usbharu.httpsignature.common.HttpRequest +import dev.usbharu.httpsignature.common.PrivateKey +import dev.usbharu.httpsignature.sign.HttpSignatureSigner import io.ktor.client.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* +import io.ktor.util.* +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Service import java.net.URL @@ -46,14 +51,14 @@ class APRequestServiceImpl( } val sign = httpSignatureSigner.sign( - url = url, - method = HttpMethod.Get, - headers = headers, - requestBody = "", - keyPair = Key( + httpRequest = HttpRequest( + url = u, + headers = HttpHeaders(headers.toMap()), + HttpMethod.GET + ), + privateKey = PrivateKey( keyId = "${signer.url}#pubkey", privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey), - publicKey = RsaUtil.decodeRsaPublicKeyPem(signer.publicKey) ), signHeaders = listOf("(request-target)", "date", "host", "accept") ) @@ -61,7 +66,8 @@ class APRequestServiceImpl( val bodyAsText = httpClient.get(url) { headers { headers { - appendAll(sign.headers) + appendAll(headers) + append("Signature", sign.signatureHeader) remove("Host") } } @@ -97,6 +103,8 @@ class APRequestServiceImpl( val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT"))) val u = URL(url) if (signer?.privateKey == null) { + logger.debug("NOT SIGN Request: {}", url) + logger.trace("{}", signer) return httpClient.post(url) { header("Accept", ContentType.Application.Activity) header("Date", date) @@ -106,6 +114,8 @@ class APRequestServiceImpl( }.bodyAsText() } + logger.debug("SIGN Request: {}", url) + val headers = headers { append("Accept", ContentType.Application.Activity) append("Date", date) @@ -114,14 +124,14 @@ class APRequestServiceImpl( } 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) + httpRequest = HttpRequest( + u, + HttpHeaders(headers.toMap()), + HttpMethod.POST + ), + privateKey = PrivateKey( + keyId = signer.keyId, + privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey) ), signHeaders = listOf("(request-target)", "date", "host", "digest") ) @@ -129,11 +139,17 @@ class APRequestServiceImpl( return httpClient.post(url) { headers { headers { - appendAll(sign.headers) + appendAll(headers) + append("Signature", sign.signatureHeader) + remove("Host") } } setBody(requestBody) contentType(ContentType.Application.Activity) }.bodyAsText() } + + companion object { + private val logger = LoggerFactory.getLogger(APRequestServiceImpl::class.java) + } } 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 5909d3f5..23f7b57b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt @@ -64,11 +64,13 @@ class APUserServiceImpl( publicKey = Key( type = emptyList(), name = "Public Key", - id = "$userUrl#pubkey", + id = userEntity.keyId, owner = userUrl, publicKeyPem = userEntity.publicKey ), - endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox") + endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox"), + followers = userEntity.followers, + following = userEntity.following ) } @@ -96,11 +98,13 @@ class APUserServiceImpl( publicKey = Key( type = emptyList(), name = "Public Key", - id = "$url#pubkey", + id = userEntity.keyId, owner = url, publicKeyPem = userEntity.publicKey ), - endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox") + endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox"), + followers = userEntity.followers, + following = userEntity.following ) to userEntity } catch (ignore: FailedToGetResourcesException) { val person = apResourceResolveService.resolve(url, null as Long?) @@ -118,6 +122,9 @@ class APUserServiceImpl( url = url, publicKey = person.publicKey?.publicKeyPem ?: throw IllegalActivityPubObjectException("publicKey is null"), + keyId = person.publicKey?.id ?: throw IllegalActivityPubObjectException("publicKey keyId is null"), + following = person.following, + followers = person.followers ) ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyService.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyService.kt deleted file mode 100644 index f0009080..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyService.kt +++ /dev/null @@ -1,38 +0,0 @@ -package dev.usbharu.hideout.service.auth - -import dev.usbharu.hideout.config.ApplicationConfig -import dev.usbharu.hideout.plugins.KtorKeyMap -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.core.Transaction -import io.ktor.http.* -import org.springframework.stereotype.Service -import tech.barbero.http.message.signing.SignatureHeaderVerifier - -@Service -interface HttpSignatureVerifyService { - fun verify(headers: Headers): Boolean -} - -@Service -class HttpSignatureVerifyServiceImpl( - private val userQueryService: UserQueryService, - private val transaction: Transaction, - private val applicationConfig: ApplicationConfig -) : HttpSignatureVerifyService { - override fun verify(headers: Headers): Boolean { - val build = - SignatureHeaderVerifier.builder().keyMap(KtorKeyMap(userQueryService, transaction, applicationConfig)) - .build() - return true -// build.verify(object : HttpMessage { -// override fun headerValues(name: String?): MutableList { -// return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf() -// } -// -// override fun addHeader(name: String?, value: String?) { -// TODO() -// } -// -// }) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt new file mode 100644 index 00000000..2a7e15b8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt @@ -0,0 +1,46 @@ +package dev.usbharu.hideout.service.signature + +import dev.usbharu.httpsignature.common.HttpHeaders +import dev.usbharu.httpsignature.common.HttpMethod +import dev.usbharu.httpsignature.common.HttpRequest +import dev.usbharu.httpsignature.verify.SignatureHeaderParser +import jakarta.servlet.http.HttpServletRequest +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter +import java.net.URL + +class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) : + AbstractPreAuthenticatedProcessingFilter() { + override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any { + val headersList = request?.headerNames?.toList().orEmpty() + + val headers = + headersList.associateWith { header -> request?.getHeaders(header)?.toList().orEmpty() } + + val signature = httpSignatureHeaderParser.parse(HttpHeaders(headers)) + return signature.keyId + } + + override fun getPreAuthenticatedCredentials(request: HttpServletRequest?): Any { + requireNotNull(request) + val url = request.requestURL.toString() + + val headersList = request.headerNames?.toList().orEmpty() + + val headers = + headersList.associateWith { header -> request.getHeaders(header)?.toList().orEmpty() } + + val method = when (val method = request.method.lowercase()) { + "get" -> HttpMethod.GET + "post" -> HttpMethod.POST + else -> { + throw IllegalArgumentException("Unsupported method: $method") + } + } + + return HttpRequest( + URL(url + request.queryString.orEmpty()), + HttpHeaders(headers), + method + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSigner.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSigner.kt deleted file mode 100644 index f920f048..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSigner.kt +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 193658ba..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImpl.kt +++ /dev/null @@ -1,84 +0,0 @@ -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/HttpSignatureUser.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt new file mode 100644 index 00000000..d9c55816 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt @@ -0,0 +1,26 @@ +package dev.usbharu.hideout.service.signature + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.User +import java.io.Serial + +class HttpSignatureUser( + username: String, + val domain: String, + credentialsNonExpired: Boolean, + accountNonLocked: Boolean, + authorities: MutableCollection? +) : User( + username, + "", + true, + true, + credentialsNonExpired, + accountNonLocked, + authorities +) { + companion object { + @Serial + private const val serialVersionUID: Long = -3330552099960982997L + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt new file mode 100644 index 00000000..f86d7bfb --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt @@ -0,0 +1,58 @@ +package dev.usbharu.hideout.service.signature + +import dev.usbharu.hideout.exception.FailedToGetResourcesException +import dev.usbharu.hideout.exception.HttpSignatureVerifyException +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.core.Transaction +import dev.usbharu.hideout.util.RsaUtil +import dev.usbharu.httpsignature.common.HttpRequest +import dev.usbharu.httpsignature.common.PublicKey +import dev.usbharu.httpsignature.verify.FailedVerification +import dev.usbharu.httpsignature.verify.HttpSignatureVerifier +import kotlinx.coroutines.runBlocking +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken + +class HttpSignatureUserDetailsService( + private val userQueryService: UserQueryService, + private val httpSignatureVerifier: HttpSignatureVerifier, + private val transaction: Transaction +) : + AuthenticationUserDetailsService { + override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking { + transaction.transaction { + if (token.principal !is String) { + throw IllegalStateException("Token is not String") + } + if (token.credentials !is HttpRequest) { + throw IllegalStateException("Credentials is not HttpRequest") + } + + val keyId = token.principal as String + val findByKeyId = try { + userQueryService.findByKeyId(keyId) + } catch (e: FailedToGetResourcesException) { + throw UsernameNotFoundException("User not found", e) + } + + val verify = httpSignatureVerifier.verify( + token.credentials as HttpRequest, + PublicKey(RsaUtil.decodeRsaPublicKeyPem(findByKeyId.publicKey), keyId) + ) + + if (verify is FailedVerification) { + throw HttpSignatureVerifyException(verify.reason) + } + + HttpSignatureUser( + username = findByKeyId.name, + domain = findByKeyId.domain, + credentialsNonExpired = true, + accountNonLocked = true, + authorities = mutableListOf() + ) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifierComposite.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifierComposite.kt new file mode 100644 index 00000000..c6a0041f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifierComposite.kt @@ -0,0 +1,22 @@ +package dev.usbharu.hideout.service.signature + +import dev.usbharu.httpsignature.common.HttpRequest +import dev.usbharu.httpsignature.common.PublicKey +import dev.usbharu.httpsignature.verify.HttpSignatureVerifier +import dev.usbharu.httpsignature.verify.SignatureHeaderParser +import dev.usbharu.httpsignature.verify.VerificationResult + +class HttpSignatureVerifierComposite( + private val map: Map, + private val httpSignatureHeaderParser: SignatureHeaderParser +) : HttpSignatureVerifier { + override fun verify(httpRequest: HttpRequest, key: PublicKey): VerificationResult { + val signature = httpSignatureHeaderParser.parse(httpRequest.headers) + val verify = map[signature.algorithm]?.verify(httpRequest, key) + if (verify != null) { + return verify + } + + throw IllegalArgumentException("Unsupported algorithm. ${signature.algorithm}") + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/Key.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/Key.kt deleted file mode 100644 index 0eb5171f..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/Key.kt +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 75711759..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/Sign.kt +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 346dca87..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/SignedRequest.kt +++ /dev/null @@ -1,23 +0,0 @@ -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/service/user/UserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/user/UserServiceImpl.kt index 0f865526..bb349ff3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/user/UserServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/user/UserServiceImpl.kt @@ -34,6 +34,7 @@ class UserServiceImpl( val nextId = userRepository.nextId() val hashedPassword = userAuthService.hash(user.password) val keyPair = userAuthService.generateKeyPair() + val userUrl = "${applicationConfig.url}/users/${user.name}" val userEntity = User.of( id = nextId, name = user.name, @@ -41,12 +42,15 @@ class UserServiceImpl( screenName = user.screenName, description = user.description, password = hashedPassword, - inbox = "${applicationConfig.url}/users/${user.name}/inbox", - outbox = "${applicationConfig.url}/users/${user.name}/outbox", - url = "${applicationConfig.url}/users/${user.name}", + inbox = "$userUrl/inbox", + outbox = "$userUrl/outbox", + url = userUrl, publicKey = keyPair.public.toPem(), privateKey = keyPair.private.toPem(), - createdAt = Instant.now() + createdAt = Instant.now(), + following = "$userUrl/following", + followers = "$userUrl/followers", + keyId = "$userUrl#pubkey" ) return userRepository.save(userEntity) } @@ -63,7 +67,10 @@ class UserServiceImpl( outbox = user.outbox, url = user.url, publicKey = user.publicKey, - createdAt = Instant.now() + createdAt = Instant.now(), + followers = user.followers, + following = user.following, + keyId = user.keyId ) return try { userRepository.save(userEntity) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 60814a17..a2d2690f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,7 +19,7 @@ spring: default-property-inclusion: always datasource: driver-class-name: org.h2.Driver - url: "jdbc:h2:./test-dev2;MODE=POSTGRESQL" + url: "jdbc:h2:./test-dev3;MODE=POSTGRESQL" username: "" password: "" data: diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 5853405c..73cc4b4a 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -13,4 +13,5 @@ + diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt deleted file mode 100644 index 6eeca290..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -package dev.usbharu.hideout.plugins - -import dev.usbharu.hideout.domain.model.hideout.entity.User -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.user.toPem -import org.junit.jupiter.api.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.mock -import utils.TestApplicationConfig.testApplicationConfig -import utils.TestTransaction -import java.security.KeyPairGenerator -import java.time.Instant - -class KtorKeyMapTest { - - @Test - fun getPrivateKey() { - val userQueryService = mock { - onBlocking { findByNameAndDomain(any(), any()) } doAnswer { - val keyPairGenerator = KeyPairGenerator.getInstance("RSA") - keyPairGenerator.initialize(1024) - val generateKeyPair = keyPairGenerator.generateKeyPair() - User.of( - 1, - "test", - "localhost", - "test", - "", - "", - "https://example.com/inbox", - "https://example.com/outbox", - "https://example.com", - "", - generateKeyPair.private.toPem(), - createdAt = Instant.now() - ) - } - } - val ktorKeyMap = KtorKeyMap(userQueryService, TestTransaction, testApplicationConfig) - - ktorKeyMap.getPrivateKey("test") - } -} 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 9d6975f7..2543c8e9 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt @@ -48,7 +48,8 @@ class APNoteServiceImplTest { "https://follower.example.com", "https://follower.example.com", publicKey = "", - createdAt = Instant.now() + createdAt = Instant.now(), + keyId = "a" ), User.of( 3L, @@ -61,7 +62,8 @@ class APNoteServiceImplTest { "https://follower2.example.com", "https://follower2.example.com", publicKey = "", - createdAt = Instant.now() + createdAt = Instant.now(), + keyId = "a" ) ) val userQueryService = mock { @@ -77,7 +79,8 @@ class APNoteServiceImplTest { "https://example.com", publicKey = "", privateKey = "a", - createdAt = Instant.now() + createdAt = Instant.now(), + keyId = "a" ) } val followerQueryService = mock { @@ -86,17 +89,16 @@ class APNoteServiceImplTest { val jobQueueParentService = mock() val activityPubNoteService = APNoteServiceImpl( - httpClient = mock(), jobQueueParentService = jobQueueParentService, postRepository = mock(), apUserService = mock(), userQueryService = userQueryService, followerQueryService = followerQueryService, postQueryService = mock(), + mediaQueryService = mediaQueryService, objectMapper = objectMapper, applicationConfig = testApplicationConfig, postService = mock(), - mediaQueryService = mediaQueryService, apResourceResolveService = mock(), apRequestService = mock(), transaction = mock() @@ -129,20 +131,19 @@ class APNoteServiceImplTest { } ) val activityPubNoteService = APNoteServiceImpl( - httpClient = httpClient, jobQueueParentService = mock(), postRepository = mock(), apUserService = mock(), userQueryService = mock(), followerQueryService = mock(), postQueryService = mock(), + mediaQueryService = mediaQueryService, objectMapper = objectMapper, applicationConfig = testApplicationConfig, postService = mock(), - mediaQueryService = mediaQueryService, apResourceResolveService = mock(), - transaction = mock(), - apRequestService = mock() + apRequestService = mock(), + transaction = 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 d16fcc2e..d1d51fa7 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt @@ -102,7 +102,9 @@ class APReceiveFollowServiceImplTest { id = "https://follower.example.com#main-key", owner = "https://follower.example.com", publicKeyPem = "BEGIN PUBLIC KEY...END PUBLIC KEY", - ) + ), + followers = "", + following = "" ) val apUserService = mock { @@ -111,30 +113,32 @@ class APReceiveFollowServiceImplTest { val userQueryService = mock { onBlocking { findByUrl(eq("https://example.com")) } doReturn User.of( - id = 1L, - name = "test", - domain = "example.com", - screenName = "testUser", - description = "This user is test user.", - inbox = "https://example.com/inbox", - outbox = "https://example.com/outbox", - url = "https://example.com", - publicKey = "", - createdAt = Instant.now() - ) + id = 1L, + name = "test", + domain = "example.com", + screenName = "testUser", + description = "This user is test user.", + inbox = "https://example.com/inbox", + outbox = "https://example.com/outbox", + url = "https://example.com", + publicKey = "", + createdAt = Instant.now(), + keyId = "a" + ) onBlocking { findByUrl(eq("https://follower.example.com")) } doReturn User.of( - id = 2L, - name = "follower", - domain = "follower.example.com", - screenName = "followerUser", - description = "This user is test follower user.", - inbox = "https://follower.example.com/inbox", - outbox = "https://follower.example.com/outbox", - url = "https://follower.example.com", - publicKey = "", - createdAt = Instant.now() - ) + id = 2L, + name = "follower", + domain = "follower.example.com", + screenName = "followerUser", + description = "This user is test follower user.", + inbox = "https://follower.example.com/inbox", + outbox = "https://follower.example.com/outbox", + url = "https://follower.example.com", + publicKey = "", + createdAt = Instant.now(), + keyId = "a" + ) } val userService = mock { 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 index fa244184..344a07d8 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImplTest.kt @@ -47,7 +47,8 @@ class APResourceResolveServiceImplTest { "https://follower.example.com", "https://follower.example.com", publicKey = "", - createdAt = Instant.now() + createdAt = Instant.now(), + keyId = "" ) ) @@ -82,7 +83,8 @@ class APResourceResolveServiceImplTest { "https://follower.example.com", "https://follower.example.com", publicKey = "", - createdAt = Instant.now() + createdAt = Instant.now(), + keyId = "" ) ) @@ -120,7 +122,8 @@ class APResourceResolveServiceImplTest { "https://follower.example.com", "https://follower.example.com", publicKey = "", - createdAt = Instant.now() + createdAt = Instant.now(), + keyId = "" ) ) @@ -169,7 +172,8 @@ class APResourceResolveServiceImplTest { "https://follower.example.com", "https://follower.example.com", publicKey = "", - createdAt = Instant.now() + createdAt = Instant.now(), + keyId = "" ) ) diff --git a/src/test/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImplTest.kt deleted file mode 100644 index 09c006db..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureSignerImplTest.kt +++ /dev/null @@ -1,340 +0,0 @@ -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) - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/user/UserServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/user/UserServiceTest.kt index 650fd81c..26e4ebf8 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/user/UserServiceTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/user/UserServiceTest.kt @@ -58,14 +58,17 @@ class UserServiceTest { } val userService = UserServiceImpl(userRepository, mock(), mock(), mock(), mock(), testApplicationConfig) val user = RemoteUserCreateDto( - "test", - "example.com", - "testUser", - "test user", - "https://example.com/inbox", - "https://example.com/outbox", - "https://example.com", - "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----" + name = "test", + domain = "example.com", + screenName = "testUser", + description = "test user", + inbox = "https://example.com/inbox", + outbox = "https://example.com/outbox", + url = "https://example.com", + publicKey = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", + keyId = "a", + following = "", + followers = "" ) userService.createRemoteUser(user) verify(userRepository, times(1)).save(any())