webauthn4j をGraalVMでnativeビルドしてクラウド(cloud run)へデプロイするまでに出会ったエラー達

# 結論
いったん、解決まで辿り着かなかったので、備忘として残しておきます。

# 構成
| ライブラリ | version|
|:----|---:|
|GraalVM||
|org.springframework.experimental.aot|0.11.3|
|Spring Boot|2.6.4|
|org.springframework.boot:spring-boot-starter-security||
|org.springframework.boot:spring-boot-starter-thymeleaf||
|com.webauthn4j:webauthn4j-spring-security-core|0.7.3.RELEASE|

# 前提
アプリ自体は動作確認済み。

# Spring SecurityでCSRFトークンを生成できない

# LogFileうんたらかんたらのエラーが止まらない
https://qiita.com/OmeletteCurry19/items/7a8a3577da3bbcead64b

# herokuへデプロイできない

# Cloud RunでSpring Profileの指定方法が分からない。
エントリポイントへの引数で`-Dspring...`で指定可能。

```
-Dspring.profiles.active=production
```

# デプロイ後、動かすと、webauthn4j のエラーが出る

契機は、認証器登録時のattestationObjectを取得する際。

>com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.webauthn4j.springframework.security.endpoint.ErrorResponse and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) at com.webauthn4j.converter.util.JsonConverter.writeValueAsString(JsonConverter.java:104) ~[na:na] at

シリアライズ可能なクラスが見つからない。

SerializationFeature.FAIL_ON_EMPTY_BEANSとは、

> Feature that determines what happens when no accessors are found for a type (and there are no annotations to indicate it is meant to be serialized). If enabled (default), an exception is thrown to indicate these as non-serializable types; if disabled, they are serialized as empty Objects, i.e. without any properties.
Note that empty types that this feature has only effect on those "empty" beans that do not have any recognized annotations (like @JsonSerialize): ones that do have annotations do not result in an exception being thrown.
>
> Feature is enabled by default.

空のJSONを作ろうとした際にエラーとなる設定の模様。
そもそも、空のJSONを返される想定は無いのだが、何らかの原因で空になっている疑いがある。
GraalVMではリフレクションを用いた処理に制限があるので、jacksonはそもそも動かせるのだろうか。

spring-nativeのissueを漁ったら、そのものズバリがあった。
https://github.com/spring-projects-experimental/spring-native/issues/1454

Hintアノテーションが必要そうである。
`com.webauthn4j.springframework.security.endpoint.ErrorResponse`クラスが`Serializable`を実装しており、
`@SerializationHint`が必要そう。

```
@SerializationHint(types = {ErrorResponse.class})
```

をSpringBootApplicationに追加して再度チャレンジ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87538/778117e7-17a3-62f6-a2fb-d08355657ca6.png)

うん、エラーは帰ってくるようになった。意味はない。

原因不明のため、webauthn4jをtraceレベルへ変更した。

```application.properties
logging.level.com.webauthn4j=trace
```

### error

> java.io.UncheckedIOException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Class com.webauthn4j.springframework.security.converter.jackson.deserializer.ByteArraySerializer has no default (no arg) constructor (through reference chain: com.webauthn4j.springframework.security.options.AttestationOptions["user"])

`com.webauthn4j.springframework.security.converter.jackson.deserializer.ByteArraySerializer`がリフレクションで作ろうとされているようなので、以下を追加する。

```
@SerializationHint(types = {com.webauthn4j.springframework.security.converter.jackson.deserializer.ByteArraySerializer.class})
```

### error
> com.webauthn4j.springframework.security.exception.PrincipalNotFoundException: principal not found.
        at com.webauthn4j.springframework.security.authenticator.InMemoryWebAuthnAuthenticatorManager.loadAuthenticatorsByUserPrincipal(InMemoryWebAuthnAuthenticatorManager.java:50) ~[na:na]
        at 

`principal`とは認証結果（ユーザ含む）のはず。
`Principal`インタフェースを継承したクラスが見当たらない。

該当のソースを見てみると、

```java
    public List<WebAuthnAuthenticator> loadAuthenticatorsByUserPrincipal(Object userPrincipal) {
        Map<String, WebAuthnAuthenticator> innerMap = map.get(userPrincipal);
        if(innerMap == null || innerMap.isEmpty()){
            throw new PrincipalNotFoundException("principal not found.");
        }
        return Collections.unmodifiableList(new ArrayList<>(innerMap.values()));
    }
```

