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 9. 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 timeout 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 constraints The constraints to apply during retrieval. 126 * Must not be {@code null}. 127 * @param statementRetriever The entity statement retriever to use. 128 * Must not be {@code null}. 129 */ 130 public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors, 131 final TrustChainConstraints constraints, 132 final EntityStatementRetriever statementRetriever) { 133 134 if (MapUtils.isEmpty(trustAnchors)) { 135 throw new IllegalArgumentException("The trust anchors map must not be empty or null"); 136 } 137 this.trustAnchors = trustAnchors; 138 139 if (constraints == null) { 140 throw new IllegalArgumentException("The trust chain constraints must not be null"); 141 } 142 this.constraints = constraints; 143 144 if (statementRetriever == null) { 145 throw new IllegalArgumentException("The entity statement retriever must not be null"); 146 } 147 this.statementRetriever = statementRetriever; 148 } 149 150 151 /** 152 * Returns the configured trust anchors. 153 * 154 * @return The trust anchors with their public JWK sets (if available). 155 * Contains at least one anchor. 156 */ 157 public Map<EntityID, JWKSet> getTrustAnchors() { 158 return Collections.unmodifiableMap(trustAnchors); 159 } 160 161 162 /** 163 * Returns the configured entity statement retriever. 164 * 165 * @return The entity statement retriever. 166 */ 167 public EntityStatementRetriever getEntityStatementRetriever() { 168 return statementRetriever; 169 } 170 171 172 /** 173 * Returns the configured trust chain constraints. 174 * 175 * @return The constraints. 176 */ 177 public TrustChainConstraints getConstraints() { 178 return constraints; 179 } 180 181 182 /** 183 * Resolves the trust chains for the specified target. 184 * 185 * @param target The target. Must not be {@code null}. 186 * 187 * @return The resolved trust chains, containing at least one valid and 188 * verified chain. 189 * 190 * @throws ResolveException If no trust chain could be resolved. 191 */ 192 public TrustChainSet resolveTrustChains(final EntityID target) 193 throws ResolveException { 194 195 try { 196 return resolveTrustChains(target, null); 197 } catch (InvalidEntityMetadataException e) { 198 // Should never occur if target metadata validator omitted 199 throw new IllegalStateException("Unexpected exception: " + e.getMessage(), e); 200 } 201 } 202 203 204 /** 205 * Resolves the trust chains for the specified target, with optional 206 * validation of the target entity metadata. The validator can for 207 * example check that for an entity which is expected to be an OpenID 208 * relying party the required party metadata is present. 209 * 210 * @param target The target. Must not be {@code null}. 211 * @param targetMetadataValidator To perform optional validation of the 212 * retrieved target entity metadata, 213 * before proceeding with retrieving the 214 * entity statements from the 215 * authorities, {@code null} if not 216 * specified. 217 * 218 * @return The resolved trust chains, containing at least one valid and 219 * verified chain. 220 * 221 * @throws ResolveException If a trust chain could not be 222 * resolved. 223 * @throws InvalidEntityMetadataException If the optional target entity 224 * metadata validation didn't 225 * pass. 226 */ 227 public TrustChainSet resolveTrustChains(final EntityID target, 228 final EntityMetadataValidator targetMetadataValidator) 229 throws ResolveException, InvalidEntityMetadataException { 230 231 if (trustAnchors.get(target) != null) { 232 throw new ResolveException("Target is trust anchor"); 233 } 234 235 TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints); 236 Set<TrustChain> fetchedTrustChains = retriever.retrieve(target, targetMetadataValidator, trustAnchors.keySet()); 237 return verifyTrustChains( 238 fetchedTrustChains, 239 retriever.getAccumulatedTrustAnchorJWKSets(), 240 retriever.getAccumulatedExceptions()); 241 } 242 243 244 /** 245 * Resolves the trust chains for the specified target. 246 * 247 * @param targetStatement The target entity statement. Must not be 248 * {@code null}. 249 * 250 * @return The resolved trust chains, containing at least one valid and 251 * verified chain. 252 * 253 * @throws ResolveException If no trust chain could be resolved. 254 */ 255 public TrustChainSet resolveTrustChains(final EntityStatement targetStatement) 256 throws ResolveException { 257 258 if (trustAnchors.get(targetStatement.getEntityID()) != null) { 259 throw new ResolveException("Target is trust anchor"); 260 } 261 262 TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints); 263 Set<TrustChain> fetchedTrustChains = retriever.retrieve(targetStatement, trustAnchors.keySet()); 264 return verifyTrustChains( 265 fetchedTrustChains, 266 retriever.getAccumulatedTrustAnchorJWKSets(), 267 retriever.getAccumulatedExceptions()); 268 } 269 270 271 /** 272 * Verifies the specified fetched trust chains. 273 * 274 * @param fetchedTrustChains The fetched trust chains. Must 275 * not be {@code null}, 276 * @param accumulatedTrustAnchorJWKSets The accumulated trust anchor(s) 277 * JWK sets, empty if none. Must 278 * not be {@code null}. 279 * @param accumulatedExceptions The accumulated exceptions, 280 * empty if none. Must not be 281 * {@code null}. 282 * @return The verified trust chain set. 283 * 284 * @throws ResolveException If no trust chain could be verified. 285 */ 286 private TrustChainSet verifyTrustChains(final Set<TrustChain> fetchedTrustChains, 287 final Map<EntityID, JWKSet> accumulatedTrustAnchorJWKSets, 288 final List<Throwable> accumulatedExceptions) 289 throws ResolveException { 290 291 if (fetchedTrustChains.isEmpty()) { 292 if (accumulatedExceptions.isEmpty()) { 293 throw new ResolveException("No trust chain leading up to a trust anchor"); 294 } else if (accumulatedExceptions.size() == 1){ 295 Throwable cause = accumulatedExceptions.get(0); 296 throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), cause); 297 } else { 298 throw new ResolveException("Couldn't resolve trust chain due to multiple causes", accumulatedExceptions); 299 } 300 } 301 302 List<Throwable> verificationExceptions = new LinkedList<>(); 303 304 TrustChainSet verifiedTrustChains = new TrustChainSet(); 305 306 for (TrustChain chain: fetchedTrustChains) { 307 308 EntityID anchor = chain.getTrustAnchorEntityID(); 309 JWKSet anchorJWKSet = trustAnchors.get(anchor); 310 if (anchorJWKSet == null) { 311 anchorJWKSet = accumulatedTrustAnchorJWKSets.get(anchor); 312 } 313 314 try { 315 chain.verifySignatures(anchorJWKSet); 316 } catch (BadJOSEException | JOSEException e) { 317 verificationExceptions.add(e); 318 continue; 319 } 320 321 verifiedTrustChains.add(chain); 322 } 323 324 if (verifiedTrustChains.isEmpty()) { 325 326 List<Throwable> moreAccumulatedExceptions = new LinkedList<>(accumulatedExceptions); 327 moreAccumulatedExceptions.addAll(verificationExceptions); 328 329 if (verificationExceptions.size() == 1) { 330 Throwable cause = verificationExceptions.get(0); 331 throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), moreAccumulatedExceptions); 332 } else { 333 throw new ResolveException("Couldn't resolve trust chain due to multiple causes", moreAccumulatedExceptions); 334 } 335 } 336 337 return verifiedTrustChains; 338 } 339}