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.security.ProviderException; 022import java.util.Date; 023import java.util.Iterator; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.concurrent.atomic.AtomicReference; 027 028import net.jcip.annotations.Immutable; 029 030import com.nimbusds.jose.JOSEException; 031import com.nimbusds.jose.jwk.JWK; 032import com.nimbusds.jose.jwk.JWKSet; 033import com.nimbusds.jose.proc.BadJOSEException; 034import com.nimbusds.jose.util.Base64URL; 035import com.nimbusds.oauth2.sdk.id.Subject; 036import com.nimbusds.oauth2.sdk.util.CollectionUtils; 037import com.nimbusds.openid.connect.sdk.federation.entities.EntityID; 038import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement; 039import com.nimbusds.openid.connect.sdk.federation.entities.FederationMetadataType; 040import com.nimbusds.openid.connect.sdk.federation.policy.MetadataPolicy; 041import com.nimbusds.openid.connect.sdk.federation.policy.MetadataPolicyEntry; 042import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyViolationException; 043import com.nimbusds.openid.connect.sdk.federation.policy.operations.DefaultPolicyOperationCombinationValidator; 044import com.nimbusds.openid.connect.sdk.federation.policy.operations.PolicyOperationCombinationValidator; 045 046 047/** 048 * Federation entity trust chain. 049 * 050 * <p>Related specifications: 051 * 052 * <ul> 053 * <li>OpenID Connect Federation 1.0, sections 2.2 and 7. 054 * </ul> 055 */ 056@Immutable 057public final class TrustChain { 058 059 060 /** 061 * The leaf entity self-statement. 062 */ 063 private final EntityStatement leaf; 064 065 066 /** 067 * The superior entity statements. 068 */ 069 private final List<EntityStatement> superiors; 070 071 072 /** 073 * Caches the resolved expiration time for this trust chain. 074 */ 075 private Date exp; 076 077 078 /** 079 * Creates a new federation entity trust chain. Validates the subject - 080 * issuer chain, the signatures are not verified. 081 * 082 * @param leaf The leaf entity self-statement. Must not be 083 * {@code null}. 084 * @param superiors The superior entity statements, starting with a 085 * statement of the first superior about the leaf, 086 * ending with the statement of the trust anchor about 087 * the last intermediate or the leaf (for a minimal 088 * trust chain). Must contain at least one entity 089 * statement. 090 * 091 * @throws IllegalArgumentException If the subject - issuer chain is 092 * broken. 093 */ 094 public TrustChain(final EntityStatement leaf, List<EntityStatement> superiors) { 095 if (leaf == null) { 096 throw new IllegalArgumentException("The leaf statement must not be null"); 097 } 098 this.leaf = leaf; 099 100 if (CollectionUtils.isEmpty(superiors)) { 101 throw new IllegalArgumentException("There must be at least one superior statement (issued by the trust anchor)"); 102 } 103 this.superiors = superiors; 104 if (! hasValidIssuerSubjectChain(leaf, superiors)) { 105 throw new IllegalArgumentException("Broken subject - issuer chain"); 106 } 107 } 108 109 110 private static boolean hasValidIssuerSubjectChain(final EntityStatement leaf, final List<EntityStatement> superiors) { 111 112 Subject nextExpectedSubject = leaf.getClaimsSet().getSubject(); 113 114 for (EntityStatement superiorStmt : superiors) { 115 if (! nextExpectedSubject.equals(superiorStmt.getClaimsSet().getSubject())) { 116 return false; 117 } 118 nextExpectedSubject = new Subject(superiorStmt.getClaimsSet().getIssuer().getValue()); 119 } 120 return true; 121 } 122 123 124 /** 125 * Returns the leaf entity self-statement. 126 * 127 * @return The leaf entity self-statement. 128 */ 129 public EntityStatement getLeafSelfStatement() { 130 return leaf; 131 } 132 133 134 /** 135 * Returns the superior entity statements. 136 * 137 * @return The superior entity statements, starting with a statement of 138 * the first superior about the leaf, ending with the statement 139 * of the trust anchor about the last intermediate or the leaf 140 * (for a minimal trust chain). 141 */ 142 public List<EntityStatement> getSuperiorStatements() { 143 return superiors; 144 } 145 146 147 /** 148 * Returns the entity ID of the trust anchor. 149 * 150 * @return The entity ID of the trust anchor. 151 */ 152 public EntityID getTrustAnchorEntityID() { 153 154 // Return last in superiors 155 return getSuperiorStatements() 156 .get(getSuperiorStatements().size() - 1) 157 .getClaimsSet() 158 .getIssuerEntityID(); 159 } 160 161 162 /** 163 * Returns the length of this trust chain. A minimal trust chain with a 164 * leaf and anchor has a length of one. 165 * 166 * @return The trust chain length. 167 */ 168 public int length() { 169 170 return getSuperiorStatements().size(); 171 } 172 173 174 /** 175 * Resolves the combined metadata policy for this trust chain. Uses the 176 * {@link DefaultPolicyOperationCombinationValidator default policy 177 * combination validator}. 178 * 179 * @param type The metadata type, such as {@code openid_relying_party}. 180 * Must not be {@code null}. 181 * 182 * @return The combined metadata policy, with no policy operations if 183 * no policies were found. 184 * 185 * @throws PolicyViolationException On a policy violation exception. 186 */ 187 public MetadataPolicy resolveCombinedMetadataPolicy(final FederationMetadataType type) 188 throws PolicyViolationException { 189 190 return resolveCombinedMetadataPolicy(type, MetadataPolicyEntry.DEFAULT_POLICY_COMBINATION_VALIDATOR); 191 } 192 193 194 /** 195 * Resolves the combined metadata policy for this trust chain. 196 * 197 * @param type The metadata type, such as 198 * {@code openid_relying_party}. Must not 199 * be {@code null}. 200 * @param combinationValidator The policy operation combination 201 * validator. Must not be {@code null}. 202 * 203 * @return The combined metadata policy, with no policy operations if 204 * no policies were found. 205 * 206 * @throws PolicyViolationException On a policy violation exception. 207 */ 208 public MetadataPolicy resolveCombinedMetadataPolicy(final FederationMetadataType type, 209 final PolicyOperationCombinationValidator combinationValidator) 210 throws PolicyViolationException { 211 212 List<MetadataPolicy> policies = new LinkedList<>(); 213 214 for (EntityStatement stmt: getSuperiorStatements()) { 215 216 MetadataPolicy metadataPolicy = stmt.getClaimsSet().getMetadataPolicy(type); 217 218 if (metadataPolicy == null) { 219 continue; 220 } 221 222 policies.add(metadataPolicy); 223 } 224 225 return MetadataPolicy.combine(policies, combinationValidator); 226 } 227 228 229 /** 230 * Return an iterator starting from the leaf entity statement. 231 * 232 * @return The iterator. 233 */ 234 public Iterator<EntityStatement> iteratorFromLeaf() { 235 236 // Init 237 final AtomicReference<EntityStatement> next = new AtomicReference<>(getLeafSelfStatement()); 238 final Iterator<EntityStatement> superiorsIterator = getSuperiorStatements().iterator(); 239 240 return new Iterator<EntityStatement>() { 241 @Override 242 public boolean hasNext() { 243 return next.get() != null; 244 } 245 246 247 @Override 248 public EntityStatement next() { 249 EntityStatement toReturn = next.get(); 250 if (toReturn == null) { 251 return null; // reached end on last iteration 252 } 253 254 // Set statement to return on next iteration 255 if (toReturn.equals(getLeafSelfStatement())) { 256 // Return first superior 257 next.set(superiorsIterator.next()); 258 } else { 259 // Return next superior or end 260 if (superiorsIterator.hasNext()) { 261 next.set(superiorsIterator.next()); 262 } else { 263 next.set(null); 264 } 265 } 266 267 return toReturn; 268 } 269 270 271 @Override 272 public void remove() { 273 throw new UnsupportedOperationException(); 274 } 275 }; 276 } 277 278 279 /** 280 * Resolves the expiration time for this trust chain. Equals the 281 * nearest expiration when all entity statements in the trust chain are 282 * considered. 283 * 284 * @return The expiration time for this trust chain. 285 */ 286 public Date resolveExpirationTime() { 287 288 if (exp != null) { 289 return exp; 290 } 291 292 Iterator<EntityStatement> it = iteratorFromLeaf(); 293 294 Date nearestExp = null; 295 296 while (it.hasNext()) { 297 298 Date stmtExp = it.next().getClaimsSet().getExpirationTime(); 299 300 if (nearestExp == null) { 301 nearestExp = stmtExp; // on first iteration 302 } else if (stmtExp.before(nearestExp)) { 303 nearestExp = stmtExp; // replace nearest 304 } 305 } 306 307 exp = nearestExp; 308 return exp; 309 } 310 311 312 /** 313 * Verifies the signatures in this trust chain. 314 * 315 * @param trustAnchorJWKSet The trust anchor JWK set. Must not be 316 * {@code null}. 317 * 318 * @throws BadJOSEException If a signature is invalid or a statement is 319 * expired or before the issue time. 320 * @throws JOSEException On a internal JOSE exception. 321 */ 322 public void verifySignatures(final JWKSet trustAnchorJWKSet) 323 throws BadJOSEException, JOSEException { 324 325 Base64URL signingJWKThumbprint; 326 try { 327 signingJWKThumbprint = leaf.verifySignatureOfSelfStatement(); 328 } catch (BadJOSEException e) { 329 throw new BadJOSEException("Invalid leaf statement: " + e.getMessage(), e); 330 } 331 332 for (int i=0; i < superiors.size(); i++) { 333 334 EntityStatement stmt = superiors.get(i); 335 336 JWKSet verificationJWKSet; 337 if (i+1 == superiors.size()) { 338 verificationJWKSet = trustAnchorJWKSet; 339 } else { 340 verificationJWKSet = superiors.get(i+1).getClaimsSet().getJWKSet(); 341 } 342 343 // Check that the signing JWK is registered with the superior 344 if (! hasJWKWithThumbprint(stmt.getClaimsSet().getJWKSet(), signingJWKThumbprint)) { 345 throw new BadJOSEException("Signing JWK with thumbprint " + signingJWKThumbprint + " not found in entity statement issued from superior " + stmt.getClaimsSet().getIssuerEntityID()); 346 } 347 348 try { 349 signingJWKThumbprint = stmt.verifySignature(verificationJWKSet); 350 } catch (BadJOSEException e) { 351 throw new BadJOSEException("Invalid statement from " + stmt.getClaimsSet().getIssuer() + ": " + e.getMessage(), e); 352 } 353 } 354 } 355 356 357 private static boolean hasJWKWithThumbprint(final JWKSet jwkSet, final Base64URL thumbprint) { 358 359 if (jwkSet == null) { 360 return false; 361 } 362 363 for (JWK jwk: jwkSet.getKeys()) { 364 365 try { 366 if (thumbprint.equals(jwk.computeThumbprint())) { 367 return true; 368 } 369 } catch (JOSEException e) { 370 throw new ProviderException(e.getMessage(), e); 371 } 372 373 } 374 375 return false; 376 } 377}