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.entities;
019
020
021import java.security.PublicKey;
022import java.util.List;
023
024import net.jcip.annotations.Immutable;
025
026import com.nimbusds.jose.*;
027import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
028import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory;
029import com.nimbusds.jose.jwk.*;
030import com.nimbusds.jose.proc.BadJOSEException;
031import com.nimbusds.jose.proc.JWSVerifierFactory;
032import com.nimbusds.jose.util.Base64URL;
033import com.nimbusds.jwt.JWTClaimsSet;
034import com.nimbusds.jwt.SignedJWT;
035import com.nimbusds.oauth2.sdk.ParseException;
036import com.nimbusds.oauth2.sdk.util.CollectionUtils;
037
038
039/**
040 * Federation entity statement.
041 *
042 * <p>Related specifications:
043 *
044 * <ul>
045 *     <li>OpenID Connect Federation 1.0, section 2.1.
046 * </ul>
047 */
048@Immutable
049public final class EntityStatement {
050        
051        
052        /**
053         * The signed statement as signed JWT.
054         */
055        private final SignedJWT statementJWT;
056        
057        
058        /**
059         * The statement claims.
060         */
061        private final EntityStatementClaimsSet statementClaimsSet;
062        
063        
064        /**
065         * Creates a new federation entity statement.
066         *
067         * @param statementJWT       The signed statement as signed JWT. Must
068         *                           not be {@code null}.
069         * @param statementClaimsSet The statement claims. Must not be
070         *                           {@code null}.
071         */
072        private EntityStatement(final SignedJWT statementJWT,
073                                final EntityStatementClaimsSet statementClaimsSet) {
074                
075                if (statementJWT == null) {
076                        throw new IllegalArgumentException("The entity statement must not be null");
077                }
078                if (JWSObject.State.UNSIGNED.equals(statementJWT.getState())) {
079                        throw new IllegalArgumentException("The statement is not signed");
080                }
081                this.statementJWT = statementJWT;
082                
083                if (statementClaimsSet == null) {
084                        throw new IllegalArgumentException("The entity statement claims set must not be null");
085                }
086                this.statementClaimsSet = statementClaimsSet;
087        }
088        
089        
090        /**
091         * Returns the entity ID.
092         *
093         * @return The entity ID.
094         */
095        public EntityID getEntityID() {
096                return getClaimsSet().getSubjectEntityID();
097        }
098        
099        
100        /**
101         * Returns the signed statement.
102         *
103         * @return The signed statement as signed JWT.
104         */
105        public SignedJWT getSignedStatement() {
106                return statementJWT;
107        }
108        
109        
110        /**
111         * Returns the statement claims.
112         *
113         * @return The statement claims.
114         */
115        public EntityStatementClaimsSet getClaimsSet() {
116                return statementClaimsSet;
117        }
118        
119        
120        /**
121         * Returns {@code true} if this entity statement is for a
122         * {@link EntityRole#TRUST_ANCHOR trust anchor}.
123         *
124         * @return {@code true} for a trust anchor, else {@code false}.
125         */
126        public boolean isTrustAnchor() {
127                
128                return getClaimsSet().isSelfStatement() && CollectionUtils.isEmpty(getClaimsSet().getAuthorityHints());
129        }
130        
131        
132        /**
133         * Verifies the signature for a self-statement (typically for a trust
134         * anchor or leaf) and checks the statement issue and expiration times.
135         *
136         * @return The SHA-256 thumbprint of the key used to successfully
137         *         verify the signature.
138         *
139         * @throws BadJOSEException If the signature is invalid or the
140         *                          statement is expired or before the issue
141         *                          time.
142         * @throws JOSEException    On a internal JOSE exception.
143         */
144        public Base64URL verifySignatureOfSelfStatement() throws BadJOSEException, JOSEException {
145                
146                if (! getClaimsSet().isSelfStatement()) {
147                        throw new BadJOSEException("Entity statement not self-issued");
148                }
149                
150                return verifySignature(getClaimsSet().getJWKSet());
151        }
152        
153        
154        /**
155         * Verifies the signature and checks the statement issue and expiration
156         * times.
157         *
158         * @param jwkSet The JWK set to use for the signature verification.
159         *               Must not be {@code null}.
160         *
161         * @return The SHA-256 thumbprint of the key used to successfully
162         *         verify the signature.
163         *
164         * @throws BadJOSEException If the signature is invalid or the
165         *                          statement is expired or before the issue
166         *                          time.
167         * @throws JOSEException    On a internal JOSE exception.
168         */
169        public Base64URL verifySignature(final JWKSet jwkSet)
170                throws BadJOSEException, JOSEException {
171                
172                List<JWK> jwkMatches = new JWKSelector(JWKMatcher.forJWSHeader(statementJWT.getHeader())).select(jwkSet);
173                
174                if (jwkMatches.isEmpty()) {
175                        throw new BadJOSEException("Entity statement rejected: Another JOSE algorithm expected, or no matching key(s) found");
176                }
177                
178                JWSVerifierFactory verifierFactory = new DefaultJWSVerifierFactory();
179                
180                JWK signingJWK = null;
181                
182                for (JWK candidateJWK: jwkMatches) {
183                        
184                        if (candidateJWK instanceof AsymmetricJWK) {
185                                PublicKey publicKey = ((AsymmetricJWK)candidateJWK).toPublicKey();
186                                JWSVerifier jwsVerifier = verifierFactory.createJWSVerifier(statementJWT.getHeader(), publicKey);
187                                if (statementJWT.verify(jwsVerifier)) {
188                                        // success
189                                        signingJWK = candidateJWK;
190                                }
191                        }
192                }
193                
194                if (signingJWK == null) {
195                        throw new BadJOSEException("Entity statement rejected: Invalid signature");
196                }
197                
198                // Double check claims with JWT framework
199                
200                try {
201                        new EntityStatementClaimsVerifier(null).verify(statementJWT.getJWTClaimsSet(), null);
202                } catch (java.text.ParseException e) {
203                        throw new BadJOSEException(e.getMessage(), e);
204                }
205                
206                return signingJWK.computeThumbprint();
207        }
208        
209        
210        /**
211         * Signs the specified federation entity claims set.
212         *
213         * @param claimsSet  The claims set. Must not be {@code null}.
214         * @param signingJWK The private signing JWK. Must be contained in the
215         *                   entity JWK set and not {@code null}.
216         *
217         * @return The signed federation entity statement.
218         *
219         * @throws JOSEException On a internal signing exception.
220         */
221        public static EntityStatement sign(final EntityStatementClaimsSet claimsSet,
222                                           final JWK signingJWK)
223                throws JOSEException {
224                
225                return sign(claimsSet, signingJWK, resolveSigningAlgorithm(signingJWK));
226        }
227        
228        
229        /**
230         * Signs the specified federation entity claims set.
231         *
232         * @param claimsSet  The claims set. Must not be {@code null}.
233         * @param signingJWK The private signing JWK. Must be contained in the
234         *                   entity JWK set and not {@code null}.
235         * @param jwsAlg     The signing algorithm. Must be supported by the
236         *                   JWK and not {@code null}.
237         *
238         * @return The signed federation entity statement.
239         *
240         * @throws JOSEException On a internal signing exception.
241         */
242        public static EntityStatement sign(final EntityStatementClaimsSet claimsSet,
243                                           final JWK signingJWK,
244                                           final JWSAlgorithm jwsAlg)
245                throws JOSEException {
246                
247                if (claimsSet.isSelfStatement() && ! claimsSet.getJWKSet().containsJWK(signingJWK)) {
248                        throw new JOSEException("Signing JWK not found in JWK set of self-statement");
249                }
250                
251                JWSSigner jwsSigner = new DefaultJWSSignerFactory().createJWSSigner(signingJWK, jwsAlg);
252                
253                JWSHeader jwsHeader = new JWSHeader.Builder(jwsAlg)
254                        .keyID(signingJWK.getKeyID())
255                        .build();
256                
257                SignedJWT signedJWT;
258                try {
259                        signedJWT = new SignedJWT(jwsHeader, claimsSet.toJWTClaimsSet());
260                } catch (ParseException e) {
261                        throw new JOSEException(e.getMessage(), e);
262                }
263                signedJWT.sign(jwsSigner);
264                return new EntityStatement(signedJWT, claimsSet);
265        }
266        
267        
268        private static JWSAlgorithm resolveSigningAlgorithm(final JWK jwk)
269                throws JOSEException {
270                
271                KeyType jwkType = jwk.getKeyType();
272                
273                if (KeyType.RSA.equals(jwkType)) {
274                        if (jwk.getAlgorithm() != null) {
275                                return new JWSAlgorithm(jwk.getAlgorithm().getName());
276                        } else {
277                                return JWSAlgorithm.RS256; // assume RS256 as default
278                        }
279                } else if (KeyType.EC.equals(jwkType)) {
280                        ECKey ecJWK = jwk.toECKey();
281                        if (jwk.getAlgorithm() != null) {
282                                return new JWSAlgorithm(ecJWK.getAlgorithm().getName());
283                        } else {
284                                if (Curve.P_256.equals(ecJWK.getCurve())) {
285                                        return JWSAlgorithm.ES256;
286                                } else if (Curve.P_384.equals(ecJWK.getCurve())) {
287                                        return JWSAlgorithm.ES384;
288                                } else if (Curve.P_521.equals(ecJWK.getCurve())) {
289                                        return JWSAlgorithm.ES512;
290                                } else {
291                                        throw new JOSEException("Unsupported ECDSA curve: " + ecJWK.getCurve());
292                                }
293                        }
294                } else if (KeyType.OKP.equals(jwkType)){
295                        OctetKeyPair okp = jwk.toOctetKeyPair();
296                        if (Curve.Ed25519.equals(okp.getCurve())) {
297                                return JWSAlgorithm.EdDSA;
298                        } else {
299                                throw new JOSEException("Unsupported EdDSA curve: " + okp.getCurve());
300                        }
301                } else {
302                        throw new JOSEException("Unsupported JWK type: " + jwkType);
303                }
304        }
305        
306        
307        /**
308         * Parses a federation entity statement.
309         *
310         * @param signedStmt The signed statement as a signed JWT. Must not
311         *                   be {@code null}.
312         *
313         * @return The federation entity statement.
314         *
315         * @throws ParseException If parsing failed.
316         */
317        public static EntityStatement parse(final SignedJWT signedStmt)
318                throws ParseException {
319                
320                if (JWSObject.State.UNSIGNED.equals(signedStmt.getState())) {
321                        throw new ParseException("The statement is not signed");
322                }
323                
324                JWTClaimsSet jwtClaimsSet;
325                try {
326                        jwtClaimsSet = signedStmt.getJWTClaimsSet();
327                } catch (java.text.ParseException e) {
328                        throw new ParseException(e.getMessage(), e);
329                }
330                
331                EntityStatementClaimsSet claimsSet = new EntityStatementClaimsSet(jwtClaimsSet);
332                return new EntityStatement(signedStmt, claimsSet);
333        }
334        
335        
336        /**
337         * Parses a federation entity statement.
338         *
339         * @param signedStmtString The signed statement as a signed JWT string.
340         *                         Must not be {@code null}.
341         *
342         * @return The federation entity statement.
343         *
344         * @throws ParseException If parsing failed.
345         */
346        public static EntityStatement parse(final String signedStmtString)
347                throws ParseException {
348                
349                try {
350                        return parse(SignedJWT.parse(signedStmtString));
351                } catch (java.text.ParseException e) {
352                        throw new ParseException("Invalid entity statement: " + e.getMessage(), e);
353                }
354        }
355}