Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Objects;

import javax.annotation.Nonnull;
Expand All @@ -23,6 +24,7 @@

import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.exception.X509SvidException;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.svid.x509svid.X509Svid;
import io.spiffe.workloadapi.DefaultX509Source;
Expand Down Expand Up @@ -109,16 +111,57 @@ X509Source initX509Source()
final String socketPath = Option.of(System.getenv(SOCKET_ENVIRONMENT_VARIABLE)).getOrElse(DEFAULT_SOCKET_PATH);
log.info("Using socket path {} for ZTIS agent.", socketPath);

final X509SourceOptions x509SourceOptions =
X509SourceOptions.builder().spiffeSocketPath(socketPath).initTimeout(DEFAULT_SOCKET_TIMEOUT).build();
// The SPIRE agent may return multiple SVIDs when overlapping registration selectors exist
// (e.g. a second service key on the same ZTIS instance, or a co-located workload). We must pick
// the one whose SPIFFE ID matches our binding — see pickSvid.
final SpiffeId expectedSpiffeId;
try {
return DefaultX509Source.newSource(x509SourceOptions);
expectedSpiffeId = SpiffeId.parse(mapView.getMapView("workload").getString("spiffeID"));
}
catch( Exception e ) {
throw new CloudPlatformException("Invalid SPIFFE ID in Zero Trust Identity Service binding.", e);
}
try {
return DefaultX509Source.newSource(buildX509SourceOptions(socketPath, expectedSpiffeId));
}
catch( final Exception e ) {
throw new CloudPlatformException("Failed to load the certificate from the unix socket: " + socketPath, e);
}
}

X509SourceOptions
buildX509SourceOptions( @Nonnull final String socketPath, @Nonnull final SpiffeId expectedSpiffeId )
{
return X509SourceOptions
.builder()
.spiffeSocketPath(socketPath)
.initTimeout(DEFAULT_SOCKET_TIMEOUT)
.svidPicker(svids -> pickSvid(svids, expectedSpiffeId))

@CharlesDuboisSAP CharlesDuboisSAP Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove the svidPicker, the tests are still green apart from assertThat(picker).isNotNull();
I don't think any test checks the actual change of this PR, the svidPicker

@Jonas-Isr Jonas-Isr Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is the same issue discussed here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then wdym it's a compromise, if you revert the production code the tests are still green

.build();
}

/**
* Selects the {@link X509Svid} whose SPIFFE ID equals {@code expectedSpiffeId} from the given list. Used as the
* {@code svidPicker} for {@link DefaultX509Source} to avoid non-deterministic selection when the SPIRE agent
* returns multiple SVIDs (e.g. after a second service key is created for the same ZTIS instance).
*/
@Nonnull
static X509Svid pickSvid( @Nonnull final List<X509Svid> svids, @Nonnull final SpiffeId expectedSpiffeId )
{
log.debug("SPIRE agent returned {} SVID(s); selecting the one matching '{}'.", svids.size(), expectedSpiffeId);
return svids
.stream()
.filter(svid -> expectedSpiffeId.equals(svid.getSpiffeId()))
.findFirst()
.orElseThrow(
() -> new CloudPlatformException(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why throw? If not found we could use the old behaviour of not setting a svidPicker?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Falling back to the old behavior would reproduce the bug in a smaller scope — it would only behave correctly when there's exactly one SVID, or when the library happens to pick the right one from an unordered list. That's the non-determinism we're fixing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only throws when it doesn’t find the svid from the list right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, just improved the tests to address the other concern regarding removing the svidPicker

String
.format(
"No SVID matching SPIFFE ID '%s' among %d returned by the SPIRE agent.",
expectedSpiffeId,
svids.size())));
}

@Nonnull
X509Svid getX509Svid()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -26,6 +28,7 @@
import com.sap.cloud.environment.servicebinding.api.exception.ServiceBindingAccessException;
import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException;

import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.svid.x509svid.X509Svid;

class ZeroTrustIdentityServiceTest
Expand Down Expand Up @@ -152,6 +155,25 @@ void testCertOnFileSystem()
.isInstanceOf(IllegalStateException.class);
}

@Test
void testSvidPickerIsWiredAndSelectsMatchingSvid()
{
final SpiffeId expected = SpiffeId.parse("spiffe://example.org/my-app");
final X509Svid own = mockSvidWithSpiffeId(expected);
final X509Svid other = mockSvidWithSpiffeId(SpiffeId.parse("spiffe://example.org/other-app"));

final Function<List<X509Svid>, X509Svid> picker =
sut.buildX509SourceOptions("unix:///tmp/test.sock", expected).getSvidPicker();

assertThat(picker).isNotNull();
assertThat(picker.apply(List.of(own))).isSameAs(own);
assertThat(picker.apply(List.of(other, own))).isSameAs(own);
assertThatThrownBy(() -> picker.apply(List.of(other)))
.isInstanceOf(CloudPlatformException.class)
.hasMessageContaining("spiffe://example.org/my-app");
assertThatThrownBy(() -> picker.apply(List.of())).isInstanceOf(CloudPlatformException.class);
}

private void mockSvid( Instant notAfter )
{
final X509Svid svid = mock(X509Svid.class);
Expand All @@ -162,6 +184,13 @@ private void mockSvid( Instant notAfter )
svidMock = svid;
}

private static X509Svid mockSvidWithSpiffeId( final SpiffeId spiffeId )
{
final X509Svid svid = mock(X509Svid.class);
doReturn(spiffeId).when(svid).getSpiffeId();
return svid;
}

private static ServiceBinding mockBinding()
{
return new DefaultServiceBindingBuilder()
Expand Down
2 changes: 1 addition & 1 deletion release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@

### 🐛 Fixed Issues

-
- [Connectivity] Fixed a bug in `ZeroTrustIdentityService` where a non-deterministic SVID could be selected when the SPIRE agent returns multiple SVIDs to the workload (e.g. after creating a second service key for the same ZTIS instance).