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

grpc / grpc-java / #19544

08 Nov 2024 05:03AM UTC coverage: 84.607% (+0.04%) from 84.566%
#19544

push

github

web-flow
xds: Spiffe Trust Bundle Support (#11627)

Adds verification of SPIFFE based identities using SPIFFE trust bundles.

For in-progress gRFC A87.

34100 of 40304 relevant lines covered (84.61%)

0.85 hits per line

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

90.4
/../xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java
1
/*
2
 * Copyright 2019 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.xds.internal.security.trust;
18

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

21
import com.google.common.annotations.VisibleForTesting;
22
import com.google.common.base.Optional;
23
import com.google.common.base.Strings;
24
import com.google.common.collect.ImmutableMap;
25
import com.google.re2j.Pattern;
26
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext;
27
import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher;
28
import io.envoyproxy.envoy.type.matcher.v3.StringMatcher;
29
import io.grpc.internal.SpiffeUtil;
30
import java.net.Socket;
31
import java.security.cert.CertificateException;
32
import java.security.cert.CertificateParsingException;
33
import java.security.cert.X509Certificate;
34
import java.util.Arrays;
35
import java.util.Collection;
36
import java.util.HashSet;
37
import java.util.List;
38
import java.util.Locale;
39
import java.util.Map;
40
import java.util.Set;
41
import javax.annotation.Nullable;
42
import javax.net.ssl.SSLEngine;
43
import javax.net.ssl.SSLParameters;
44
import javax.net.ssl.SSLSocket;
45
import javax.net.ssl.X509ExtendedTrustManager;
46
import javax.net.ssl.X509TrustManager;
47

48
/**
49
 * Extension of {@link X509ExtendedTrustManager} that implements verification of
50
 * SANs (subject-alternate-names) against the list in CertificateValidationContext.
51
 */
52
final class XdsX509TrustManager extends X509ExtendedTrustManager implements X509TrustManager {
53

54
  // ref: io.grpc.okhttp.internal.OkHostnameVerifier and
55
  // sun.security.x509.GeneralNameInterface
56
  private static final int ALT_DNS_NAME = 2;
57
  private static final int ALT_URI_NAME = 6;
58
  private static final int ALT_IPA_NAME = 7;
59

60
  private final X509ExtendedTrustManager delegate;
61
  private final Map<String, X509ExtendedTrustManager> spiffeTrustMapDelegates;
62
  private final CertificateValidationContext certContext;
63

64
  XdsX509TrustManager(@Nullable CertificateValidationContext certContext,
65
                      X509ExtendedTrustManager delegate) {
1✔
66
    checkNotNull(delegate, "delegate");
1✔
67
    this.certContext = certContext;
1✔
68
    this.delegate = delegate;
1✔
69
    this.spiffeTrustMapDelegates = null;
1✔
70
  }
1✔
71

72
  XdsX509TrustManager(@Nullable CertificateValidationContext certContext,
73
      Map<String, X509ExtendedTrustManager> spiffeTrustMapDelegates) {
1✔
74
    checkNotNull(spiffeTrustMapDelegates, "spiffeTrustMapDelegates");
1✔
75
    this.spiffeTrustMapDelegates = ImmutableMap.copyOf(spiffeTrustMapDelegates);
1✔
76
    this.certContext = certContext;
1✔
77
    this.delegate = null;
1✔
78
  }
1✔
79

80
  private static boolean verifyDnsNameInPattern(
81
      String altNameFromCert, StringMatcher sanToVerifyMatcher) {
82
    if (Strings.isNullOrEmpty(altNameFromCert)) {
1✔
83
      return false;
×
84
    }
85
    switch (sanToVerifyMatcher.getMatchPatternCase()) {
1✔
86
      case EXACT:
87
        return verifyDnsNameExact(
1✔
88
            altNameFromCert, sanToVerifyMatcher.getExact(), sanToVerifyMatcher.getIgnoreCase());
1✔
89
      case PREFIX:
90
        return verifyDnsNamePrefix(
1✔
91
            altNameFromCert, sanToVerifyMatcher.getPrefix(), sanToVerifyMatcher.getIgnoreCase());
1✔
92
      case SUFFIX:
93
        return verifyDnsNameSuffix(
1✔
94
            altNameFromCert, sanToVerifyMatcher.getSuffix(), sanToVerifyMatcher.getIgnoreCase());
1✔
95
      case CONTAINS:
96
        return verifyDnsNameContains(
1✔
97
            altNameFromCert, sanToVerifyMatcher.getContains(), sanToVerifyMatcher.getIgnoreCase());
1✔
98
      case SAFE_REGEX:
99
        return verifyDnsNameSafeRegex(altNameFromCert, sanToVerifyMatcher.getSafeRegex());
1✔
100
      default:
101
        throw new IllegalArgumentException(
×
102
            "Unknown match-pattern-case " + sanToVerifyMatcher.getMatchPatternCase());
×
103
    }
104
  }
105

106
  private static boolean verifyDnsNameSafeRegex(
107
          String altNameFromCert, RegexMatcher sanToVerifySafeRegex) {
108
    Pattern safeRegExMatch = Pattern.compile(sanToVerifySafeRegex.getRegex());
1✔
109
    return safeRegExMatch.matches(altNameFromCert);
1✔
110
  }
111

112
  private static boolean verifyDnsNamePrefix(
113
      String altNameFromCert, String sanToVerifyPrefix, boolean ignoreCase) {
114
    if (Strings.isNullOrEmpty(sanToVerifyPrefix)) {
1✔
115
      return false;
×
116
    }
117
    return ignoreCase
1✔
118
        ? altNameFromCert.toLowerCase(Locale.ROOT).startsWith(
1✔
119
            sanToVerifyPrefix.toLowerCase(Locale.ROOT))
1✔
120
        : altNameFromCert.startsWith(sanToVerifyPrefix);
1✔
121
  }
122

123
  private static boolean verifyDnsNameSuffix(
124
          String altNameFromCert, String sanToVerifySuffix, boolean ignoreCase) {
125
    if (Strings.isNullOrEmpty(sanToVerifySuffix)) {
1✔
126
      return false;
×
127
    }
128
    return ignoreCase
1✔
129
            ? altNameFromCert.toLowerCase(Locale.ROOT).endsWith(
1✔
130
                sanToVerifySuffix.toLowerCase(Locale.ROOT))
1✔
131
            : altNameFromCert.endsWith(sanToVerifySuffix);
1✔
132
  }
133

134
  private static boolean verifyDnsNameContains(
135
          String altNameFromCert, String sanToVerifySubstring, boolean ignoreCase) {
136
    if (Strings.isNullOrEmpty(sanToVerifySubstring)) {
1✔
137
      return false;
×
138
    }
139
    return ignoreCase
1✔
140
            ? altNameFromCert.toLowerCase(Locale.ROOT).contains(
1✔
141
                sanToVerifySubstring.toLowerCase(Locale.ROOT))
1✔
142
            : altNameFromCert.contains(sanToVerifySubstring);
1✔
143
  }
144

145
  private static boolean verifyDnsNameExact(
146
      String altNameFromCert, String sanToVerifyExact, boolean ignoreCase) {
147
    if (Strings.isNullOrEmpty(sanToVerifyExact)) {
1✔
148
      return false;
×
149
    }
150
    return ignoreCase
1✔
151
        ? sanToVerifyExact.equalsIgnoreCase(altNameFromCert)
1✔
152
        : sanToVerifyExact.equals(altNameFromCert);
1✔
153
  }
154

155
  private static boolean verifyDnsNameInSanList(
156
      String altNameFromCert, List<StringMatcher> verifySanList) {
157
    for (StringMatcher verifySan : verifySanList) {
1✔
158
      if (verifyDnsNameInPattern(altNameFromCert, verifySan)) {
1✔
159
        return true;
1✔
160
      }
161
    }
1✔
162
    return false;
1✔
163
  }
164

165
  private static boolean verifyOneSanInList(List<?> entry, List<StringMatcher> verifySanList)
166
      throws CertificateParsingException {
167
    // from OkHostnameVerifier.getSubjectAltNames
168
    if (entry == null || entry.size() < 2) {
1✔
169
      throw new CertificateParsingException("Invalid SAN entry");
×
170
    }
171
    Integer altNameType = (Integer) entry.get(0);
1✔
172
    if (altNameType == null) {
1✔
173
      throw new CertificateParsingException("Invalid SAN entry: null altNameType");
×
174
    }
175
    switch (altNameType) {
1✔
176
      case ALT_DNS_NAME:
177
      case ALT_URI_NAME:
178
      case ALT_IPA_NAME:
179
        return verifyDnsNameInSanList((String) entry.get(1), verifySanList);
1✔
180
      default:
181
        return false;
1✔
182
    }
183
  }
184

185
  // logic from Envoy::Extensions::TransportSockets::Tls::ContextImpl::verifySubjectAltName
186
  private static void verifySubjectAltNameInLeaf(
187
      X509Certificate cert, List<StringMatcher> verifyList) throws CertificateException {
188
    Collection<List<?>> names = cert.getSubjectAlternativeNames();
1✔
189
    if (names == null || names.isEmpty()) {
1✔
190
      throw new CertificateException("Peer certificate SAN check failed");
1✔
191
    }
192
    for (List<?> name : names) {
1✔
193
      if (verifyOneSanInList(name, verifyList)) {
1✔
194
        return;
1✔
195
      }
196
    }
1✔
197
    // at this point there's no match
198
    throw new CertificateException("Peer certificate SAN check failed");
1✔
199
  }
200

201
  /**
202
   * Verifies SANs in the peer cert chain against verify_subject_alt_name in the certContext.
203
   * This is called from various check*Trusted methods.
204
   */
205
  @VisibleForTesting
206
  void verifySubjectAltNameInChain(X509Certificate[] peerCertChain) throws CertificateException {
207
    if (certContext == null) {
1✔
208
      return;
1✔
209
    }
210
    List<StringMatcher> verifyList = certContext.getMatchSubjectAltNamesList();
1✔
211
    if (verifyList.isEmpty()) {
1✔
212
      return;
1✔
213
    }
214
    if (peerCertChain == null || peerCertChain.length < 1) {
1✔
215
      throw new CertificateException("Peer certificate(s) missing");
1✔
216
    }
217
    // verify SANs only in the top cert (leaf cert)
218
    verifySubjectAltNameInLeaf(peerCertChain[0], verifyList);
1✔
219
  }
1✔
220

221
  @Override
222
  public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
223
      throws CertificateException {
224
    chooseDelegate(chain).checkClientTrusted(chain, authType, socket);
×
225
    verifySubjectAltNameInChain(chain);
×
226
  }
×
227

228
  @Override
229
  public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine)
230
      throws CertificateException {
231
    chooseDelegate(chain).checkClientTrusted(chain, authType, sslEngine);
1✔
232
    verifySubjectAltNameInChain(chain);
1✔
233
  }
1✔
234

235
  @Override
236
  public void checkClientTrusted(X509Certificate[] chain, String authType)
237
      throws CertificateException {
238
    chooseDelegate(chain).checkClientTrusted(chain, authType);
1✔
239
    verifySubjectAltNameInChain(chain);
1✔
240
  }
1✔
241

242
  @Override
243
  public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
244
      throws CertificateException {
245
    if (socket instanceof SSLSocket) {
1✔
246
      SSLSocket sslSocket = (SSLSocket) socket;
1✔
247
      SSLParameters sslParams = sslSocket.getSSLParameters();
1✔
248
      if (sslParams != null) {
1✔
249
        sslParams.setEndpointIdentificationAlgorithm("");
1✔
250
        sslSocket.setSSLParameters(sslParams);
1✔
251
      }
252
    }
253
    chooseDelegate(chain).checkServerTrusted(chain, authType, socket);
1✔
254
    verifySubjectAltNameInChain(chain);
1✔
255
  }
1✔
256

257
  @Override
258
  public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine)
