You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2025/11/05 05:48:53 UTC

(causeway) 01/03: CAUSEWAY-3939: removal of IdStringifier for Serializable

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit e6bf00c63c33cfa894a19d6122526a1aec227d14
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Oct 30 14:27:54 2025 +0100

    CAUSEWAY-3939: removal of IdStringifier for Serializable
    
    - id de-serialization is exposed to untrusted input
    CAUSEWAY-3939: adds HmacAuthority interface
    
    - designated for devs to opt in on secure Serialization fallbacks
    - yet only implemented for ViewModelFacetForSerializableInterface
    
    CAUSEWAY-3939: adds default impl. f. HmacAuthority
    
    CAUSEWAY-3939: more java doc and test fixes
    
    CAUSEWAY-3939: hardening HmacAuthority
    
    CAUSEWAY-3939: introduces HmacUtils
    
    refactors verifaction and signing of byte arrays into HmacUtils
    CAUSEWAY-3939: moves Mementos from commons to metamodel artifact
    
    such that we can later make use of the HmacAuthority that is in Applib
    CAUSEWAY-3939: makes HMAC mandatory for viewmodels and composite values
    CAUSEWAY-3939: HMAC support for
    ViewModelFacetForXmlRootElementAnnotation
    CAUSEWAY-3939: fixes to prev. commit
    CAUSEWAY-3939: HMAC support for ViewModelFacetForViewModelInterface
    CAUSEWAY-3939: consolidates all encoding/decoding into shared abstract
    ViewmodelFacet
    CAUSEWAY-3939: ViewModel java-doc update
    CAUSEWAY-3939: refactors SerializingAdapter to ValueCodec (from Service
    to just a Bean)
    
    (tried to encapsulate, however that made test setup hard)
    CAUSEWAY-3939: removes temporary code
    CAUSEWAY-3939: introduces DigitalVerificationException
    CAUSEWAY-3939: on DigitalVerificationException disable error page
    tickets
---
 .../java/org/apache/causeway/applib/ViewModel.java |  23 ++-
 .../unrecoverable/BookmarkNotFoundException.java   |   4 -
 ...tion.java => DigitalVerificationException.java} |  22 +--
 .../applib/layout/grid/bootstrap/BSUtil.java       |   9 +-
 .../applib/services/bookmark/HmacAuthority.java    | 121 +++++++++++++
 .../services/urlencoding/UrlEncodingService.java   |  26 +--
 .../causeway/applib/value/semantics/Converter.java |   3 -
 .../applib/value/semantics/DefaultsProvider.java   |   3 -
 .../applib/value/semantics/OrderRelation.java      |   3 -
 commons/src/main/java/module-info.java             |   1 -
 .../causeway/commons/internal/base/_Bytes.java     |   6 +
 .../commons/internal/memento/_MementoDefault.java  | 125 -------------
 .../commons/internal/memento/_Mementos.java        | 193 ---------------------
 .../commons/internal/memento/package-info.java     |  28 ---
 .../commons/internal/resources/_Serializables.java |  71 ++++++--
 core/metamodel/src/main/java/module-info.java      |   1 +
 .../metamodel/CausewayModuleCoreMetamodel.java     |  23 ++-
 .../DomainObjectAnnotationFacetFactory.java        |  23 ++-
 .../object/viewmodel/SecureViewModelFacet.java     | 167 ++++++++++++++++++
 .../object/viewmodel/ViewModelFacetAbstract.java   | 119 -------------
 .../object/viewmodel/ViewModelFacetFactory.java    |  38 +++-
 .../ViewModelFacetForDomainObjectAnnotation.java   |  74 +++-----
 .../viewmodel/ViewModelFacetForJavaRecord.java     |  79 +++------
 .../ViewModelFacetForSerializableInterface.java    |  68 +++-----
 .../ViewModelFacetForViewModelInterface.java       |  82 +++++----
 .../ViewModelFacetForXmlRootElementAnnotation.java |  77 +++-----
 .../core/metamodel/util/hmac/HmacUrlCodec.java     |  67 +++++++
 .../core/metamodel/util/hmac/HmacUtils.java        |  96 ++++++++++
 .../causeway/core/metamodel/util/hmac/Memento.java |  60 +++++++
 .../metamodel/util/hmac/MementoHmacContext.java    |  55 ++++++
 .../core/metamodel/util/hmac/SecureMemento.java    | 119 +++++++++++++
 .../IdStringifierForSerializable.java              | 113 ------------
 .../valuesemantics/TreeNodeValueSemantics.java     |  51 ++++--
 .../core/metamodel/valuesemantics/ValueCodec.java} |  65 +++----
 .../IdStringifierForSerializable_Test.java         |  71 --------
 .../mmtestsupport/MetaModelContext_forTesting.java |  11 +-
 .../runtimeservices/src/main/java/module-info.java |   4 +-
 .../CausewayModuleCoreRuntimeServices.java         |  19 +-
 .../runtimeservices/urlencoding/MementosTest.java  |  36 ++--
 .../security/EncodabilityContractTest.java         |  26 +--
 .../domainmodel/MetaModelRegressionTest.java       |  14 +-
 .../testdomain/factory/ViewModelFactoryTest.java   |   9 +-
 .../wicket/ui/errors/ExceptionStackTracePanel.java |  27 +--
 .../integration/WebRequestCycleForCauseway.java    |  21 ++-
 44 files changed, 1152 insertions(+), 1101 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java b/api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java
index 4fc820bbc1d..435dffe0cd2 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/ViewModel.java
@@ -27,10 +27,10 @@
 
 /**
  * Indicates that an object belongs to the UI/application layer and is intended to be used as a view-model.
- * <p>
- * Instances of {@link ViewModel} must include (at least) one public constructor.
- * <p>
- * Contract:
+ *
+ * <p> Instances of {@link ViewModel} must include (at least) one public constructor.
+ *
+ * <p> Contract:
  * <ul>
  * <li>there is either exactly one public constructor or if there are more than one,
  * then only one of these is annotated with any of {@code @Inject} or {@code @Autowired(required=true)}
@@ -44,8 +44,8 @@
  * Naturally this also allows for the idiom of passing in the {@link ServiceInjector} as an argument
  * and programmatically resolve any field-style injection points via {@link ServiceInjector#injectServicesInto(Object)},
  * that is, if already required during <i>construction</i>.
- * <p>
- * After a view-model got new-ed up by the framework (or programmatically via the {@link FactoryService}),
+ *
+ * <p> After a view-model got new-ed up by the framework (or programmatically via the {@link FactoryService}),
  * {@link ServiceInjector#injectServicesInto(Object)} is called on the viewmodel instance,
  * regardless of what happened during <i>construction</i>.
  *
@@ -55,13 +55,12 @@ public interface ViewModel {
 
     /**
      * Obtain a memento of the view-model. (Optional)
-     * <p>
-     * Captures the state of this view-model as {@link String},
+     *
+     * <p> Captures the state of this view-model as {@link String},
      * which can be passed in to this view-model's constructor for later re-construction.
-     * <p>
-     * The framework automatically takes care of non-URL-safe strings,
-     * by passing them through {@link java.net.URLEncoder}/
-     * {@link java.net.URLDecoder} for encoding and decoding respectively.
+     *
+     * <p> The framework automatically takes care of non-URL-safe strings, null and the empty String.
+     * Those view-model mementos are also digitally signed, before the are handed out to clients.
      */
     @Programmatic
     String viewModelMemento();
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java b/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
index 5372c4f16ba..cad9163fdb6 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
@@ -25,12 +25,8 @@
  * Indicates that a bookmark cannot be found.
  *
  * @since 2.1, 3.1 {@index}
