001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2020, Connect2id Ltd and contributors. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.openid.connect.sdk.federation.trust; 019 020 021import java.util.*; 022 023import com.nimbusds.jose.JOSEException; 024import com.nimbusds.jose.jwk.JWKSet; 025import com.nimbusds.jose.proc.BadJOSEException; 026import com.nimbusds.oauth2.sdk.util.MapUtils; 027import com.nimbusds.openid.connect.sdk.federation.entities.EntityID; 028import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement; 029import com.nimbusds.openid.connect.sdk.federation.trust.constraints.TrustChainConstraints; 030 031 032/** 033 * Trust chain resolver. 034 * 035 * <p>Related specifications: 036 * 037 * <ul> 038 * <li>OpenID Connect Federation 1.0, section 7. 039 * </ul> 040 */ 041public class TrustChainResolver { 042 043 044 /** 045 * The configured trust anchors with their public JWK sets. 046 */ 047 private final Map<EntityID, JWKSet> trustAnchors; 048 049 050 /** 051 * The entity statement retriever. 052 */ 053 private final EntityStatementRetriever statementRetriever; 054 055 056 /** 057 * The trust chain constraints. 058 */ 059 private final TrustChainConstraints constraints; 060 061 062 /** 063 * Creates a new trust chain resolver with a single trust anchor, with 064 * {@link TrustChainConstraints#NO_CONSTRAINTS no trust chain 065 * constraints}. 066 * 067 * @param trustAnchor The trust anchor. Must not be {@code null}. 068 */ 069 public TrustChainResolver(final EntityID trustAnchor) { 070 this(trustAnchor, null); 071 } 072 073 074 /** 075 * Creates a new trust chain resolver with a single trust anchor, with 076 * {@link TrustChainConstraints#NO_CONSTRAINTS no trust chain 077 * constraints}. 078 * 079 * @param trustAnchor The trust anchor. Must not be {@code null}. 080 * @param trustAnchorJWKSet The trust anchor public JWK set, 081 * {@code null} if not available. 082 */ 083 public TrustChainResolver(final EntityID trustAnchor, 084 final JWKSet trustAnchorJWKSet) { 085 this( 086 Collections.singletonMap(trustAnchor, trustAnchorJWKSet), 087 TrustChainConstraints.NO_CONSTRAINTS, 088 new DefaultEntityStatementRetriever() 089 ); 090 } 091 092 093 /** 094 * Creates a new trust chain resolver with multiple trust anchors, with 095 * {@link TrustChainConstraints#NO_CONSTRAINTS no trust chain 096 * constraints}. 097 * 098 * @param trustAnchors The trust anchors with their public JWK 099 * sets (if available). Must contain at 100 * least one anchor. 101 * @param httpConnectTimeoutMs The HTTP connect timeout in 102 * milliseconds, zero means timeout 103 * determined by the underlying HTTP 104 * client. 105 * @param httpReadTimeoutMs The HTTP read timeout in milliseconds, 106 * zero means timout determined by the 107 * underlying HTTP client. 108 */ 109 public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors, 110 final int httpConnectTimeoutMs, 111 final int httpReadTimeoutMs) { 112 this( 113 trustAnchors, 114 TrustChainConstraints.NO_CONSTRAINTS, 115 new DefaultEntityStatementRetriever(httpConnectTimeoutMs, httpReadTimeoutMs) 116 ); 117 } 118 119 120 /** 121 * Creates new trust chain resolver. 122 * 123 * @param trustAnchors The trust anchors with their public JWK 124 * sets. Must contain at least one anchor. 125 * @param statementRetriever The entity statement retriever to use. 126 * Must not be {@code null}. 127 */ 128 public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors, 129 final TrustChainConstraints constraints, 130 final EntityStatementRetriever statementRetriever) { 131 132 if (MapUtils.isEmpty(trustAnchors)) { 133 throw new IllegalArgumentException("The trust anchors map must not be empty or null"); 134 } 135 this.trustAnchors = trustAnchors; 136 137 if (constraints == null) { 138 throw new IllegalArgumentException("The trust chain constraints must not be null"); 139 } 140 this.constraints = constraints; 141 142 if (statementRetriever == null) { 143 throw new IllegalArgumentException("The entity statement retriever must not be null"); 144 } 145 this.statementRetriever = statementRetriever; 146 } 147 148 149 /** 150 * Returns the configured trust anchors. 151 * 152 * @return The trust anchors with their public JWK sets (if available). 153 * Contains at least one anchor. 154 */ 155 public Map<EntityID, JWKSet> getTrustAnchors() { 156 return Collections.unmodifiableMap(trustAnchors); 157 } 158 159 160 /** 161 * Returns the configured entity statement retriever. 162 * 163 * @return The entity statement retriever. 164 */ 165 public EntityStatementRetriever getEntityStatementRetriever() { 166 return statementRetriever; 167 } 168 169 170 /** 171 * Returns the configured trust chain constraints. 172 * 173 * @return The constraints. 174 */ 175 public TrustChainConstraints getConstraints() { 176 return constraints; 177 } 178 179 180 /** 181 * Resolves the trust chains for the specified target. 182 * 183 * @param target The target. Must not be {@code null}. 184 * 185 * @return The resolved trust chains, containing at least one valid and 186 * verified chain. 187 * 188 * @throws ResolveException If no trust chain could be resolved. 189 */ 190 public TrustChainSet resolveTrustChains(final EntityID target) 191 throws ResolveException { 192 193 try { 194 return resolveTrustChains(target, null); 195 } catch (InvalidEntityMetadataException e) { 196 // Should never occur if target metadata validator omitted 197 throw new IllegalStateException("Unexpected exception: " + e.getMessage(), e); 198 } 199 } 200 201 202 /** 203 * Resolves the trust chains for the specified target, with optional 204 * validation of the target entity metadata. The validator can for 205 * example check that for an entity which is expected to be an OpenID 206 * relying party the required party metadata is present. 207 * 208 * @param target The target. Must not be {@code null}. 209 * @param targetMetadataValidator To perform optional validation of the 210 * retrieved target entity metadata, 211 * before proceeding with retrieving the 212 * entity statements from the 213 * authorities, {@code null} if not 214 * specified. 215 * 216 * @return The resolved trust chains, containing at least one valid and 217 * verified chain. 218 * 219 * @throws ResolveException If no trust chain could be resolved. 220 */ 221 public TrustChainSet resolveTrustChains(final EntityID target, 222 final EntityMetadataValidator targetMetadataValidator) 223 throws ResolveException, InvalidEntityMetadataException { 224 225 if (trustAnchors.get(target) != null) { 226 throw new ResolveException("Target is trust anchor"); 227 } 228 229 TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints); 230 Set<TrustChain> fetchedTrustChains = retriever.retrieve(target, targetMetadataValidator, trustAnchors.keySet()); 231 return verifyTrustChains( 232 fetchedTrustChains, 233 retriever.getAccumulatedTrustAnchorJWKSets(), 234 retriever.getAccumulatedExceptions()); 235 } 236 237 238 /** 239 * Resolves the trust chains for the specified target. 240 * 241 * @param targetStatement The target entity statement. Must not be 242 * {@code null}. 243 * 244 * @return The resolved trust chains, containing at least one valid and 245 * verified chain. 246 * 247 * @throws ResolveException If no trust chain could be resolved. 248 */ 249 public TrustChainSet resolveTrustChains(final EntityStatement targetStatement) 250 throws ResolveException { 251 252 if (trustAnchors.get(targetStatement.getEntityID()) != null) { 253 throw new ResolveException("Target is trust anchor"); 254 } 255 256 TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints); 257 Set<TrustChain> fetchedTrustChains = retriever.retrieve(targetStatement, trustAnchors.keySet()); 258 return verifyTrustChains( 259 fetchedTrustChains, 260 retriever.getAccumulatedTrustAnchorJWKSets(), 261 retriever.getAccumulatedExceptions()); 262 } 263 264 265 /** 266 * Verifies the specified fetched trust chains. 267 * 268 * @param fetchedTrustChains The fetched trust chains. Must 269 * not be {@code null}, 270 * @param accumulatedTrustAnchorJWKSets The accumulated trust anchor(s) 271 * JWK sets, empty if none. Must 272 * not be {@code null}. 273 * @param accumulatedExceptions The accumulated exceptions, 274 * empty if none. Must not be 275 * {@code null}. 276 * @return The verified trust chain set. 277 * 278 * @throws ResolveException If no trust chain could be verified. 279 */ 280 private TrustChainSet verifyTrustChains(final Set<TrustChain> fetchedTrustChains, 281 final Map<EntityID, JWKSet> accumulatedTrustAnchorJWKSets, 282 final List<Throwable> accumulatedExceptions) 283 throws ResolveException { 284 285 if (fetchedTrustChains.isEmpty()) { 286 if (accumulatedExceptions.isEmpty()) { 287 throw new ResolveException("No trust chain leading up to a trust anchor"); 288 } else if (accumulatedExceptions.size() == 1){ 289 Throwable cause = accumulatedExceptions.get(0); 290 throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), cause); 291 } else { 292 throw new ResolveException("Couldn't resolve trust chain due to multiple causes", accumulatedExceptions); 293 } 294 } 295 296 List<Throwable> verificationExceptions = new LinkedList<>(); 297 298 TrustChainSet verifiedTrustChains = new TrustChainSet(); 299 300 for (TrustChain chain: fetchedTrustChains) { 301 302 EntityID anchor = chain.getTrustAnchorEntityID(); 303 JWKSet anchorJWKSet = trustAnchors.get(anchor); 304 if (anchorJWKSet == null) { 305 anchorJWKSet = accumulatedTrustAnchorJWKSets.get(anchor); 306 } 307 308 try { 309 chain.verifySignatures(anchorJWKSet); 310 } catch (BadJOSEException | JOSEException e) { 311 verificationExceptions.add(e); 312 continue; 313 } 314 315 verifiedTrustChains.add(chain); 316 } 317 318 if (verifiedTrustChains.isEmpty()) { 319 320 List<Throwable> moreAccumulatedExceptions = new LinkedList<>(accumulatedExceptions); 321 moreAccumulatedExceptions.addAll(verificationExceptions); 322 323 if (verificationExceptions.size() == 1) { 324 Throwable cause = verificationExceptions.get(0); 325 throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), moreAccumulatedExceptions); 326 } else { 327 throw new ResolveException("Couldn't resolve trust chain due to multiple causes", moreAccumulatedExceptions); 328 } 329 } 330 331 return verifiedTrustChains; 332 } 333}