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.jwt.SignedJWT;
036import com.nimbusds.oauth2.sdk.ParseException;
037import com.nimbusds.oauth2.sdk.id.Subject;
038import com.nimbusds.oauth2.sdk.util.CollectionUtils;
039import com.nimbusds.oauth2.sdk.util.ListUtils;
040import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
041import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
042import com.nimbusds.openid.connect.sdk.federation.entities.EntityType;
043import com.nimbusds.openid.connect.sdk.federation.policy.MetadataPolicy;
044import com.nimbusds.openid.connect.sdk.federation.policy.MetadataPolicyEntry;
045import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyViolationException;
046import com.nimbusds.openid.connect.sdk.federation.policy.operations.DefaultPolicyOperationCombinationValidator;
047import com.nimbusds.openid.connect.sdk.federation.policy.operations.PolicyOperationCombinationValidator;
048
049
050/**
051 * Federation entity trust chain.
052 *
053 * <p>Related specifications:
054 *
055 * <ul>
056 *     <li>OpenID Connect Federation 1.0, sections 3.2 and 7.1.
057 * </ul>
058 */
059@Immutable
060public final class TrustChain {
061        
062        
063        /**
064         * The leaf entity configuration.
065         */
066        private final EntityStatement leaf;
067        
068        
069        /**
070         * The superior entity statements.
071         */
072        private final List<EntityStatement> superiors;
073        
074        
075        /**
076         * The optional trust anchor entity configuration.
077         */
078        private final EntityStatement trustAnchor;
079        
080        
081        /**
082         * Caches the resolved expiration time for this trust chain.
083         */
084        private Date exp;
085        
086        
087        /**
088         * Creates a new trust chain. Validates the subject - issuer chain, the
089         * signatures are not verified.
090         *
091         * @param leaf      The leaf entity configuration. Must not be
092         *                  {@code null}.
093         * @param superiors The superior entity statements, starting with a
094         *                  statement of the first superior about the leaf,
095         *                  ending with the statement of the trust anchor about
096         *                  the last intermediate or the leaf (for a minimal
097         *                  trust chain). Must contain at least one entity
098         *                  statement.
099         *
100         * @throws IllegalArgumentException If the subject - issuer chain is
101         *                                  broken.
102         */
103        public TrustChain(final EntityStatement leaf, final List<EntityStatement> superiors) {
104                this(leaf, superiors, null);
105        }
106        
107        
108        /**
109         * Creates a new trust chain. Validates the subject - issuer chain, the
110         * signatures are not verified.
111         *
112         * @param leaf        The leaf entity configuration. Must not be
113         *                    {@code null}.
114         * @param superiors   The superior entity statements, starting with a
115         *                    statement of the first superior about the leaf,
116         *                    ending with the statement of the trust anchor
117         *                    about the last intermediate or the leaf (for a
118         *                    minimal trust chain). Must contain at least one
119         *                    entity statement.
120         * @param trustAnchor The optional trust anchor entity configuration,
121         *                    {@code null} if not specified.
122         *
123         * @throws IllegalArgumentException If the subject - issuer chain is
124         *                                  broken.
125         */
126        public TrustChain(final EntityStatement leaf, final List<EntityStatement> superiors, final EntityStatement trustAnchor) {
127                
128                // leaf config checks
129                if (leaf == null) {
130                        throw new IllegalArgumentException("The leaf entity configuration must not be null");
131                }
132                if (! leaf.getClaimsSet().isSelfStatement()) {
133                        throw new IllegalArgumentException("The leaf entity configuration must be a self-statement");
134                }
135                this.leaf = leaf;
136                
137                // superior statements check
138                if (CollectionUtils.isEmpty(superiors)) {
139                        throw new IllegalArgumentException("There must be at least one superior statement (issued by the trust anchor)");
140                }
141                this.superiors = superiors;
142                
143                // optional trust anchor config checks
144                this.trustAnchor = trustAnchor;
145                
146                if (trustAnchor != null && ! trustAnchor.getClaimsSet().isSelfStatement()) {
147                        throw new IllegalArgumentException("The trust anchor entity configuration must be a self-statement");
148                }
149                
150                if (! hasValidIssuerSubjectChain(leaf, superiors, trustAnchor)) {
151                        throw new IllegalArgumentException("Broken subject - issuer chain");
152                }
153        }
154        
155        
156        private static boolean hasValidIssuerSubjectChain(final EntityStatement leaf,
157                                                          final List<EntityStatement> superiors,
158                                                          final EntityStatement trustAnchor) {
159                
160                Subject nextExpectedSubject = leaf.getClaimsSet().getSubject();
161                
162                for (EntityStatement superiorStmt : superiors) {
163                        if (! nextExpectedSubject.equals(superiorStmt.getClaimsSet().getSubject())) {
164                                return false; // chain breaks
165                        }
166                        nextExpectedSubject = new Subject(superiorStmt.getClaimsSet().getIssuer().getValue());
167                }
168                
169                if (trustAnchor == null) {
170                        // No optional trust anchor config
171                        return true;
172                }
173                
174                // The last issuer in the chain is the trust anchor
175                EntityStatement topSuperior = superiors.get(superiors.size() - 1);
176                return topSuperior.getClaimsSet().getIssuer().equals(trustAnchor.getClaimsSet().getIssuer());
177        }
178        
179        
180        /**
181         * Returns the leaf entity configuration.
182         *
183         * @return The leaf entity configuration.
184         */
185        public EntityStatement getLeafConfiguration() {
186                return leaf;
187        }
188        
189        
190        /**
191         * Returns the superior entity statements.
192         *
193         * @return The superior entity statements, starting with a statement of
194         *         the first superior about the leaf, ending with the statement
195         *         of the trust anchor about the last intermediate or the leaf
196         *         (for a minimal trust chain).
197         */
198        public List<EntityStatement> getSuperiorStatements() {
199                return superiors;
200        }
201        
202        
203        /**
204         * Returns the optional trust anchor entity configuration.
205         *
206         * @return The trust anchor entity configuration, {@code null} if not
207         *         specified.
208         */
209        public EntityStatement getTrustAnchorConfiguration() {
210                return trustAnchor;
211        }
212        
213        
214        /**
215         * Returns the entity ID of the trust anchor.
216         *
217         * @return The entity ID of the trust anchor.
218         */
219        public EntityID getTrustAnchorEntityID() {
220                
221                // Return last in superiors
222                return getSuperiorStatements()
223                        .get(getSuperiorStatements().size() - 1)
224                        .getClaimsSet()
225                        .getIssuerEntityID();
226        }
227        
228        
229        /**
230         * Returns the length of this trust chain. A minimal trust chain with a
231         * leaf and anchor has a length of one.
232         *
233         * @return The trust chain length, with a minimal length of one.
234         */
235        public int length() {
236                
237                return getSuperiorStatements().size();
238        }
239        
240        
241        /**
242         * Resolves the combined metadata policy for this trust chain. Uses the
243         * {@link DefaultPolicyOperationCombinationValidator default policy
244         * combination validator}.
245         *
246         * @param type The entity type, such as {@code openid_relying_party}.
247         *             Must not be {@code null}.
248         *
249         * @return The combined metadata policy, with no policy operations if
250         *         no policies were found.
251         *
252         * @throws PolicyViolationException On a policy violation exception.
253         */
254        public MetadataPolicy resolveCombinedMetadataPolicy(final EntityType type)
255                throws PolicyViolationException {
256                
257                return resolveCombinedMetadataPolicy(type, MetadataPolicyEntry.DEFAULT_POLICY_COMBINATION_VALIDATOR);
258        }
259        
260        
261        /**
262         * Resolves the combined metadata policy for this trust chain.
263         *
264         * @param type                 The entity type, such as
265         *                             {@code openid_relying_party}. Must not
266         *                             be {@code null}.
267         * @param combinationValidator The policy operation combination
268         *                             validator. Must not be {@code null}.
269         *
270         * @return The combined metadata policy, with no policy operations if
271         *         no policies were found.
272         *
273         * @throws PolicyViolationException On a policy violation exception.
274         */
275        public MetadataPolicy resolveCombinedMetadataPolicy(final EntityType type,
276                                                            final PolicyOperationCombinationValidator combinationValidator)
277                throws PolicyViolationException {
278                
279                List<MetadataPolicy> policies = new LinkedList<>();
280                
281                for (EntityStatement stmt: getSuperiorStatements()) {
282                        
283                        MetadataPolicy metadataPolicy = stmt.getClaimsSet().getMetadataPolicy(type);
284                        
285                        if (metadataPolicy == null) {
286                                continue;
287                        }
288                        
289                        policies.add(metadataPolicy);
290                }
291                
292                return MetadataPolicy.combine(policies, combinationValidator);
293        }
294        
295        
296        /**
297         * Return an iterator starting from the leaf entity statement. The
298         * optional trust anchor entity configuration is omitted.
299         *
300         * @return The iterator.
301         */
302        public Iterator<EntityStatement> iteratorFromLeaf() {
303                
304                // Init
305                final AtomicReference<EntityStatement> next = new AtomicReference<>(leaf);
306                final Iterator<EntityStatement> superiorsIterator = superiors.iterator();
307                
308                return new Iterator<EntityStatement>() {
309                        @Override
310                        public boolean hasNext() {
311                                return next.get() != null;
312                        }
313                        
314                        
315                        @Override
316                        public EntityStatement next() {
317                                EntityStatement toReturn = next.get();
318                                if (toReturn == null) {
319                                        return null; // reached end on last iteration
320                                }
321                                
322                                // Set statement to return on next iteration
323                                if (toReturn.equals(leaf)) {
324                                        // Return first superior
325                                        next.set(superiorsIterator.next());
326                                } else {
327                                        // Return next superior or end
328                                        if (superiorsIterator.hasNext()) {
329                                                next.set(superiorsIterator.next());
330                                        } else {
331                                                next.set(null);
332                                        }
333                                }
334                                
335                                return toReturn;
336                        }
337                        
338                        
339                        @Override
340                        public void remove() {
341                                throw new UnsupportedOperationException();
342                        }
343                };
344        }
345        
346        
347        /**
348         * Resolves the expiration time for this trust chain. Equals the next
349         * expiration in time when all entity statements in the trust chain are
350         * considered.
351         *
352         * @return The expiration time for this trust chain.
353         */
354        public Date resolveExpirationTime() {
355                
356                if (exp != null) {
357                        return exp;
358                }
359                
360                Iterator<EntityStatement> it = iteratorFromLeaf();
361                
362                Date nearestExp = null;
363                
364                while (it.hasNext()) {
365                        
366                        Date stmtExp = it.next().getClaimsSet().getExpirationTime();
367                        
368                        if (nearestExp == null) {
369                                nearestExp = stmtExp; // on first iteration
370                        } else if (stmtExp.before(nearestExp)) {
371                                nearestExp = stmtExp; // replace nearest
372                        }
373                }
374                
375                exp = nearestExp;
376                return exp;
377        }
378        
379        
380        /**
381         * Verifies the signatures in this trust chain.
382         *
383         * @param trustAnchorJWKSet The trust anchor JWK set. Must not be
384         *                          {@code null}.
385         *
386         * @throws BadJOSEException If a signature is invalid or a statement is
387         *                          expired or before the issue time.
388         * @throws JOSEException    On an internal JOSE exception.
389         */
390        public void verifySignatures(final JWKSet trustAnchorJWKSet)
391                throws BadJOSEException, JOSEException {
392                
393                Base64URL signingJWKThumbprint;
394                try {
395                        signingJWKThumbprint = leaf.verifySignatureOfSelfStatement();
396                } catch (BadJOSEException e) {
397                        throw new BadJOSEException("Invalid leaf entity configuration: " + e.getMessage(), e);
398                }
399                
400                for (int i=0; i < superiors.size(); i++) {
401                        
402                        EntityStatement stmt = superiors.get(i);
403                        
404                        JWKSet verificationJWKSet;
405                        if (i+1 == superiors.size()) {
406                                verificationJWKSet = trustAnchorJWKSet;
407                        } else {
408                                verificationJWKSet = superiors.get(i+1).getClaimsSet().getJWKSet();
409                        }
410                        
411                        // Check that the signing JWK is registered with the superior
412                        if (! hasJWKWithThumbprint(stmt.getClaimsSet().getJWKSet(), signingJWKThumbprint)) {
413                                throw new BadJOSEException("Signing JWK with thumbprint " + signingJWKThumbprint + " not found in entity statement issued from superior " + stmt.getClaimsSet().getIssuerEntityID());
414                        }
415                        
416                        try {
417                                signingJWKThumbprint = stmt.verifySignature(verificationJWKSet);
418                        } catch (BadJOSEException e) {
419                                throw new BadJOSEException("Invalid statement from " + stmt.getClaimsSet().getIssuer() + ": " + e.getMessage(), e);
420                        }
421                }
422                
423                if (trustAnchor != null) {
424                        
425                        if (! hasJWKWithThumbprint(trustAnchor.getClaimsSet().getJWKSet(), signingJWKThumbprint)) {
426                                throw new BadJOSEException("Signing JWK with thumbprint " + signingJWKThumbprint + " not found in trust anchor entity configuration");
427                        }
428                        
429                        try {
430                                trustAnchor.verifySignatureOfSelfStatement();
431                        } catch (BadJOSEException e) {
432                                throw new BadJOSEException("Invalid trust anchor entity configuration: " + e.getMessage(), e);
433                        }
434                }
435        }
436        
437        
438        private static boolean hasJWKWithThumbprint(final JWKSet jwkSet, final Base64URL thumbprint) {
439                
440                if (jwkSet == null) {
441                        return false;
442                }
443                
444                for (JWK jwk: jwkSet.getKeys()) {
445                        try {
446                                if (thumbprint.equals(jwk.computeThumbprint())) {
447                                        return true;
448                                }
449                        } catch (JOSEException e) {
450                                throw new ProviderException(e.getMessage(), e);
451                        }
452                }
453                
454                return false;
455        }
456        
457        
458        /**
459         * Returns a JWT list representation of this trust chain.
460         *
461         * @return The JWT list.
462         */
463        public List<SignedJWT> toJWTs() {
464        
465                List<SignedJWT> out = new LinkedList<>();
466                out.add(leaf.getSignedStatement());
467                for (EntityStatement s: superiors) {
468                        out.add(s.getSignedStatement());
469                }
470                if (trustAnchor != null) {
471                        out.add(trustAnchor.getSignedStatement());
472                }
473                return out;
474        }
475        
476        
477        /**
478         * Returns a serialised JWT list representation of this trust chain.
479         *
480         * @return The serialised JWT list.
481         */
482        public List<String> toSerializedJWTs() {
483                
484                List<String> out = new LinkedList<>();
485                for (SignedJWT jwt: toJWTs()) {
486                        out.add(jwt.serialize());
487                }
488                return out;
489        }
490        
491        
492        /**
493         * Parses a trust chain from the specified JWT list.
494         *
495         * @param statementJWTs The JWT list. Must not be {@code null}.
496         *
497         * @return The trust chain.
498         *
499         * @throws ParseException If parsing failed.
500         */
501        public static TrustChain parse(final List<SignedJWT> statementJWTs)
502                throws ParseException {
503                
504                if (statementJWTs.size() < 2) {
505                        throw new ParseException("There must be at least 2 statement JWTs");
506                }
507                
508                EntityStatement leaf = null;
509                List<EntityStatement> superiors = new LinkedList<>();
510                EntityStatement trustAnchor = null;
511                
512                for (SignedJWT jwt: ListUtils.removeNullItems(statementJWTs)) {
513                        
514                        if (leaf == null) {
515                                try {
516                                        leaf = EntityStatement.parse(jwt);
517                                } catch (ParseException e) {
518                                        throw new ParseException("Invalid leaf entity configuration: " + e.getMessage(), e);
519                                }
520                        } else {
521                                EntityStatement statement;
522                                try {
523                                        statement = EntityStatement.parse(jwt);
524                                } catch (ParseException e) {
525                                        throw new ParseException("Invalid superior entity statement: " + e.getMessage(), e);
526                                }
527                                if (! statement.getClaimsSet().isSelfStatement()) {
528                                        superiors.add(statement);
529                                } else {
530                                        trustAnchor = statement; // assume optional TA config
531                                }
532                        }
533                }
534                try {
535                        return new TrustChain(leaf, superiors, trustAnchor);
536                } catch (Exception e) {
537                        throw new ParseException("Illegal trust chain: " + e.getMessage(), e);
538                }
539        }
540        
541        
542        /**
543         * Parses a trust chain from the specified serialised JWT list.
544         *
545         * @param statementJWTs The serialised JWT list. Must not be
546         *                      {@code null}.
547         *
548         * @return The trust chain.
549         *
550         * @throws ParseException If parsing failed.
551         */
552        public static TrustChain parseSerialized(final List<String> statementJWTs)
553                throws ParseException {
554                
555                List<SignedJWT> jwtList = new LinkedList<>();
556                
557                for (String s: ListUtils.removeNullItems(statementJWTs)) {
558                        try {
559                                jwtList.add(SignedJWT.parse(s));
560                        } catch (java.text.ParseException e) {
561                                throw new ParseException("Invalid JWT in trust chain: " + e.getMessage(), e);
562                        }
563                }
564                
565                return parse(jwtList);
566        }
567}