- *
  */
 public class BookmarkNotFoundException extends UnrecoverableException {
-    /**
-     *
-     */
     private static final long serialVersionUID = 1L;
 
     public BookmarkNotFoundException(final String msg) {
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java b/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/DigitalVerificationException.java
similarity index 72%
copy from api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
copy to api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/DigitalVerificationException.java
index 5372c4f16ba..3bd5c7cddb8 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/BookmarkNotFoundException.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/exceptions/unrecoverable/DigitalVerificationException.java
@@ -22,36 +22,36 @@
 import org.apache.causeway.applib.services.i18n.TranslatableString;
 
 /**
- * Indicates that a bookmark cannot be found.
+ * Indicates that a digital verification failed.
  *
- * @since 2.1, 3.1 {@index}
+ * <p>E.g. could be an invalid (or no longer valid) bookmark.
+ *
+ * @since 3.5 {@index}
  *
  */
-public class BookmarkNotFoundException extends UnrecoverableException {
-    /**
-     *
-     */
+public class DigitalVerificationException extends UnrecoverableException {
     private static final long serialVersionUID = 1L;
 
-    public BookmarkNotFoundException(final String msg) {
+    public DigitalVerificationException(final String msg) {
         super(msg);
     }
 
-    public BookmarkNotFoundException(final TranslatableString translatableMessage,
+    public DigitalVerificationException(final TranslatableString translatableMessage,
             final Class<?> translationContextClass, final String translationContextMethod) {
         super(translatableMessage, translationContextClass, translationContextMethod);
     }
 
-    public BookmarkNotFoundException(final Throwable cause) {
+    public DigitalVerificationException(final Throwable cause) {
         super(cause);
     }
 
-    public BookmarkNotFoundException(final String msg, final Throwable cause) {
+    public DigitalVerificationException(final String msg, final Throwable cause) {
         super(msg, cause);
     }
 
-    public BookmarkNotFoundException(final TranslatableString translatableMessage,
+    public DigitalVerificationException(final TranslatableString translatableMessage,
             final Class<?> translationContextClass, final String translationContextMethod, final Throwable cause) {
         super(translatableMessage, translationContextClass, translationContextMethod, cause);
     }
+
 }
\ No newline at end of file
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java b/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
index ff1ef2165fe..9d207ef9da7 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
@@ -21,6 +21,8 @@
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.springframework.util.SerializationUtils;
+
 import org.apache.causeway.applib.layout.component.ActionLayoutData;
 import org.apache.causeway.applib.layout.component.ActionLayoutDataOwner;
 import org.apache.causeway.applib.layout.component.CollectionLayoutData;
@@ -30,9 +32,6 @@
 import org.apache.causeway.applib.layout.component.FieldSet;
 import org.apache.causeway.applib.layout.component.PropertyLayoutData;
 import org.apache.causeway.applib.layout.grid.bootstrap.BSElement.BSElementVisitor;
-import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.resources._Serializables;
-
 import lombok.experimental.UtilityClass;
 
 @UtilityClass
@@ -76,9 +75,7 @@ public void visit(final CollectionLayoutData collectionLayoutData) {
      * Creates a deep copy of given original grid.
      */
     public BSGrid deepCopy(final BSGrid orig) {
-        var bytes = _Serializables.write(orig);
-        return _Casts.uncheckedCast(
-                _Serializables.read(BSGrid.class, bytes));
+        return SerializationUtils.clone(orig);
     }
 
     public BSGrid resolveOwners(final BSGrid grid) {
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/bookmark/HmacAuthority.java b/api/applib/src/main/java/org/apache/causeway/applib/services/bookmark/HmacAuthority.java
new file mode 100644
index 00000000000..45a8fa78752
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/bookmark/HmacAuthority.java
@@ -0,0 +1,121 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.applib.services.bookmark;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Objects;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.jspecify.annotations.Nullable;
+
+import lombok.SneakyThrows;
+
+/**
+ * Can be registered with Spring, to override the build in default, which has an application scoped random secret.
+ *
+ * <pre>
+ * {@code @Configuration}
+ * class EnableHmacAuthority {
+ *     {@code @Bean}
+ *     public HmacAuthority hmacAuthority() {
+ *         return HmacAuthority.HmacSHA256.randomInstance();
+ *     }
+ * }
+ * </pre>
+ *
+ * <p>Note that bookmark's validity is bound to the (server-side) secret key of the {@link HmacAuthority}.
+ * Once the secret changes, bookmarks that are stored client-side for later use, will be rendered invalid.
+ *
+ * @apiNote the default bean is auto-configured with 'CausewayModuleCoreRuntimeServices.HmacAutorityAutoconfigure'
+ *
+ * @since 3.5
+ */
+public interface HmacAuthority {
+
+    /**
+     * HMAC as byte array, for given input data.
+     */
+    byte[] generateHmac(byte[] data);
+
+    /**
+     * Verifies that given dataToVerify when passed to {@link #generateHmac(byte[])} yields given hmacToVerify.
+     *
+     * <p>If any of the arguments is null returns false.
+     *
+     * <p>If hmacToVerify does not conform with {@link #isValidHmacLength(int)} returns false.
+     */
+    default boolean verifyHmac(final @Nullable byte[] data, final @Nullable byte[] hmacToVerify) {
+        if(data == null || hmacToVerify == null) return false; // invalid by definition
+        if(!isValidHmacLength(hmacToVerify.length)) return false; // shortcut
+        return Arrays.equals(generateHmac(data), hmacToVerify);
+    }
+
+    /**
+     * Whether HMAC length in bytes is expected/valid.
+     */
+    boolean isValidHmacLength(final int hmacLength);
+
+    // -- IMPL
+
+    record HmacSHA256(
+        SecretKeySpec secretKey) implements HmacAuthority {
+
+        private final static String ALGORITHM = "HmacSHA256";
+
+        public HmacSHA256(final byte[] secret) {
+            this(new SecretKeySpec(secret, ALGORITHM));
+        }
+
+        @SneakyThrows
+        public static HmacSHA256 randomInstance() {
+            var secret = new byte[32]; // double the minimum requirement of 16
+            SecureRandom.getInstanceStrong().nextBytes(secret);
+            return new HmacSHA256(secret);
+        }
+
+        @Override
+        public byte[] generateHmac(final byte[] data) {
+            Objects.requireNonNull(data);
+            var mac = newMac();
+            return mac.doFinal(data);
+        }
+
+        @Override
+        public boolean isValidHmacLength(final int hmacLength) {
+            return 32 == hmacLength;
+        }
+
+        // -- HELPER
+
+        @SneakyThrows
+        private Mac newMac() {
+            var mac = Mac.getInstance(ALGORITHM);
+            mac.init(secretKey);
+            return mac;
+        }
+    }
+
+    // JUNIT SUPPORT
+
+    static HmacAuthority forTesting() {
+        return new HmacSHA256("secret for testing onyl".getBytes());
+    }
+}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java
index 70105be3824..41d3acf1f44 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/urlencoding/UrlEncodingService.java
@@ -22,39 +22,32 @@
 
 import org.apache.causeway.commons.internal.base._Bytes;
 import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
 
 /**
  * Defines a consistent way to convert strings to/from a form safe for use
  * within a URL.
  *
- * <p>
- *     The service is used by the framework to map mementos (derived from the
- *     state of the view model itself) into a form that can be used as a view
- *     model. When the framework needs to recreate the view model (for example
- *     to invoke an action on it), this URL is converted back into a view model
- *     memento, from which the view model can be hydrated.
- * </p>
+ * <p>The service is used by the framework to map mementos (derived from the
+ * state of the view model itself) into a form that can be used as a view
+ * model. When the framework needs to recreate the view model (for example
+ * to invoke an action on it), this URL is converted back into a view model
+ * memento, from which the view model can be hydrated.
  *
  * @since 1.x {@index}
  */
-public interface UrlEncodingService extends EncoderDecoder {
+public interface UrlEncodingService {
 
     /**
-     * Converts the string (eg view model memento) into a string safe for use
+     * Converts given data bytes (eg view model memento) into a string safe for use
      * within an URL
      */
-    @Override
     String encode(final byte[] bytes);
 
     /**
-     * Unconverts the string from its URL form into its original form URL.
+     * Converts the plain URL safe string back to its original data bytes.
      *
-     * <p>
-     *     Reciprocal of {@link #encode(byte[])}.
-     * </p>
+     * <p>Inverse of {@link #encode(byte[])}.
      */
-    @Override
     byte[] decode(String str);
 
     default String encodeString(final String str) {
@@ -101,7 +94,6 @@ public byte[] decode(final String str) {
                 return _Bytes.ofUrlBase64.apply(_Strings.toBytes(str, StandardCharsets.UTF_8));
             }
         };
-
     }
 
 }
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java
index 40974b26014..c108b4ed889 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/Converter.java
@@ -18,8 +18,6 @@
  */
 package org.apache.causeway.applib.value.semantics;
 
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
-
 /**
  * Provides forth and back conversion between 2 types.
  *
@@ -28,7 +26,6 @@
  *
  * @see DefaultsProvider
  * @see Parser
- * @see EncoderDecoder
  * @see ValueSemanticsProvider
  *
  * @since 2.x {@index}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java
index aca71505e9d..443e835d3ed 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/DefaultsProvider.java
@@ -18,8 +18,6 @@
  */
 package org.apache.causeway.applib.value.semantics;
 
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
-
 /**
  * Provides a mechanism for providing a default value for an object.
  *
@@ -40,7 +38,6 @@
  * the object reflectively.
  *
  * @see Parser
- * @see EncoderDecoder
  * @see ValueSemanticsProvider
  *
  * @since 1.x {@index}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java
index feac2561a00..4275a394374 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/OrderRelation.java
@@ -18,8 +18,6 @@
  */
 package org.apache.causeway.applib.value.semantics;
 
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
-
 /**
  * Provides an ordering relation for a given value-type.
  * <p>
@@ -36,7 +34,6 @@
  *
  * @see DefaultsProvider
  * @see Parser
- * @see EncoderDecoder
  * @see ValueSemanticsProvider
  *
  * @since 2.x {@index}
diff --git a/commons/src/main/java/module-info.java b/commons/src/main/java/module-info.java
index 1da63b2ab4a..7b19feb18c3 100644
--- a/commons/src/main/java/module-info.java
+++ b/commons/src/main/java/module-info.java
@@ -50,7 +50,6 @@
     exports org.apache.causeway.commons.internal.html;
     exports org.apache.causeway.commons.internal.image;
     exports org.apache.causeway.commons.internal.ioc;
-    exports org.apache.causeway.commons.internal.memento;
     exports org.apache.causeway.commons.internal.os;
     exports org.apache.causeway.commons.internal.primitives;
     exports org.apache.causeway.commons.internal.proxy;
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java b/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java
index 451b8c0b854..8e1d5a47714 100644
--- a/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java
+++ b/commons/src/main/java/org/apache/causeway/commons/internal/base/_Bytes.java
@@ -195,6 +195,12 @@ public static byte[] ofHexDump(final @Nullable String hexDump) {
         return ofHexDump(hexDump, " ");
     }
 
+    // -- NULLABLE
+
+    public static byte[] nullToEmpty(final byte[] bytes) {
+        return bytes!=null ? bytes : new byte[0];
+    }
+
     // -- PREPEND/APPEND
 
     /**
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/memento/_MementoDefault.java b/commons/src/main/java/org/apache/causeway/commons/internal/memento/_MementoDefault.java
deleted file mode 100644
index 5afd2bb40df..00000000000
--- a/commons/src/main/java/org/apache/causeway/commons/internal/memento/_MementoDefault.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you 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 org.apache.causeway.commons.internal.memento;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.ObjectStreamClass;
-import java.io.Serializable;
-import java.util.HashMap;
-import java.util.Set;
-
-import org.jspecify.annotations.Nullable;
-
-import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.base._NullSafe;
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.collections._Maps;
-import org.apache.causeway.commons.internal.collections._Sets;
-import org.apache.causeway.commons.internal.context._Context;
-import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.commons.internal.memento._Mementos.EncoderDecoder;
-import org.apache.causeway.commons.internal.memento._Mementos.Memento;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
-
-import org.jspecify.annotations.NonNull;
-
-/**
- * package private helper for utility class {@link _Mementos}
- *
- * Memento default implementation.
- */
-record _MementoDefault(
-        @NonNull EncoderDecoder codec,
-        @NonNull SerializingAdapter serializer,
-        // we need a Serializable Map
-        @NonNull HashMap<String, Serializable> valuesByKey
-        ) implements _Mementos.Memento {
-
-    _MementoDefault(final EncoderDecoder codec, final SerializingAdapter serializer) {
-        this(codec, serializer, _Maps.newHashMap());
-    }
-
-    @Override
-    public Memento put(final @NonNull String name, final Object value) {
-        if(value==null) {
-            return this; //no-op, there is no point in storing null values
-        }
-        valuesByKey.put(name, serializer.write(value));
-        return this;
-    }
-
-    @Override
-    public <T> T get(final String name, final Class<T> cls) {
-        final Serializable value = valuesByKey.get(name);
-        if(value==null) {
-            return null;
-        }
-        return serializer.read(cls, value);
-    }
-
-    @Override
-    public Set<String> keySet() {
-        return _Sets.unmodifiable(valuesByKey.keySet());
-    }
-
-    @Override
-    public String asString() {
-        final ByteArrayOutputStream os = new ByteArrayOutputStream(16*1024); // 16k initial size
-        try(ObjectOutputStream oos = new ObjectOutputStream(os)){
-            oos.writeObject(valuesByKey); // write the entire map to the byte-buffer
-        } catch (Exception e) {
-            throw new IllegalArgumentException("failed to serialize memento", e);
-        }
-        return codec.encode(os.toByteArray()); // convert bytes from byte-buffer to encoded string
-    }
-
-    // -- PARSER
-
-    static Memento parse(
-            final @NonNull EncoderDecoder codec,
-            final SerializingAdapter serializer,
-            final @Nullable String str) {
-        if(_NullSafe.isEmpty(str)) {
-            return null;
-        }
-        try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(codec.decode(str))) {
-            //override ObjectInputStream's class-loading
-            @Override
-            protected Class<?> resolveClass(ObjectStreamClass desc)
-                    throws IOException, ClassNotFoundException
-            {
-                String name = desc.getName();
-                return Class.forName(name, false, _Context.getDefaultClassLoader());
-            }
-        }) {
-            // read in the entire map
-            final HashMap<String, Serializable> valuesByKey = _Casts.uncheckedCast(ois.readObject());
-            return new _MementoDefault(codec, serializer, valuesByKey);
-        } catch (Exception e) {
-            throw _Exceptions.illegalArgument(e,
-                    "failed to parse memento from serialized string '%s'",
-                    _Strings.ellipsifyAtEnd(str, 200, "..."));
-        }
-    }
-
-}
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/memento/_Mementos.java b/commons/src/main/java/org/apache/causeway/commons/internal/memento/_Mementos.java
deleted file mode 100644
index 2bad8cfe42b..00000000000
--- a/commons/src/main/java/org/apache/causeway/commons/internal/memento/_Mementos.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you 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 org.apache.causeway.commons.internal.memento;
-
-import java.io.ObjectInput;
-import java.io.ObjectOutput;
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.exceptions._Exceptions;
-
-import org.jspecify.annotations.NonNull;
-
-/**
- * <h1>- internal use only -</h1>
- * <p>
- * Provides framework internal memento support.
- * </p>
- * <p>
- * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
- * These may be changed or removed without notice!
- * </p>
- *
- * @since 2.0
- */
-public final class _Mementos {
-
-    private _Mementos(){}
-
-    // -- ENCODE-DECODER INTERFACE
-
-    public static interface EncoderDecoder {
-        public String encode(final byte[] bytes);
-        public byte[] decode(String str);
-    }
-
-    // -- MEMENTO INTERFACE
-
-    /**
-     * Similar to a {@link Map}&lt;String, Object&gt; for key/value pairs,
-     * but in addition allows to-String <em>serialization</em> and
-     * from-String <em>de-serialization</em> of the entire map.
-     */
-    public static interface Memento {
-
-        /**
-         * Returns the Object associated with {@code name}
-         * @param name
-         * @param cls the expected type which to cast the retrieved value to (required)
-         */
-        public <T> T get(String name, Class<T> cls);
-
-        /**
-         * Behaves like a {@link HashMap}, but returns the Memento itself.
-         * @param name
-         * @param value
-         * @return self
-         */
-        public Memento put(String name, Object value);
-
-        /**
-         * @return an unmodifiable key-set of this map
-         */
-        public Set<String> keySet();
-
-        /**
-         * @return to-String <em>serialization</em> of this map
-         */
-        public String asString();
-    }
-
-    // -- SERIALIZER INTERFACE
-
-    /**
-     * Coder/Decoder from {@link Object} to {@link Serializable}
-     */
-    public static interface SerializingAdapter {
-
-        /**
-         * Converts the value into a {@link Serializable} that is write-able to an {@link ObjectOutput}.<br/>
-         * Note: write and read are complementary operators.
-         * @param value
-         */
-        public Serializable write(@NonNull Object value);
-
-        /**
-         * Converts the {@link Serializable} {@code value} as read from an {@link ObjectInput} back into its
-         * original (typically a Pojo).<br/>
-         * Note: write and read are complementary operators.
-         * @param cls the expected type which to cast the {@code value} to (required)
-         * @param value
-         */
-        public <T> T read(@NonNull Class<T> cls, @NonNull Serializable value);
-    }
-
-    // -- MEMENTO CONSTRUCTION
-
-    /**
-     * Creates an empty {@link Memento}.
-     *
-     * <p>
-     * Typically followed by {@link Memento#put(String, Object)} for each of the data values to
-     * add to the {@link Memento}, then {@link Memento#asString()} to convert to a string format.
-     * </p>
-     *
-     * @param codec (required)
-     * @param serializer (required)
-     * @return non-null
-     */
-    public static Memento create(final EncoderDecoder codec, final SerializingAdapter serializer) {
-        return new _MementoDefault(codec, serializer);
-    }
-
-    /**
-     * Parse string returned from {@link Memento#asString()}
-     *
-     * <p>
-     * Typically followed by {@link Memento#get(String, Class)} for each of the data values held
-     * in the {@link Memento}.
-     * </p>
-     *
-     * @param codec (required)
-     * @param serializer (required)
-     * @param input
-     * @return {@code empty()} if {@code input} is empty
-     *
-     * @throws IllegalArgumentException if parsing fails
-     *
-     */
-    public static Memento parse(
-            final EncoderDecoder codec,
-            final SerializingAdapter serializer,
-            final String input) {
-
-        if(_Strings.isNullOrEmpty(input)) {
-            return empty();
-        }
-        return _MementoDefault.parse(codec, serializer, input);
-    }
-
-    // -- EMPTY MEMENTO
-
-    private static final class EmptyMemento implements Memento {
-
-        @Override
-        public <T> T get(final String name, final Class<T> cls) {
-            return null;
-        }
-
-        @Override
-        public Memento put(final String name, final Object value) {
-            throw _Exceptions.notImplemented();
-        }
-
-        @Override
-        public Set<String> keySet() {
-            return Collections.emptySet();
-        }
-
-        @Override
-        public String asString() {
-            return "EmptyMemento";
-        }
-
-    }
-
-    private static final Memento EMPTY_MEMENTO = new EmptyMemento();
-
-    public static Memento empty() {
-        return EMPTY_MEMENTO;
-    }
-
-}
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/memento/package-info.java b/commons/src/main/java/org/apache/causeway/commons/internal/memento/package-info.java
deleted file mode 100644
index 3246904976f..00000000000
--- a/commons/src/main/java/org/apache/causeway/commons/internal/memento/package-info.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you 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.
- */
-/**
- * <h1>Internal API</h1>
- * Internal classes, contributing to the internal proprietary API.
- * These may be changed or removed without notice!
- * <p>
- * <b>WARNING</b>:
- * Do NOT use any of the classes provided by this package!
- * </p>
- */
-package org.apache.causeway.commons.internal.memento;
\ No newline at end of file
diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java b/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java
index b623f79f1f5..5048c58ce18 100644
--- a/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java
+++ b/commons/src/main/java/org/apache/causeway/commons/internal/resources/_Serializables.java
@@ -20,32 +20,38 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
 import java.io.Serializable;
 
+import org.jspecify.annotations.NonNull;
+
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 
-import org.jspecify.annotations.NonNull;
 import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
 
 /**
  * <h1>- internal use only -</h1>
- * <p>
- * Utilities for marshalling Serializable.
- * </p>
- * <p>
- * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
+ * <p>Utilities for marshalling {@link Serializable}.
+ *
+ * <p><b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
  * These may be changed or removed without notice!
- * </p>
+ *
+ * @apiNote Every code path within the framework, that requires {@link java.io.ObjectInputStream#readObject()}
+ * should use this utility class to access it indirectly. This allows for easier security related code reviews.
+ *
  * @since 2.0
  */
+@UtilityClass
 public class _Serializables {
 
     @SneakyThrows
-    public static byte[] write(
+    public byte[] write(
             final @NonNull Serializable object) {
         var bos = new ByteArrayOutputStream(16*4096); // 16k initial buffer size
         try(var oos = new ObjectOutputStream(bos)) {
@@ -53,14 +59,18 @@ public static byte[] write(
             oos.flush();
         }
         return bos.toByteArray();
-
     }
 
+    /**
+     * This utility uses Java Object Serialization, which allows
+     * arbitrary code to be run and is known for being the source of many Remote
+     * Code Execution (RCE) vulnerabilities.
+     */
     @SneakyThrows
-    public static <T extends Serializable> T read(
+    public <T extends Serializable> T read(
             final @NonNull Class<T> requiredClass,
-            final @NonNull InputStream content) {
-        try(var ois = new ObjectInputStream(content)){
+            final @NonNull InputStream trustedContent) {
+        try(var ois = new ObjectInputStream(trustedContent)){
             var pojo = ois.readObject();
             if(!(requiredClass.isAssignableFrom(pojo.getClass()))) {
                 throw _Exceptions.unrecoverable(
@@ -71,13 +81,44 @@ public static <T extends Serializable> T read(
         }
     }
 
+    /**
+     * This utility uses Java Object Serialization, which allows
+     * arbitrary code to be run and is known for being the source of many Remote
+     * Code Execution (RCE) vulnerabilities.
+     */
     @SneakyThrows
-    public static <T extends Serializable> T read(
+    public <T extends Serializable> T read(
             final @NonNull Class<T> requiredClass,
-            final @NonNull byte[] input) {
-        try(var bis = new ByteArrayInputStream(input)) {
+            final @NonNull byte[] trustedBytes) {
+        try(var bis = new ByteArrayInputStream(trustedBytes)) {
             return read(requiredClass, bis);
         }
     }
 
+    /**
+     * This utility uses Java Object Serialization, which allows
+     * arbitrary code to be run and is known for being the source of many Remote
+     * Code Execution (RCE) vulnerabilities.
+     */
+    @SneakyThrows
+    public <T extends Serializable> T readWithCustomClassLoader(
+        final @NonNull Class<T> requiredClass,
+        final @NonNull ClassLoader classLoader,
+        final @NonNull byte[] trustedBytes) {
+        try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(trustedBytes)) {
+            @Override
+            protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
+                return Class.forName(desc.getName(), false, classLoader);
+            }
+        }) {
+            var pojo = ois.readObject();
+            if(!(requiredClass.isAssignableFrom(pojo.getClass()))) {
+                throw _Exceptions.unrecoverable(
+                        "de-serializion of input stream did not yield an object of required type %s",
+                        requiredClass.getName());
+            }
+            return _Casts.uncheckedCast(pojo);
+        }
+    }
+
 }
diff --git a/core/metamodel/src/main/java/module-info.java b/core/metamodel/src/main/java/module-info.java
index 49ed9a4f34a..a5d00a19d97 100644
--- a/core/metamodel/src/main/java/module-info.java
+++ b/core/metamodel/src/main/java/module-info.java
@@ -130,6 +130,7 @@
     exports org.apache.causeway.core.metamodel.services.grid.spi;
     exports org.apache.causeway.core.metamodel.specloader.validator;
     exports org.apache.causeway.core.metamodel.util;
+    exports org.apache.causeway.core.metamodel.util.hmac;
     exports org.apache.causeway.core.metamodel.util.pchain;
     exports org.apache.causeway.core.metamodel.util.snapshot;
     exports org.apache.causeway.core.metamodel.valuesemantics;
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
index ed3a517e9ff..51e2202f8fa 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
@@ -19,6 +19,7 @@
 package org.apache.causeway.core.metamodel;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.stream.Stream;
 
 import jakarta.inject.Provider;
@@ -34,8 +35,10 @@
 import org.apache.causeway.applib.graph.tree.TreeAdapter;
 import org.apache.causeway.applib.layout.resource.LayoutResourceLoader;
 import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort;
+import org.apache.causeway.applib.services.bookmark.BookmarkService;
 import org.apache.causeway.applib.services.grid.GridMarshaller;
 import org.apache.causeway.applib.services.message.MessageService;
+import org.apache.causeway.applib.value.semantics.ValueSemanticsResolver;
 import org.apache.causeway.commons.functional.Either;
 import org.apache.causeway.commons.functional.Railway;
 import org.apache.causeway.commons.functional.Try;
@@ -84,7 +87,6 @@
 import org.apache.causeway.core.metamodel.valuesemantics.CommandDtoValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.DoubleValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.FloatValueSemantics;
-import org.apache.causeway.core.metamodel.valuesemantics.IdStringifierForSerializable;
 import org.apache.causeway.core.metamodel.valuesemantics.IntValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.InteractionDtoValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.LocalResourcePathValueSemantics;
@@ -99,6 +101,7 @@
 import org.apache.causeway.core.metamodel.valuesemantics.TreePathValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.URLValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.UUIDValueSemantics;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
 import org.apache.causeway.core.metamodel.valuesemantics.temporal.LocalDateTimeValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.temporal.LocalDateValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.temporal.LocalTimeValueSemantics;
@@ -174,8 +177,6 @@
         JavaUtilDateValueSemantics.class,
         // Value Semantics (meta-model)
         ApplicationFeatureIdValueSemantics.class,
-        // fallback IdStringifier
-        IdStringifierForSerializable.class,
 
         // @Service's
         ColumnOrderTxtFileServiceDefault.class,
@@ -239,15 +240,21 @@ public PreloadableTypes collectionTypes() {
 
     @Bean
     public PreloadableTypes treeAdapterTypes() {
-        return ()->Stream.of(
-                TreeAdapter.class);
+        return ()->Stream.of(TreeAdapter.class);
     }
 
     @Bean
     public PreloadableTypes metamodelViewTypes() {
-        return ()->Stream.of(
-                MetamodelInspectView.class
-                );
+        return ()->Stream.of(MetamodelInspectView.class);
+    }
+
+    @Bean
+    public ValueCodec valueCodec(
+            final BookmarkService bookmarkService,
+            final Provider<ValueSemanticsResolver> valueSemanticsResolverProvider) {
+        Objects.requireNonNull(bookmarkService);
+        Objects.requireNonNull(valueSemanticsResolverProvider);
+        return new ValueCodec(bookmarkService, valueSemanticsResolverProvider);
     }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
index e7e7a16c5c5..dfb6fef4e2b 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
@@ -20,6 +20,7 @@
 
 import java.lang.reflect.Modifier;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
@@ -46,6 +47,8 @@
 import org.apache.causeway.applib.events.lifecycle.ObjectUpdatedEvent;
 import org.apache.causeway.applib.events.lifecycle.ObjectUpdatingEvent;
 import org.apache.causeway.applib.id.LogicalType;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.internal.collections._Multimaps;
 import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
@@ -80,6 +83,9 @@
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
 
 import static org.apache.causeway.commons.internal.base._NullSafe.stream;
 
@@ -95,10 +101,24 @@ public class DomainObjectAnnotationFacetFactory
     private final MetaModelValidatorForMixinTypes mixinTypeValidator =
             new MetaModelValidatorForMixinTypes("@DomainObject#nature=MIXIN");
 
-    @Inject
+    // self-managed injection point resolving via constructor ..
+    @Inject HmacAuthority hmacAuthority;
+    @Inject UrlEncodingService urlCodec;
+    @Inject ValueCodec valueCodec;
+
+    private final MementoHmacContext mementoContext;
+
     public DomainObjectAnnotationFacetFactory(
             final MetaModelContext mmc) {
         super(mmc, FeatureType.OBJECTS_ONLY);
+
+        mmc.getServiceInjector().injectServicesInto(this);
+        Objects.requireNonNull(hmacAuthority);
+        Objects.requireNonNull(urlCodec);
+        Objects.requireNonNull(valueCodec);
+
+        this.mementoContext = new MementoHmacContext(
+            new HmacUrlCodec(hmacAuthority, urlCodec), valueCodec);
     }
 
     @Override
@@ -332,6 +352,7 @@ void processNature(
                 ViewModelFacetForDomainObjectAnnotation
                 .create(
                         domainObjectIfAny,
+                        mementoContext,
                         facetHolder))
                 .isPresent()) {
             return;
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/SecureViewModelFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/SecureViewModelFacet.java
new file mode 100644
index 00000000000..80f77472844
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/SecureViewModelFacet.java
@@ -0,0 +1,167 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.facets.object.viewmodel;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.util.StringUtils;
+
+import org.apache.causeway.applib.exceptions.unrecoverable.DigitalVerificationException;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.commons.internal.assertions._Assert;
+import org.apache.causeway.commons.internal.reflection._ClassCache;
+import org.apache.causeway.core.metamodel.commons.CanonicalInvoker;
+import org.apache.causeway.core.metamodel.commons.ClassExtensions;
+import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.core.metamodel.facetapi.FacetAbstract;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+
+sealed abstract class SecureViewModelFacet
+extends FacetAbstract implements ViewModelFacet
+permits
+    ViewModelFacetForDomainObjectAnnotation,
+    ViewModelFacetForJavaRecord,
+    ViewModelFacetForSerializableInterface,
+    ViewModelFacetForViewModelInterface,
+    ViewModelFacetForXmlRootElementAnnotation {
+
+    private static final Class<? extends Facet> type() {
+        return ViewModelFacet.class;
+    }
+
+    private final HmacUrlCodec hmacUrlCodec;
+
+    protected SecureViewModelFacet(
+            final HmacUrlCodec hmacUrlCodec,
+            final FacetHolder holder) {
+        super(type(), holder);
+        this.hmacUrlCodec = Objects.requireNonNull(hmacUrlCodec);
+    }
+
+    protected SecureViewModelFacet(
+            final HmacUrlCodec hmacUrlCodec,
+            final FacetHolder holder,
+            final Facet.Precedence precedence) {
+        super(type(), holder, precedence);
+        this.hmacUrlCodec = Objects.requireNonNull(hmacUrlCodec);
+    }
+
+    @Override
+    public final ManagedObject instantiate(
+            final ObjectSpecification viewmodelSpec,
+            final Optional<Bookmark> untrustedBookmarkOpt) {
+
+        Objects.requireNonNull(viewmodelSpec);
+
+        if(untrustedBookmarkOpt==null || untrustedBookmarkOpt.isEmpty()) {
+            // this code path needs NO bookmark checking
+
+            var viewModel = createViewmodel(viewmodelSpec);
+            initialize(viewModel.getPojo());
+
+            viewModel.getBookmark(); // trigger bookmark memoization, if not memoized already
+
+            _Assert.assertTrue(viewModel.isBookmarkMemoized(),
+                    ()->"Framework Bug: Viewmodel should have its bookmark memoized once initialized.");
+            return viewModel;
+        }
+
+        // this code path needs untrusted bookmark checking ..
+
+        var untrustedBookmark = untrustedBookmarkOpt.orElseThrow();
+
+        byte[] trustedBookmarkIdAsBytes = Optional.ofNullable(untrustedBookmark.identifier())
+            .filter(StringUtils::hasText)
+            .map(untrustedBookmarkId->hmacUrlCodec.decodeFromUrl(untrustedBookmarkId).orElse(null))
+            .orElse(null);
+
+        if(trustedBookmarkIdAsBytes==null) {
+            // verification failed
+            throw new DigitalVerificationException("invalid request for " + viewmodelSpec.logicalTypeName());
+        }
+
+        var viewModel = ManagedObject.bookmarked(
+            viewmodelSpec,
+            createViewmodelPojo(viewmodelSpec, trustedBookmarkIdAsBytes),
+            untrustedBookmark /* now trusted */);
+
+        initialize(viewModel.getPojo());
+
+        viewModel.getBookmark(); // trigger bookmark memoization, if not memoized already
+
+        _Assert.assertTrue(viewModel.isBookmarkMemoized(),
+                ()->"Framework Bug: Viewmodel should have its bookmark memoized once initialized.");
+        return viewModel;
+    }
+
+    @Override
+    public final void initialize(final @Nullable Object pojo) {
+        if(pojo==null) return;
+        getServiceInjector().injectServicesInto(pojo);
+        invokePostConstructMethod(pojo);
+    }
+
+    @Override
+    public final Bookmark serializeToBookmark(final @NonNull ManagedObject managedObject) {
+        var digitallySignedBookmarId = hmacUrlCodec.encodeForUrl(encodeState(managedObject));
+        return managedObject.createBookmark(digitallySignedBookmarId);
+    }
+
+    // -- ABSTRACT
+
+    /**
+     * Create viewmodel instance from given validated viewmodel state data.
+     */
+    protected abstract Object createViewmodelPojo(
+            @NonNull ObjectSpecification viewmodelSpec,
+            @NonNull byte[] trustedViewmodelState);
+
+    /**
+     * Encodes given viewmodel's state into a byte array for further processing
+     * (digital signing and url-safe encoding).
+     *
+     * <p> The resulting byte array is eventually fed into {@link #createViewmodelPojo(ObjectSpecification, byte[])}
+     * for re-creaton of the viewmodel instance.
+     */
+    protected abstract @NonNull byte[] encodeState(
+            @NonNull ManagedObject viewmodel);
+
+    /**
+     * Create default viewmodel instance (without any {@link Bookmark} available).
+     */
+    protected ManagedObject createViewmodel(final @NonNull ObjectSpecification spec) {
+        return ManagedObject.viewmodel(spec, ClassExtensions.newInstance(spec.getCorrespondingClass()),
+            Optional.empty());
+    }
+
+    // -- HELPER
+
+    private void invokePostConstructMethod(final Object viewModel) {
+        _ClassCache.getInstance().streamPostConstructMethods(viewModel.getClass())
+        .forEach(postConstructMethod->
+        CanonicalInvoker.invoke(postConstructMethod, viewModel));
+    }
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java
deleted file mode 100644
index dc1425ea964..00000000000
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you 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 org.apache.causeway.core.metamodel.facets.object.viewmodel;
-
-import java.util.Optional;
-
-import org.jspecify.annotations.Nullable;
-
-import org.apache.causeway.applib.services.bookmark.Bookmark;
-import org.apache.causeway.commons.internal.assertions._Assert;
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.reflection._ClassCache;
-import org.apache.causeway.core.metamodel.commons.CanonicalInvoker;
-import org.apache.causeway.core.metamodel.commons.ClassExtensions;
-import org.apache.causeway.core.metamodel.facetapi.Facet;
-import org.apache.causeway.core.metamodel.facetapi.FacetAbstract;
-import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
-import org.apache.causeway.core.metamodel.object.ManagedObject;
-import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
-
-import org.jspecify.annotations.NonNull;
-
-public abstract class ViewModelFacetAbstract
-extends FacetAbstract
-implements ViewModelFacet {
-
-    private static final Class<? extends Facet> type() {
-        return ViewModelFacet.class;
-    }
-
-    protected ViewModelFacetAbstract(
-            final FacetHolder holder) {
-        super(type(), holder);
-    }
-
-    protected ViewModelFacetAbstract(
-            final FacetHolder holder,
-            final Facet.Precedence precedence) {
-        super(type(), holder, precedence);
-    }
-
-    @Override
-    public final ManagedObject instantiate(
-            final ObjectSpecification spec,
-            final Optional<Bookmark> bookmarkIfAny) {
-
-        var bookmark = bookmarkIfAny.orElse(null);
-        var isBookmarkAvailable = bookmarkIfAny.map(Bookmark::identifier)
-                .map(_Strings::isNotEmpty)
-                .orElse(false);
-
-        var viewModel = !isBookmarkAvailable
-                ? createViewmodel(spec)
-                : createViewmodel(spec, bookmark);
-
-        initialize(viewModel.getPojo());
-
-        viewModel.getBookmark(); // trigger bookmark memoization, if not memoized already
-
-        _Assert.assertTrue(viewModel.isBookmarkMemoized(),
-                ()->"Framework Bug: Viewmodel should have its bookmark memoized once initialized.");
-        return viewModel;
-    }
-
-    @Override
-    public final void initialize(final @Nullable Object pojo) {
-        if(pojo==null) return;
-        getServiceInjector().injectServicesInto(pojo);
-        invokePostConstructMethod(pojo);
-    }
-
-    /**
-     * Create default viewmodel instance (without any {@link Bookmark} available).
-     */
-    protected ManagedObject createViewmodel(final @NonNull ObjectSpecification spec) {
-        return ManagedObject.viewmodel(spec, ClassExtensions.newInstance(spec.getCorrespondingClass()),
-                Optional.empty());
-    }
-
-    /**
-     * Create viewmodel instance from a given valid {@link Bookmark}.
-     * <p>
-     * The resulting {@link ManagedObject} is not required to have its bookmark memoized.
-     * We trigger bookmark memoization later in {@link #instantiate(ObjectSpecification, Optional)}.
-     */
-    protected abstract ManagedObject createViewmodel(
-            @NonNull ObjectSpecification spec,
-            @NonNull Bookmark bookmark);
-
-    private void invokePostConstructMethod(final Object viewModel) {
-        _ClassCache.getInstance().streamPostConstructMethods(viewModel.getClass())
-        .forEach(postConstructMethod->
-            CanonicalInvoker.invoke(postConstructMethod, viewModel));
-    }
-
-    @Override
-    public final Bookmark serializeToBookmark(final @NonNull ManagedObject managedObject) {
-        return managedObject.createBookmark(serialize(managedObject));
-    }
-
-    protected abstract @NonNull String serialize(@NonNull ManagedObject managedObject);
-
-}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
index 06aa4ccaa13..99185f20688 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetFactory.java
@@ -18,8 +18,13 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.viewmodel;
 
+import java.util.Objects;
+
 import jakarta.inject.Inject;
 
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import org.apache.causeway.applib.services.jaxb.JaxbService;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
 import org.apache.causeway.commons.internal.reflection._ClassCache;
 import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
@@ -29,16 +34,37 @@
 import org.apache.causeway.core.metamodel.facets.FacetFactoryAbstract;
 import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel;
 import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
 
 public class ViewModelFacetFactory
 extends FacetFactoryAbstract
 implements
     MetaModelRefiner {
 
-    @Inject
+    // self-managed injection point resolving via constructor ..
+    @Inject HmacAuthority hmacAuthority;
+    @Inject UrlEncodingService urlCodec;
+    @Inject JaxbService jaxbService;
+    @Inject ValueCodec valueCodec;
+
+    private final HmacUrlCodec hmacUrlCodec;
+    private final MementoHmacContext mementoHmacContext;
+
     public ViewModelFacetFactory(
             final MetaModelContext mmc) {
         super(mmc, FeatureType.OBJECTS_ONLY);
+
+        mmc.getServiceInjector().injectServicesInto(this);
+        Objects.requireNonNull(hmacAuthority);
+        Objects.requireNonNull(urlCodec);
+
+        Objects.requireNonNull(jaxbService);
+        Objects.requireNonNull(valueCodec);
+
+        this.hmacUrlCodec = new HmacUrlCodec(hmacAuthority, urlCodec);
+        this.mementoHmacContext = new MementoHmacContext(hmacUrlCodec, valueCodec);
     }
 
     /**
@@ -58,23 +84,23 @@ public void process(final ProcessClassContext processClassContext) {
         FacetUtil
         .addFacetIfPresent(
                 ViewModelFacetForXmlRootElementAnnotation
-                .create(hasXmlRootElementAnnotation, facetHolder));
+                .create(hasXmlRootElementAnnotation, hmacUrlCodec, jaxbService, facetHolder));
 
         // (with high precedence)
         FacetUtil
         .addFacetIfPresent(
             // either ViewModel interface (highest precedence)
-            ViewModelFacetForViewModelInterface.create(type, facetHolder)
+            ViewModelFacetForViewModelInterface.create(type, hmacUrlCodec, facetHolder)
             // or Serializable interface (if any)
-            .or(()->ViewModelFacetForSerializableInterface.create(type, facetHolder))
+            .or(()->ViewModelFacetForSerializableInterface.create(type, hmacUrlCodec, facetHolder))
             // or else Java record (if any)
-            .or(()->ViewModelFacetForJavaRecord.create(type, facetHolder))
+            .or(()->ViewModelFacetForJavaRecord.create(type, mementoHmacContext, facetHolder))
         );
 
         // DomainObject(nature=VIEW_MODEL) is managed by the DomainObjectAnnotationFacetFactory as a fallback strategy
     }
 
-    // //////////////////////////////////////
+    // --
 
     @Override
     public void refineProgrammingModel(final ProgrammingModel programmingModel) {
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java
index f5a93d6ec47..65d2e8509a3 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForDomainObjectAnnotation.java
@@ -22,13 +22,11 @@
 import java.util.Optional;
 import java.util.stream.Stream;
 
+import org.jspecify.annotations.NonNull;
+
 import org.apache.causeway.applib.annotation.DomainObject;
-import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.metamodel.BeanSort;
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
 import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.memento._Mementos;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
 import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.facets.properties.update.modify.PropertySetterFacet;
@@ -36,17 +34,19 @@
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.causeway.core.metamodel.util.hmac.Memento;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
 
-import org.jspecify.annotations.NonNull;
-
-public class ViewModelFacetForDomainObjectAnnotation
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForDomainObjectAnnotation
+extends SecureViewModelFacet {
 
     public static Optional<ViewModelFacetForDomainObjectAnnotation> create(
             final Optional<DomainObject> domainObjectIfAny,
+            final MementoHmacContext mementoContext,
             final FacetHolder holder) {
 
-        return domainObjectIfAny
+        return mementoContext!=null
+            ? domainObjectIfAny
                 .map(DomainObject::nature)
                 .map(nature -> {
                     switch (nature) {
@@ -70,36 +70,36 @@ public static Optional<ViewModelFacetForDomainObjectAnnotation> create(
                         }
                         // else fall through
                     case VIEW_MODEL:
-                        return new ViewModelFacetForDomainObjectAnnotation(holder);
+                        return new ViewModelFacetForDomainObjectAnnotation(mementoContext, holder);
                     }
                     // shouldn't happen, the above switch should match all cases
                     throw new IllegalArgumentException("nature of '" + nature + "' not recognized");
                 })
-                .filter(Objects::nonNull);
+                .filter(Objects::nonNull)
+            : Optional.empty();
     }
 
-    private UrlEncodingService codec;
-    private SerializingAdapter serializer;
+    private final MementoHmacContext mementoContext;
 
     protected ViewModelFacetForDomainObjectAnnotation(
-            final FacetHolder holder) {
+            final MementoHmacContext mementoContext, final FacetHolder holder) {
         // is overruled by any other ViewModelFacet type
-        super(holder, Precedence.LOW);
+        super(mementoContext.hmacUrlCodec(), holder, Precedence.LOW);
+        this.mementoContext = mementoContext;
     }
 
     @Override
-    protected ManagedObject createViewmodel(
+    protected Object createViewmodelPojo(
             final @NonNull ObjectSpecification viewmodelSpec,
-            final @NonNull Bookmark bookmark) {
+            final @NonNull byte[] trustedBookmarkIdAsBytes) {
 
-        var viewmodel = viewmodelSpec.createObject();
+        // throws on de-marshalling failure
+        var memento = mementoContext.parseTrustedMemento(trustedBookmarkIdAsBytes);
 
-        var memento = parseMemento(bookmark);
+        var viewmodel = viewmodelSpec.createObject();
         var mementoKeys = memento.keySet();
 
-        if(mementoKeys.isEmpty()) {
-            return viewmodel;
-        }
+        if(mementoKeys.isEmpty()) return viewmodel.getPojo();
 
         var objectManager = super.getObjectManager();
 
@@ -119,13 +119,13 @@ protected ManagedObject createViewmodel(
             property.set(viewmodel, propertyValue, InteractionInitiatedBy.PASS_THROUGH);
         });
 
-        return viewmodel;
+        return viewmodel.getPojo();
     }
 
     @Override
-    public String serialize(final ManagedObject viewModel) {
+    public byte[] encodeState(final ManagedObject viewModel) {
 
-        final _Mementos.Memento memento = newMemento();
+        final Memento memento = mementoContext.newMemento();
 
         var viewmodelSpec = viewModel.objSpec();
 
@@ -141,7 +141,7 @@ public String serialize(final ManagedObject viewModel) {
             }
         });
 
-        return memento.asString();
+        return memento.stateAsBytes();
     }
 
     // -- HELPER
@@ -155,26 +155,4 @@ private Stream<OneToOneAssociation> streamPersistableProperties(
                 .filter(property->property.isIncludedWithSnapshots());
     }
 
-    private void initDependencies() {
-        var serviceRegistry = getServiceRegistry();
-        this.codec = serviceRegistry.lookupServiceElseFail(UrlEncodingService.class);
-        this.serializer = serviceRegistry.lookupServiceElseFail(SerializingAdapter.class);
-    }
-
-    private void ensureDependenciesInited() {
-        if(codec==null) {
-            initDependencies();
-        }
-    }
-
-    private _Mementos.Memento newMemento() {
-        ensureDependenciesInited();
-        return _Mementos.create(codec, serializer);
-    }
-
-    private _Mementos.Memento parseMemento(final Bookmark bookmark) {
-        ensureDependenciesInited();
-        return _Mementos.parse(codec, serializer, bookmark.identifier());
-    }
-
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java
index 5e8b1234bbf..617d70b3ad0 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForJavaRecord.java
@@ -24,76 +24,73 @@
 import java.util.Optional;
 import java.util.stream.Stream;
 
-import org.apache.causeway.applib.services.bookmark.Bookmark;
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
+import org.jspecify.annotations.NonNull;
+
 import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.commons.internal.memento._Mementos;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
 import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.causeway.core.metamodel.util.hmac.Memento;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
 
-import org.jspecify.annotations.NonNull;
 import lombok.SneakyThrows;
 
 /**
  * @since 3.0.0
  */
-public class ViewModelFacetForJavaRecord
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForJavaRecord
+extends SecureViewModelFacet {
 
-    public static Optional<ViewModelFacetForJavaRecord> create(
+    static Optional<ViewModelFacetForJavaRecord> create(
             final Class<?> cls,
+            final MementoHmacContext mementoContext,
             final FacetHolder holder) {
-        return cls.isRecord()
-                ? Optional.of(new ViewModelFacetForJavaRecord(cls, holder))
-                : Optional.empty();
+        return mementoContext!=null
+                && cls.isRecord()
+            ? Optional.of(new ViewModelFacetForJavaRecord(cls, mementoContext, holder))
+            : Optional.empty();
     }
 
-    private UrlEncodingService codec;
-    private SerializingAdapter serializer;
-
+    private final MementoHmacContext mementoContext;
     private final Constructor<?> canonicalConstructor;
 
     protected ViewModelFacetForJavaRecord(
             final Class<?> recordClass,
+            final MementoHmacContext mementoContext,
             final FacetHolder holder) {
         // is overruled by ViewModel interface semantics
-        super(holder, Precedence.DEFAULT);
+        super(mementoContext.hmacUrlCodec(), holder, Precedence.DEFAULT);
+        this.mementoContext = mementoContext;
         this.canonicalConstructor = canonicalConstructor(recordClass);
     }
 
     @Override @SneakyThrows
-    protected ManagedObject createViewmodel(
+    protected Object createViewmodelPojo(
             final @NonNull ObjectSpecification viewmodelSpec,
-            final @NonNull Bookmark bookmark) {
+            final @NonNull byte[] trustedBookmarkIdAsBytes) {
 
-        var memento = parseMemento(bookmark);
+        // throws on de-marshalling failure
+        var memento = mementoContext.parseTrustedMemento(trustedBookmarkIdAsBytes);
 
         var recordComponentPojos = streamRecordComponents(viewmodelSpec)
         .map(association->{
-            var associationId = association.getId();
-            var elementType = association.getElementType();
-            var elementClass = elementType.getCorrespondingClass();
             var associationPojo = association.isProperty()
-                    ? memento.get(associationId, elementClass)
-                    //TODO collection values not yet supported by memento (as workaround use Serializable record)
-                    : null;
+                ? memento.get(association.getId(), association.getElementType().getCorrespondingClass())
+                //TODO collection values not yet supported by memento (as workaround use Serializable record)
+                : null;
             return associationPojo;
         }).toArray();
 
-        return ManagedObject.viewmodel(viewmodelSpec,
-                canonicalConstructor.newInstance(recordComponentPojos),
-                Optional.of(bookmark));
+        return canonicalConstructor.newInstance(recordComponentPojos);
     }
 
     @Override
-    public String serialize(final ManagedObject viewModel) {
+    public byte[] encodeState(final ManagedObject viewModel) {
 
-        final _Mementos.Memento memento = newMemento();
+        final Memento memento = mementoContext.newMemento();
 
         var viewmodelSpec = viewModel.objSpec();
 
@@ -111,7 +108,7 @@ public String serialize(final ManagedObject viewModel) {
             }
         });
 
-        return memento.asString();
+        return memento.stateAsBytes();
     }
 
     // -- HELPER
@@ -125,28 +122,6 @@ private Stream<ObjectAssociation> streamRecordComponents(
         return recordComponentsAsAssociations.stream();
     }
 
-    private void initDependencies() {
-        var serviceRegistry = getServiceRegistry();
-        this.codec = serviceRegistry.lookupServiceElseFail(UrlEncodingService.class);
-        this.serializer = serviceRegistry.lookupServiceElseFail(SerializingAdapter.class);
-    }
-
-    private void ensureDependenciesInited() {
-        if(codec==null) {
-            initDependencies();
-        }
-    }
-
-    private _Mementos.Memento newMemento() {
-        ensureDependenciesInited();
-        return _Mementos.create(codec, serializer);
-    }
-
-    private _Mementos.Memento parseMemento(final Bookmark bookmark) {
-        ensureDependenciesInited();
-        return _Mementos.parse(codec, serializer, bookmark.identifier());
-    }
-
     private static Can<ObjectAssociation> recordComponentsAsAssociations(
             final @NonNull ObjectSpecification viewmodelSpec) {
         return Arrays.stream(viewmodelSpec.getCorrespondingClass().getRecordComponents())
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java
index 6f7321006ba..be90964f04c 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForSerializableInterface.java
@@ -18,79 +18,59 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.viewmodel;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.io.Serializable;
-import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 
-import org.apache.causeway.applib.services.bookmark.Bookmark;
-import org.apache.causeway.commons.internal.base._Bytes;
-import org.apache.causeway.commons.internal.base._Strings;
+import org.jspecify.annotations.NonNull;
+
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.internal.resources._Serializables;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
-
-import org.jspecify.annotations.NonNull;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
 import lombok.SneakyThrows;
 
 /**
  * Corresponds to {@link Serializable} interface.
+ *
+ * <p> requires a {@link HmacAuthority}, otherwise disabled.
  */
-public class ViewModelFacetForSerializableInterface
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForSerializableInterface
+extends SecureViewModelFacet {
 
-    public static Optional<ViewModelFacet> create(
+    static Optional<ViewModelFacet> create(
             final Class<?> cls,
+            final HmacUrlCodec hmacUrlCodec,
             final FacetHolder holder) {
 
-        return Serializable.class.isAssignableFrom(cls)
-                ? Optional.of(new ViewModelFacetForSerializableInterface(holder))
+        return hmacUrlCodec!=null
+            && Serializable.class.isAssignableFrom(cls)
+                ? Optional.of(new ViewModelFacetForSerializableInterface(hmacUrlCodec, holder))
                 : Optional.empty();
     }
 
     protected ViewModelFacetForSerializableInterface(
+            final HmacUrlCodec hmacUrlCodec,
             final FacetHolder holder) {
-        super(holder, Precedence.HIGH);
+        super(hmacUrlCodec, holder, Precedence.HIGH);
     }
 
     @SneakyThrows
     @Override
-    protected ManagedObject createViewmodel(
+    protected Object createViewmodelPojo(
             final @NonNull ObjectSpecification viewmodelSpec,
-            final @NonNull Bookmark bookmark) {
-        return ManagedObject.bookmarked(
-                        viewmodelSpec,
-                        deserialize(viewmodelSpec, bookmark.identifier()),
-                        bookmark);
-    }
+            final @NonNull byte[] trustedBookmarkIdAsBytes) {
 
-    @SneakyThrows
-    @Override
-    public String serialize(final ManagedObject viewModel) {
-        var baos = new ByteArrayOutputStream();
-        try(var oos = new ObjectOutputStream(baos)) {
-            oos.writeObject(viewModel.getPojo());
-            var mementoStr = _Strings.ofBytes(
-                    _Bytes.asUrlBase64.apply(baos.toByteArray()),
-                    StandardCharsets.UTF_8);
-            return mementoStr;
-        }
+        Class<? extends Serializable> expectedType = _Casts.uncheckedCast(viewmodelSpec.getCorrespondingClass());
+        return _Serializables.read(expectedType, trustedBookmarkIdAsBytes);
     }
 
-    // -- HELPER
-
     @SneakyThrows
-    private Object deserialize(
-            final @NonNull ObjectSpecification viewmodelSpec,
-            final @NonNull String memento) {
-        var bytes = _Bytes.ofUrlBase64.apply(_Strings.toBytes(memento, StandardCharsets.UTF_8));
-        try(var ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
-            var viewModelPojo = ois.readObject();
-            return viewModelPojo;
-        }
+    @Override
+    public byte[] encodeState(final ManagedObject viewModel) {
+        return _Serializables.write((Serializable) viewModel.getPojo());
     }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java
index c06cc5b9113..bae3845c90a 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForViewModelInterface.java
@@ -21,25 +21,25 @@
 import java.lang.reflect.Constructor;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.ViewModel;
-import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.registry.ServiceRegistry;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.functional.IndexedConsumer;
-import org.apache.causeway.commons.internal.assertions._Assert;
 import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedConstructor;
-import org.apache.causeway.commons.io.UrlUtils;
 import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
 import org.apache.causeway.core.metamodel.commons.ClassExtensions;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
 
 import lombok.Getter;
 import org.jspecify.annotations.NonNull;
@@ -50,16 +50,16 @@
 /**
  * Corresponds to {@link ViewModel} interface.
  */
-public class ViewModelFacetForViewModelInterface
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForViewModelInterface
+extends SecureViewModelFacet {
 
     public static <T> Optional<ViewModelFacet> create(
             final Class<T> cls,
+            final HmacUrlCodec hmacUrlCodec,
             final FacetHolder holder) {
 
-        if(!ViewModel.class.isAssignableFrom(cls)) {
-            return Optional.empty();
-        }
+        if(!ViewModel.class.isAssignableFrom(cls)) return Optional.empty();
+        if(hmacUrlCodec==null) return Optional.empty();
 
         ResolvedConstructor pickedConstructor = null; // not used for abstract types
 
@@ -103,15 +103,16 @@ public static <T> Optional<ViewModelFacet> create(
 
         }
 
-        return Optional.of(new ViewModelFacetForViewModelInterface(holder, pickedConstructor));
+        return Optional.of(new ViewModelFacetForViewModelInterface(pickedConstructor, hmacUrlCodec, holder));
     }
 
-    private ResolvedConstructor constructorAnyArgs;
+    private final ResolvedConstructor constructorAnyArgs;
 
     protected ViewModelFacetForViewModelInterface(
-            final FacetHolder holder,
-            final @Nullable ResolvedConstructor constructorAnyArgs) {
-        super(holder, Precedence.HIGH);
+            final @Nullable ResolvedConstructor constructorAnyArgs,
+            final HmacUrlCodec hmacUrlCodec,
+            final FacetHolder holder) {
+        super(hmacUrlCodec, holder, Precedence.HIGH);
         this.constructorAnyArgs = constructorAnyArgs;
     }
 
@@ -126,19 +127,31 @@ protected ManagedObject createViewmodel(
 
     @SneakyThrows
     @Override
-    protected ManagedObject createViewmodel(
+    protected Object createViewmodelPojo(
             final @NonNull ObjectSpecification viewmodelSpec,
-            final @NonNull Bookmark bookmark) {
-        return ManagedObject.bookmarked(
-                        viewmodelSpec,
-                        deserialize(viewmodelSpec, bookmark.identifier()),
-                        bookmark);
+            final @NonNull byte[] trustedBookmarkIdAsBytes) {
+
+        var mementoDecoded = new String(trustedBookmarkIdAsBytes, StandardCharsets.UTF_8);
+
+        if(SpecialMemento.NULL.matches(mementoDecoded)) {
+            mementoDecoded = null;
+        } else if(SpecialMemento.EMPTY.matches(mementoDecoded)) {
+            mementoDecoded = "";
+        }
+
+        return deserialize(viewmodelSpec, mementoDecoded);
     }
 
     @Override
-    public String serialize(final ManagedObject viewModel) {
+    public byte[] encodeState(final ManagedObject viewModel) {
         final ViewModel viewModelPojo = (ViewModel) viewModel.getPojo();
-        return SpecialMemento.encode(viewModelPojo.viewModelMemento());
+        String memento = viewModelPojo.viewModelMemento();
+        if(memento==null) {
+            memento = SpecialMemento.NULL.representationInUrl();
+        } else if(memento.isEmpty()) {
+            memento = SpecialMemento.EMPTY.representationInUrl();
+        }
+        return memento.getBytes(StandardCharsets.UTF_8);
     }
 
     // -- HELPER
@@ -153,25 +166,11 @@ public String serialize(final ManagedObject viewModel) {
      *      hence gets processed by {@link URLEncoder} and {@link URLDecoder},
      *      which makes it safe for us to use with special meaning
      */
-    @Getter @Accessors(fluent=true)
     @RequiredArgsConstructor
-    enum SpecialMemento {
+    private enum SpecialMemento {
         EMPTY("||"),
         NULL("|");
-        static String encode(final @Nullable String memento) {
-            return memento==null
-                    ? NULL.representationInUrl()
-                    : memento.isEmpty()
-                            ? EMPTY.representationInUrl()
-                            : UrlUtils.urlEncodeUtf8(memento);
-        }
-        static String decode(final @Nullable String memento) {
-            return NULL.matches(memento)
-                    ? null
-                    : EMPTY.matches(memento)
-                            ? ""
-                            : UrlUtils.urlDecodeUtf8(memento);
-        }
+        @Getter @Accessors(fluent=true)
         final String representationInUrl;
         boolean matches(final String other) {
             return representationInUrl.equals(other);
@@ -181,13 +180,12 @@ boolean matches(final String other) {
     @SneakyThrows
     private Object deserialize(
             final @NonNull ObjectSpecification viewmodelSpec,
-            final @Nullable String mementoEncoded) {
+            final @Nullable String trustedMemento) {
 
-        _Assert.assertNotNull(constructorAnyArgs, ()->"framework bug: required non-null, "
-                + "this can only happen, if we try to deserialize an abstract type");
+        Objects.requireNonNull(constructorAnyArgs, ()->"framework bug: required non-null, "
+            + "this can only happen, if we try to deserialize an abstract type");
 
-        var memento = SpecialMemento.decode(mementoEncoded);
-        var resolvedArgs = resolveArgsForConstructor(constructorAnyArgs, getServiceRegistry(), memento);
+        var resolvedArgs = resolveArgsForConstructor(constructorAnyArgs, getServiceRegistry(), trustedMemento);
         var viewmodelPojo = constructorAnyArgs.constructor().newInstance(resolvedArgs);
         return viewmodelPojo;
     }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java
index 2960f16c26d..9fef84b58a1 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/viewmodel/ViewModelFacetForXmlRootElementAnnotation.java
@@ -18,83 +18,58 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.viewmodel;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 
-import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.jspecify.annotations.NonNull;
+
 import org.apache.causeway.applib.services.jaxb.JaxbService;
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
-import org.apache.causeway.commons.internal.debug._Debug;
-import org.apache.causeway.commons.internal.debug.xray.XrayUi;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
 
-import lombok.Getter;
-import org.jspecify.annotations.NonNull;
-
-public class ViewModelFacetForXmlRootElementAnnotation
-extends ViewModelFacetAbstract {
+public final class ViewModelFacetForXmlRootElementAnnotation
+extends SecureViewModelFacet {
 
     public static Optional<ViewModelFacet> create(
             final boolean hasRootElementAnnotation,
+            final HmacUrlCodec hmacUrlCodec,
+            final JaxbService jaxbService,
             final FacetHolder facetHolder) {
 
         return hasRootElementAnnotation
-                ? Optional.of(new ViewModelFacetForXmlRootElementAnnotation(facetHolder))
-                : Optional.empty();
+                && hmacUrlCodec!=null
+                && jaxbService!=null
+            ? Optional.of(new ViewModelFacetForXmlRootElementAnnotation(hmacUrlCodec, jaxbService, facetHolder))
+            : Optional.empty();
     }
 
+    private final JaxbService jaxbService;
+
     private ViewModelFacetForXmlRootElementAnnotation(
+            final HmacUrlCodec hmacUrlCodec,
+            final JaxbService jaxbService,
             final FacetHolder facetHolder) {
         // overruled by other non fallback ViewModelFacet types
-        super(facetHolder, Precedence.DEFAULT);
+        super(hmacUrlCodec, facetHolder, Precedence.DEFAULT);
+        this.jaxbService = jaxbService;
     }
 
     @Override
-    protected ManagedObject createViewmodel(
+    protected Object createViewmodelPojo(
             final @NonNull ObjectSpecification viewmodelSpec,
-            final @NonNull Bookmark bookmark) {
-        final String xmlStr = getUrlEncodingService().decodeToString(bookmark.identifier());
+            final @NonNull byte[] trustedBookmarkIdAsBytes) {
 
-        _Debug.onCondition(XrayUi.isXrayEnabled(), ()->{
-            _Debug.log("[JAXB] de-serializing viewmodel %s\n"
-                    + "--- XML ---\n"
-                    + "%s"
-                    + "-----------\n",
-                    viewmodelSpec.logicalTypeName(),
-                    xmlStr);
-        });
-
-        var viewmodelPojo = getJaxbService().fromXml(viewmodelSpec.getCorrespondingClass(), xmlStr);
-        return viewmodelPojo!=null
-                ? ManagedObject.bookmarked(viewmodelSpec, viewmodelPojo, bookmark)
-                : ManagedObject.empty(viewmodelSpec);
+        var trustedXml = new String(trustedBookmarkIdAsBytes, StandardCharsets.UTF_8);
+        var viewmodelPojo = jaxbService.fromXml(viewmodelSpec.getCorrespondingClass(), trustedXml);
+        return viewmodelPojo;
     }
 
     @Override
-    protected String serialize(final ManagedObject managedObject) {
-
-        final String xml = getJaxbService().toXml(managedObject.getPojo());
-        final String encoded = getUrlEncodingService().encodeString(xml);
-        _Debug.onCondition(XrayUi.isXrayEnabled(), ()->{
-            _Debug.log("[JAXB] serializing viewmodel %s\n"
-                    + "--- XML ---\n"
-                    + "%s"
-                    + "-----------\n",
-                    managedObject.objSpec().logicalTypeName(),
-                    xml);
-        });
-        return encoded;
+    protected byte[] encodeState(final ManagedObject managedObject) {
+        final String xml = jaxbService.toXml(managedObject.getPojo());
+        return xml.getBytes(StandardCharsets.UTF_8);
     }
 
-    // -- DEPENDENCIES
-
-    @Getter(lazy=true)
-    private final JaxbService jaxbService =
-        getServiceRegistry().lookupServiceElseFail(JaxbService.class);
-
-    @Getter(lazy=true)
-    private final UrlEncodingService urlEncodingService =
-        getServiceRegistry().lookupServiceElseFail(UrlEncodingService.class);
-
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUrlCodec.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUrlCodec.java
new file mode 100644
index 00000000000..3959c617cd8
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUrlCodec.java
@@ -0,0 +1,67 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.util.hmac;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.jspecify.annotations.NonNull;
+
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
+
+/**
+ * Thread-safe coder/decoder with digital signing and validity verification support.
+ *
+ * <p> can be used as an application scoped singleton
+ *
+ * @since 3.5
+ */
+public record HmacUrlCodec(
+        HmacAuthority hmacAuthority,
+        UrlEncodingService urlSafeCodec) {
+
+    public HmacUrlCodec {
+        Objects.requireNonNull(hmacAuthority);
+        Objects.requireNonNull(urlSafeCodec);
+    }
+
+    public String encodeForUrl(final @NonNull byte[] byteArray) {
+        var digitallySignedPayload = HmacUtils.digitallySign(hmacAuthority, byteArray);
+        return urlSafeCodec.encode(digitallySignedPayload);
+    }
+
+    public Optional<byte[]> decodeFromUrl(final @NonNull String untrustedUrlEncodedString) {
+        var trustedBytes = HmacUtils.verify(hmacAuthority, urlSafeCodec.decode(untrustedUrlEncodedString));
+        return Optional.ofNullable(trustedBytes);
+    }
+
+    // -- STRING SUPPORT
+
+    public String encodeForUrlAsUtf8(final @NonNull String string) {
+        return encodeForUrl(string.getBytes(StandardCharsets.UTF_8));
+    }
+
+    public Optional<String> decodeFromUrlAsUtf8(final @NonNull String untrustedUrlEncodedString) {
+        return decodeFromUrl(untrustedUrlEncodedString)
+            .map(trustedBytes->new String(trustedBytes, StandardCharsets.UTF_8));
+    }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUtils.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUtils.java
new file mode 100644
index 00000000000..2803243b331
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/HmacUtils.java
@@ -0,0 +1,96 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.util.hmac;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+import org.springframework.util.Assert;
+
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
+
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @since 3.5
+ */
+@UtilityClass
+@Slf4j
+public class HmacUtils {
+
+    /**
+     * Returns an output byte array, as a concatenation of:<br>
+     * (1) the HMAC length as 2 byte short value<br>
+     * (2) HMAC as byte array<br>
+     * (3) input data as byte array<br>
+     */
+    public byte[] digitallySign(final HmacAuthority hmacAuthority, final byte[] data) {
+        Objects.requireNonNull(hmacAuthority);
+        Objects.requireNonNull(data);
+
+        var hmac = hmacAuthority.generateHmac(data);
+
+        // safety guard on the cast to short
+        Assert.isTrue(hmac.length<=Short.MAX_VALUE, ()->"unexpected HMAC length encountered: " + hmac.length);
+        short hmacLengthAsShort = (short) hmac.length;
+
+        var buf = ByteBuffer.allocate(2 + hmac.length + data.length);
+
+        var digitallySignedPayload = buf
+            .putShort(hmacLengthAsShort)
+            .put(hmac)
+            .put(data)
+            .array();
+
+        return digitallySignedPayload;
+    }
+
+    /**
+     * Interprets an input byte array containing:<br>
+     * (1) the HMAC length as 2 byte short value<br>
+     * (2) HMAC as byte array<br>
+     * (3) actual data as byte array<br>
+     * then returns the actual data if it can be verified against given {@link HmacAuthority}, otherwise returns null.
+     */
+    public byte[] verify(final HmacAuthority hmacAuthority, final byte[] untrustedPayload) {
+        Objects.requireNonNull(hmacAuthority);
+        Objects.requireNonNull(untrustedPayload);
+
+        var buf = ByteBuffer.wrap(untrustedPayload);
+
+        int hmacLength = buf.getShort();
+
+        if(!hmacAuthority.isValidHmacLength(hmacLength)) return null; // invalid HMAC length encountered in untrusted payload
+
+        var hmacToVerify = new byte[hmacLength];
+        buf.get(hmacToVerify);
+
+        var data = new byte[untrustedPayload.length - hmacLength - 2];
+        buf.get(data);
+
+        if(!hmacAuthority.verifyHmac(data, hmacToVerify)) {
+            log.info("digital verification failed (HMAC either expired or forgery detected)");
+            return null; // validation failed
+        }
+
+        return data;
+    }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/Memento.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/Memento.java
new file mode 100644
index 00000000000..755a5b0542c
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/Memento.java
@@ -0,0 +1,60 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.util.hmac;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Similar to a {@link Map}&lt;String, Object&gt; for key/value pairs,
+ * but in addition allows to-String <em>serialization</em> and
+ * from-String <em>de-serialization</em> of the entire map.
+ */
+public sealed interface Memento
+permits SecureMemento {
+
+    /**
+     * Returns the Object associated with {@code name}
+     * @param name
+     * @param cls the expected type which to cast the retrieved value to (required)
+     */
+    <T> T get(String name, Class<T> cls);
+
+    /**
+     * Behaves like a {@link HashMap}, but returns the Memento itself.
+     * @param name
+     * @param value
+     * @return self
+     */
+    Memento put(String name, Object value);
+
+    /**
+     * @return an unmodifiable key-set of this map
+     */
+    Set<String> keySet();
+
+    /**
+     * @return to-String <em>serialization</em> of this map (digitally signed)
+     */
+    String toExternalForm();
+
+    byte[] stateAsBytes();
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/MementoHmacContext.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/MementoHmacContext.java
new file mode 100644
index 00000000000..a4aaa38700d
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/MementoHmacContext.java
@@ -0,0 +1,55 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.util.hmac;
+
+import java.util.Objects;
+
+import org.jspecify.annotations.Nullable;
+
+import org.apache.causeway.applib.exceptions.unrecoverable.DigitalVerificationException;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
+
+public record MementoHmacContext(
+    HmacUrlCodec hmacUrlCodec,
+    ValueCodec valueCodec) {
+
+    public MementoHmacContext {
+        Objects.requireNonNull(hmacUrlCodec);
+        Objects.requireNonNull(valueCodec);
+    }
+
+    public Memento newMemento() {
+        return new SecureMemento(this);
+    }
+
+    public Memento parseTrustedMemento(final byte[] trustedInput) {
+        return SecureMemento.parseTrustedMemento(this, trustedInput);
+    }
+
+    public Memento parseDigitallySignedMemento(final String untrustedInput) {
+        return SecureMemento.parseDigitallySignedMemento(this, untrustedInput);
+    }
+
+    public Memento parseMemento(final @Nullable Bookmark untrustedBookmark) {
+        if(untrustedBookmark==null) throw new DigitalVerificationException("invalid memento data");
+        return parseDigitallySignedMemento(untrustedBookmark.identifier());
+    }
+
+}
\ No newline at end of file
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/SecureMemento.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/SecureMemento.java
new file mode 100644
index 00000000000..dc5855025ac
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/hmac/SecureMemento.java
@@ -0,0 +1,119 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.util.hmac;
+
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import org.apache.causeway.applib.exceptions.unrecoverable.DigitalVerificationException;
+import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.internal.collections._Sets;
+import org.apache.causeway.commons.internal.context._Context;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import org.apache.causeway.commons.internal.resources._Serializables;
+
+record SecureMemento(
+        MementoHmacContext context,
+        // we need a Serializable Map
+        Map<String, Serializable> valuesByKey
+        ) implements Memento {
+
+    SecureMemento(final MementoHmacContext context) {
+        this(context, new HashMap<>());
+    }
+
+    SecureMemento {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(valuesByKey);
+        Assert.isTrue(Serializable.class.isInstance(valuesByKey), ()->"map is expected to be serializable");
+    }
+
+    @Override
+    public Memento put(final @NonNull String name, final Object value) {
+        if(value==null) return this; //no-op, there is no point in storing null values
+
+        valuesByKey.put(name, context.valueCodec().encode(value));
+        return this;
+    }
+
+    @Override
+    public <T> T get(final String name, final Class<T> cls) {
+        final Serializable value = valuesByKey.get(name);
+        if(value==null) return null;
+
+        return context.valueCodec().decode(cls, value);
+    }
+
+    @Override
+    public Set<String> keySet() {
+        return _Sets.unmodifiable(valuesByKey.keySet());
+    }
+
+    @Override
+    public byte[] stateAsBytes() {
+        return _Serializables.write((Serializable) valuesByKey);
+    }
+
+    @Override
+    public String toExternalForm() {
+        try {
+            return context.hmacUrlCodec().encodeForUrl(stateAsBytes());
+        } catch (Exception e) {
+            throw new IllegalArgumentException("failed to serialize memento", e);
+        }
+    }
+
+    // -- PARSER
+
+    static Memento parseDigitallySignedMemento(
+            final MementoHmacContext context,
+            final @Nullable String untrustedEncodedString) {
+        if(!StringUtils.hasText(untrustedEncodedString)) throw new DigitalVerificationException("invalid memento data");
+
+        var trustedBytes = context.hmacUrlCodec().decodeFromUrl(untrustedEncodedString).orElse(null);
+        if(trustedBytes==null) throw new DigitalVerificationException("invalid memento data");
+
+        return parseTrustedMemento(context, trustedBytes);
+    }
+
+    static Memento parseTrustedMemento(
+        final MementoHmacContext context,
+        final byte[] trustedBytes) {
+        try {
+            final HashMap<String, Serializable> valuesByKey = _Casts.uncheckedCast(
+                _Serializables.readWithCustomClassLoader(HashMap.class, _Context.getDefaultClassLoader(), trustedBytes));
+            return new SecureMemento(context, valuesByKey);
+        } catch (Exception e) {
+            throw _Exceptions.illegalArgument(e,
+                    "failed to parse memento from serialized string '%s'",
+                    _Strings.ellipsifyAtEnd(new String(trustedBytes, StandardCharsets.UTF_8) , 200, "..."));
+        }
+    }
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable.java
deleted file mode 100644
index 5abf4957859..00000000000
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you 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 org.apache.causeway.core.metamodel.valuesemantics;
-
-import java.io.Serializable;
-
-import jakarta.annotation.Priority;
-import jakarta.inject.Inject;
-import jakarta.inject.Named;
-
-import org.springframework.stereotype.Component;
-
-import org.apache.causeway.applib.annotation.PriorityPrecedence;
-import org.apache.causeway.applib.services.bookmark.IdStringifier;
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
-import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.resources._Serializables;
-
-import org.jspecify.annotations.NonNull;
-
-/**
- * Used as a fallback if no other {@link ValueSemanticsProvider}
- * is available to handle the corresponding value type.
- */
-@Component
-@Named("causeway.metamodel.value.IdStringifierForSerializable")
-@Priority(PriorityPrecedence.LAST)
-public class IdStringifierForSerializable
-implements
-    IdStringifier.EntityAgnostic<Serializable>{
-
-    private final UrlEncodingService codec;
-
-    @Inject
-    public IdStringifierForSerializable(
-            final @NonNull UrlEncodingService codec) {
-        this.codec = codec;
-    }
-
-//    @Override
-//    public ValueType getSchemaValueType() {
-//        return ValueType.STRING;
-//    }
-
-    @Override
-    public Class<Serializable> getCorrespondingClass() {
-        return Serializable.class;
-    }
-
-//    // -- COMPOSER
-//
-//    @Override
-//    public ValueDecomposition decompose(final Serializable value) {
-//        return decomposeAsString(value, this::enstring, ()->null);
-//    }
-//
-//    @Override
-//    public Serializable compose(final ValueDecomposition decomposition) {
-//        return composeFromString(decomposition, this::destring, ()->null);
-//    }
-
-    // -- ID STRINGIFIER
-
-    @Override
-    public String enstring(final @NonNull Serializable id) {
-        // even though null case is guarded by lombok - keep null check for symmetry
-        return id != null
-                ? codec.encode(_Serializables.write(id))
-                : null;
-    }
-
-    @Override
-    public Serializable destring(
-            final @NonNull String stringified) {
-        return destringAs(stringified, Serializable.class);
-    }
-
-//    @Override
-//    public Can<Serializable> getExamples() {
-//        return Can.of(
-//                Integer.MAX_VALUE,
-//                "Hallo World",
-//                new BigDecimal("3.1415"));
-//    }
-
-    // -- HELPER
-
-    private <T extends Serializable> T destringAs(
-            final @NonNull String stringified,
-            final @NonNull Class<T> requiredClass) {
-        return _Strings.isNotEmpty(stringified)
-                ? _Serializables.read(requiredClass, codec.decode(stringified))
-                : null;
-    }
-
-}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java
index 2311284d236..40a9e2fbd74 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/TreeNodeValueSemantics.java
@@ -18,11 +18,13 @@
  */
 package org.apache.causeway.core.metamodel.valuesemantics;
 
+import java.util.Objects;
 import java.util.stream.Stream;
 
 import jakarta.annotation.Priority;
 import jakarta.inject.Inject;
 import jakarta.inject.Named;
+import jakarta.inject.Provider;
 
 import org.springframework.stereotype.Component;
 
@@ -31,16 +33,19 @@
 import org.apache.causeway.applib.graph.tree.TreeNode;
 import org.apache.causeway.applib.graph.tree.TreePath;
 import org.apache.causeway.applib.graph.tree.TreeState;
+import org.apache.causeway.applib.services.bookmark.BookmarkService;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
 import org.apache.causeway.applib.services.factory.FactoryService;
 import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
 import org.apache.causeway.applib.value.semantics.Renderer;
 import org.apache.causeway.applib.value.semantics.ValueDecomposition;
 import org.apache.causeway.applib.value.semantics.ValueSemanticsAbstract;
+import org.apache.causeway.applib.value.semantics.ValueSemanticsResolver;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.memento._Mementos;
-import org.apache.causeway.commons.internal.memento._Mementos.Memento;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+import org.apache.causeway.core.metamodel.util.hmac.Memento;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
 import org.apache.causeway.schema.common.v2.ValueType;
 
 @Component
@@ -51,9 +56,27 @@ public class TreeNodeValueSemantics
 implements
     Renderer<TreeNode<?>> {
 
-    @Inject UrlEncodingService urlEncodingService;
-    @Inject SerializingAdapter serializingAdapter;
-    @Inject FactoryService factoryService;
+    private final FactoryService factoryService;
+    private final MementoHmacContext mementoContext;
+
+    @Inject
+    public TreeNodeValueSemantics(
+        final HmacAuthority hmacAuthority,
+        final UrlEncodingService urlEncodingService,
+        final FactoryService factoryService,
+        final BookmarkService bookmarkService,
+        final Provider<ValueSemanticsResolver> valueSemanticsResolverProvider,
+        final ValueCodec valueCodec) {
+
+        Objects.requireNonNull(hmacAuthority);
+        Objects.requireNonNull(urlEncodingService);
+        Objects.requireNonNull(bookmarkService);
+        Objects.requireNonNull(valueSemanticsResolverProvider);
+        this.factoryService = Objects.requireNonNull(factoryService);
+
+        this.mementoContext = new MementoHmacContext(
+            new HmacUrlCodec(hmacAuthority, urlEncodingService), valueCodec);
+    }
 
     @Override
     public Class<TreeNode<?>> getCorrespondingClass() {
@@ -78,17 +101,17 @@ public TreeNode<?> compose(final ValueDecomposition decomposition) {
     }
 
     private String toEncodedString(final TreeNode<?> treeNode) {
-        final Memento memento = newMemento();
+        final Memento memento = mementoContext.newMemento();
         memento.put("rootValue", treeNode.rootValue());
         memento.put("adapterClass", treeNode.treeAdapter().getClass());
         memento.put("treeState", treeNode.treeState());
         memento.put("treePath", treeNode.treePath());
-        return memento.asString();
+        return memento.toExternalForm();
     }
 
     @SuppressWarnings("unchecked")
     private TreeNode<?> fromEncodedString(final String input) {
-        final Memento memento = parseMemento(input);
+        final Memento memento = mementoContext.parseDigitallySignedMemento(input);
         final TreeNode<?> rootNode = TreeNode.root(
                 memento.get("rootValue", Object.class),
                 memento.get("adapterClass", Class.class),
@@ -127,14 +150,4 @@ class TreeAdapterString implements TreeAdapter<String> {
                 TreeNode.root("another TreeRoot", new TreeAdapterString(), TreeState.rootCollapsed()));
     }
 
-    // -- HELPER
-
-    private _Mementos.Memento newMemento(){
-        return _Mementos.create(urlEncodingService, serializingAdapter);
-    }
-
-    private _Mementos.Memento parseMemento(final String input){
-        return _Mementos.parse(urlEncodingService, serializingAdapter, input);
-    }
-
 }
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/serializing/SerializingAdapterDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ValueCodec.java
similarity index 74%
rename from core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/serializing/SerializingAdapterDefault.java
rename to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ValueCodec.java
index a6e7d66f939..c58e2862774 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/serializing/SerializingAdapterDefault.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/ValueCodec.java
@@ -16,20 +16,17 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.runtimeservices.serializing;
+package org.apache.causeway.core.metamodel.valuesemantics;
 
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
 import java.io.Serializable;
 import java.util.Optional;
 
-import jakarta.annotation.Priority;
-import jakarta.inject.Inject;
-import jakarta.inject.Named;
+import jakarta.inject.Provider;
 
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.stereotype.Service;
+import org.jspecify.annotations.NonNull;
 
-import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.bookmark.BookmarkService;
 import org.apache.causeway.applib.services.bookmark.idstringifiers.PredefinedSerializables;
@@ -37,38 +34,35 @@
 import org.apache.causeway.applib.value.semantics.ValueSemanticsResolver;
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
-import org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
-
-import org.jspecify.annotations.NonNull;
 
 /**
- * Default implementation of {@link SerializingAdapter}, intended as an 'internal' service.
+ * Coder/Decoder from {@link Object} to {@link Serializable}
+ * As used to encode values to bookmarks.
  *
- * <p>
  * @implNote uses {@link Bookmark} or {@link ValueDecomposition}
  * for identifiable objects or value types,
  * while some predefined serializable types that implement {@link Serializable}
  * are written/read directly
- * </p>
  *
  * @see PredefinedSerializables
  *
- * @since 2.0 {@index}
+ * @since 3.5 (refactored from SerializingAdapter)
  */
-@Service
-@Named(CausewayModuleCoreRuntimeServices.NAMESPACE + ".SerializingAdapterDefault")
-@Priority(PriorityPrecedence.MIDPOINT)
-@Qualifier("Default")
-public class SerializingAdapterDefault implements SerializingAdapter {
+public record ValueCodec(
+    BookmarkService bookmarkService,
+    Provider<ValueSemanticsResolver> valueSemanticsResolverProvider) {
 
-    @Inject private BookmarkService bookmarkService;
-
-    @Lazy
-    @Inject private ValueSemanticsResolver valueSemanticsResolver;
+    /** JUnit testing default */
+    public static ValueCodec forTesting() {
+        return new ValueCodec(null, () -> null);
+    }
 
-    @Override
-    public Serializable write(final @NonNull Object value) {
+    /**
+     * Converts the value into a {@link Serializable} that is write-able to an {@link ObjectOutput}.
+     *
+     * <p>Note: write and read are complementary operations
+     */
+    public Serializable encode(final @NonNull Object value) {
         if(PredefinedSerializables.isPredefinedSerializable(value.getClass())) {
             // the value can be stored/written directly without conversion to a bookmark
             return (Serializable) value;
@@ -83,8 +77,15 @@ public Serializable write(final @NonNull Object value) {
                     _Exceptions.unrecoverable("cannot create a memento for object of type %s", value.getClass()));
     }
 
-    @Override
-    public <T> T read(final @NonNull Class<T> valueClass, final @NonNull Serializable value) {
+    /**
+     * Converts the {@link Serializable} {@code value} as read from an {@link ObjectInput} back into its
+     * original (typically a Pojo).
+     *
+     * <p>Note: write and read are complementary operations
+     *
+     * @param valueClass the expected type which to cast the {@code value} to (required)
+     */
+    public <T> T decode(final @NonNull Class<T> valueClass, final @NonNull Serializable value) {
 
         // see if required/desired value-class is Bookmark, then just cast
         if(Bookmark.class.equals(valueClass)) {
@@ -120,7 +121,7 @@ private <T> Optional<ValueDecomposition> toValueDecomposition(
 
         final Class<T> valueClass = _Casts.uncheckedCast(value.getClass());
 
-        return valueSemanticsResolver.streamValueSemantics(valueClass)
+        return valueSemanticsResolverProvider().get().streamValueSemantics(valueClass)
                 .findFirst()
                 .map(vs->vs.decompose(value));
     }
@@ -129,9 +130,9 @@ private <T> Optional<T> fromValueDecomposition(
             final @NonNull Class<T> valueClass,
             final @NonNull ValueDecomposition decomposition) {
 
-        return valueSemanticsResolver.streamValueSemantics(valueClass)
+        return valueSemanticsResolverProvider().get().streamValueSemantics(valueClass)
                 .findFirst()
                 .map(vs->vs.compose(decomposition));
     }
 
-}
+}
\ No newline at end of file
diff --git a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable_Test.java b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable_Test.java
deleted file mode 100644
index 1facb53f69a..00000000000
--- a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/valuesemantics/IdStringifierForSerializable_Test.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 org.apache.causeway.core.metamodel.valuesemantics;
-
-import java.io.Serializable;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
-
-class IdStringifierForSerializable_Test {
-
-    private UrlEncodingService codec = UrlEncodingService.forTesting();
-
-    // -- SCENARIO
-
-    static record CustomerPK(
-            int lower,
-            int upper) implements Serializable{
-    }
-
-    // -- TEST
-
-    static Stream<Arguments> roundtrip() {
-        return Stream.of(
-                Arguments.of(Byte.MAX_VALUE),
-                Arguments.of(Byte.MIN_VALUE),
-                Arguments.of((byte)0),
-                Arguments.of((byte)12345),
-                Arguments.of((byte)-12345),
-                Arguments.of(new CustomerPK(5,6))
-                // Arguments.of((Serializable)null) ... throws NPE as expected
-        );
-    }
-
-    @ParameterizedTest
-    @MethodSource()
-    void roundtrip(final Serializable value) {
-
-        var stringifier = new IdStringifierForSerializable(codec);
-
-        String stringified = stringifier.enstring(value);
-        Serializable parse = stringifier.destring(stringified);
-
-        assertThat(parse).isEqualTo(value);
-    }
-
-}
diff --git a/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java b/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
index a59d0be3483..4e0f8eef81d 100644
--- a/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
+++ b/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
@@ -30,10 +30,10 @@
 import static java.util.Objects.requireNonNull;
 
 import org.jspecify.annotations.NonNull;
-
 import org.springframework.boot.test.util.TestPropertyValues;
 import org.springframework.util.ClassUtils;
 
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
 import org.apache.causeway.applib.services.factory.FactoryService;
 import org.apache.causeway.applib.services.grid.GridMarshaller;
 import org.apache.causeway.applib.services.grid.GridService;
@@ -50,6 +50,7 @@
 import org.apache.causeway.applib.services.render.PlaceholderRenderService;
 import org.apache.causeway.applib.services.repository.RepositoryService;
 import org.apache.causeway.applib.services.title.TitleService;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
 import org.apache.causeway.applib.services.wrapper.WrapperFactory;
 import org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.applib.services.xactn.TransactionState;
@@ -103,6 +104,7 @@
 import org.apache.causeway.core.metamodel.valuesemantics.TreePathValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.URLValueSemantics;
 import org.apache.causeway.core.metamodel.valuesemantics.UUIDValueSemantics;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
 import org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault;
 import org.apache.causeway.core.security.authentication.manager.AuthenticationManager;
 import org.apache.causeway.core.security.authorization.manager.AuthorizationManager;
@@ -301,8 +303,11 @@ Stream<SingletonBeanProvider> streamBeanAdapters() {
                     SingletonBeanProvider.forTestingLazy(JaxbService.class, this::getJaxbService),
                     SingletonBeanProvider.forTestingLazy(MenuBarsService.class, this::getMenuBarsService),
                     SingletonBeanProvider.forTestingLazy(LayoutService.class, this::getLayoutService),
-                    SingletonBeanProvider.forTestingLazy(SpecificationLoader.class, this::getSpecificationLoader)
-                )
+                    SingletonBeanProvider.forTestingLazy(SpecificationLoader.class, this::getSpecificationLoader),
+                    SingletonBeanProvider.forTestingLazy(HmacAuthority.class, HmacAuthority::forTesting),
+                    SingletonBeanProvider.forTestingLazy(UrlEncodingService.class, UrlEncodingService::forTestingNoCompression),
+                    SingletonBeanProvider.forTestingLazy(ValueCodec.class, ()->ValueCodec.forTesting())
+                    )
                 );
     }
 
diff --git a/core/runtimeservices/src/main/java/module-info.java b/core/runtimeservices/src/main/java/module-info.java
index de4ab855e19..23e9b93a01e 100644
--- a/core/runtimeservices/src/main/java/module-info.java
+++ b/core/runtimeservices/src/main/java/module-info.java
@@ -38,7 +38,6 @@
     exports org.apache.causeway.core.runtimeservices.recognizer.dae;
     exports org.apache.causeway.core.runtimeservices.routing;
     exports org.apache.causeway.core.runtimeservices.scratchpad;
-    exports org.apache.causeway.core.runtimeservices.serializing;
     exports org.apache.causeway.core.runtimeservices.session;
     exports org.apache.causeway.core.runtimeservices.sitemap;
     exports org.apache.causeway.core.runtimeservices.spring;
@@ -79,7 +78,8 @@
     requires org.apache.causeway.core.codegen.bytebuddy;
     requires spring.aop;
     requires java.management;
-    
+    requires spring.boot.autoconfigure;
+
     opens org.apache.causeway.core.runtimeservices;
     opens org.apache.causeway.core.runtimeservices.wrapper;
     opens org.apache.causeway.core.runtimeservices.wrapper.handlers; //to org.apache.causeway.core.codegen.bytebuddy
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
index 513e4dcf80a..3a619bb9cd9 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/CausewayModuleCoreRuntimeServices.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.core.runtimeservices;
 
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
@@ -25,6 +27,8 @@
 import org.springframework.core.OrderComparator;
 import org.springframework.core.annotation.AnnotationAwareOrderComparator;
 
+import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
 import org.apache.causeway.core.codegen.bytebuddy.CausewayModuleCoreCodegenByteBuddy;
 import org.apache.causeway.core.runtime.CausewayModuleCoreRuntime;
 import org.apache.causeway.core.runtimeservices.bookmarks.BookmarkServiceDefault;
@@ -57,7 +61,6 @@
 import org.apache.causeway.core.runtimeservices.render.PlaceholderRenderServiceDefault;
 import org.apache.causeway.core.runtimeservices.routing.RoutingServiceDefault;
 import org.apache.causeway.core.runtimeservices.scratchpad.ScratchpadDefault;
-import org.apache.causeway.core.runtimeservices.serializing.SerializingAdapterDefault;
 import org.apache.causeway.core.runtimeservices.session.InteractionIdGeneratorDefault;
 import org.apache.causeway.core.runtimeservices.session.InteractionServiceDefault;
 import org.apache.causeway.core.runtimeservices.sitemap.SitemapServiceDefault;
@@ -109,7 +112,6 @@
         LifecycleCallbackNotifier.class,
         SchemaValueMarshallerDefault.class,
         ScratchpadDefault.class,
-        SerializingAdapterDefault.class,
         SitemapServiceDefault.class,
         SpringBeansService.class,
         TransactionServiceSpring.class,
@@ -129,6 +131,9 @@
         // Exception Recognizers
         ExceptionRecognizerForDataAccessException.class,
 
+        // auto configuration
+        CausewayModuleCoreRuntimeServices.HmacAutorityAutoconfigure.class
+
 })
 @ComponentScan(basePackages = "org.apache.causeway.core.runtimeservices.icons")
 public class CausewayModuleCoreRuntimeServices {
@@ -140,4 +145,14 @@ public OrderComparator orderComparator() {
         return new AnnotationAwareOrderComparator();
     }
 
+    @AutoConfigureOrder(PriorityPrecedence.LATE)
+    @Configuration
+    static class HmacAutorityAutoconfigure {
+        @Bean(NAMESPACE + ".fallbackHmacAuthority")
+        @ConditionalOnMissingBean(HmacAuthority.class)
+        public HmacAuthority fallbackHmacAuthority() {
+            return HmacAuthority.HmacSHA256.randomInstance();
+        }
+    }
+
 }
diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java
index c2f4a4fedec..05eb3363203 100644
--- a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java
+++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/urlencoding/MementosTest.java
@@ -18,7 +18,6 @@
  */
 package org.apache.causeway.core.runtimeservices.urlencoding;
 
-import java.io.Serializable;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.time.LocalDate;
@@ -32,11 +31,12 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
 import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
-import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.memento._Mementos;
-import org.apache.causeway.commons.internal.memento._Mementos.Memento;
-import org.apache.causeway.commons.internal.memento._Mementos.SerializingAdapter;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
+import org.apache.causeway.core.metamodel.util.hmac.Memento;
+import org.apache.causeway.core.metamodel.util.hmac.MementoHmacContext;
+import org.apache.causeway.core.metamodel.valuesemantics.ValueCodec;
 
 class MementosTest {
 
@@ -46,26 +46,13 @@ static enum DOW {
 
     UrlEncodingServiceWithCompression serviceWithCompression;
     UrlEncodingService serviceBaseEncoding;
-    SerializingAdapter serializingAdapter;
+    ValueCodec valueCodec;
 
     @BeforeEach
     void setUp() throws Exception {
         serviceWithCompression = new UrlEncodingServiceWithCompression();
-        serviceBaseEncoding = UrlEncodingService.forTestingNoCompression();;
-
-        serializingAdapter = new SerializingAdapter() {
-
-            @Override
-            public Serializable write(final Object value) {
-                return (Serializable) value;
-            }
-
-            @Override
-            public <T> T read(final Class<T> cls, final Serializable value) {
-                return _Casts.castToOrElseNull(value, cls);
-            }
-        };
-
+        serviceBaseEncoding = UrlEncodingService.forTestingNoCompression();
+        valueCodec = ValueCodec.forTesting();
     }
 
     @Test
@@ -79,7 +66,8 @@ void roundtrip_with_compression() {
     }
 
     private void roundtrip(final UrlEncodingService codec) {
-        final Memento memento = _Mementos.create(codec, serializingAdapter);
+        final var mementoContext = new MementoHmacContext(new HmacUrlCodec(HmacAuthority.HmacSHA256.randomInstance(), codec), valueCodec);
+        final Memento memento = mementoContext.newMemento();
 
         memento.put("someString", "a string");
         memento.put("someStringWithDoubleSpaces", "a  string");
@@ -101,9 +89,9 @@ private void roundtrip(final UrlEncodingService codec) {
 
         memento.put("someEnum", DOW.Wed);
 
-        final String str = memento.asString();
+        final String str = memento.toExternalForm();
 
-        final Memento memento2 = _Mementos.parse(codec, serializingAdapter, str);
+        final Memento memento2 = mementoContext.parseDigitallySignedMemento(str);
 
         assertThat(memento2.get("someString", String.class), is("a string"));
         assertThat(memento2.get("someStringWithDoubleSpaces", String.class), is("a  string"));
diff --git a/core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java b/core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java
index 80b091357b5..f31c975d729 100644
--- a/core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java
+++ b/core/security/src/test/java/org/apache/causeway/security/EncodabilityContractTest.java
@@ -18,11 +18,7 @@
  */
 package org.apache.causeway.security;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.io.Serializable;
 
 import org.junit.jupiter.api.BeforeEach;
@@ -32,11 +28,10 @@
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import org.springframework.util.SerializationUtils;
+
 import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
 import org.apache.causeway.applib.services.user.UserMemento;
-import org.apache.causeway.commons.internal.base._Casts;
-
-import lombok.SneakyThrows;
 
 public abstract class EncodabilityContractTest {
 
@@ -67,25 +62,10 @@ public void shouldImplementEncodeable() throws Exception {
 
     @Test
     public void shouldRoundTrip() throws IOException, ClassNotFoundException {
-        var decodedObject = doRoundTrip(serializable);
+        var decodedObject = SerializationUtils.clone(serializable);
         assertRoundtripped(decodedObject, serializable);
     }
 
-    @SneakyThrows
-    private static <T extends Serializable> T doRoundTrip(final T serializable) {
-
-        var buffer = new ByteArrayOutputStream();
-
-        try(var out = new ObjectOutputStream(buffer)) {
-            out.writeObject(serializable);
-        }
-
-        try(var in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray()))) {
-            var decodedObject = in.readObject();
-            return _Casts.uncheckedCast(decodedObject);
-        }
-    }
-
     protected abstract void assertRoundtripped(Object decodedEncodable, Object originalEncodable);
 
 }
\ No newline at end of file
diff --git a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java
index 58214b032f6..a11b730ce89 100644
--- a/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java
+++ b/regressiontests/domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/MetaModelRegressionTest.java
@@ -32,14 +32,18 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
 import org.springframework.test.context.TestPropertySource;
 
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
 import org.apache.causeway.applib.services.factory.FactoryService;
 import org.apache.causeway.applib.services.metamodel.MetaModelServiceMenu;
 import org.apache.causeway.applib.services.metamodel.MetaModelServiceMenu.ExportFormat;
 import org.apache.causeway.applib.value.Clob;
 import org.apache.causeway.core.config.presets.CausewayPresets;
 import org.apache.causeway.testdomain.conf.Configuration_headless;
+import org.apache.causeway.testdomain.domainmodel.MetaModelRegressionTest.EnableHmacAuthority;
 import org.apache.causeway.testdomain.model.good.Configuration_usingValidDomain;
 import org.apache.causeway.testing.integtestsupport.applib.ApprovalsOptions;
 
@@ -49,7 +53,7 @@
         classes = {
                 Configuration_headless.class,
                 Configuration_usingValidDomain.class,
-
+                EnableHmacAuthority.class
         },
         properties = {
                 "causeway.core.meta-model.introspector.mode=FULL",
@@ -65,6 +69,14 @@ class MetaModelRegressionTest {
     @Inject MetaModelServiceMenu metaModelServiceMenu;
     @Inject FactoryService factoryService;
 
+    @Configuration(proxyBeanMethods = false)
+    static class EnableHmacAuthority {
+        @Bean
+        public HmacAuthority hmacAuthority() {
+            return HmacAuthority.HmacSHA256.randomInstance();
+        }
+    }
+
     @BeforeEach
     void setUp() {
         assertNotNull(metaModelServiceMenu);
diff --git a/regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java b/regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java
index 5e0fde8013a..a5ac1852971 100644
--- a/regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java
+++ b/regressiontests/factory/src/test/java/org/apache/causeway/testdomain/factory/ViewModelFactoryTest.java
@@ -32,8 +32,11 @@
 import org.apache.causeway.applib.annotation.DomainObject;
 import org.apache.causeway.applib.annotation.Nature;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.applib.services.bookmark.HmacAuthority;
 import org.apache.causeway.applib.services.registry.ServiceRegistry;
 import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.applib.services.urlencoding.UrlEncodingService;
+import org.apache.causeway.core.metamodel.util.hmac.HmacUrlCodec;
 import org.apache.causeway.testdomain.conf.Configuration_headless;
 import org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract;
 
@@ -121,6 +124,9 @@ void onPostConstruct() {
 
     // -- TESTS
 
+    @Inject HmacAuthority hmacAuthority;
+    @Inject UrlEncodingService urlEncodingService;
+
     @Test
     void sampleViewModel_shouldHave_injectionPointsResolved() {
         var sampleViewModel = factoryService.viewModel(SimpleViewModel.class);
@@ -138,7 +144,8 @@ void viewModel_shouldHave_constructorArgsResolved() {
         ViewModelWithInjectableConstructorArgs viewModel = factoryService.viewModel(ViewModelWithInjectableConstructorArgs.class,
                 Bookmark.forLogicalTypeNameAndIdentifier(
                         ViewModelWithInjectableConstructorArgs.class.getName(),
-                        "aName"));
+                        new HmacUrlCodec(hmacAuthority, urlEncodingService).encodeForUrlAsUtf8("aName")
+                        ));
         viewModel.assertInitialized();
     }
 
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java
index 8e2b34f34a5..1e40c83c7ac 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/errors/ExceptionStackTracePanel.java
@@ -19,6 +19,7 @@
 package org.apache.causeway.viewer.wicket.ui.errors;
 
 import java.util.Objects;
+import java.util.Optional;
 import java.util.stream.Stream;
 
 import org.apache.wicket.AttributeModifier;
@@ -88,16 +89,22 @@ public ExceptionStackTracePanel(
 
         super(id, Model.of(exceptionModel));
 
-        var ticketOptional = lookupService(ErrorReportingService.class)
-            .map(errorReportingService->
-                errorReportingService.reportError(exceptionModel.asErrorDetails()));
+        final boolean unautorizedOrRecognized =
+            exceptionModel.isAuthorizationException()
+            || exceptionModel.isRecognized();
 
-        var mainMessage = ticketOptional
+        var ticketOpt = unautorizedOrRecognized
+            ? Optional.<Ticket>empty()
+            : lookupService(ErrorReportingService.class)
+                .map(errorReportingService->
+                    errorReportingService.reportError(exceptionModel.asErrorDetails()));
+
+        var mainMessage = ticketOpt
             .map(Ticket::getUserMessage)
             .orElseGet(exceptionModel::getMainMessage);
         Wkt.labelAdd(this, ID_MAIN_MESSAGE, mainMessage);
 
-        ticketOptional
+        ticketOpt
             .map(Ticket::getMarkup)
             .ifPresentOrElse(ticketMarkup->{
                 Wkt.markupAdd(this, ID_TICKET_MARKUP, ticketMarkup);
@@ -105,10 +112,8 @@ public ExceptionStackTracePanel(
                 WktComponents.permanentlyHide(this, ID_TICKET_MARKUP);
             });
 
-        final boolean suppressExceptionDetail =
-                exceptionModel.isAuthorizationException()
-                || exceptionModel.isRecognized()
-                || ticketOptional
+        final boolean suppressExceptionDetail = unautorizedOrRecognized
+                || ticketOpt
                     .map(Ticket::getStackTracePolicy)
                     .map(Ticket.StackTracePolicy.HIDE::equals)
                     .orElse(false);
@@ -150,7 +155,7 @@ public void renderHead(final IHeaderResponse response) {
 
     // -- HELPER
 
-    private static String convertToHtml(ExceptionModel exceptionModel) {
+    private static String convertToHtml(final ExceptionModel exceptionModel) {
         var html = new StringBuilder();
         _NullSafe.stream(exceptionModel.causalChain())
             .filter(Objects::nonNull)
@@ -175,7 +180,7 @@ private static String convertToHtml(ExceptionModel exceptionModel) {
         return html.toString();
     }
 
-    private static String convertMessageToHtml(Throwable cause) {
+    private static String convertMessageToHtml(final Throwable cause) {
         return StringUtils.hasLength(cause.getMessage())
             ? org.springframework.web.util.HtmlUtils.htmlEscape(cause.getMessage().replace("\n", "<br>"))
             : "";
diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java
index c9fbabead88..6b04e2e1a0f 100644
--- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java
+++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/integration/WebRequestCycleForCauseway.java
@@ -46,6 +46,8 @@
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.exceptions.unrecoverable.BookmarkNotFoundException;
+import org.apache.causeway.applib.exceptions.unrecoverable.DigitalVerificationException;
+import org.apache.causeway.applib.services.exceprecog.Category;
 import org.apache.causeway.applib.services.exceprecog.ExceptionRecognizer;
 import org.apache.causeway.applib.services.exceprecog.ExceptionRecognizerForType;
 import org.apache.causeway.applib.services.exceprecog.ExceptionRecognizerService;
@@ -412,13 +414,17 @@ protected PageProvider errorPageProviderFor(final Exception ex) {
 
     // special case handling for PageExpiredException, otherwise infinite loop
     private static final ExceptionRecognizerForType pageExpiredExceptionRecognizer =
-            new ExceptionRecognizerForType(
-                    PageExpiredException.class,
-                    __->"Requested page is no longer available.");
+        new ExceptionRecognizerForType(
+            PageExpiredException.class, __->"Requested page is no longer available.");
+
+    private static final ExceptionRecognizerForType exceptionRecognizerForDigitalVerificationException =
+        new ExceptionRecognizerForType(Category.NOT_FOUND,
+            DigitalVerificationException.class, __->"Digital verification failed for this request. "
+                + "Perhaps bookmark is not (or no longer) valid.");
 
     private static final ExceptionRecognizerForType exceptionRecognizerForBookmarkNotFoundException =
-            new ExceptionRecognizerForType(
-                    BookmarkNotFoundException.class, __ -> "Bookmark is not found.");
+        new ExceptionRecognizerForType(Category.NOT_FOUND,
+            BookmarkNotFoundException.class, __->"Bookmark is not found.");
 
     protected IRequestablePage errorPageFor(final Exception ex) {
 
@@ -430,20 +436,21 @@ protected IRequestablePage errorPageFor(final Exception ex) {
         }
 
         // using side-effect free access to MM validation result
-        var validationResult = getMetaModelContext().getSpecificationLoader().getValidationResult()
+        var validationResult = mmc.getSpecificationLoader().getValidationResult()
                 .orElse(null);
         if(validationResult!=null
                 && validationResult.hasFailures()) {
             return new MmvErrorPage(validationResult.getMessages("[%d] %s"));
         }
 
-        var exceptionRecognizerService = getMetaModelContext().getServiceRegistry()
+        var exceptionRecognizerService = mmc.getServiceRegistry()
             .lookupServiceElseFail(ExceptionRecognizerService.class);
 
         final Optional<Recognition> recognition = exceptionRecognizerService
                 .recognizeFromSelected(
                         Can.<ExceptionRecognizer>of(
                                         pageExpiredExceptionRecognizer,
+                                        exceptionRecognizerForDigitalVerificationException,
                                         exceptionRecognizerForBookmarkNotFoundException)
                                 .addAll(exceptionRecognizerService.getExceptionRecognizers()),
                         ex);