feat: HTTP Signatureに関連したHTTP ヘッダーのチェックを追加

This commit is contained in:
usbharu 2024-02-19 19:30:45 +09:00
parent e21fc00eae
commit cbd8fe610e
3 changed files with 164 additions and 8 deletions

View File

@ -26,6 +26,7 @@ import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.application.infrastructure.springframework.RoleHierarchyAuthorizationManagerFactory import dev.usbharu.hideout.application.infrastructure.springframework.RoleHierarchyAuthorizationManagerFactory
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureFilter import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureFilter
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureHeaderChecker
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureVerifierComposite import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureVerifierComposite
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsImpl import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsImpl
@ -35,6 +36,9 @@ import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser
import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import jakarta.servlet.*
import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
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.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
@ -58,6 +62,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.annotation.web.invoke import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolderStrategy
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
import org.springframework.security.oauth2.core.AuthorizationGrantType import org.springframework.security.oauth2.core.AuthorizationGrantType
@ -67,20 +72,28 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer
import org.springframework.security.web.FilterChainProxy
import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.access.ExceptionTranslationFilter import org.springframework.security.web.access.ExceptionTranslationFilter
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler
import org.springframework.security.web.authentication.HttpStatusEntryPoint import org.springframework.security.web.authentication.HttpStatusEntryPoint
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.authentication.preauth.PreAuthenticatedAuthenticationProvider
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer
import org.springframework.security.web.debug.DebugFilter
import org.springframework.security.web.firewall.HttpFirewall
import org.springframework.security.web.firewall.RequestRejectedHandler
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
import org.springframework.security.web.util.matcher.AnyRequestMatcher import org.springframework.security.web.util.matcher.AnyRequestMatcher
import org.springframework.web.filter.CompositeFilter
import java.io.IOException
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey import java.security.interfaces.RSAPublicKey
import java.util.* import java.util.*
@EnableWebSecurity(debug = false)
@EnableWebSecurity(debug = true)
@Configuration @Configuration
@Suppress("FunctionMaxLength", "TooManyFunctions", "LongMethod") @Suppress("FunctionMaxLength", "TooManyFunctions", "LongMethod")
class SecurityConfig { class SecurityConfig {
@ -94,7 +107,7 @@ class SecurityConfig {
@Order(1) @Order(1)
fun httpSignatureFilterChain( fun httpSignatureFilterChain(
http: HttpSecurity, http: HttpSecurity,
httpSignatureFilter: HttpSignatureFilter httpSignatureFilter: HttpSignatureFilter,
): SecurityFilterChain { ): SecurityFilterChain {
http { http {
securityMatcher("/users/*/posts/*") securityMatcher("/users/*/posts/*")
@ -122,9 +135,10 @@ class SecurityConfig {
@Bean @Bean
fun getHttpSignatureFilter( fun getHttpSignatureFilter(
authenticationManager: AuthenticationManager, authenticationManager: AuthenticationManager,
httpSignatureHeaderChecker: HttpSignatureHeaderChecker,
): HttpSignatureFilter { ): HttpSignatureFilter {
val httpSignatureFilter = val httpSignatureFilter =
HttpSignatureFilter(DefaultSignatureHeaderParser()) HttpSignatureFilter(DefaultSignatureHeaderParser(), httpSignatureHeaderChecker)
httpSignatureFilter.setAuthenticationManager(authenticationManager) httpSignatureFilter.setAuthenticationManager(authenticationManager)
httpSignatureFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false) httpSignatureFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false)
val authenticationEntryPointFailureHandler = val authenticationEntryPointFailureHandler =
@ -147,7 +161,7 @@ class SecurityConfig {
@Order(1) @Order(1)
fun httpSignatureAuthenticationProvider( fun httpSignatureAuthenticationProvider(
transaction: Transaction, transaction: Transaction,
actorRepository: ActorRepository actorRepository: ActorRepository,
): PreAuthenticatedAuthenticationProvider { ): PreAuthenticatedAuthenticationProvider {
val provider = PreAuthenticatedAuthenticationProvider() val provider = PreAuthenticatedAuthenticationProvider()
val signatureHeaderParser = DefaultSignatureHeaderParser() val signatureHeaderParser = DefaultSignatureHeaderParser()
@ -190,7 +204,7 @@ class SecurityConfig {
@Order(4) @Order(4)
fun defaultSecurityFilterChain( fun defaultSecurityFilterChain(
http: HttpSecurity, http: HttpSecurity,
rf: RoleHierarchyAuthorizationManagerFactory rf: RoleHierarchyAuthorizationManagerFactory,
): SecurityFilterChain { ): SecurityFilterChain {
http { http {
authorizeHttpRequests { authorizeHttpRequests {
@ -401,6 +415,82 @@ class SecurityConfig {
return roleHierarchyImpl return roleHierarchyImpl
} }
@Bean
fun beanDefinitionRegistryPostProcessor(): BeanDefinitionRegistryPostProcessor {
return BeanDefinitionRegistryPostProcessor { registry: BeanDefinitionRegistry ->
registry.getBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME).beanClassName =
CompositeFilterChainProxy::class.java.name
}
}
internal class CompositeFilterChainProxy(filters: List<Filter?>) : FilterChainProxy() {
private val doFilterDelegate: Filter
private val springSecurityFilterChain: FilterChainProxy
init {
this.doFilterDelegate = createDoFilterDelegate(filters)
this.springSecurityFilterChain = findFilterChainProxy(filters)
}
override fun afterPropertiesSet() {
springSecurityFilterChain.afterPropertiesSet()
}
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
doFilterDelegate.doFilter(request, response, chain)
}
override fun getFilters(url: String): List<Filter> {
return springSecurityFilterChain.getFilters(url)
}
override fun getFilterChains(): List<SecurityFilterChain> {
return springSecurityFilterChain.filterChains
}
override fun setSecurityContextHolderStrategy(securityContextHolderStrategy: SecurityContextHolderStrategy) {
springSecurityFilterChain.setSecurityContextHolderStrategy(securityContextHolderStrategy)
}
override fun setFilterChainValidator(filterChainValidator: FilterChainValidator) {
springSecurityFilterChain.setFilterChainValidator(filterChainValidator)
}
override fun setFilterChainDecorator(filterChainDecorator: FilterChainDecorator) {
springSecurityFilterChain.setFilterChainDecorator(filterChainDecorator)
}
override fun setFirewall(firewall: HttpFirewall) {
springSecurityFilterChain.setFirewall(firewall)
}
override fun setRequestRejectedHandler(requestRejectedHandler: RequestRejectedHandler) {
springSecurityFilterChain.setRequestRejectedHandler(requestRejectedHandler)
}
companion object {
private fun createDoFilterDelegate(filters: List<Filter?>): Filter {
val delegate: CompositeFilter = CompositeFilter()
delegate.setFilters(filters)
return delegate
}
private fun findFilterChainProxy(filters: List<Filter?>): FilterChainProxy {
for (filter in filters) {
if (filter is FilterChainProxy) {
return filter
}
if (filter is DebugFilter) {
return filter.filterChainProxy
}
}
throw IllegalStateException("Couldn't find FilterChainProxy in $filters")
}
}
}
} }
@ConfigurationProperties("hideout.security.jwt") @ConfigurationProperties("hideout.security.jwt")
@ -408,14 +498,14 @@ class SecurityConfig {
data class JwkConfig( data class JwkConfig(
val keyId: String, val keyId: String,
val publicKey: String, val publicKey: String,
val privateKey: String val privateKey: String,
) )
@Configuration @Configuration
class PostSecurityConfig( class PostSecurityConfig(
val auth: AuthenticationManagerBuilder, val auth: AuthenticationManagerBuilder,
val daoAuthenticationProvider: DaoAuthenticationProvider, val daoAuthenticationProvider: DaoAuthenticationProvider,
val httpSignatureAuthenticationProvider: PreAuthenticatedAuthenticationProvider val httpSignatureAuthenticationProvider: PreAuthenticatedAuthenticationProvider,
) { ) {
@PostConstruct @PostConstruct

View File

@ -24,7 +24,10 @@ import jakarta.servlet.http.HttpServletRequest
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter
import java.net.URL import java.net.URL
class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) : class HttpSignatureFilter(
private val httpSignatureHeaderParser: SignatureHeaderParser,
private val httpSignatureHeaderChecker: HttpSignatureHeaderChecker,
) :
AbstractPreAuthenticatedProcessingFilter() { AbstractPreAuthenticatedProcessingFilter() {
override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? { override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? {
val headersList = request?.headerNames?.toList().orEmpty() val headersList = request?.headerNames?.toList().orEmpty()
@ -59,6 +62,15 @@ class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeader
} }
} }
httpSignatureHeaderChecker.checkDate(request.getHeader("date"))
httpSignatureHeaderChecker.checkHost(request.getHeader("host"))
if (request.method.equals("post", true)) {
httpSignatureHeaderChecker.checkDigest(request.inputStream.readAllBytes(), request.getHeader("digest"))
}
return HttpRequest( return HttpRequest(
URL(url + request.queryString.orEmpty()), URL(url + request.queryString.orEmpty()),
HttpHeaders(headers), HttpHeaders(headers),

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.infrastructure.springframework.httpsignature
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.util.Base64Util
import org.springframework.stereotype.Component
import java.security.MessageDigest
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.*
@Component
class HttpSignatureHeaderChecker(private val applicationConfig: ApplicationConfig) {
fun checkDate(date: String) {
val from = Instant.from(dateFormat.parse(date))
if (from.isAfter(Instant.now()) || from.isBefore(Instant.now().minusSeconds(86400))) {
throw IllegalArgumentException("未来")
}
}
fun checkHost(host: String) {
if (applicationConfig.url.host.equals(host, true).not()) {
throw IllegalArgumentException("ホスト名が違う")
}
}
fun checkDigest(byteArray: ByteArray, digest: String) {
val sha256 = MessageDigest.getInstance("SHA-256")
if (Base64Util.encode(sha256.digest(byteArray)).equals(digest, true).not()) {
throw IllegalArgumentException("リクエストボディが違う")
}
}
companion object {
private val dateFormat = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
}
}