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