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}