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}