diff --git a/NEWS b/NEWS index 645119bc2..74f310f93 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,22 @@ +== Version 2.9.0 (unreleased) == + +`webauthn-server-core`: + +Fixes: + +* Added `@since` tags to `AttestationTrustSource` javadoc. + +`webauthn-server-attestation`: + +New features: + +* Added `AuthenticatorStatus.RETIRED` and `Filters.notRetired()`. + +Fixes: + +* Added `@since` tags to `AuthenticatorStatus` and `FidoMetadataService` javadoc. + + == Version 2.8.1 == Fixes: diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorStatus.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorStatus.java index 49c66572c..b05b9ef7c 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorStatus.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorStatus.java @@ -7,18 +7,24 @@ * or attestationCertificateKeyIdentifiers and potentially some additional information (such as a * specific attestation key). * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum */ public enum AuthenticatorStatus { - /** (NOT DEFINED IN SPEC) Placeholder for any unknown {@link AuthenticatorStatus} value. */ + /** + * (NOT DEFINED IN SPEC) Placeholder for any unknown {@link AuthenticatorStatus} value. + * + * @since 2.0.0 + */ @JsonEnumDefaultValue UNKNOWN(0), /** * This authenticator is not FIDO certified. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -29,6 +35,7 @@ public enum AuthenticatorStatus { * This authenticator has passed FIDO functional certification. This certification scheme is * phased out and will be replaced by {@link #FIDO_CERTIFIED_L1}. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -40,6 +47,7 @@ public enum AuthenticatorStatus { * authenticator could be used without the user’s consent and potentially even without the user’s * knowledge. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -53,6 +61,7 @@ public enum AuthenticatorStatus { * new registrations of the compromised authenticator. The Authenticator manufacturer should set * the date to the date when compromise has occurred. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -65,6 +74,7 @@ public enum AuthenticatorStatus { * to be generated or side channels that allow keys or signatures to be forged, guessed or * extracted. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -75,6 +85,7 @@ public enum AuthenticatorStatus { * This authenticator has known weaknesses in its key protection mechanism(s) that allow user keys * to be extracted by an adversary in physical possession of the device. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -93,17 +104,32 @@ public enum AuthenticatorStatus { * #USER_KEY_PHYSICAL_COMPROMISE}, {@link #REVOKED}. The Relying party MUST reject the Metadata * Statement if the authenticatorVersion has not increased * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum */ UPDATE_AVAILABLE(0), + /** + * The authenticator vendor has decided to retire the product, that this authenticator should not + * be accepted any longer. For example if a prototype version of the authenticator was added to + * FIDO MDS and has now been superseded by the final product, the entry for the prototype might be + * set to "retired". + * + * @since 2.9.0 + * @see FIDO + * Metadata Service §3.1.4. AuthenticatorStatus enum + */ + RETIRED(0), + /** * The FIDO Alliance has determined that this authenticator should not be trusted for any reason. * For example if it is known to be a fraudulent product or contain a deliberate backdoor. Relying * parties SHOULD reject any future registration of this authenticator model. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -115,6 +141,7 @@ public enum AuthenticatorStatus { * FIDO Alliance. If this completed checklist is publicly available, the URL will be specified in * url. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -125,6 +152,7 @@ public enum AuthenticatorStatus { * The authenticator has passed FIDO Authenticator certification at level 1. This level is the * more strict successor of {@link #FIDO_CERTIFIED}. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -135,6 +163,7 @@ public enum AuthenticatorStatus { * The authenticator has passed FIDO Authenticator certification at level 1+. This level is the * more than level 1. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -145,6 +174,7 @@ public enum AuthenticatorStatus { * The authenticator has passed FIDO Authenticator certification at level 2. This level is more * strict than level 1+. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -155,6 +185,7 @@ public enum AuthenticatorStatus { * The authenticator has passed FIDO Authenticator certification at level 2+. This level is more * strict than level 2. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -165,6 +196,7 @@ public enum AuthenticatorStatus { * The authenticator has passed FIDO Authenticator certification at level 3. This level is more * strict than level 2+. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum @@ -175,6 +207,7 @@ public enum AuthenticatorStatus { * The authenticator has passed FIDO Authenticator certification at level 3+. This level is more * strict than level 3. * + * @since 2.0.0 * @see FIDO * Metadata Service §3.1.4. AuthenticatorStatus enum diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java index fdf4b2c95..f5c756e07 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataService.java @@ -87,6 +87,8 @@ * *

Use the {@link #builder() builder} to configure settings, then use the {@link * #findEntries(List, AAGUID)} method or its overloads to retrieve metadata entries. + * + * @since 2.0.0 */ @Slf4j public final class FidoMetadataService implements AttestationTrustSource { @@ -234,6 +236,7 @@ public static class Step1 { * *

This is an alias of useBlob(blob.getPayload(). * + * @since 2.0.0 * @see FidoMetadataDownloader#loadCachedBlob() * @see #useBlob(MetadataBLOBPayload) */ @@ -247,6 +250,7 @@ public FidoMetadataServiceBuilder useBlob(@NonNull MetadataBLOB blob) { *

The {@link FidoMetadataDownloader#loadCachedBlob()} method returns a value whose {@link * MetadataBLOB#getPayload() .getPayload()} result is suitable for use here. * + * @since 2.0.0 * @see FidoMetadataDownloader#loadCachedBlob() * @see #useBlob(MetadataBLOB) */ @@ -263,9 +267,12 @@ public FidoMetadataServiceBuilder useBlob(@NonNull MetadataBLOBPayload blobPaylo * *

The default is {@link Filters#notRevoked() Filters.notRevoked()}. Setting a different * filter overrides this default; to preserve the "not revoked" condition in addition to the new - * filter, you must explicitly include the condition in the few filter. For example, by using - * {@link Filters#allOf(Predicate[]) Filters.allOf(Predicate...)}. + * filter, you must explicitly include the condition in the new filter, for example by using + * {@link Filters#allOf(Predicate[]) Filters.allOf(Predicate...)}. To add the {@link + * Filters#notRetired() Filters.notRetired()} filter, use: + * .prefilter(Filters.allOf(Filters.notRevoked(), Filters.notRetired())). * + * @since 2.0.0 * @param prefilter a {@link Predicate} which returns true for metadata entries to * include in the data source. * @see #filter(Predicate) @@ -300,6 +307,7 @@ public FidoMetadataServiceBuilder prefilter( * @param filter a {@link Predicate} which returns true for metadata entries to * allow for the corresponding authenticator during credential registration and metadata * lookup. + * @since 2.0.0 * @see #prefilter(Predicate) * @see AuthenticatorToBeFiltered * @see Filters#allOf(Predicate[]) @@ -318,6 +326,7 @@ public FidoMetadataServiceBuilder filter( * * @param certStore a {@link CertStore} of additional CRLs and/or intermediate certificates to * use while validating attestation certificate paths. + * @since 2.0.0 */ public FidoMetadataServiceBuilder certStore(@NonNull CertStore certStore) { this.certStore = certStore; @@ -345,6 +354,7 @@ public FidoMetadataService build() * FidoMetadataServiceBuilder#prefilter(Predicate) prefilter} and {@link * FidoMetadataServiceBuilder#filter(Predicate) filter} settings. * + * @since 2.0.0 * @see FidoMetadataServiceBuilder#prefilter(Predicate) * @see FidoMetadataServiceBuilder#filter(Predicate) */ @@ -358,6 +368,7 @@ public static class Filters { * @param filters A set of filters. * @return A filter which only accepts inputs that satisfy ALL of the given * filters. + * @since 2.0.0 */ @SafeVarargs public static Predicate allOf(Predicate... filters) { @@ -369,6 +380,7 @@ public static Predicate allOf(Predicate... filters) { * statusReports} array contains no entry with {@link AuthenticatorStatus#REVOKED REVOKED} * status. * + * @since 2.0.0 * @see AuthenticatorStatus#REVOKED */ public static Predicate notRevoked() { @@ -378,6 +390,21 @@ public static Predicate notRevoked() { statusReport -> AuthenticatorStatus.REVOKED.equals(statusReport.getStatus())); } + /** + * Include any metadata entry whose {@link MetadataBLOBPayloadEntry#getStatusReports() + * statusReports} array contains no entry with {@link AuthenticatorStatus#RETIRED RETIRED} + * status. + * + * @since 2.9.0 + * @see AuthenticatorStatus#RETIRED + */ + public static Predicate notRetired() { + return (entry) -> + entry.getStatusReports().stream() + .noneMatch( + statusReport -> AuthenticatorStatus.RETIRED.equals(statusReport.getStatus())); + } + /** * Accept any authenticator whose matched metadata entry does NOT indicate a compromised * attestation key. @@ -390,6 +417,7 @@ public static Predicate notRevoked() { * {@link AuthenticatorToBeFiltered#getAttestationCertificateChain() attestation certificate * chain}. * + * @since 2.0.0 * @see AuthenticatorStatus#ATTESTATION_KEY_COMPROMISE */ public static Predicate noAttestationKeyCompromise() { @@ -417,6 +445,8 @@ public static Predicate noAttestationKeyCompromise() /** * This class encapsulates parameters for filtering authenticators in the {@link * FidoMetadataServiceBuilder#filter(Predicate) filter} setting of {@link FidoMetadataService}. + * + * @since 2.0.0 */ @Value @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -426,12 +456,16 @@ public static class AuthenticatorToBeFiltered { * The attestation certificate chain from the attestation * statement from an authenticator about ot be registered. + * + * @since 2.0.0 */ @NonNull List attestationCertificateChain; /** * A metadata BLOB entry that matches the {@link #getAttestationCertificateChain()} and {@link * #getAaguid()} in this same {@link AuthenticatorToBeFiltered} object. + * + * @since 2.0.0 */ @NonNull MetadataBLOBPayloadEntry metadataEntry; @@ -444,6 +478,8 @@ public static class AuthenticatorToBeFiltered { * *

This will not be present if the attested credential data contained an AAGUID of all * zeroes. + * + * @since 2.0.0 */ public Optional getAaguid() { return Optional.ofNullable(aaguid); @@ -491,6 +527,7 @@ public Optional getAaguid() { * attestationCertificateChain, if any. * * + * @since 2.0.0 * @see #findEntries(List) * @see #findEntries(List, AAGUID) */ @@ -566,6 +603,7 @@ public Set findEntries( /** * Alias of findEntries(attestationCertificateChain, Optional.empty()). * + * @since 2.0.0 * @see #findEntries(List, Optional) */ public Set findEntries( @@ -576,6 +614,7 @@ public Set findEntries( /** * Alias of findEntries(attestationCertificateChain, Optional.of(aaguid)). * + * @since 2.0.0 * @see #findEntries(List, Optional) */ public Set findEntries( @@ -594,6 +633,7 @@ public Set findEntries( * .orElseGet(Collections::emptySet) * * + * @since 2.0.0 * @see #findEntries(List, Optional) */ public Set findEntries(@NonNull RegistrationResult registrationResult) { @@ -606,6 +646,7 @@ public Set findEntries(@NonNull RegistrationResult reg /** * Find metadata entries matching the given AAGUID. * + * @since 2.0.0 * @see #findEntries(List, Optional) */ public Set findEntries(@NonNull AAGUID aaguid) { @@ -623,6 +664,7 @@ public Set findEntries(@NonNull AAGUID aaguid) { * @return All metadata entries which satisfy the {@link * FidoMetadataServiceBuilder#prefilter(Predicate) prefilter} AND for which the filter * returns true. + * @since 2.0.0 * @see #findEntries(List, Optional) */ public Set findEntries( @@ -637,6 +679,9 @@ public Set findEntries( .collect(Collectors.toSet()); } + /** + * @since 2.0.0 + */ @Override public TrustRootsResult findTrustRoots( List attestationCertificateChain, Optional aaguid) { diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala index 6a0feb143..069b548a9 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMds3Spec.scala @@ -562,6 +562,54 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { } } + it("RETIRED AuthenticatorStatus is parsed correctly.") { + val (blobJwt, cert, crls) = makeBlob("""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [ + { + "aaguid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "statusReports": [ + { + "status": "RETIRED", + "effectiveDate": "2022-02-15" + } + ], + "timeOfLastStatusChange": "2022-02-15" + } + ] + }""") + val downloader: FidoMetadataDownloader = FidoMetadataDownloader + .builder() + .expectLegalHeader("Kom ihåg att du aldrig får snyta dig i mattan!") + .useTrustRoot(cert) + .useBlob(blobJwt) + .clock( + Clock.fixed(Instant.parse("2022-02-15T18:00:00Z"), ZoneOffset.UTC) + ) + .useCrls(crls) + .build() + val mds = + FidoMetadataService.builder().useBlob(downloader.loadCachedBlob()).build() + mds should not be null + + val entries = mds + .findEntries( + Collections.emptyList(), + Some( + new AAGUID(ByteArray.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + ).toJava, + ) + .asScala + entries should not be empty + entries should have size 1 + entries.head.getStatusReports should have size 1 + entries.head.getStatusReports.get(0).getStatus should be( + AuthenticatorStatus.RETIRED + ) + } + it("More [AuthenticatorTransport] values might be added in the future. FIDO Servers MUST silently ignore all unknown AuthenticatorStatus values.") { val (blobJwt, cert, crls) = makeBlob("""{ "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", @@ -989,4 +1037,95 @@ class FidoMds3Spec extends AnyFunSpec with Matchers { } + describe("The notRetired filter") { + val aaguidRetired = + new AAGUID(ByteArray.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + + val aaguidNotRetired = + new AAGUID(ByteArray.fromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + + val blob: MetadataBLOBPayload = + JacksonCodecs.jsonWithDefaultEnums.readValue( + s"""{ + "legalHeader" : "Kom ihåg att du aldrig får snyta dig i mattan!", + "nextUpdate" : "2022-12-01", + "no" : 0, + "entries": [ + { + "aaguid": "${aaguidRetired.asGuidString()}", + "attestationCertificateKeyIdentifiers": [], + "metadataStatement": { + "aaguid": "${aaguidRetired.asGuidString()}", + "authenticatorVersion": 1, + "attestationRootCertificates": [], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": [ + { "status": "RETIRED", "effectiveDate": "2022-02-01" } + ], + "timeOfLastStatusChange": "2022-02-15" + }, + { + "aaguid": "${aaguidNotRetired.asGuidString()}", + "attestationCertificateKeyIdentifiers": [], + "metadataStatement": { + "aaguid": "${aaguidNotRetired.asGuidString()}", + "authenticatorVersion": 1, + "attestationRootCertificates": [], + "attestationTypes" : ["basic_full"], + "authenticationAlgorithms" : ["secp256r1_ecdsa_sha256_raw"], + "keyProtection" : ["software"], + "matcherProtection" : ["software"], + "protocolFamily" : "u2f", + "publicKeyAlgAndEncodings" : ["ecc_x962_raw"], + "schema" : 3, + "tcDisplay" : [], + "upv" : [{ "major" : 1, "minor" : 1 }], + "userVerificationDetails" : [[{ "userVerificationMethod" : "presence_internal" }]] + }, + "statusReports": [], + "timeOfLastStatusChange": "2022-02-15" + } + ] + }""".stripMargin, + classOf[MetadataBLOBPayload], + ) + + it("is not enabled by default.") { + val mds = FidoMetadataService.builder().useBlob(blob).build() + + mds + .findEntries(aaguidRetired) + .asScala should not be empty + mds + .findEntries(aaguidNotRetired) + .asScala should not be empty + } + + it("can be enabled explicitly as a prefilter.") { + val mds = FidoMetadataService + .builder() + .useBlob(blob) + .prefilter(FidoMetadataService.Filters.notRetired()) + .build() + + mds + .findEntries(aaguidRetired) + .asScala shouldBe empty + mds + .findEntries(aaguidNotRetired) + .asScala should not be empty + } + + } + } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java index 290437efc..76ae15b7b 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/attestation/AttestationTrustSource.java @@ -57,6 +57,7 @@ public interface AttestationTrustSource { * an empty result. Implementations MAY reuse the same result object, or parts of it, for * multiple calls of this method, even with different arguments, but MUST return an empty set * of trust roots for authenticators that should not be trusted. + * @since 2.0.0 */ TrustRootsResult findTrustRoots( List attestationCertificateChain, Optional aaguid); @@ -77,6 +78,8 @@ TrustRootsResult findTrustRoots( *

  • define a policy tree validator for the PKIX policy tree result - see {@link * TrustRootsResultBuilder#policyTreeValidator(Predicate) policyTreeValidator(Predicate)}. * + * + * @since 2.0.0 */ @Value @Builder(toBuilder = true) @@ -86,6 +89,8 @@ class TrustRootsResult { * A set of attestation root certificates trusted to certify the relevant attestation statement. * If the attestation statement is not trusted, or if no trust roots were found, this should be * an empty set. + * + * @since 2.0.0 */ @NonNull private final Set trustRoots; @@ -99,6 +104,8 @@ class TrustRootsResult { * trustRoots}. * *

    The default is null. + * + * @since 2.0.0 */ @Builder.Default private final CertStore certStore = null; @@ -106,6 +113,8 @@ class TrustRootsResult { * Whether certificate revocation should be checked during certificate path validation. * *

    The default is true. + * + * @since 2.0.0 */ @Builder.Default private final boolean enableRevocationChecking = true; @@ -129,6 +138,8 @@ class TrustRootsResult { * Predicate}. * *

    The default is null. + * + * @since 2.1.0 */ @Builder.Default private final Predicate policyTreeValidator = null; @@ -153,6 +164,8 @@ private TrustRootsResult( * trustRoots}. * *

    The default is null. + * + * @since 2.0.0 */ public Optional getCertStore() { return Optional.ofNullable(certStore); @@ -178,6 +191,8 @@ public Optional getCertStore() { * Predicate}. * *

    The default is null. + * + * @since 2.1.0 */ public Optional> getPolicyTreeValidator() { return Optional.ofNullable(policyTreeValidator); @@ -193,6 +208,8 @@ public static class Step1 { * A set of attestation root certificates trusted to certify the relevant attestation * statement. If the attestation statement is not trusted, or if no trust roots were found, * this should be an empty set. + * + * @since 2.0.0 */ public TrustRootsResultBuilder trustRoots(@NonNull Set trustRoots) { return new TrustRootsResultBuilder().trustRoots(trustRoots); @@ -203,6 +220,8 @@ public TrustRootsResultBuilder trustRoots(@NonNull Set trustRoo * A set of attestation root certificates trusted to certify the relevant attestation * statement. If the attestation statement is not trusted, or if no trust roots were found, * this should be an empty set. + * + * @since 2.0.0 */ // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder trustRoots( @@ -224,6 +243,8 @@ public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder trustRoot * TrustRootsResultBuilder#trustRoots(Set) trustRoots}. * *

    The default is null. + * + * @since 2.0.0 */ // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder certStore( @@ -237,6 +258,8 @@ public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder certStore * Whether certificate revocation should be checked during certificate path validation. * *

    The default is true. + * + * @since 2.0.0 */ // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder @@ -267,6 +290,8 @@ public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder certStore * Predicate}. * *

    The default is null. + * + * @since 2.1.0 */ // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder policyTreeValidator( @@ -281,6 +306,8 @@ public AttestationTrustSource.TrustRootsResult.TrustRootsResultBuilder policyTre * A set of attestation root certificates trusted to certify the relevant attestation statement. * If the attestation statement is not trusted, or if no trust roots were found, this should be * an empty set. + * + * @since 2.0.0 */ // TODO: Let this auto-generate (investigate why Lombok fails to copy javadoc) @NonNull