From 4b5cf2cbab72c9179be4386b99bf1213e2d6fc71 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 30 Jul 2026 12:29:51 -0600 Subject: [PATCH 1/2] feat(feature-flagging): send API key fingerprint --- .../AgentlessConfigurationSource.java | 40 +++++++++++++++++-- .../AgentlessConfigurationSourceTest.java | 22 ++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java index f4fc35cce30..8d2192ee831 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java @@ -15,7 +15,11 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.io.InterruptedIOException; +import java.math.BigInteger; import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executors; @@ -41,6 +45,12 @@ final class AgentlessConfigurationSource implements ConfigurationSourceService { private static final String DATADOG_UFC_RULES_BASED_SERVER_PATH = "/api/v2/feature-flagging/config/rules-based/server"; + private static final String BASE62_ALPHABET = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String API_KEY_FINGERPRINT_HEADER = "DD-Api-Key-Fingerprint"; + private static final String CLIFFORD_SHA256_PREFIX = "rijn_"; + private static final int CLIFFORD_SHA256_BASE62_LENGTH = 43; + private static final BigInteger BASE_62 = BigInteger.valueOf(62); private static final int MAX_ATTEMPTS = 3; private static final int MINUTES_BETWEEN_WARNINGS = 5; private static final long FIRST_RETRY_MIN_MILLIS = 2_000; @@ -289,6 +299,27 @@ static long millis(final int seconds) { return TimeUnit.SECONDS.toMillis(seconds); } + static String apiKeyFingerprint(final String apiKey) { + final byte[] digest; + try { + digest = MessageDigest.getInstance("SHA-256").digest(apiKey.getBytes(StandardCharsets.UTF_8)); + } catch (final NoSuchAlgorithmException error) { + throw new IllegalStateException("SHA-256 is not available", error); + } + + BigInteger value = new BigInteger(1, digest); + final StringBuilder encoded = new StringBuilder(CLIFFORD_SHA256_BASE62_LENGTH); + while (value.signum() > 0) { + final BigInteger[] quotientAndRemainder = value.divideAndRemainder(BASE_62); + encoded.append(BASE62_ALPHABET.charAt(quotientAndRemainder[1].intValue())); + value = quotientAndRemainder[0]; + } + while (encoded.length() < CLIFFORD_SHA256_BASE62_LENGTH) { + encoded.append('0'); + } + return CLIFFORD_SHA256_PREFIX + encoded.reverse(); + } + static long retryDelayMillis( final long pollIntervalMillis, final int attempt, final double jitter) { final long baseDelay; @@ -363,11 +394,14 @@ public UfcHttpResponse fetch(final HttpUrl endpoint, final Config config, final if (etag != null) { headers.put("If-None-Match", etag); } + final boolean datadogManagedEndpoint = isDatadogManagedEndpoint(endpoint, config); + final String apiKey = config.getApiKey(); + if (datadogManagedEndpoint && apiKey != null) { + headers.put(API_KEY_FINGERPRINT_HEADER, apiKeyFingerprint(apiKey)); + } // Leave Accept-Encoding unset so OkHttp negotiates gzip and transparently decompresses it. final Request request = - prepareRequest(endpoint, headers, config, isDatadogManagedEndpoint(endpoint, config)) - .get() - .build(); + prepareRequest(endpoint, headers, config, datadogManagedEndpoint).get().build(); if (!fetching.compareAndSet(false, true)) { throw new IllegalStateException("Feature Flagging HTTP request already in flight"); } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessConfigurationSourceTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessConfigurationSourceTest.java index b02bd05642b..0baa7995fc6 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessConfigurationSourceTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessConfigurationSourceTest.java @@ -137,6 +137,22 @@ void defaultConstructorBuildsHttpClientFromConfig() { service.close(); } + @Test + void generatesCliffordV1ApiKeyFingerprints() { + assertEquals( + "rijn_RZwTDmWjELXeEmMEb0eIIegKayGGUPNsuJweEPhlXi5", + AgentlessConfigurationSource.apiKeyFingerprint("")); + assertEquals( + "rijn_SddeOoHbx2gFQTblESRn88xKs2B9zEA9ii1CclRfD6s", + AgentlessConfigurationSource.apiKeyFingerprint("7b58e278012eec8316224b052d87e6e8b2ba9e49")); + assertEquals( + "rijn_eFLHeyLxwaiNs2hY16pjkjNjVSHWRgf2rlveKc8YA1K", + AgentlessConfigurationSource.apiKeyFingerprint("!@#$%^𐍈한€हИ£")); + assertEquals( + "rijn_053ybBRXypQt9AC6UIlqH1YCFYSV1rQl8HCDIcBZs3D", + AgentlessConfigurationSource.apiKeyFingerprint("padding-171")); + } + @Test void realHttpClientDoesNotSendApiKeyOverHttp() throws Exception { try (JavaTestHttpServer server = @@ -162,6 +178,7 @@ void realHttpClientDoesNotSendApiKeyOverHttp() throws Exception { assertEquals("etag-b", response.etag); assertEquals(emptyConfig(), new String(response.body, UTF_8)); assertNull(server.getLastRequest().getHeader("DD-API-KEY")); + assertNull(server.getLastRequest().getHeader("DD-Api-Key-Fingerprint")); assertEquals("etag-a", server.getLastRequest().getHeader("If-None-Match")); assertEquals("java", server.getLastRequest().getHeader("Datadog-Meta-Lang")); assertEquals("gzip", server.getLastRequest().getHeader("Accept-Encoding")); @@ -182,6 +199,9 @@ void sendsApiKeyToDefaultDatadogHttpsEndpoint() throws Exception { client.fetch(endpoint, config(), null); assertEquals("test-api-key", requests.get(0).header("DD-API-KEY")); + assertEquals( + "rijn_i8Jug5ocjALL7JZiV1a8HzXqkwDRKcE7hK9IouPQwio", + requests.get(0).header("DD-Api-Key-Fingerprint")); } @Test @@ -198,6 +218,7 @@ void stripsApiKeyFromCustomHttpsEndpoint() throws Exception { client.fetch(endpoint, config, null); assertNull(requests.get(0).header("DD-API-KEY")); + assertNull(requests.get(0).header("DD-Api-Key-Fingerprint")); } @Test @@ -210,6 +231,7 @@ void stripsApiKeyFromUnexpectedHttpsEndpoint() throws Exception { client.fetch(endpoint, config(), null); assertNull(requests.get(0).header("DD-API-KEY")); + assertNull(requests.get(0).header("DD-Api-Key-Fingerprint")); } @Test From fe91ced2b2c595845472d66d2fd0bb3ec7a793d0 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 30 Jul 2026 12:35:16 -0600 Subject: [PATCH 2/2] fix(feature-flagging): capitalize fingerprint header --- .../datadog/featureflag/AgentlessConfigurationSource.java | 2 +- .../featureflag/AgentlessConfigurationSourceTest.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java index 8d2192ee831..e408ba4f453 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java @@ -47,7 +47,7 @@ final class AgentlessConfigurationSource implements ConfigurationSourceService { "/api/v2/feature-flagging/config/rules-based/server"; private static final String BASE62_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - private static final String API_KEY_FINGERPRINT_HEADER = "DD-Api-Key-Fingerprint"; + private static final String API_KEY_FINGERPRINT_HEADER = "DD-API-KEY-FINGERPRINT"; private static final String CLIFFORD_SHA256_PREFIX = "rijn_"; private static final int CLIFFORD_SHA256_BASE62_LENGTH = 43; private static final BigInteger BASE_62 = BigInteger.valueOf(62); diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessConfigurationSourceTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessConfigurationSourceTest.java index 0baa7995fc6..224549cfd6b 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessConfigurationSourceTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessConfigurationSourceTest.java @@ -178,7 +178,7 @@ void realHttpClientDoesNotSendApiKeyOverHttp() throws Exception { assertEquals("etag-b", response.etag); assertEquals(emptyConfig(), new String(response.body, UTF_8)); assertNull(server.getLastRequest().getHeader("DD-API-KEY")); - assertNull(server.getLastRequest().getHeader("DD-Api-Key-Fingerprint")); + assertNull(server.getLastRequest().getHeader("DD-API-KEY-FINGERPRINT")); assertEquals("etag-a", server.getLastRequest().getHeader("If-None-Match")); assertEquals("java", server.getLastRequest().getHeader("Datadog-Meta-Lang")); assertEquals("gzip", server.getLastRequest().getHeader("Accept-Encoding")); @@ -201,7 +201,7 @@ void sendsApiKeyToDefaultDatadogHttpsEndpoint() throws Exception { assertEquals("test-api-key", requests.get(0).header("DD-API-KEY")); assertEquals( "rijn_i8Jug5ocjALL7JZiV1a8HzXqkwDRKcE7hK9IouPQwio", - requests.get(0).header("DD-Api-Key-Fingerprint")); + requests.get(0).header("DD-API-KEY-FINGERPRINT")); } @Test @@ -218,7 +218,7 @@ void stripsApiKeyFromCustomHttpsEndpoint() throws Exception { client.fetch(endpoint, config, null); assertNull(requests.get(0).header("DD-API-KEY")); - assertNull(requests.get(0).header("DD-Api-Key-Fingerprint")); + assertNull(requests.get(0).header("DD-API-KEY-FINGERPRINT")); } @Test @@ -231,7 +231,7 @@ void stripsApiKeyFromUnexpectedHttpsEndpoint() throws Exception { client.fetch(endpoint, config(), null); assertNull(requests.get(0).header("DD-API-KEY")); - assertNull(requests.get(0).header("DD-Api-Key-Fingerprint")); + assertNull(requests.get(0).header("DD-API-KEY-FINGERPRINT")); } @Test