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.jwt.SignedJWT; 036import com.nimbusds.oauth2.sdk.ParseException; 037import com.nimbusds.oauth2.sdk.id.Subject; 038import com.nimbusds.oauth2.sdk.util.CollectionUtils; 039import com.nimbusds.oauth2.sdk.util.ListUtils; 040import com.nimbusds.openid.connect.sdk.federation.entities.EntityID; 041import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement; 042import com.nimbusds.openid.connect.sdk.federation.entities.EntityType; 043import com.nimbusds.openid.connect.sdk.federation.policy.MetadataPolicy; 044import com.nimbusds.openid.connect.sdk.federation.policy.MetadataPolicyEntry; 045import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyViolationException; 046import com.nimbusds.openid.connect.sdk.federation.policy.operations.DefaultPolicyOperationCombinationValidator; 047import com.nimbusds.openid.connect.sdk.federation.policy.operations.PolicyOperationCombinationValidator; 048 049 050/** 051 * Federation entity trust chain. 052 * 053 * <p>Related specifications: 054 * 055 * <ul> 056 * <li>OpenID Connect Federation 1.0, sections 3.2 and 7.1. 057 * </ul> 058 */ 059@Immutable 060public final class TrustChain { 061 062 063 /** 064 * The leaf entity configuration. 065 */ 066 private final EntityStatement leaf; 067 068 069 /** 070 * The superior entity statements. 071 */ 072 private final List<EntityStatement> superiors; 073 074 075 /** 076 * The optional trust anchor entity configuration. 077 */ 078 private final EntityStatement trustAnchor; 079 080 081 /** 082 * Caches the resolved expiration time for this trust chain. 083 */ 084 private Date exp; 085 086 087 /** 088 * Creates a new trust chain. Validates the subject - issuer chain, the 089 * signatures are not verified. 090 * 091 * @param leaf The leaf entity configuration. Must not be 092 * {@code null}. 093 * @param superiors The superior entity statements, starting with a 094 * statement of the first superior about the leaf, 095 * ending with the statement of the trust anchor about 096 * the last intermediate or the leaf (for a minimal 097 * trust chain). Must contain at least one entity 098 * statement. 099 * 100 * @throws IllegalArgumentException If the subject - issuer chain is 101 * broken. 102 */ 103 public TrustChain(final EntityStatement leaf, final List<EntityStatement> superiors) { 104 this(leaf, superiors, null); 105 } 106 107 108 /** 109 * Creates a new trust chain. Validates the subject - issuer chain, the 110 * signatures are not verified. 111 * 112 * @param leaf The leaf entity configuration. Must not be 113 * {@code null}. 114 * @param superiors The superior entity statements, starting with a 115 * statement of the first superior about the leaf, 116 * ending with the statement of the trust anchor 117 * about the last intermediate or the leaf (for a 118 * minimal trust chain). Must contain at least one 119 * entity statement. 120 * @param trustAnchor The optional trust anchor entity configuration, 121 * {@code null} if not specified. 122 * 123 * @throws IllegalArgumentException If the subject - issuer chain is 124 * broken. 125 */ 126 public TrustChain(final EntityStatement leaf, final List<EntityStatement> superiors, final EntityStatement trustAnchor) { 127 128 // leaf config checks 129 if (leaf == null) { 130 throw new IllegalArgumentException("The leaf entity configuration must not be null"); 131 } 132 if (! leaf.getClaimsSet().isSelfStatement()) { 133 throw new IllegalArgumentException("The leaf entity configuration must be a self-statement"); 134 } 135 this.leaf = leaf; 136 137 // superior statements check 138 if (CollectionUtils.isEmpty(superiors)) { 139 throw new IllegalArgumentException("There must be at least one superior statement (issued by the trust anchor)"); 140 } 141 this.superiors = superiors; 142 143 // optional trust anchor config checks 144 this.trustAnchor = trustAnchor; 145 146 if (trustAnchor != null && ! trustAnchor.getClaimsSet().isSelfStatement()) { 147 throw new IllegalArgumentException("The trust anchor entity configuration must be a self-statement"); 148 } 149 150 if (! hasValidIssuerSubjectChain(leaf, superiors, trustAnchor)) { 151 throw new IllegalArgumentException("Broken subject - issuer chain"); 152 } 153 } 154 155 156 private static boolean hasValidIssuerSubjectChain(final EntityStatement leaf, 157 final List<EntityStatement> superiors, 158 final EntityStatement trustAnchor) { 159 160 Subject nextExpectedSubject = leaf.getClaimsSet().getSubject(); 161 162 for (EntityStatement superiorStmt : superiors) { 163 if (! nextExpectedSubject.equals(superiorStmt.getClaimsSet().getSubject())) { 164 return false; // chain breaks 165 } 166 nextExpectedSubject = new Subject(superiorStmt.getClaimsSet().getIssuer().getValue()); 167 } 168 169 if (trustAnchor == null) { 170 // No optional trust anchor config 171 return true; 172 } 173 174 // The last issuer in the chain is the trust anchor 175 EntityStatement topSuperior = superiors.get(superiors.size() - 1); 176 return topSuperior.getClaimsSet().getIssuer().equals(trustAnchor.getClaimsSet().getIssuer()); 177 } 178 179 180 /** 181 * Returns the leaf entity configuration. 182 * 183 * @return The leaf entity configuration. 184 */ 185 public EntityStatement getLeafConfiguration() { 186 return leaf; 187 } 188 189 190 /** 191 * Returns the superior entity statements. 192 * 193 * @return The superior entity statements, starting with a statement of 194 * the first superior about the leaf, ending with the statement 195 * of the trust anchor about the last intermediate or the leaf 196 * (for a minimal trust chain). 197 */ 198 public List<EntityStatement> getSuperiorStatements() { 199 return superiors; 200 } 201 202 203 /** 204 * Returns the optional trust anchor entity configuration. 205 * 206 * @return The trust anchor entity configuration, {@code null} if not 207 * specified. 208 */ 209 public EntityStatement getTrustAnchorConfiguration() { 210 return trustAnchor; 211 } 212 213 214 /** 215 * Returns the entity ID of the trust anchor. 216 * 217 * @return The entity ID of the trust anchor. 218 */ 219 public EntityID getTrustAnchorEntityID() { 220 221 // Return last in superiors 222 return getSuperiorStatements() 223 .get(getSuperiorStatements().size() - 1) 224 .getClaimsSet() 225 .getIssuerEntityID(); 226 } 227 228 229 /** 230 * Returns the length of this trust chain. A minimal trust chain with a 231 * leaf and anchor has a length of one. 232 * 233 * @return The trust chain length, with a minimal length of one. 234 */ 235 public int length() { 236 237 return getSuperiorStatements().size(); 238 } 239 240 241 /** 242 * Resolves the combined metadata policy for this trust chain. Uses the 243 * {@link DefaultPolicyOperationCombinationValidator default policy 244 * combination validator}. 245 * 246 * @param type The entity type, such as {@code openid_relying_party}. 247 * Must not be {@code null}. 248 * 249 * @return The combined metadata policy, with no policy operations if 250 * no policies were found. 251 * 252 * @throws PolicyViolationException On a policy violation exception. 253 */ 254 public MetadataPolicy resolveCombinedMetadataPolicy(final EntityType type) 255 throws PolicyViolationException { 256 257 return resolveCombinedMetadataPolicy(type, MetadataPolicyEntry.DEFAULT_POLICY_COMBINATION_VALIDATOR); 258 } 259 260 261 /** 262 * Resolves the combined metadata policy for this trust chain. 263 * 264 * @param type The entity type, such as 265 * {@code openid_relying_party}. Must not 266 * be {@code null}. 267 * @param combinationValidator The policy operation combination 268 * validator. Must not be {@code null}. 269 * 270 * @return The combined metadata policy, with no policy operations if 271 * no policies were found. 272 * 273 * @throws PolicyViolationException On a policy violation exception. 274 */ 275 public MetadataPolicy resolveCombinedMetadataPolicy(final EntityType type, 276 final PolicyOperationCombinationValidator combinationValidator) 277 throws PolicyViolationException { 278 279 List<MetadataPolicy> policies = new LinkedList<>(); 280 281 for (EntityStatement stmt: getSuperiorStatements()) { 282 283 MetadataPolicy metadataPolicy = stmt.getClaimsSet().getMetadataPolicy(type); 284 285 if (metadataPolicy == null) { 286 continue; 287 } 288 289 policies.add(metadataPolicy); 290 } 291 292 return MetadataPolicy.combine(policies, combinationValidator); 293 } 294 295 296 /** 297 * Return an iterator starting from the leaf entity statement. The 298 * optional trust anchor entity configuration is omitted. 299 * 300 * @return The iterator. 301 */ 302 public Iterator<EntityStatement> iteratorFromLeaf() { 303 304 // Init 305 final AtomicReference<EntityStatement> next = new AtomicReference<>(leaf); 306 final Iterator<EntityStatement> superiorsIterator = superiors.iterator(); 307 308 return new Iterator<EntityStatement>() { 309 @Override 310 public boolean hasNext() { 311 return next.get() != null; 312 } 313 314 315 @Override 316 public EntityStatement next() { 317 EntityStatement toReturn = next.get(); 318 if (toReturn == null) { 319 return null; // reached end on last iteration 320 } 321 322 // Set statement to return on next iteration 323 if (toReturn.equals(leaf)) { 324 // Return first superior 325 next.set(superiorsIterator.next()); 326 } else { 327 // Return next superior or end 328 if (superiorsIterator.hasNext()) { 329 next.set(superiorsIterator.next()); 330 } else { 331 next.set(null); 332 } 333 } 334 335 return toReturn; 336 } 337 338 339 @Override 340 public void remove() { 341 throw new UnsupportedOperationException(); 342 } 343 }; 344 } 345 346 347 /** 348 * Resolves the expiration time for this trust chain. Equals the next 349 * expiration in time when all entity statements in the trust chain are 350 * considered. 351 * 352 * @return The expiration time for this trust chain. 353 */ 354 public Date resolveExpirationTime() { 355 356 if (exp != null) { 357 return exp; 358 } 359 360 Iterator<EntityStatement> it = iteratorFromLeaf(); 361 362 Date nearestExp = null; 363 364 while (it.hasNext()) { 365 366 Date stmtExp = it.next().getClaimsSet().getExpirationTime(); 367 368 if (nearestExp == null) { 369 nearestExp = stmtExp; // on first iteration 370 } else if (stmtExp.before(nearestExp)) { 371 nearestExp = stmtExp; // replace nearest 372 } 373 } 374 375 exp = nearestExp; 376 return exp; 377 } 378 379 380 /** 381 * Verifies the signatures in this trust chain. 382 * 383 * @param trustAnchorJWKSet The trust anchor JWK set. Must not be 384 * {@code null}. 385 * 386 * @throws BadJOSEException If a signature is invalid or a statement is 387 * expired or before the issue time. 388 * @throws JOSEException On an internal JOSE exception. 389 */ 390 public void verifySignatures(final JWKSet trustAnchorJWKSet) 391 throws BadJOSEException, JOSEException { 392 393 Base64URL signingJWKThumbprint; 394 try { 395 signingJWKThumbprint = leaf.verifySignatureOfSelfStatement(); 396 } catch (BadJOSEException e) { 397 throw new BadJOSEException("Invalid leaf entity configuration: " + e.getMessage(), e); 398 } 399 400 for (int i=0; i < superiors.size(); i++) { 401 402 EntityStatement stmt = superiors.get(i); 403 404 JWKSet verificationJWKSet; 405 if (i+1 == superiors.size()) { 406 verificationJWKSet = trustAnchorJWKSet; 407 } else { 408 verificationJWKSet = superiors.get(i+1).getClaimsSet().getJWKSet(); 409 } 410 411 // Check that the signing JWK is registered with the superior 412 if (! hasJWKWithThumbprint(stmt.getClaimsSet().getJWKSet(), signingJWKThumbprint)) { 413 throw new BadJOSEException("Signing JWK with thumbprint " + signingJWKThumbprint + " not found in entity statement issued from superior " + stmt.getClaimsSet().getIssuerEntityID()); 414 } 415 416 try { 417 signingJWKThumbprint = stmt.verifySignature(verificationJWKSet); 418 } catch (BadJOSEException e) { 419 throw new BadJOSEException("Invalid statement from " + stmt.getClaimsSet().getIssuer() + ": " + e.getMessage(), e); 420 } 421 } 422 423 if (trustAnchor != null) { 424 425 if (! hasJWKWithThumbprint(trustAnchor.getClaimsSet().getJWKSet(), signingJWKThumbprint)) { 426 throw new BadJOSEException("Signing JWK with thumbprint " + signingJWKThumbprint + " not found in trust anchor entity configuration"); 427 } 428 429 try { 430 trustAnchor.verifySignatureOfSelfStatement(); 431 } catch (BadJOSEException e) { 432 throw new BadJOSEException("Invalid trust anchor entity configuration: " + e.getMessage(), e); 433 } 434 } 435 } 436 437 438 private static boolean hasJWKWithThumbprint(final JWKSet jwkSet, final Base64URL thumbprint) { 439 440 if (jwkSet == null) { 441 return false; 442 } 443 444 for (JWK jwk: jwkSet.getKeys()) { 445 try { 446 if (thumbprint.equals(jwk.computeThumbprint())) { 447 return true; 448 } 449 } catch (JOSEException e) { 450 throw new ProviderException(e.getMessage(), e); 451 } 452 } 453 454 return false; 455 } 456 457 458 /** 459 * Returns a JWT list representation of this trust chain. 460 * 461 * @return The JWT list. 462 */ 463 public List<SignedJWT> toJWTs() { 464 465 List<SignedJWT> out = new LinkedList<>(); 466 out.add(leaf.getSignedStatement()); 467 for (EntityStatement s: superiors) { 468 out.add(s.getSignedStatement()); 469 } 470 if (trustAnchor != null) { 471 out.add(trustAnchor.getSignedStatement()); 472 } 473 return out; 474 } 475 476 477 /** 478 * Returns a serialised JWT list representation of this trust chain. 479 * 480 * @return The serialised JWT list. 481 */ 482 public List<String> toSerializedJWTs() { 483 484 List<String> out = new LinkedList<>(); 485 for (SignedJWT jwt: toJWTs()) { 486 out.add(jwt.serialize()); 487 } 488 return out; 489 } 490 491 492 /** 493 * Parses a trust chain from the specified JWT list. 494 * 495 * @param statementJWTs The JWT list. Must not be {@code null}. 496 * 497 * @return The trust chain. 498 * 499 * @throws ParseException If parsing failed. 500 */ 501 public static TrustChain parse(final List<SignedJWT> statementJWTs) 502 throws ParseException { 503 504 if (statementJWTs.size() < 2) { 505 throw new ParseException("There must be at least 2 statement JWTs"); 506 } 507 508 EntityStatement leaf = null; 509 List<EntityStatement> superiors = new LinkedList<>(); 510 EntityStatement trustAnchor = null; 511 512 for (SignedJWT jwt: ListUtils.removeNullItems(statementJWTs)) { 513 514 if (leaf == null) { 515 try { 516 leaf = EntityStatement.parse(jwt); 517 } catch (ParseException e) { 518 throw new ParseException("Invalid leaf entity configuration: " + e.getMessage(), e); 519 } 520 } else { 521 EntityStatement statement; 522 try { 523 statement = EntityStatement.parse(jwt); 524 } catch (ParseException e) { 525 throw new ParseException("Invalid superior entity statement: " + e.getMessage(), e); 526 } 527 if (! statement.getClaimsSet().isSelfStatement()) { 528 superiors.add(statement); 529 } else { 530 trustAnchor = statement; // assume optional TA config 531 } 532 } 533 } 534 try { 535 return new TrustChain(leaf, superiors, trustAnchor); 536 } catch (Exception e) { 537 throw new ParseException("Illegal trust chain: " + e.getMessage(), e); 538 } 539 } 540 541 542 /** 543 * Parses a trust chain from the specified serialised JWT list. 544 * 545 * @param statementJWTs The serialised JWT list. Must not be 546 * {@code null}. 547 * 548 * @return The trust chain. 549 * 550 * @throws ParseException If parsing failed. 551 */ 552 public static TrustChain parseSerialized(final List<String> statementJWTs) 553 throws ParseException { 554 555 List<SignedJWT> jwtList = new LinkedList<>(); 556 557 for (String s: ListUtils.removeNullItems(statementJWTs)) { 558 try { 559 jwtList.add(SignedJWT.parse(s)); 560 } catch (java.text.ParseException e) { 561 throw new ParseException("Invalid JWT in trust chain: " + e.getMessage(), e); 562 } 563 } 564 565 return parse(jwtList); 566 } 567}