`WebAuthnAuthenticator`のインスタンスが無い模様。
実装クラスは、`WebAuthnAuthenticatorImpl`。
これも`@SerializationHint`へ追加する。

```java
@SerializationHint(types = {WebAuthnAuthenticatorImpl.class})
```

### 次のerror
> Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [collection type; class java.util.HashSet, contains [simple type, class java.lang.Object]]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.util.HashSet` (no Creators, like default constructor, exist): no default no-arguments constructor found
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 563] (through reference chain: *.security.SpringSecurityPractice.dto.Fido2AttestationObject["response"]->*.security.SpringSecurityPractice.dto.Fido2AttestationObject$Response["transports"])] with root cause
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.util.HashSet` (no Creators, like default constructor, exist): no default no-arguments constructor found
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 563] (through reference chain: *.security.SpringSecurityPractice.dto.Fido2AttestationObject["response"]->*.security.SpringSecurityPractice.dto.Fido2AttestationObject$Response["transports"])

1. java.util.Hashsetがインスタンス化できない？
2. Fido2AttestationObject$Responseのコンストラクタが明示的に必要か？

2から試してみる。

結果：意味なし

1およびTypeHintを追加してみる。リフレクション経由で生成されていると思われるため。

```
@TypeHint(types={
        Fido2AttestationObject.class,
        Fido2AttestationObject.Response.class,
        java.util.HashSet.class
})
```

エラーは解消し、進んだ。認証器の登録まで出来た。

----
## WebAuthn Login
> c.w.s.security.WebAuthnProcessingFilter  : Failed to process authentication request
>
com.webauthn4j.springframework.security.exception.UserNotVerifiedException: Validator is configured to check user verified, but UV flag in authenticatorData is not set.

resultを送ったら、エラーに。
恐らく認証情報のインスタンス化をリフレクションで行っており、それができないのだろう。
TypeHintに認証時のリクエストを受けるDTOと思われる`WebAuthnAuthenticationRequest`を追加する。

```java
@TypeHint(types={
WebAuthnAuthenticationRequest.class,
}}
```

> com.webauthn4j.springframework.security.exception.BadSignatureException: Assertion signature is not valid.

まだ解決しない。
ソースを追うと、WebAuthnAssertionAuthenticationTokenのインスタンス化に失敗してそう。

`WebAuthnAssertionAuthenticationToken`,`WebAuthenticationDetails`を見つけたので、追加。

```java
@TypeHint(types={
        WebAuthnAssertionAuthenticationToken.class,
        WebAuthenticationDetails.class
}}
```

あてずっぽうは辞める・・・。
graalvmのドキュメント読むと、`-H:+TraceSecurityServices`というオプションがあり、Security関係のログ出力を詳細化できそう。

