[java-idp-plugin-vci] 03/07: Rounding issuance to improve unlinkability for batch issuance

Codeberg noreply at shibboleth.net
Thu Sep 24 13:16:28 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch main
in repository java-idp-plugin-vci.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/4b8a237b33d6495edd10add6ef276c3a6c24c9c9

commit 4b8a237b33d6495edd10add6ef276c3a6c24c9c9
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Thu Sep 24 16:10:46 2026 +0300

    Rounding issuance to improve unlinkability for batch issuance
---
 .../profile/impl/ResolveCredentialLifetime.java    | 42 +++++++++++++++++++---
 .../impl/ResolveCredentialLifetimeTest.java        | 25 +++++++++++++
 2 files changed, 63 insertions(+), 4 deletions(-)

diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ResolveCredentialLifetime.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ResolveCredentialLifetime.java
index b6671da..883d8a3 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ResolveCredentialLifetime.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ResolveCredentialLifetime.java
@@ -72,6 +72,10 @@ public class ResolveCredentialLifetime extends AbstractProfileAction {
     @Nullable
     private CredentialIssuanceConfiguration settings;
 
+    /** Granularity the instant of issuance is rounded down to. */
+    @Nonnull
+    private Duration timestampPrecision = Duration.ofHours(1);
+
     /** Credentials context. */
     @Nullable
     private CredentialsContext ctx;
@@ -96,6 +100,22 @@ public class ResolveCredentialLifetime extends AbstractProfileAction {
                 "RelyingPartyContext lookup strategy cannot be null");
     }
 
+    /**
+     * Set the granularity the instant of issuance is rounded down to.
+     *
+     * @param precision granularity the instant of issuance is rounded down to,
+     *                  at least a second and shorter than the lifetime of any
+     *                  credential configuration
+     */
+    public void setTimestampPrecision(@Nonnull final Duration precision) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        Constraint.isNotNull(precision, "Timestamp precision cannot be null");
+        Constraint.isFalse(precision.getSeconds() < 1, "Timestamp precision cannot be shorter than a second");
+
+        timestampPrecision = precision;
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -148,17 +168,19 @@ public class ResolveCredentialLifetime extends AbstractProfileAction {
 
         assert ctx != null && lifetime != null;
 
-        final Instant issuedAt = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+        final Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+        final Instant issuedAt = roundDown(now);
         final Instant notBefore = (settings == null ? issuedAt : settings.resolveNotBefore(issuedAt))
                 .truncatedTo(ChronoUnit.SECONDS);
         final Instant expiresAt = notBefore.plus(lifetime);
 
-        if (!expiresAt.isAfter(issuedAt)) {
+        if (!expiresAt.isAfter(now)) {
             // TODO: A closed window deserves an error the wallet is able to act on.
             log.error(
                     "{} Validity window of the requested credential closed at {}, there is nothing to issue."
-                            + " Move the 'not_before' or the 'lifetime' of its credential configuration.",
-                    getLogPrefix(), expiresAt);
+                            + " Move the 'not_before' or the 'lifetime' of its credential configuration, or"
+                            + " shorten the timestamp precision {}.",
+                    getLogPrefix(), expiresAt, timestampPrecision);
             ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
             return;
         }
@@ -175,4 +197,16 @@ public class ResolveCredentialLifetime extends AbstractProfileAction {
                 ctx.getExpiresAt());
     }
 
+    /**
+     * Round an instant down to a multiple of {@link #timestampPrecision}.
+     *
+     * @param instant instant to round down
+     * @return the rounded instant
+     */
+    @Nonnull
+    private Instant roundDown(@Nonnull final Instant instant) {
+        final long precision = timestampPrecision.getSeconds();
+
+        return Instant.ofEpochSecond(Math.floorDiv(instant.getEpochSecond(), precision) * precision);
+    }
 }
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ResolveCredentialLifetimeTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ResolveCredentialLifetimeTest.java
index 2c90557..4171294 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ResolveCredentialLifetimeTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ResolveCredentialLifetimeTest.java
@@ -18,6 +18,7 @@ package org.geant.shibboleth.plugin.openidvci.profile.impl;
 
 import java.time.Duration;
 import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 
 import org.geant.shibboleth.plugin.openidvci.credential.CredentialConfiguration;
 import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
@@ -85,6 +86,30 @@ public class ResolveCredentialLifetimeTest {
         Assert.assertEquals(ctx.getExpiresAt(), ctx.getIssuedAt().plus(PROFILE_LIFETIME));
     }
 
+    @Test
+    public void testIssuanceIsRoundedDownToPrecision() throws Exception {
+        credentialWith(null);
+
+        final Instant before = Instant.now();
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+
+        final CredentialsContext ctx = resolved();
+        Assert.assertEquals(ctx.getIssuedAt(), ctx.getIssuedAt().truncatedTo(ChronoUnit.HOURS));
+        Assert.assertFalse(ctx.getIssuedAt().isAfter(before));
+        Assert.assertTrue(Duration.between(ctx.getIssuedAt(), before).compareTo(Duration.ofHours(1)) < 0);
+    }
+
+    @Test
+    public void testPrecisionCoarserThanTheLifetimeIssuesNothing() throws Exception {
+        action = new ResolveCredentialLifetime();
+        action.setTimestampPrecision(Duration.ofDays(365));
+        action.initialize();
+        credentialWith("{\"lifetime\":\"PT1M\"}");
+
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), IdPEventIds.INVALID_PROFILE_CONFIG);
+        Assert.assertNull(resolved().getExpiresAt());
+    }
+
     @Test
     public void testCredentialLifetimeOverridesTheProfileConfiguration() throws Exception {
         credentialWith("{\"lifetime\":\"P7D\"}");

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list