259
      throws CertificateException {
260
    SSLParameters sslParams = sslEngine.getSSLParameters();
1✔
261
    if (sslParams != null) {
1✔
262
      sslParams.setEndpointIdentificationAlgorithm("");
1✔
263
      sslEngine.setSSLParameters(sslParams);
1✔
264
    }
265
    chooseDelegate(chain).checkServerTrusted(chain, authType, sslEngine);
1✔
266
    verifySubjectAltNameInChain(chain);
1✔
267
  }
1✔
268

269
  @Override
270
  public void checkServerTrusted(X509Certificate[] chain, String authType)
271
      throws CertificateException {
272
    chooseDelegate(chain).checkServerTrusted(chain, authType);
1✔
273
    verifySubjectAltNameInChain(chain);
1✔
274
  }
1✔
275

276
  private X509ExtendedTrustManager chooseDelegate(X509Certificate[] chain)
277
      throws CertificateException {
278
    if (spiffeTrustMapDelegates != null) {
1✔
279
      Optional<SpiffeUtil.SpiffeId> spiffeId = SpiffeUtil.extractSpiffeId(chain);
1✔
280
      if (!spiffeId.isPresent()) {
1✔
281
        throw new CertificateException("Failed to extract SPIFFE ID from peer leaf certificate");
1✔
282
      }
283
      String trustDomain = spiffeId.get().getTrustDomain();
1✔
284
      if (!spiffeTrustMapDelegates.containsKey(trustDomain)) {
1✔
285
        throw new CertificateException(String.format("Spiffe Trust Map doesn't contain trust"
1✔
286
            + " domain '%s' from peer leaf certificate", trustDomain));
287
      }
288
      return spiffeTrustMapDelegates.get(trustDomain);
1✔
289
    } else {
290
      return delegate;
1✔
291
    }
292
  }
293

294
  @Override
295
  public X509Certificate[] getAcceptedIssuers() {
296
    if (spiffeTrustMapDelegates != null) {
1✔
297
      Set<X509Certificate> result = new HashSet<>();
1✔
298
      for (X509ExtendedTrustManager tm: spiffeTrustMapDelegates.values()) {
1✔
299
        result.addAll(Arrays.asList(tm.getAcceptedIssuers()));
1✔
300
      }
1✔
301
      return result.toArray(new X509Certificate[0]);
1✔
302
    }
303
    return delegate.getAcceptedIssuers();
1✔
304
  }
305
}
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