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; 028 029 030/** 031 * Trust chain resolver. 032 * 033 * <p>Related specifications: 034 * 035 * <ul> 036 * <li>OpenID Connect Federation 1.0, section 7. 037 * </ul> 038 */ 039public class TrustChainResolver { 040 041 042 /** 043 * The configured trust anchors with their public JWK sets. 044 */ 045 private final Map<EntityID, JWKSet> trustAnchors; 046 047 048 /** 049 * The entity statement retriever. 050 */ 051 private final EntityStatementRetriever statementRetriever; 052 053 054 /** 055 * Creates a new trust chain resolver with a single trust anchor. 056 * 057 * @param trustAnchor The trust anchor. Must not be {@code null}. 058 * @param trustAnchorJWKSet The trust anchor public JWK set. Must not 059 * be {@code null}. 060 */ 061 public TrustChainResolver(final EntityID trustAnchor, 062 final JWKSet trustAnchorJWKSet) { 063 this(Collections.singletonMap(trustAnchor, trustAnchorJWKSet), new DefaultEntityStatementRetriever()); 064 } 065 066 067 /** 068 * Creates a new trust chain resolver with multiple trust anchors. 069 * 070 * @param trustAnchors The trust anchors with their public JWK 071 * sets. Must contain at least one anchor. 072 * @param httpConnectTimeoutMs The HTTP connect timeout in 073 * milliseconds, zero means timeout 074 * determined by the underlying HTTP 075 * client. 076 * @param httpReadTimeoutMs The HTTP read timeout in milliseconds, 077 * zero means timout determined by the 078 * underlying HTTP client. 079 */ 080 public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors, 081 final int httpConnectTimeoutMs, 082 final int httpReadTimeoutMs) { 083 this(trustAnchors, new DefaultEntityStatementRetriever(httpConnectTimeoutMs, httpReadTimeoutMs)); 084 } 085 086 087 /** 088 * Creates new trust chain resolver. 089 * 090 * @param trustAnchors The trust anchors with their public JWK 091 * sets. Must contain at least one anchor. 092 * @param statementRetriever The entity statement retriever to use. 093 * Must not be {@code null}. 094 */ 095 public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors, 096 final EntityStatementRetriever statementRetriever) { 097 if (MapUtils.isEmpty(trustAnchors)) { 098 throw new IllegalArgumentException("The trust anchors map must not be empty or null"); 099 } 100 this.trustAnchors = trustAnchors; 101 102 if (statementRetriever == null) { 103 throw new IllegalArgumentException("The entity statement retriever must not be null"); 104 } 105 this.statementRetriever = statementRetriever; 106 } 107 108 109 /** 110 * Returns the configured trust anchors. 111 * 112 * @return The trust anchors with their public JWK sets. Contains at 113 * least one anchor. 114 */ 115 public Map<EntityID, JWKSet> getTrustAnchors() { 116 return Collections.unmodifiableMap(trustAnchors); 117 } 118 119 120 /** 121 * Returns the configured entity statement retriever. 122 * 123 * @return The configured entity statement retriever. 124 */ 125 public EntityStatementRetriever getEntityStatementRetriever() { 126 return statementRetriever; 127 } 128 129 130 /** 131 * Resolves the trust chains for the specified target. 132 * 133 * @param target The target. Must not be {@code null}. 134 * 135 * @return The resolved trust chains, containing at least one valid and 136 * verified chain. 137 * 138 * @throws ResolveException If no trust chain could be resolved. 139 */ 140 public TrustChainSet resolveTrustChains(final EntityID target) 141 throws ResolveException { 142 143 if (trustAnchors.get(target) != null) { 144 throw new ResolveException("Target is trust anchor"); 145 } 146 147 TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever); 148 149 Set<TrustChain> fetchedTrustChains = retriever.fetch(target, trustAnchors.keySet()); 150 151 if (fetchedTrustChains.isEmpty()) { 152 153 if (retriever.getAccumulatedExceptions().isEmpty()) { 154 throw new ResolveException("No trust chain leading up to a trust anchor"); 155 } else if (retriever.getAccumulatedExceptions().size() == 1){ 156 Throwable cause = retriever.getAccumulatedExceptions().get(0); 157 throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), cause); 158 } else { 159 throw new ResolveException("Couldn't resolve trust chain due to multiple causes", retriever.getAccumulatedExceptions()); 160 } 161 } 162 163 List<Throwable> verificationExceptions = new LinkedList<>(); 164 165 TrustChainSet verifiedTrustChains = new TrustChainSet(); 166 167 for (TrustChain chain: fetchedTrustChains) { 168 169 EntityID anchor = chain.getTrustAnchorEntityID(); 170 JWKSet anchorJWKSet = trustAnchors.get(anchor); 171 if (anchorJWKSet == null) { 172 continue; 173 } 174 175 try { 176 chain.verifySignatures(anchorJWKSet); 177 } catch (BadJOSEException | JOSEException e) { 178 verificationExceptions.add(e); 179 continue; 180 } 181 182 verifiedTrustChains.add(chain); 183 } 184 185 if (verifiedTrustChains.isEmpty()) { 186 187 List<Throwable> accumulatedExceptions = new LinkedList<>(retriever.getAccumulatedExceptions()); 188 accumulatedExceptions.addAll(verificationExceptions); 189 190 if (verificationExceptions.size() == 1) { 191 Throwable cause = verificationExceptions.get(0); 192 throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), accumulatedExceptions); 193 } else { 194 throw new ResolveException("Couldn't resolve trust chain due to multiple causes", accumulatedExceptions); 195 } 196 } 197 198 return verifiedTrustChains; 199 } 200}