• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #19596

19 Dec 2024 03:54PM UTC coverage: 88.598% (-0.009%) from 88.607%
#19596

push

github

web-flow
Re-enable animalsniffer, fixing violations

In 61f19d707a I swapped the signatures to use the version catalog. But I
failed to preserve the `@signature` extension and it all seemed to
work... But in fact all the animalsniffer tasks were completing as
SKIPPED as they lacked signatures. The build.gradle changes in this
commit are to fix that while still using version catalog.

But while it was broken violations crept in. Most violations weren't
too important and we're not surprised went unnoticed. For example, Netty
with TLS has long required the Java 8 API
`setEndpointIdentificationAlgorithm()`, so using `Optional` in the same
code path didn't harm anything in particular. I still swapped it to
Guava's `Optional` to avoid overuse of `@IgnoreJRERequirement`.

One important violation has not been fixed and instead I've disabled the
android signature in api/build.gradle for the moment.  The violation is
in StatusException using the `fillInStackTrace` overload of Exception.
This problem [had been noticed][PR11066], but we couldn't figure out
what was going on. AnimalSniffer is now noticing this and agreeing with
the internal linter. There is still a question of why our interop tests
failed to notice this, but given they are no longer running on pre-API
level 24, that may forever be a mystery.

[PR11066]: https://github.com/grpc/grpc-java/pull/11066

