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