mirror of https://github.com/usbharu/Hideout.git
Merge pull request #90 from usbharu/feature/http-signature
Feature/http signature
This commit is contained in:
commit
3fac808293
|
@ -73,6 +73,18 @@ tasks.create<GenerateTask>("openApiGenerateMastodonCompatibleApi", GenerateTask:
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
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 {
|
kotlin {
|
||||||
|
@ -125,7 +137,7 @@ dependencies {
|
||||||
implementation("software.amazon.awssdk:s3:2.20.157")
|
implementation("software.amazon.awssdk:s3:2.20.157")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor: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")
|
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-cio:$ktor_version")
|
||||||
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
|
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
|
||||||
testImplementation("io.ktor:ktor-client-mock:$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.junit.jupiter:junit-jupiter:5.8.1")
|
||||||
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
|
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
|
||||||
|
|
|
@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
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.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
@ -26,4 +28,7 @@ class ActivityPubConfig {
|
||||||
@Bean
|
@Bean
|
||||||
@Qualifier("http")
|
@Qualifier("http")
|
||||||
fun dateTimeFormatter(): DateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
|
fun dateTimeFormatter(): DateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun httpSignatureSigner(): HttpSignatureSigner = RsaSha256HttpSignatureSigner()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
package dev.usbharu.hideout.config
|
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.*
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
import io.ktor.client.plugins.cache.*
|
import io.ktor.client.plugins.cache.*
|
||||||
import io.ktor.client.plugins.logging.*
|
import io.ktor.client.plugins.logging.*
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import tech.barbero.http.message.signing.KeyMap
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
class HttpClientConfig {
|
class HttpClientConfig {
|
||||||
@Bean
|
@Bean
|
||||||
fun httpClient(keyMap: KeyMap): HttpClient = HttpClient(CIO).config {
|
fun httpClient(): HttpClient = HttpClient(CIO).config {
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
logger = Logger.DEFAULT
|
logger = Logger.DEFAULT
|
||||||
level = LogLevel.INFO
|
level = LogLevel.INFO
|
||||||
|
@ -23,17 +19,4 @@ class HttpClientConfig {
|
||||||
}
|
}
|
||||||
expectSuccess = true
|
expectSuccess = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun keyMap(
|
|
||||||
userQueryService: UserQueryService,
|
|
||||||
transaction: Transaction,
|
|
||||||
applicationConfig: ApplicationConfig
|
|
||||||
): KtorKeyMap {
|
|
||||||
return KtorKeyMap(
|
|
||||||
userQueryService,
|
|
||||||
transaction,
|
|
||||||
applicationConfig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,16 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet
|
||||||
import com.nimbusds.jose.jwk.source.JWKSource
|
import com.nimbusds.jose.jwk.source.JWKSource
|
||||||
import com.nimbusds.jose.proc.SecurityContext
|
import com.nimbusds.jose.proc.SecurityContext
|
||||||
import dev.usbharu.hideout.domain.model.UserDetailsImpl
|
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.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.condition.ConditionalOnProperty
|
||||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
|
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
|
||||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
|
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.HttpMethod
|
||||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
|
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.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.builders.HttpSecurity
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
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.core.Authentication
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder
|
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.oauth2.server.authorization.token.OAuth2TokenCustomizer
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
|
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.security.web.servlet.util.matcher.MvcRequestMatcher
|
||||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
|
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
|
@ -42,11 +56,64 @@ import java.util.*
|
||||||
|
|
||||||
@EnableWebSecurity(debug = false)
|
@EnableWebSecurity(debug = false)
|
||||||
@Configuration
|
@Configuration
|
||||||
@Suppress("FunctionMaxLength ")
|
@Suppress("FunctionMaxLength", "TooManyFunctions")
|
||||||
class SecurityConfig {
|
class SecurityConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var userQueryService: UserQueryService
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager? =
|
||||||
|
authenticationConfiguration.authenticationManager
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Order(1)
|
@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 {
|
fun oauth2SecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
||||||
val builder = MvcRequestMatcher.Builder(introspector)
|
val builder = MvcRequestMatcher.Builder(introspector)
|
||||||
|
|
||||||
|
@ -64,7 +131,7 @@ class SecurityConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Order(2)
|
@Order(4)
|
||||||
fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
||||||
val builder = MvcRequestMatcher.Builder(introspector)
|
val builder = MvcRequestMatcher.Builder(introspector)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiSe
|
||||||
override suspend fun apiV1StatusesPost(
|
override suspend fun apiV1StatusesPost(
|
||||||
devUsbharuHideoutDomainModelMastodonStatusesRequest: StatusesRequest
|
devUsbharuHideoutDomainModelMastodonStatusesRequest: StatusesRequest
|
||||||
): ResponseEntity<Status> {
|
): ResponseEntity<Status> {
|
||||||
|
|
||||||
val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt
|
val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt
|
||||||
|
|
||||||
return ResponseEntity(
|
return ResponseEntity(
|
||||||
|
@ -25,6 +24,5 @@ class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiSe
|
||||||
),
|
),
|
||||||
HttpStatus.OK
|
HttpStatus.OK
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ open class Person : Object {
|
||||||
private var icon: Image? = null
|
private var icon: Image? = null
|
||||||
var publicKey: Key? = null
|
var publicKey: Key? = null
|
||||||
var endpoints: Map<String, String> = emptyMap()
|
var endpoints: Map<String, String> = emptyMap()
|
||||||
|
var following: String? = null
|
||||||
|
var followers: String? = null
|
||||||
|
|
||||||
protected constructor() : super()
|
protected constructor() : super()
|
||||||
|
|
||||||
|
@ -24,7 +26,9 @@ open class Person : Object {
|
||||||
url: String?,
|
url: String?,
|
||||||
icon: Image?,
|
icon: Image?,
|
||||||
publicKey: Key?,
|
publicKey: Key?,
|
||||||
endpoints: Map<String, String> = emptyMap()
|
endpoints: Map<String, String> = emptyMap(),
|
||||||
|
followers: String?,
|
||||||
|
following: String?
|
||||||
) : super(add(type, "Person"), name, id = id) {
|
) : super(add(type, "Person"), name, id = id) {
|
||||||
this.preferredUsername = preferredUsername
|
this.preferredUsername = preferredUsername
|
||||||
this.summary = summary
|
this.summary = summary
|
||||||
|
@ -34,6 +38,8 @@ open class Person : Object {
|
||||||
this.icon = icon
|
this.icon = icon
|
||||||
this.publicKey = publicKey
|
this.publicKey = publicKey
|
||||||
this.endpoints = endpoints
|
this.endpoints = endpoints
|
||||||
|
this.followers = followers
|
||||||
|
this.following = following
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|
|
@ -9,4 +9,7 @@ data class RemoteUserCreateDto(
|
||||||
val outbox: String,
|
val outbox: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val publicKey: String,
|
val publicKey: String,
|
||||||
|
val keyId: String,
|
||||||
|
val followers: String?,
|
||||||
|
val following: String?
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,18 +16,22 @@ data class User private constructor(
|
||||||
val url: String,
|
val url: String,
|
||||||
val publicKey: String,
|
val publicKey: String,
|
||||||
val privateKey: String? = null,
|
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 {
|
override fun toString(): String =
|
||||||
return "User(id=$id, name='$name', domain='$domain', screenName='$screenName', description='$description'," +
|
"User(id=$id, name='$name', domain='$domain', screenName='$screenName', description='$description'," +
|
||||||
" password=****, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," +
|
" password=$password, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," +
|
||||||
" privateKey=****, createdAt=$createdAt)"
|
" privateKey=$privateKey, createdAt=$createdAt, keyId='$keyId', followers=$followers," +
|
||||||
}
|
" following=$following)"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(User::class.java)
|
private val logger = LoggerFactory.getLogger(User::class.java)
|
||||||
|
|
||||||
@Suppress("LongParameterList", "FunctionMinLength")
|
@Suppress("LongParameterList", "FunctionMinLength", "LongMethod")
|
||||||
fun of(
|
fun of(
|
||||||
id: Long,
|
id: Long,
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -40,7 +44,10 @@ data class User private constructor(
|
||||||
url: String,
|
url: String,
|
||||||
publicKey: String,
|
publicKey: String,
|
||||||
privateKey: String? = null,
|
privateKey: String? = null,
|
||||||
createdAt: Instant
|
createdAt: Instant,
|
||||||
|
keyId: String,
|
||||||
|
following: String? = null,
|
||||||
|
followers: String? = null
|
||||||
): User {
|
): User {
|
||||||
val characterLimit = Config.configData.characterLimit
|
val characterLimit = Config.configData.characterLimit
|
||||||
|
|
||||||
|
@ -115,6 +122,10 @@ data class User private constructor(
|
||||||
"outbox must not exceed ${characterLimit.general.url} characters."
|
"outbox must not exceed ${characterLimit.general.url} characters."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require(keyId.isNotBlank()) {
|
||||||
|
"keyId must contain non-blank characters."
|
||||||
|
}
|
||||||
|
|
||||||
return User(
|
return User(
|
||||||
id = id,
|
id = id,
|
||||||
name = limitedName,
|
name = limitedName,
|
||||||
|
@ -127,7 +138,10 @@ data class User private constructor(
|
||||||
url = url,
|
url = url,
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
privateKey = privateKey,
|
privateKey = privateKey,
|
||||||
createdAt = createdAt
|
createdAt = createdAt,
|
||||||
|
keyId = keyId,
|
||||||
|
followers = followers,
|
||||||
|
following = following
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<HttpSignaturePluginConfig> = 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<String>()
|
|
||||||
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<String> =
|
|
||||||
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")
|
|
||||||
}
|
|
|
@ -34,7 +34,10 @@ class FollowerQueryServiceImpl : FollowerQueryService {
|
||||||
followers[Users.url],
|
followers[Users.url],
|
||||||
followers[Users.publicKey],
|
followers[Users.publicKey],
|
||||||
followers[Users.privateKey],
|
followers[Users.privateKey],
|
||||||
followers[Users.createdAt]
|
followers[Users.createdAt],
|
||||||
|
followers[Users.keyId],
|
||||||
|
followers[Users.following],
|
||||||
|
followers[Users.followers]
|
||||||
)
|
)
|
||||||
.select { Users.id eq id }
|
.select { Users.id eq id }
|
||||||
.map {
|
.map {
|
||||||
|
@ -50,7 +53,10 @@ class FollowerQueryServiceImpl : FollowerQueryService {
|
||||||
url = it[followers[Users.url]],
|
url = it[followers[Users.url]],
|
||||||
publicKey = it[followers[Users.publicKey]],
|
publicKey = it[followers[Users.publicKey]],
|
||||||
privateKey = it[followers[Users.privateKey]],
|
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.url],
|
||||||
followers[Users.publicKey],
|
followers[Users.publicKey],
|
||||||
followers[Users.privateKey],
|
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) }
|
.select { Users.name eq name and (Users.domain eq domain) }
|
||||||
.map {
|
.map {
|
||||||
|
@ -95,7 +104,10 @@ class FollowerQueryServiceImpl : FollowerQueryService {
|
||||||
url = it[followers[Users.url]],
|
url = it[followers[Users.url]],
|
||||||
publicKey = it[followers[Users.publicKey]],
|
publicKey = it[followers[Users.publicKey]],
|
||||||
privateKey = it[followers[Users.privateKey]],
|
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.url],
|
||||||
followers[Users.publicKey],
|
followers[Users.publicKey],
|
||||||
followers[Users.privateKey],
|
followers[Users.privateKey],
|
||||||
followers[Users.createdAt]
|
followers[Users.createdAt],
|
||||||
|
followers[Users.keyId],
|
||||||
|
followers[Users.following],
|
||||||
|
followers[Users.followers]
|
||||||
)
|
)
|
||||||
.select { followers[Users.id] eq id }
|
.select { followers[Users.id] eq id }
|
||||||
.map {
|
.map {
|
||||||
|
@ -140,7 +155,10 @@ class FollowerQueryServiceImpl : FollowerQueryService {
|
||||||
url = it[followers[Users.url]],
|
url = it[followers[Users.url]],
|
||||||
publicKey = it[followers[Users.publicKey]],
|
publicKey = it[followers[Users.publicKey]],
|
||||||
privateKey = it[followers[Users.privateKey]],
|
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.url],
|
||||||
followers[Users.publicKey],
|
followers[Users.publicKey],
|
||||||
followers[Users.privateKey],
|
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) }
|
.select { followers[Users.name] eq name and (followers[Users.domain] eq domain) }
|
||||||
.map {
|
.map {
|
||||||
|
@ -185,7 +206,10 @@ class FollowerQueryServiceImpl : FollowerQueryService {
|
||||||
url = it[followers[Users.url]],
|
url = it[followers[Users.url]],
|
||||||
publicKey = it[followers[Users.publicKey]],
|
publicKey = it[followers[Users.publicKey]],
|
||||||
privateKey = it[followers[Users.privateKey]],
|
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]]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,5 @@ interface UserQueryService {
|
||||||
suspend fun findByUrl(url: String): User
|
suspend fun findByUrl(url: String): User
|
||||||
suspend fun findByIds(ids: List<Long>): List<User>
|
suspend fun findByIds(ids: List<Long>): List<User>
|
||||||
suspend fun existByNameAndDomain(name: String, domain: String): Boolean
|
suspend fun existByNameAndDomain(name: String, domain: String): Boolean
|
||||||
|
suspend fun findByKeyId(keyId: String): User
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,4 +44,10 @@ class UserQueryServiceImpl : UserQueryService {
|
||||||
|
|
||||||
override suspend fun existByNameAndDomain(name: String, domain: String): Boolean =
|
override suspend fun existByNameAndDomain(name: String, domain: String): Boolean =
|
||||||
Users.select { Users.name eq name and (Users.domain eq domain) }.empty().not()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,9 @@ class UserRepositoryImpl(private val idGenerateService: IdGenerateService) :
|
||||||
it[createdAt] = user.createdAt.toEpochMilli()
|
it[createdAt] = user.createdAt.toEpochMilli()
|
||||||
it[publicKey] = user.publicKey
|
it[publicKey] = user.publicKey
|
||||||
it[privateKey] = user.privateKey
|
it[privateKey] = user.privateKey
|
||||||
|
it[keyId] = user.keyId
|
||||||
|
it[following] = user.following
|
||||||
|
it[followers] = user.followers
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Users.update({ Users.id eq user.id }) {
|
Users.update({ Users.id eq user.id }) {
|
||||||
|
@ -43,6 +46,9 @@ class UserRepositoryImpl(private val idGenerateService: IdGenerateService) :
|
||||||
it[createdAt] = user.createdAt.toEpochMilli()
|
it[createdAt] = user.createdAt.toEpochMilli()
|
||||||
it[publicKey] = user.publicKey
|
it[publicKey] = user.publicKey
|
||||||
it[privateKey] = user.privateKey
|
it[privateKey] = user.privateKey
|
||||||
|
it[keyId] = user.keyId
|
||||||
|
it[following] = user.following
|
||||||
|
it[followers] = user.followers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
|
@ -89,6 +95,9 @@ object Users : Table("users") {
|
||||||
length = Config.configData.characterLimit.general.privateKey
|
length = Config.configData.characterLimit.general.privateKey
|
||||||
).nullable()
|
).nullable()
|
||||||
val createdAt: Column<Long> = long("created_at")
|
val createdAt: Column<Long> = 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)
|
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
@ -110,7 +119,10 @@ fun ResultRow.toUser(): User {
|
||||||
url = this[Users.url],
|
url = this[Users.url],
|
||||||
publicKey = this[Users.publicKey],
|
publicKey = this[Users.publicKey],
|
||||||
privateKey = this[Users.privateKey],
|
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]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@ import dev.usbharu.hideout.service.core.Transaction
|
||||||
import dev.usbharu.hideout.service.job.JobQueueParentService
|
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||||
import dev.usbharu.hideout.service.post.PostCreateInterceptor
|
import dev.usbharu.hideout.service.post.PostCreateInterceptor
|
||||||
import dev.usbharu.hideout.service.post.PostService
|
import dev.usbharu.hideout.service.post.PostService
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
import kjob.core.job.JobProps
|
import kjob.core.job.JobProps
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -58,7 +57,6 @@ interface APNoteService {
|
||||||
@Service
|
@Service
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
class APNoteServiceImpl(
|
class APNoteServiceImpl(
|
||||||
private val httpClient: HttpClient,
|
|
||||||
private val jobQueueParentService: JobQueueParentService,
|
private val jobQueueParentService: JobQueueParentService,
|
||||||
private val postRepository: PostRepository,
|
private val postRepository: PostRepository,
|
||||||
private val apUserService: APUserService,
|
private val apUserService: APUserService,
|
||||||
|
|
|
@ -49,18 +49,18 @@ class APReceiveFollowServiceImpl(
|
||||||
val person = apUserService.fetchPerson(actor, targetActor)
|
val person = apUserService.fetchPerson(actor, targetActor)
|
||||||
val follow = objectMapper.readValue<Follow>(props[ReceiveFollowJob.follow])
|
val follow = objectMapper.readValue<Follow>(props[ReceiveFollowJob.follow])
|
||||||
|
|
||||||
val signer = userQueryService.findByUrl(actor)
|
val signer = userQueryService.findByUrl(targetActor)
|
||||||
|
|
||||||
val urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found")
|
val urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found")
|
||||||
|
|
||||||
apRequestService.apPost(
|
apRequestService.apPost(
|
||||||
urlString,
|
url = urlString,
|
||||||
Accept(
|
body = Accept(
|
||||||
name = "Follow",
|
name = "Follow",
|
||||||
`object` = follow,
|
`object` = follow,
|
||||||
actor = targetActor
|
actor = targetActor
|
||||||
),
|
),
|
||||||
signer
|
signer = signer
|
||||||
)
|
)
|
||||||
|
|
||||||
val targetEntity = userQueryService.findByUrl(targetActor)
|
val targetEntity = userQueryService.findByUrl(targetActor)
|
||||||
|
|
|
@ -3,15 +3,20 @@ package dev.usbharu.hideout.service.ap
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import dev.usbharu.hideout.domain.model.ap.Object
|
import dev.usbharu.hideout.domain.model.ap.Object
|
||||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
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.Base64Util
|
||||||
import dev.usbharu.hideout.util.HttpUtil.Activity
|
import dev.usbharu.hideout.util.HttpUtil.Activity
|
||||||
import dev.usbharu.hideout.util.RsaUtil
|
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.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import io.ktor.util.*
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
@ -46,14 +51,14 @@ class APRequestServiceImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
val sign = httpSignatureSigner.sign(
|
val sign = httpSignatureSigner.sign(
|
||||||
url = url,
|
httpRequest = HttpRequest(
|
||||||
method = HttpMethod.Get,
|
url = u,
|
||||||
headers = headers,
|
headers = HttpHeaders(headers.toMap()),
|
||||||
requestBody = "",
|
HttpMethod.GET
|
||||||
keyPair = Key(
|
),
|
||||||
|
privateKey = PrivateKey(
|
||||||
keyId = "${signer.url}#pubkey",
|
keyId = "${signer.url}#pubkey",
|
||||||
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey),
|
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey),
|
||||||
publicKey = RsaUtil.decodeRsaPublicKeyPem(signer.publicKey)
|
|
||||||
),
|
),
|
||||||
signHeaders = listOf("(request-target)", "date", "host", "accept")
|
signHeaders = listOf("(request-target)", "date", "host", "accept")
|
||||||
)
|
)
|
||||||
|
@ -61,7 +66,8 @@ class APRequestServiceImpl(
|
||||||
val bodyAsText = httpClient.get(url) {
|
val bodyAsText = httpClient.get(url) {
|
||||||
headers {
|
headers {
|
||||||
headers {
|
headers {
|
||||||
appendAll(sign.headers)
|
appendAll(headers)
|
||||||
|
append("Signature", sign.signatureHeader)
|
||||||
remove("Host")
|
remove("Host")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +103,8 @@ class APRequestServiceImpl(
|
||||||
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
|
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
|
||||||
val u = URL(url)
|
val u = URL(url)
|
||||||
if (signer?.privateKey == null) {
|
if (signer?.privateKey == null) {
|
||||||
|
logger.debug("NOT SIGN Request: {}", url)
|
||||||
|
logger.trace("{}", signer)
|
||||||
return httpClient.post(url) {
|
return httpClient.post(url) {
|
||||||
header("Accept", ContentType.Application.Activity)
|
header("Accept", ContentType.Application.Activity)
|
||||||
header("Date", date)
|
header("Date", date)
|
||||||
|
@ -106,6 +114,8 @@ class APRequestServiceImpl(
|
||||||
}.bodyAsText()
|
}.bodyAsText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug("SIGN Request: {}", url)
|
||||||
|
|
||||||
val headers = headers {
|
val headers = headers {
|
||||||
append("Accept", ContentType.Application.Activity)
|
append("Accept", ContentType.Application.Activity)
|
||||||
append("Date", date)
|
append("Date", date)
|
||||||
|
@ -114,14 +124,14 @@ class APRequestServiceImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
val sign = httpSignatureSigner.sign(
|
val sign = httpSignatureSigner.sign(
|
||||||
url = url,
|
httpRequest = HttpRequest(
|
||||||
method = HttpMethod.Post,
|
u,
|
||||||
headers = headers,
|
HttpHeaders(headers.toMap()),
|
||||||
requestBody = "",
|
HttpMethod.POST
|
||||||
keyPair = Key(
|
),
|
||||||
keyId = "${signer.url}#pubkey",
|
privateKey = PrivateKey(
|
||||||
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey),
|
keyId = signer.keyId,
|
||||||
publicKey = RsaUtil.decodeRsaPublicKeyPem(signer.publicKey)
|
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey)
|
||||||
),
|
),
|
||||||
signHeaders = listOf("(request-target)", "date", "host", "digest")
|
signHeaders = listOf("(request-target)", "date", "host", "digest")
|
||||||
)
|
)
|
||||||
|
@ -129,11 +139,17 @@ class APRequestServiceImpl(
|
||||||
return httpClient.post(url) {
|
return httpClient.post(url) {
|
||||||
headers {
|
headers {
|
||||||
headers {
|
headers {
|
||||||
appendAll(sign.headers)
|
appendAll(headers)
|
||||||
|
append("Signature", sign.signatureHeader)
|
||||||
|
remove("Host")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setBody(requestBody)
|
setBody(requestBody)
|
||||||
contentType(ContentType.Application.Activity)
|
contentType(ContentType.Application.Activity)
|
||||||
}.bodyAsText()
|
}.bodyAsText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(APRequestServiceImpl::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,11 +64,13 @@ class APUserServiceImpl(
|
||||||
publicKey = Key(
|
publicKey = Key(
|
||||||
type = emptyList(),
|
type = emptyList(),
|
||||||
name = "Public Key",
|
name = "Public Key",
|
||||||
id = "$userUrl#pubkey",
|
id = userEntity.keyId,
|
||||||
owner = userUrl,
|
owner = userUrl,
|
||||||
publicKeyPem = userEntity.publicKey
|
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(
|
publicKey = Key(
|
||||||
type = emptyList(),
|
type = emptyList(),
|
||||||
name = "Public Key",
|
name = "Public Key",
|
||||||
id = "$url#pubkey",
|
id = userEntity.keyId,
|
||||||
owner = url,
|
owner = url,
|
||||||
publicKeyPem = userEntity.publicKey
|
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
|
) to userEntity
|
||||||
} catch (ignore: FailedToGetResourcesException) {
|
} catch (ignore: FailedToGetResourcesException) {
|
||||||
val person = apResourceResolveService.resolve<Person>(url, null as Long?)
|
val person = apResourceResolveService.resolve<Person>(url, null as Long?)
|
||||||
|
@ -118,6 +122,9 @@ class APUserServiceImpl(
|
||||||
url = url,
|
url = url,
|
||||||
publicKey = person.publicKey?.publicKeyPem
|
publicKey = person.publicKey?.publicKeyPem
|
||||||
?: throw IllegalActivityPubObjectException("publicKey is null"),
|
?: throw IllegalActivityPubObjectException("publicKey is null"),
|
||||||
|
keyId = person.publicKey?.id ?: throw IllegalActivityPubObjectException("publicKey keyId is null"),
|
||||||
|
following = person.following,
|
||||||
|
followers = person.followers
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String> {
|
|
||||||
// return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun addHeader(name: String?, value: String?) {
|
|
||||||
// TODO()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String>
|
|
||||||
): SignedRequest
|
|
||||||
|
|
||||||
suspend fun signRaw(signString: String, keyPair: Key, signHeaders: List<String>): Sign
|
|
||||||
}
|
|
|
@ -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<String>
|
|
||||||
): 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<String>): 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>
|
|
||||||
): 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"
|
|
||||||
}
|
|
|
@ -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<out GrantedAuthority>?
|
||||||
|
) : User(
|
||||||
|
username,
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
credentialsNonExpired,
|
||||||
|
accountNonLocked,
|
||||||
|
authorities
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Serial
|
||||||
|
private const val serialVersionUID: Long = -3330552099960982997L
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<PreAuthenticatedAuthenticationToken> {
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, HttpSignatureVerifier>,
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
)
|
|
|
@ -1,6 +0,0 @@
|
||||||
package dev.usbharu.hideout.service.signature
|
|
||||||
|
|
||||||
data class Sign(
|
|
||||||
val signature: String,
|
|
||||||
val signatureHeader: String
|
|
||||||
)
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -34,6 +34,7 @@ class UserServiceImpl(
|
||||||
val nextId = userRepository.nextId()
|
val nextId = userRepository.nextId()
|
||||||
val hashedPassword = userAuthService.hash(user.password)
|
val hashedPassword = userAuthService.hash(user.password)
|
||||||
val keyPair = userAuthService.generateKeyPair()
|
val keyPair = userAuthService.generateKeyPair()
|
||||||
|
val userUrl = "${applicationConfig.url}/users/${user.name}"
|
||||||
val userEntity = User.of(
|
val userEntity = User.of(
|
||||||
id = nextId,
|
id = nextId,
|
||||||
name = user.name,
|
name = user.name,
|
||||||
|
@ -41,12 +42,15 @@ class UserServiceImpl(
|
||||||
screenName = user.screenName,
|
screenName = user.screenName,
|
||||||
description = user.description,
|
description = user.description,
|
||||||
password = hashedPassword,
|
password = hashedPassword,
|
||||||
inbox = "${applicationConfig.url}/users/${user.name}/inbox",
|
inbox = "$userUrl/inbox",
|
||||||
outbox = "${applicationConfig.url}/users/${user.name}/outbox",
|
outbox = "$userUrl/outbox",
|
||||||
url = "${applicationConfig.url}/users/${user.name}",
|
url = userUrl,
|
||||||
publicKey = keyPair.public.toPem(),
|
publicKey = keyPair.public.toPem(),
|
||||||
privateKey = keyPair.private.toPem(),
|
privateKey = keyPair.private.toPem(),
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
following = "$userUrl/following",
|
||||||
|
followers = "$userUrl/followers",
|
||||||
|
keyId = "$userUrl#pubkey"
|
||||||
)
|
)
|
||||||
return userRepository.save(userEntity)
|
return userRepository.save(userEntity)
|
||||||
}
|
}
|
||||||
|
@ -63,7 +67,10 @@ class UserServiceImpl(
|
||||||
outbox = user.outbox,
|
outbox = user.outbox,
|
||||||
url = user.url,
|
url = user.url,
|
||||||
publicKey = user.publicKey,
|
publicKey = user.publicKey,
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
followers = user.followers,
|
||||||
|
following = user.following,
|
||||||
|
keyId = user.keyId
|
||||||
)
|
)
|
||||||
return try {
|
return try {
|
||||||
userRepository.save(userEntity)
|
userRepository.save(userEntity)
|
||||||
|
|
|
@ -19,7 +19,7 @@ spring:
|
||||||
default-property-inclusion: always
|
default-property-inclusion: always
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.h2.Driver
|
driver-class-name: org.h2.Driver
|
||||||
url: "jdbc:h2:./test-dev2;MODE=POSTGRESQL"
|
url: "jdbc:h2:./test-dev3;MODE=POSTGRESQL"
|
||||||
username: ""
|
username: ""
|
||||||
password: ""
|
password: ""
|
||||||
data:
|
data:
|
||||||
|
|
|
@ -13,4 +13,5 @@
|
||||||
<logger name="Exposed" level="INFO"/>
|
<logger name="Exposed" level="INFO"/>
|
||||||
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
|
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
|
||||||
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="INFO"/>
|
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="INFO"/>
|
||||||
|
<logger name="org.mongodb.driver.protocol.command" level="INFO"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
|
@ -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<UserQueryService> {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -48,7 +48,8 @@ class APNoteServiceImplTest {
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
keyId = "a"
|
||||||
),
|
),
|
||||||
User.of(
|
User.of(
|
||||||
3L,
|
3L,
|
||||||
|
@ -61,7 +62,8 @@ class APNoteServiceImplTest {
|
||||||
"https://follower2.example.com",
|
"https://follower2.example.com",
|
||||||
"https://follower2.example.com",
|
"https://follower2.example.com",
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
keyId = "a"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val userQueryService = mock<UserQueryService> {
|
val userQueryService = mock<UserQueryService> {
|
||||||
|
@ -77,7 +79,8 @@ class APNoteServiceImplTest {
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
privateKey = "a",
|
privateKey = "a",
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
keyId = "a"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val followerQueryService = mock<FollowerQueryService> {
|
val followerQueryService = mock<FollowerQueryService> {
|
||||||
|
@ -86,17 +89,16 @@ class APNoteServiceImplTest {
|
||||||
val jobQueueParentService = mock<JobQueueParentService>()
|
val jobQueueParentService = mock<JobQueueParentService>()
|
||||||
val activityPubNoteService =
|
val activityPubNoteService =
|
||||||
APNoteServiceImpl(
|
APNoteServiceImpl(
|
||||||
httpClient = mock(),
|
|
||||||
jobQueueParentService = jobQueueParentService,
|
jobQueueParentService = jobQueueParentService,
|
||||||
postRepository = mock(),
|
postRepository = mock(),
|
||||||
apUserService = mock(),
|
apUserService = mock(),
|
||||||
userQueryService = userQueryService,
|
userQueryService = userQueryService,
|
||||||
followerQueryService = followerQueryService,
|
followerQueryService = followerQueryService,
|
||||||
postQueryService = mock(),
|
postQueryService = mock(),
|
||||||
|
mediaQueryService = mediaQueryService,
|
||||||
objectMapper = objectMapper,
|
objectMapper = objectMapper,
|
||||||
applicationConfig = testApplicationConfig,
|
applicationConfig = testApplicationConfig,
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
mediaQueryService = mediaQueryService,
|
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
apRequestService = mock(),
|
apRequestService = mock(),
|
||||||
transaction = mock()
|
transaction = mock()
|
||||||
|
@ -129,20 +131,19 @@ class APNoteServiceImplTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val activityPubNoteService = APNoteServiceImpl(
|
val activityPubNoteService = APNoteServiceImpl(
|
||||||
httpClient = httpClient,
|
|
||||||
jobQueueParentService = mock(),
|
jobQueueParentService = mock(),
|
||||||
postRepository = mock(),
|
postRepository = mock(),
|
||||||
apUserService = mock(),
|
apUserService = mock(),
|
||||||
userQueryService = mock(),
|
userQueryService = mock(),
|
||||||
followerQueryService = mock(),
|
followerQueryService = mock(),
|
||||||
postQueryService = mock(),
|
postQueryService = mock(),
|
||||||
|
mediaQueryService = mediaQueryService,
|
||||||
objectMapper = objectMapper,
|
objectMapper = objectMapper,
|
||||||
applicationConfig = testApplicationConfig,
|
applicationConfig = testApplicationConfig,
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
mediaQueryService = mediaQueryService,
|
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
transaction = mock(),
|
apRequestService = mock(),
|
||||||
apRequestService = mock()
|
transaction = mock()
|
||||||
)
|
)
|
||||||
activityPubNoteService.createNoteJob(
|
activityPubNoteService.createNoteJob(
|
||||||
JobProps(
|
JobProps(
|
||||||
|
|
|
@ -102,7 +102,9 @@ class APReceiveFollowServiceImplTest {
|
||||||
id = "https://follower.example.com#main-key",
|
id = "https://follower.example.com#main-key",
|
||||||
owner = "https://follower.example.com",
|
owner = "https://follower.example.com",
|
||||||
publicKeyPem = "BEGIN PUBLIC KEY...END PUBLIC KEY",
|
publicKeyPem = "BEGIN PUBLIC KEY...END PUBLIC KEY",
|
||||||
)
|
),
|
||||||
|
followers = "",
|
||||||
|
following = ""
|
||||||
|
|
||||||
)
|
)
|
||||||
val apUserService = mock<APUserService> {
|
val apUserService = mock<APUserService> {
|
||||||
|
@ -120,7 +122,8 @@ class APReceiveFollowServiceImplTest {
|
||||||
outbox = "https://example.com/outbox",
|
outbox = "https://example.com/outbox",
|
||||||
url = "https://example.com",
|
url = "https://example.com",
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
keyId = "a"
|
||||||
)
|
)
|
||||||
onBlocking { findByUrl(eq("https://follower.example.com")) } doReturn
|
onBlocking { findByUrl(eq("https://follower.example.com")) } doReturn
|
||||||
User.of(
|
User.of(
|
||||||
|
@ -133,7 +136,8 @@ class APReceiveFollowServiceImplTest {
|
||||||
outbox = "https://follower.example.com/outbox",
|
outbox = "https://follower.example.com/outbox",
|
||||||
url = "https://follower.example.com",
|
url = "https://follower.example.com",
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
keyId = "a"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,8 @@ class APResourceResolveServiceImplTest {
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
keyId = ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -82,7 +83,8 @@ class APResourceResolveServiceImplTest {
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
keyId = ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -120,7 +122,8 @@ class APResourceResolveServiceImplTest {
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
keyId = ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -169,7 +172,8 @@ class APResourceResolveServiceImplTest {
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
"https://follower.example.com",
|
"https://follower.example.com",
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
createdAt = Instant.now()
|
createdAt = Instant.now(),
|
||||||
|
keyId = ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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<String> {
|
|
||||||
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<String> {
|
|
||||||
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<String> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -58,14 +58,17 @@ class UserServiceTest {
|
||||||
}
|
}
|
||||||
val userService = UserServiceImpl(userRepository, mock(), mock(), mock(), mock(), testApplicationConfig)
|
val userService = UserServiceImpl(userRepository, mock(), mock(), mock(), mock(), testApplicationConfig)
|
||||||
val user = RemoteUserCreateDto(
|
val user = RemoteUserCreateDto(
|
||||||
"test",
|
name = "test",
|
||||||
"example.com",
|
domain = "example.com",
|
||||||
"testUser",
|
screenName = "testUser",
|
||||||
"test user",
|
description = "test user",
|
||||||
"https://example.com/inbox",
|
inbox = "https://example.com/inbox",
|
||||||
"https://example.com/outbox",
|
outbox = "https://example.com/outbox",
|
||||||
"https://example.com",
|
url = "https://example.com",
|
||||||
"-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----"
|
publicKey = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
|
||||||
|
keyId = "a",
|
||||||
|
following = "",
|
||||||
|
followers = ""
|
||||||
)
|
)
|
||||||
userService.createRemoteUser(user)
|
userService.createRemoteUser(user)
|
||||||
verify(userRepository, times(1)).save(any())
|
verify(userRepository, times(1)).save(any())
|
||||||
|
|
Loading…
Reference in New Issue