33481 of 37790 relevant lines covered (88.6%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.83
/../core/src/main/java/io/grpc/internal/SpiffeUtil.java
1
/*
2
 * Copyright 2024 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.internal;
18

19
import static com.google.common.base.Preconditions.checkArgument;
20
import static com.google.common.base.Preconditions.checkNotNull;
21

22
import com.google.common.base.Optional;
23
import com.google.common.base.Splitter;
24
import com.google.common.collect.ImmutableList;
25
import com.google.common.collect.ImmutableMap;
26
import com.google.common.io.Files;
27
import java.io.ByteArrayInputStream;
28
import java.io.File;
29
import java.io.IOException;
30
import java.io.InputStream;
31
import java.nio.charset.StandardCharsets;
32
import java.security.cert.Certificate;
33
import java.security.cert.CertificateException;
34
import java.security.cert.CertificateFactory;
35
import java.security.cert.CertificateParsingException;
36
import java.security.cert.X509Certificate;
37
import java.util.ArrayList;
38
import java.util.Collection;
39
import java.util.Collections;
40
import java.util.HashMap;
41
import java.util.List;
42
import java.util.Locale;
43
import java.util.Map;
44

45
/**
46
 * Provides utilities to manage SPIFFE bundles, extract SPIFFE IDs from X.509 certificate chains,
47
 * and parse SPIFFE IDs.
48
 * @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
49
 */
50
public final class SpiffeUtil {
51

52
  private static final Integer URI_SAN_TYPE = 6;
1✔
53
  private static final String USE_PARAMETER_VALUE = "x509-svid";
54
  private static final String KTY_PARAMETER_VALUE = "RSA";
55
  private static final String CERTIFICATE_PREFIX = "-----BEGIN CERTIFICATE-----\n";
56
  private static final String CERTIFICATE_SUFFIX = "-----END CERTIFICATE-----";
57
  private static final String PREFIX = "spiffe://";
58

59
  private SpiffeUtil() {}
60

61
  /**
62
   * Parses a URI string, applies validation rules described in SPIFFE standard, and, in case of
63
   * success, returns parsed TrustDomain and Path.
64
   *
65
   * @param uri a String representing a SPIFFE ID
66
   */
67
  public static SpiffeId parse(String uri) {
68
    doInitialUriValidation(uri);
1✔
69
    checkArgument(uri.toLowerCase(Locale.US).startsWith(PREFIX), "Spiffe Id must start with "
1✔
70
        + PREFIX);
71
    String domainAndPath = uri.substring(PREFIX.length());
1✔
72
    String trustDomain;
73
    String path;
74
    if (!domainAndPath.contains("/")) {
1✔
75
      trustDomain = domainAndPath;
1✔
76
      path =  "";
1✔
77
    } else {
78
      String[] parts = domainAndPath.split("/", 2);
1✔
79
      trustDomain = parts[0];
1✔
80
      path = parts[1];
1✔
81
      checkArgument(!path.isEmpty(), "Path must not include a trailing '/'");
1✔
82
    }
83
    validateTrustDomain(trustDomain);
1✔
84
    validatePath(path);
1✔
85
    if (!path.isEmpty()) {
1✔
86
      path = "/" + path;
1✔
87
    }
88
    return new SpiffeId(trustDomain, path);
1✔
89
  }
90

91
  private static void doInitialUriValidation(String uri) {
92
    checkArgument(checkNotNull(uri, "uri").length() > 0, "Spiffe Id can't be empty");
1✔
93
    checkArgument(uri.length() <= 2048, "Spiffe Id maximum length is 2048 characters");
1✔
94
    checkArgument(!uri.contains("#"), "Spiffe Id must not contain query fragments");
1✔
95
    checkArgument(!uri.contains("?"), "Spiffe Id must not contain query parameters");
1✔
96
  }
1✔
97

98
  private static void validateTrustDomain(String trustDomain) {
99
    checkArgument(!trustDomain.isEmpty(), "Trust Domain can't be empty");
1✔
100
    checkArgument(trustDomain.length() < 256, "Trust Domain maximum length is 255 characters");
1✔
101
    checkArgument(trustDomain.matches("[a-z0-9._-]+"),
1✔
102
        "Trust Domain must contain only letters, numbers, dots, dashes, and underscores"
103
            + " ([a-z0-9.-_])");
104
  }
1✔
105

106
  private static void validatePath(String path) {
107
    if (path.isEmpty()) {
1✔
108
      return;
1✔
109
    }
110
    checkArgument(!path.endsWith("/"), "Path must not include a trailing '/'");
1✔
111
    for (String segment : Splitter.on("/").split(path)) {
1✔
112
      validatePathSegment(segment);
1✔
113
    }
1✔
114
  }
1✔
115

116
  private static void validatePathSegment(String pathSegment) {
117
    checkArgument(!pathSegment.isEmpty(), "Individual path segments must not be empty");
1✔
118
    checkArgument(!(pathSegment.equals(".") || pathSegment.equals("..")),
1✔
119
        "Individual path segments must not be relative path modifiers (i.e. ., ..)");
120
    checkArgument(pathSegment.matches("[a-zA-Z0-9._-]+"),
1✔
121
        "Individual path segments must contain only letters, numbers, dots, dashes, and underscores"
122
            + " ([a-zA-Z0-9.-_])");
123
  }
1✔
124

125
  /**
126
   * Returns the SPIFFE ID from the leaf certificate, if present.
127
   *
128
   * @param certChain certificate chain to extract SPIFFE ID from
129
   */
130
  public static Optional<SpiffeId> extractSpiffeId(X509Certificate[] certChain)
131
      throws CertificateParsingException {
132
    checkArgument(checkNotNull(certChain, "certChain").length > 0, "certChain can't be empty");
1✔
133
    Collection<List<?>> subjectAltNames = certChain[0].getSubjectAlternativeNames();
1✔
134
    if (subjectAltNames == null) {
1✔
135
      return Optional.absent();
1✔
136
    }
137
    String uri = null;
1✔
138
    // Search for the unique URI SAN.
139
    for (List<?> altName : subjectAltNames) {
1✔
140
      if (altName.size() < 2 ) {
1✔
141
        continue;
×
142
      }
143
      if (URI_SAN_TYPE.equals(altName.get(0))) {
1✔
144
        if (uri != null) {
1✔
145
          throw new IllegalArgumentException("Multiple URI SAN values found in the leaf cert.");
1✔
146
        }
147
        uri = (String) altName.get(1);
1✔
148
      }
149
    }
1✔
150
    if (uri == null) {
1✔
151
      return Optional.absent();
1✔
152
    }
153
    return Optional.of(parse(uri));
1✔
154
  }
155

156
  /**
157
   * Loads a SPIFFE trust bundle from a file, parsing it from the JSON format.
158
   * In case of success, returns {@link SpiffeBundle}.
159
   * If any element of the JSON content is invalid or unsupported, an
160
   * {@link IllegalArgumentException} is thrown and the entire Bundle is considered invalid.
161
   *
162
   * @param trustBundleFile the file path to the JSON file containing the trust bundle
163
   * @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md">JSON format</a>
164
   * @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#61-publishing-spiffe-bundle-elements">JWK entry format</a>
165
   * @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#appendix-B">x5c (certificate) parameter</a>
166
   */
167
  public static SpiffeBundle loadTrustBundleFromFile(String trustBundleFile) throws IOException {
168
    Map<String, ?> trustDomainsNode = readTrustDomainsFromFile(trustBundleFile);
1✔
169
    Map<String, List<X509Certificate>> trustBundleMap = new HashMap<>();
1✔
170
    Map<String, Long> sequenceNumbers = new HashMap<>();
1✔
171
    for (String trustDomainName : trustDomainsNode.keySet()) {
1✔
172
      Map<String, ?> domainNode = JsonUtil.getObject(trustDomainsNode, trustDomainName);
1✔
173
      if (domainNode.size() == 0) {
1✔
174
        trustBundleMap.put(trustDomainName, Collections.emptyList());
1✔
175
        continue;
1✔
176
      }
177
      Long sequenceNumber = JsonUtil.getNumberAsLong(domainNode, "spiffe_sequence");
1✔
178
      sequenceNumbers.put(trustDomainName, sequenceNumber == null ? -1L : sequenceNumber);
1✔
179
      List<Map<String, ?>> keysNode = JsonUtil.getListOfObjects(domainNode, "keys");
1✔
180
      if (keysNode == null || keysNode.size() == 0) {
1✔
181
        trustBundleMap.put(trustDomainName, Collections.emptyList());
×
182
        continue;
×
183
      }
184
      trustBundleMap.put(trustDomainName, extractCert(keysNode, trustDomainName));
1✔
185
    }
1✔
186
    return new SpiffeBundle(sequenceNumbers, trustBundleMap);
1✔
187
  }
188

189
  private static Map<String, ?> readTrustDomainsFromFile(String filePath) throws IOException {
190
    File file = new File(checkNotNull(filePath, "trustBundleFile"));
1✔
191
    String json = new String(Files.toByteArray(file), StandardCharsets.UTF_8);
1✔
192
    Object jsonObject = JsonParser.parse(json);
1✔
193
    if (!(jsonObject instanceof Map)) {
1✔
194
      throw new IllegalArgumentException(
1✔
195
          "SPIFFE Trust Bundle should be a JSON object. Found: "
196
              + (jsonObject == null ? null : jsonObject.getClass()));
1✔
197
    }
198
    @SuppressWarnings("unchecked")
199
    Map<String, ?> root = (Map<String, ?>)jsonObject;
1✔
200
    Map<String, ?> trustDomainsNode = JsonUtil.getObject(root, "trust_domains");
1✔
201
    checkNotNull(trustDomainsNode, "Mandatory trust_domains element is missing");
1✔
202
    checkArgument(trustDomainsNode.size() > 0, "Mandatory trust_domains element is missing");
1✔
203
    return trustDomainsNode;
1✔
204
  }
205

206
  private static void checkJwkEntry(Map<String, ?> jwkNode, String trustDomainName) {
207
    String kty = JsonUtil.getString(jwkNode, "kty");
1✔
208
    if (kty == null || !kty.equals(KTY_PARAMETER_VALUE)) {
1✔
209
      throw new IllegalArgumentException(String.format("'kty' parameter must be '%s' but '%s' "
1✔
210
              + "found. Certificate loading for trust domain '%s' failed.", KTY_PARAMETER_VALUE,
211
          kty, trustDomainName));
212
    }
213
    if (jwkNode.containsKey("kid")) {
1✔
214
      throw new IllegalArgumentException(String.format("'kid' parameter must not be set. "
1✔
215
              + "Certificate loading for trust domain '%s' failed.", trustDomainName));
216
    }
217
    String use = JsonUtil.getString(jwkNode, "use");
1✔
218
    if (use == null || !use.equals(USE_PARAMETER_VALUE)) {
1✔
219
      throw new IllegalArgumentException(String.format("'use' parameter must be '%s' but '%s' "
1✔
220
              + "found. Certificate loading for trust domain '%s' failed.", USE_PARAMETER_VALUE,
221
          use, trustDomainName));
222
    }
223
  }
1✔
224

225
  private static List<X509Certificate> extractCert(List<Map<String, ?>> keysNode,
226
      String trustDomainName) {
227
    List<X509Certificate> result = new ArrayList<>();
1✔
228
    for (Map<String, ?> keyNode : keysNode) {
1✔
229
      checkJwkEntry(keyNode, trustDomainName);
1✔
230
      List<String> rawCerts = JsonUtil.getListOfStrings(keyNode, "x5c");
1✔
231
      if (rawCerts == null) {
1✔
232
        break;
×
233
      }
234
      if (rawCerts.size() != 1) {
1✔
235
        throw new IllegalArgumentException(String.format("Exactly 1 certificate is expected, but "
1✔
236
            + "%s found. Certificate loading for trust domain '%s' failed.", rawCerts.size(),
1✔
237
            trustDomainName));
238
      }
239
      InputStream stream = new ByteArrayInputStream((CERTIFICATE_PREFIX + rawCerts.get(0) + "\n"
1✔
240
          + CERTIFICATE_SUFFIX)
241
          .getBytes(StandardCharsets.UTF_8));
1✔
242
      try {
243
        Collection<? extends Certificate> certs = CertificateFactory.getInstance("X509")
1✔
244
            .generateCertificates(stream);
1✔
245
        X509Certificate[] certsArray = certs.toArray(new X509Certificate[0]);
1✔
246
        assert certsArray.length == 1;
1✔
247
        result.add(certsArray[0]);
1✔
248
      } catch (CertificateException e) {
1✔
249
        throw new IllegalArgumentException(String.format("Certificate can't be parsed. Certificate "
1✔
250
            + "loading for trust domain '%s' failed.", trustDomainName), e);
251
      }
1✔
252
    }
1✔
253
    return result;
1✔
254
  }
255

256
  /**
257
   * Represents a SPIFFE ID as defined in the SPIFFE standard.
258
   * @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
259
   */
260
  public static class SpiffeId {
261

262
    private final String trustDomain;
263
    private final String path;
264

265
    private SpiffeId(String trustDomain, String path) {
1✔
266
      this.trustDomain = trustDomain;
1✔
267
      this.path = path;
1✔
268
    }
1✔
269

270
    public String getTrustDomain() {
271
      return trustDomain;
1✔
272
    }
273

274
    public String getPath() {
275
      return path;
1✔
276
    }
277
  }
278

279
  /**
280
   * Represents a SPIFFE trust bundle; that is, a map from trust domain to set of trusted
281
   * certificates. Only trust domain's sequence numbers and x509 certificates are supported.
282
   * @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#4-spiffe-bundle-format">Standard</a>
283
   */
284
  public static final class SpiffeBundle {
285

286
    private final ImmutableMap<String, Long> sequenceNumbers;
287

288
    private final ImmutableMap<String, ImmutableList<X509Certificate>> bundleMap;
289

290
    private SpiffeBundle(Map<String, Long> sequenceNumbers,
291
        Map<String, List<X509Certificate>> trustDomainMap) {
1✔
292
      this.sequenceNumbers = ImmutableMap.copyOf(sequenceNumbers);
1✔
293
      ImmutableMap.Builder<String, ImmutableList<X509Certificate>> builder = ImmutableMap.builder();
1✔
294
      for (Map.Entry<String, List<X509Certificate>> entry : trustDomainMap.entrySet()) {
1✔
295
        builder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
1✔
296
      }
1✔
297
      this.bundleMap = builder.build();
1✔
298
    }
1✔
299

300
    public ImmutableMap<String, Long> getSequenceNumbers() {
301
      return sequenceNumbers;
1✔
302
    }
303

304
    public ImmutableMap<String, ImmutableList<X509Certificate>> getBundleMap() {
305
      return bundleMap;
1✔
306
    }
307
  }
308

309
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc