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}