このオプションは、`META-INF/native-image/native-image.properties`に定義すれば取り込まれそう。
[JCASecurityServices](https://www.graalvm.org/22.0/reference-manual/native-image/JCASecurityServices/)
[JCA](https://docs.oracle.com/en/java/javase/11/security/java-cryptography-architecture-jca-reference-guide.html#GUID-2BCFDD85-D533-4E6C-8CE9-29990DEB0190)

```native-image.properties
Args=-H:+TraceSecurityServices
```

`-H:+TraceClassInitialization`も使えそう。

50分近くビルドが終わらないので、中断。

-----
メモ。たぶん、いま時点は不要（というかデフォルトは入っていると考えられる。）
https://github.com/oracle/graal/issues/1962#issuecomment-948382054

-----

Signatureの処理を細かく見るしか無いか。
Signatureは、クライアント側で生成したSignatureとサーバ側で計算したSignatureを比較する処理。

* クライアント側からは送信は出来ている。
* 比較自体は実行出来ており、それに関する例外はなく、比較結果の取得まで至っている。

考えられるのは、

* クライアントから送られたSignatureのインスタンス化がうまくいっていない
* サーバのSignatureの計算に関わる処理で、リフレクションを用いており、クラスが見つからないが、見つからないことも仕様の範囲内で稼働している

`com.webauthn4j.validator.AssertionSignatureValidator#validate`で署名検証はスタートする。
あとは、そもそも、認証器の登録（option, result）は出来ているので、そのあたりは問題なさそう。

トップダウンから追えないため、ログをAOPで仕込むことにする。
`com.webauthn4j.validator.AssertionSignatureValidator#verifySignature`このあたり・・・。

作業中に、別の方法が気になったので試してみる。
verifySignatureは、最終的にnativeメソッド呼出をしていた。
nativeメソッド呼出は`@TypeHint`を与えるべきものらしい。
`private`メソッドは、type指定では指定できない。こういったものは、`typeNames`にて指定するとのこと。
エラーは変わらず。`@methodHint`も付けてみる。

```java
@TypeHint(
typeNames = {"sun.security.ec.ECDSASignature"}, 
methods = {@MethodHint(name = "sun.security.ec.ECDSASignature.verifySignedDigest")})
```

変わらず。

AOPを入れる。

```java
@Aspect
@Component
@Slf4j
public class LoggingAdvice {


    @Before("execution(* com.webauthn4j.springframework.security..*.*(*))")
    public void invokeBefore(JoinPoint joinPoint) {
        methodLog(joinPoint.getTarget().getClass().toString(), joinPoint.getSignature().getName(), "start");
    }

    @After("execution(* com.webauthn4j.springframework.security..*.*(*))")
    public void invokeAfter(JoinPoint joinPoint) {
        methodLog(joinPoint.getTarget().getClass().toString(), joinPoint.getSignature().getName(), "end");
    }

    private void methodLog(String className, String methodSignature, String suffix) {
        log.info("{} {} {}", className, methodSignature, suffix);
    }

}

```

buildで失敗する。つらそうなので、中断。
非推奨の`--enable-all-security-services`を有効にしてビルドしてみる。

```gradle
"BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS": "--enable-all-security-services"
```

結果、変わらず・・・。


-----
AuthenticationData生成時に、

```java
MessageDigestUtil.createSHA256().digest(collectedClientDataBytes)
```

このような処理があり、ここでもリフレクション経由で生成していそうなロジックがあった。
以下２つが実値としてあったので、追加。

```java
@TypeHint(typeNames = {
        "sun.security.provider.SHA2.SHA256",
        "sun.security.provider.Sun"
})
```

この他、内部で生成していそうなものは一通り追加したが、エラーは消えず。

------
処理は、`com.webauthn4j.springframework.security.WebAuthnProcessingFilter`で行われているため、そもそもリクエストを受け取れているかを確認する。

`com.webauthn4j.springframework.security.WebAuthnProcessingFilter`の前に独自のフィルターを配置し、リクエストの中身をロギングするようにする。

リクエストデータまでは問題なく受け入れられていることを確認。

署名検証ロジックは以下のようなシーケンスを辿る。
最後は、nativeメソッドの呼出となっている。GraalVMではネイティブメソッドもサポートされている。
https://www.graalvm.org/22.0/reference-manual/native-image/JNI/

SHA256withECDSA
└ java.security.Signature.Delegate
　└ sun.security.ec.ECDSASignature.SHA256
　　└ sun.security.ec.ECDSASignature.engineVerify
　　　└ native method sun.security.ec.ECDSASignature.verifySignedDigest

------
逆にJVM起動で成功したインプットをネイティブ環境下で流し込んで見る。

署名検証部分のロジックの実値をすべて自身のコードに持ってくる。
SpringBoot上で成功したものは、ネイティブ上でも同様に成功した。

つまり、処理へのインプットの入れ方に変化が生じている可能性がある。

-----------
状況をすべて同じにしても挙動の差異が解決しなかった。



## メモ
### GraalVM
https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/
### Pivotal slide
https://www.slideshare.net/Pivotal/spring-native-and-spring-aot
### TypeHint
https://docs.spring.io/spring-native/docs/0.11.3/api/org/springframework/nativex/hint/package-summary.html
### Oracle セキュティ開発ガイド
https://docs.oracle.com/en/java/javase/11/security/preface.html#GUID-C49D47BA-EA16-4F4F-90CE-DAD9B984D823
## Tech Blog
### Spring Bootアプリケーションのネイティブイメージが簡単に作れるSpring Nativeの紹介 
https://future-architect.github.io/articles/20210909a/