diff --git a/core/src/main/java/io/grpc/internal/SpiffeUtil.java b/core/src/main/java/io/grpc/internal/SpiffeUtil.java
new file mode 100644
index 0000000000..bddce3d035
--- /dev/null
+++ b/core/src/main/java/io/grpc/internal/SpiffeUtil.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 The gRPC Authors
+ *
+ * Licensed 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 io.grpc.internal;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Splitter;
+import java.util.Locale;
+
+/**
+ * Helper utility to work with SPIFFE URIs.
+ * @see Standard
+ */
+public final class SpiffeUtil {
+
+ private static final String PREFIX = "spiffe://";
+
+ private SpiffeUtil() {}
+
+ /**
+ * Parses a URI string, applies validation rules described in SPIFFE standard, and, in case of
+ * success, returns parsed TrustDomain and Path.
+ *
+ * @param uri a String representing a SPIFFE ID
+ */
+ public static SpiffeId parse(String uri) {
+ doInitialUriValidation(uri);
+ checkArgument(uri.toLowerCase(Locale.US).startsWith(PREFIX), "Spiffe Id must start with "
+ + PREFIX);
+ String domainAndPath = uri.substring(PREFIX.length());
+ String trustDomain;
+ String path;
+ if (!domainAndPath.contains("/")) {
+ trustDomain = domainAndPath;
+ path = "";
+ } else {
+ String[] parts = domainAndPath.split("/", 2);
+ trustDomain = parts[0];
+ path = parts[1];
+ checkArgument(!path.isEmpty(), "Path must not include a trailing '/'");
+ }
+ validateTrustDomain(trustDomain);
+ validatePath(path);
+ if (!path.isEmpty()) {
+ path = "/" + path;
+ }
+ return new SpiffeId(trustDomain, path);
+ }
+
+ private static void doInitialUriValidation(String uri) {
+ checkArgument(checkNotNull(uri, "uri").length() > 0, "Spiffe Id can't be empty");
+ checkArgument(uri.length() <= 2048, "Spiffe Id maximum length is 2048 characters");
+ checkArgument(!uri.contains("#"), "Spiffe Id must not contain query fragments");
+ checkArgument(!uri.contains("?"), "Spiffe Id must not contain query parameters");
+ }
+
+ private static void validateTrustDomain(String trustDomain) {
+ checkArgument(!trustDomain.isEmpty(), "Trust Domain can't be empty");
+ checkArgument(trustDomain.length() < 256, "Trust Domain maximum length is 255 characters");
+ checkArgument(trustDomain.matches("[a-z0-9._-]+"),
+ "Trust Domain must contain only letters, numbers, dots, dashes, and underscores"
+ + " ([a-z0-9.-_])");
+ }
+
+ private static void validatePath(String path) {
+ if (path.isEmpty()) {
+ return;
+ }
+ checkArgument(!path.endsWith("/"), "Path must not include a trailing '/'");
+ for (String segment : Splitter.on("/").split(path)) {
+ validatePathSegment(segment);
+ }
+ }
+
+ private static void validatePathSegment(String pathSegment) {
+ checkArgument(!pathSegment.isEmpty(), "Individual path segments must not be empty");
+ checkArgument(!(pathSegment.equals(".") || pathSegment.equals("..")),
+ "Individual path segments must not be relative path modifiers (i.e. ., ..)");
+ checkArgument(pathSegment.matches("[a-zA-Z0-9._-]+"),
+ "Individual path segments must contain only letters, numbers, dots, dashes, and underscores"
+ + " ([a-zA-Z0-9.-_])");
+ }
+
+ /**
+ * Represents a SPIFFE ID as defined in the SPIFFE standard.
+ * @see Standard
+ */
+ public static class SpiffeId {
+
+ private final String trustDomain;
+ private final String path;
+
+ private SpiffeId(String trustDomain, String path) {
+ this.trustDomain = trustDomain;
+ this.path = path;
+ }
+
+ public String getTrustDomain() {
+ return trustDomain;
+ }
+
+ public String getPath() {
+ return path;
+ }
+ }
+
+}
diff --git a/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java
new file mode 100644
index 0000000000..c3a98ce33e
--- /dev/null
+++ b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2024 The gRPC Authors
+ *
+ * Licensed 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 io.grpc.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+
+@RunWith(Enclosed.class)
+public class SpiffeUtilTest {
+
+ @RunWith(Parameterized.class)
+ public static class ParseSuccessTest {
+ @Parameter
+ public String uri;
+
+ @Parameter(1)
+ public String trustDomain;
+
+ @Parameter(2)
+ public String path;
+
+ @Test
+ public void parseSuccessTest() {
+ SpiffeUtil.SpiffeId spiffeId = SpiffeUtil.parse(uri);
+ assertEquals(trustDomain, spiffeId.getTrustDomain());
+ assertEquals(path, spiffeId.getPath());
+ }
+
+ @Parameters(name = "spiffeId={0}")
+ public static Collection data() {
+ return Arrays.asList(new String[][] {
+ {"spiffe://example.com", "example.com", ""},
+ {"spiffe://example.com/us", "example.com", "/us"},
+ {"spIFfe://qa-staging.final_check.example.com/us", "qa-staging.final_check.example.com",
+ "/us"},
+ {"spiffe://example.com/country/us/state/FL/city/Miami", "example.com",
+ "/country/us/state/FL/city/Miami"},
+ {"SPIFFE://example.com/Czech.Republic/region0.1/city_of-Prague", "example.com",
+ "/Czech.Republic/region0.1/city_of-Prague"},
+ {"spiffe://trust-domain-name/path", "trust-domain-name", "/path"},
+ {"spiffe://staging.example.com/payments/mysql", "staging.example.com", "/payments/mysql"},
+ {"spiffe://staging.example.com/payments/web-fe", "staging.example.com",
+ "/payments/web-fe"},
+ {"spiffe://k8s-west.example.com/ns/staging/sa/default", "k8s-west.example.com",
+ "/ns/staging/sa/default"},
+ {"spiffe://example.com/9eebccd2-12bf-40a6-b262-65fe0487d453", "example.com",
+ "/9eebccd2-12bf-40a6-b262-65fe0487d453"},
+ {"spiffe://trustdomain/.a..", "trustdomain", "/.a.."},
+ {"spiffe://trustdomain/...", "trustdomain", "/..."},
+ {"spiffe://trustdomain/abcdefghijklmnopqrstuvwxyz", "trustdomain",
+ "/abcdefghijklmnopqrstuvwxyz"},
+ {"spiffe://trustdomain/abc0123.-_", "trustdomain", "/abc0123.-_"},
+ {"spiffe://trustdomain/0123456789", "trustdomain", "/0123456789"},
+ {"spiffe://trustdomain0123456789/path", "trustdomain0123456789", "/path"},
+ });
+ }
+ }
+
+ @RunWith(Parameterized.class)
+ public static class ParseFailureTest {
+ @Parameter
+ public String uri;
+
+ @Test
+ public void parseFailureTest() {
+ assertThrows(IllegalArgumentException.class, () -> SpiffeUtil.parse(uri));
+ }
+
+ @Parameters(name = "spiffeId={0}")
+ public static Collection data() {
+ return Arrays.asList(
+ "spiffe:///",
+ "spiffe://example!com",
+ "spiffe://exampleя.com/workload-1",
+ "spiffe://example.com/us/florida/miamiя",
+ "spiffe:/trustdomain/path",
+ "spiffe:///path",
+ "spiffe://trust%20domain/path",
+ "spiffe://user@trustdomain/path",
+ "spiffe:// /",
+ "",
+ "http://trustdomain/path",
+ "//trustdomain/path",
+ "://trustdomain/path",
+ "piffe://trustdomain/path",
+ "://",
+ "://trustdomain",
+ "spiff",
+ "spiffe",
+ "spiffe:////",
+ "spiffe://trust.domain/../path"
+ );
+ }
+ }
+
+ public static class ExceptionMessageTest {
+
+ @Test
+ public void spiffeUriFormatTest() {
+ NullPointerException npe = assertThrows(NullPointerException.class, () ->
+ SpiffeUtil.parse(null));
+ assertEquals("uri", npe.getMessage());
+
+ IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("https://example.com"));
+ assertEquals("Spiffe Id must start with spiffe://", iae.getMessage());
+
+ iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("spiffe://example.com/workload#1"));
+ assertEquals("Spiffe Id must not contain query fragments", iae.getMessage());
+
+ iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("spiffe://example.com/workload-1?t=1"));
+ assertEquals("Spiffe Id must not contain query parameters", iae.getMessage());
+ }
+
+ @Test
+ public void spiffeTrustDomainFormatTest() {
+ IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("spiffe://"));
+ assertEquals("Trust Domain can't be empty", iae.getMessage());
+
+ iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("spiffe://eXample.com"));
+ assertEquals(
+ "Trust Domain must contain only letters, numbers, dots, dashes, and underscores "
+ + "([a-z0-9.-_])",
+ iae.getMessage());
+
+ StringBuilder longTrustDomain = new StringBuilder("spiffe://pi.eu.");
+ for (int i = 0; i < 50; i++) {
+ longTrustDomain.append("pi.eu");
+ }
+ iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse(longTrustDomain.toString()));
+ assertEquals("Trust Domain maximum length is 255 characters", iae.getMessage());
+
+ StringBuilder longSpiffe = new StringBuilder(String.format("spiffe://mydomain%scom/", "%21"));
+ for (int i = 0; i < 405; i++) {
+ longSpiffe.append("qwert");
+ }
+ iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse(longSpiffe.toString()));
+ assertEquals("Spiffe Id maximum length is 2048 characters", iae.getMessage());
+ }
+
+ @Test
+ public void spiffePathFormatTest() {
+ IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("spiffe://example.com//"));
+ assertEquals("Path must not include a trailing '/'", iae.getMessage());
+
+ iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("spiffe://example.com/"));
+ assertEquals("Path must not include a trailing '/'", iae.getMessage());
+
+ iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("spiffe://example.com/us//miami"));
+ assertEquals("Individual path segments must not be empty", iae.getMessage());
+
+ iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("spiffe://example.com/us/."));
+ assertEquals("Individual path segments must not be relative path modifiers (i.e. ., ..)",
+ iae.getMessage());
+
+ iae = assertThrows(IllegalArgumentException.class, () ->
+ SpiffeUtil.parse("spiffe://example.com/us!"));
+ assertEquals("Individual path segments must contain only letters, numbers, dots, dashes, and "
+ + "underscores ([a-zA-Z0-9.-_])", iae.getMessage());
+ }
+ }
+}
\ No newline at end of file