/** * personium.io * Copyright 2014 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fujitsu.dc.common.auth.token; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.security.InvalidAlgorithmParameterException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.UUID; import javax.xml.crypto.KeySelectorException; import javax.xml.crypto.MarshalException; import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.DigestMethod; import javax.xml.crypto.dsig.Reference; import javax.xml.crypto.dsig.SignatureMethod; import javax.xml.crypto.dsig.SignedInfo; import javax.xml.crypto.dsig.Transform; import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignatureException; import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.dom.DOMSignContext; import javax.xml.crypto.dsig.dom.DOMValidateContext; import javax.xml.crypto.dsig.keyinfo.KeyInfo; import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; import javax.xml.crypto.dsig.keyinfo.X509Data; import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; import javax.xml.crypto.dsig.spec.TransformParameterSpec; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import net.oauth.signature.pem.PEMReader; import net.oauth.signature.pem.PKCS1EncodedKeySpec; import org.apache.commons.lang.CharEncoding; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import com.fujitsu.dc.common.utils.DcCoreUtils; /** * TransCellのAccessTokenを扱うクラス. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public final class TransCellAccessToken extends AbstractOAuth2Token implements IExtRoleContainingToken { private SignedInfo signedInfo; private static final String URN_OASIS_NAMES_TC_SAML_2_0_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"; /** * ログ. */ static Logger log = LoggerFactory.getLogger(TransCellAccessToken.class); String id; String target; /** * トークンの有効期間. */ public static final long LIFESPAN = 1 * MILLISECS_IN_AN_HOUR; // 1時間 private static List<String> x509RootCertificateFileNames; private static XMLSignatureFactory xmlSignatureFactory; private static X509Certificate x509Certificate; private static KeyInfo keyInfo; private static PrivateKey privKey; /** * コンストラクタ. * @param id トークンの一意識別子 * @param issuedAt 発行時刻(epochからのミリ秒) * @param lifespan トークンの有効時間(ミリ秒) * @param issuer 発行 Cell URL * @param subject アクセス主体URL * @param target ターゲットURL * @param roleList ロールリスト * @param schema クライアント認証されたデータスキーマ */ public TransCellAccessToken(final String id, final long issuedAt, final long lifespan, final String issuer, final String subject, final String target, final List<Role> roleList, final String schema) { this.issuedAt = issuedAt; this.lifespan = lifespan; this.id = id; this.issuer = issuer; this.subject = subject; this.target = target; this.roleList = roleList; this.schema = schema; try { /* * creates the Reference object, which identifies the data that will be digested and signed. The Reference * object is assembled by creating and passing as parameters each of its components: the URI, the * DigestMethod, and a list of Transforms */ DigestMethod digestMethod = xmlSignatureFactory.newDigestMethod(DigestMethod.SHA1, null); Transform transform = xmlSignatureFactory.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null); Reference reference = xmlSignatureFactory.newReference("", digestMethod, Collections.singletonList(transform), null, null); /* * creates the SignedInfo object that the signature is calculated over. Like the Reference object, the * SignedInfo object is assembled by creating and passing as parameters each of its components: the * CanonicalizationMethod, the SignatureMethod, and a list of References */ CanonicalizationMethod c14nMethod = xmlSignatureFactory.newCanonicalizationMethod( CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null); SignatureMethod signatureMethod = xmlSignatureFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null); signedInfo = xmlSignatureFactory.newSignedInfo(c14nMethod, signatureMethod, Collections.singletonList(reference)); } catch (NoSuchAlgorithmException e) { // 重大な異常なので非チェックにして上に上げる throw new RuntimeException(e); } catch (InvalidAlgorithmParameterException e) { // 重大な異常なので非チェックにして上に上げる throw new RuntimeException(e); } } /** * コンストラクタ. * @param id トークンの一意識別子 * @param issuedAt 発行時刻(epochからのミリ秒) * @param issuer 発行 Cell URL * @param subject アクセス主体URL * @param target ターゲットURL * @param roleList ロールリスト * @param schema クライアント認証されたデータスキーマ */ public TransCellAccessToken(final String id, final long issuedAt, final String issuer, final String subject, final String target, final List<Role> roleList, final String schema) { this(id, issuedAt, LIFESPAN, issuer, subject, target, roleList, schema); } /** * IDにUUIDを自動採番するコンストラクタ. * @param issuedAt 発行時刻(epochからのミリ秒) * @param issuer 発行 Cell URL * @param subject アクセス主体URL * @param target ターゲットURL * @param roleList ロールリスト * @param schema クライアント認証されたデータスキーマ */ public TransCellAccessToken( final long issuedAt, final String issuer, final String subject, final String target, final List<Role> roleList, final String schema) { this(UUID.randomUUID().toString(), issuedAt, issuer, subject, target, roleList, schema); } /** * コンストラクタ. * @param issuer 発行 Cell URL * @param subject アクセス主体URL * @param target ターゲットURL * @param roleList ロールリスト * @param schema クライアント認証されたデータスキーマ */ public TransCellAccessToken( final String issuer, final String subject, final String target, final List<Role> roleList, final String schema) { this(UUID.randomUUID().toString(), new Date().getTime(), issuer, subject, target, roleList, schema); } /* (non-Javadoc) * @see com.fujitsu.dc.core.auth.token.AbstractOAuth2Token#toTokenString() */ @Override public String toTokenString() { String samlStr = this.toSamlString(); try { // Base64urlする String token = DcCoreUtils.encodeBase64Url(samlStr.getBytes(CharEncoding.UTF_8)); return token; } catch (UnsupportedEncodingException e) { // UTF8が処理できないはずがない。 throw new RuntimeException(e); } } /** * トークンからSAML文字列を生成します. * @return SAML文字列 */ public String toSamlString() { /* * Creation of SAML2.0 Document * http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf */ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); DocumentBuilder builder = null; try { builder = dbf.newDocumentBuilder(); } catch (ParserConfigurationException e) { // 重大な異常なので非チェックにして上に上げる throw new RuntimeException(e); } Document doc = builder.newDocument(); Element assertion = doc.createElementNS(URN_OASIS_NAMES_TC_SAML_2_0_ASSERTION, "Assertion"); doc.appendChild(assertion); assertion.setAttribute("ID", this.id); assertion.setAttribute("Version", "2.0"); // Dummy Date DateTime dateTime = new DateTime(this.issuedAt); assertion.setAttribute("IssueInstant", dateTime.toString()); // Issuer Element issuer = doc.createElement("Issuer"); issuer.setTextContent(this.issuer); assertion.appendChild(issuer); // Subject Element subject = doc.createElement("Subject"); Element nameId = doc.createElement("NameID"); nameId.setTextContent(this.subject); Element subjectConfirmation = doc.createElement("SubjectConfirmation"); subject.appendChild(nameId); subject.appendChild(subjectConfirmation); assertion.appendChild(subject); // Conditions Element conditions = doc.createElement("Conditions"); Element audienceRestriction = doc.createElement("AudienceRestriction"); for (String aud : new String[] {this.target, this.schema}) { Element audience = doc.createElement("Audience"); audience.setTextContent(aud); audienceRestriction.appendChild(audience); } conditions.appendChild(audienceRestriction); assertion.appendChild(conditions); // AuthnStatement Element authnStmt = doc.createElement("AuthnStatement"); authnStmt.setAttribute("AuthnInstant", dateTime.toString()); Element authnCtxt = doc.createElement("AuthnContext"); Element authnCtxtCr = doc.createElement("AuthnContextClassRef"); authnCtxtCr.setTextContent("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); authnCtxt.appendChild(authnCtxtCr); authnStmt.appendChild(authnCtxt); assertion.appendChild(authnStmt); // AttributeStatement Element attrStmt = doc.createElement("AttributeStatement"); Element attribute = doc.createElement("Attribute"); for (Role role : this.roleList) { Element attrValue = doc.createElement("AttributeValue"); Attr attr = doc.createAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "type"); attr.setPrefix("xsi"); attr.setValue("string"); attrValue.setAttributeNodeNS(attr); attrValue.setTextContent(role.schemeCreateUrlForTranceCellToken(this.issuer)); attribute.appendChild(attrValue); } attrStmt.appendChild(attribute); assertion.appendChild(attrStmt); // Normalization を実施 doc.normalizeDocument(); // Dsigをつける。 // Create a DOMSignContext and specify the RSA PrivateKey and // location of the resulting XMLSignature's parent element. DOMSignContext dsc = new DOMSignContext(privKey, doc.getDocumentElement()); // Create the XMLSignature, but don't sign it yet. XMLSignature signature = xmlSignatureFactory.newXMLSignature(signedInfo, keyInfo); // Marshal, generate, and sign the enveloped signature. try { signature.sign(dsc); // 文字列化する。 return DcCoreUtils.nodeToString(doc.getDocumentElement()); } catch (MarshalException e1) { // DOMのシリアライズに失敗するのは重大な異常 throw new RuntimeException(e1); } catch (XMLSignatureException e1) { // 署名できないような事態は異常 throw new RuntimeException(e1); } /* * ------------------------------------------------------------ * http://tools.ietf.org/html/draft-ietf-oauth-saml2-bearer-10 * ------------------------------------------------------------ 2.1. Using SAML Assertions as Authorization * Grants To use a SAML Bearer Assertion as an authorization grant, use the following parameter values and * encodings. The value of "grant_type" parameter MUST be "urn:ietf:params:oauth:grant-type:saml2-bearer" The * value of the "assertion" parameter MUST contain a single SAML 2.0 Assertion. The SAML Assertion XML data MUST * be encoded using base64url, where the encoding adheres to the definition in Section 5 of RFC4648 [RFC4648] * and where the padding bits are set to zero. To avoid the need for subsequent encoding steps (by "application/ * x-www-form-urlencoded" [W3C.REC-html401-19991224], for example), the base64url encoded data SHOULD NOT be * line wrapped and pad characters ("=") SHOULD NOT be included. */ } /** * TransCellAccessTokenをパースしてオブジェクト生成する. * @param token トークン文字列 * @return TransCellAccessTokenオブジェクト(パース成功時) * @throws AbstractOAuth2Token.TokenParseException トークンのパース失敗 * @throws AbstractOAuth2Token.TokenDsigException 証明書の署名検証エラー * @throws AbstractOAuth2Token.TokenRootCrtException ルートCA証明書の検証エラー */ public static TransCellAccessToken parse(final String token) throws AbstractOAuth2Token.TokenParseException, AbstractOAuth2Token.TokenDsigException, AbstractOAuth2Token.TokenRootCrtException { try { byte[] samlBytes = DcCoreUtils.decodeBase64Url(token); ByteArrayInputStream bais = new ByteArrayInputStream(samlBytes); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); DocumentBuilder builder = null; try { builder = dbf.newDocumentBuilder(); } catch (ParserConfigurationException e) { // 重大な異常なので非チェックにして上に上げる throw new RuntimeException(e); } Document doc = builder.parse(bais); Element assertion = doc.getDocumentElement(); Element issuer = (Element) (doc.getElementsByTagName("Issuer").item(0)); Element subject = (Element) (assertion.getElementsByTagName("Subject").item(0)); Element subjectNameID = (Element) (subject.getElementsByTagName("NameID").item(0)); String id = assertion.getAttribute("ID"); String issuedAtStr = assertion.getAttribute("IssueInstant"); DateTime dt = new DateTime(issuedAtStr); NodeList audienceList = assertion.getElementsByTagName("Audience"); Element aud1 = (Element) (audienceList.item(0)); String target = aud1.getTextContent(); String schema = null; if (audienceList.getLength() > 1) { Element aud2 = (Element) (audienceList.item(1)); schema = aud2.getTextContent(); } List<Role> roles = new ArrayList<Role>(); NodeList attrList = assertion.getElementsByTagName("AttributeValue"); for (int i = 0; i < attrList.getLength(); i++) { Element attv = (Element) (attrList.item(i)); roles.add(new Role(new URL(attv.getTextContent()))); } NodeList nl = assertion.getElementsByTagName("Signature"); if (nl.getLength() == 0) { throw new TokenParseException("Cannot find Signature element"); } Element signatureElement = (Element) nl.item(0); // 署名の有効性を確認する。以下の例外はTokenDsigException(署名検証エラー) // Create a DOMValidateContext and specify a KeySelector // and document context. X509KeySelector x509KeySelector = new X509KeySelector(issuer.getTextContent()); DOMValidateContext valContext = new DOMValidateContext(x509KeySelector, signatureElement); // Unmarshal the XMLSignature. XMLSignature signature; try { signature = xmlSignatureFactory.unmarshalXMLSignature(valContext); } catch (MarshalException e) { throw new TokenDsigException(e.getMessage(), e); } // ルートCA証明書読み込み try { x509KeySelector.readRoot(x509RootCertificateFileNames); } catch (CertificateException e) { // ルートCA証明書設定エラーのため、重大であり、500 throw new TokenRootCrtException(e.getMessage(), e); } // Validate the XMLSignature x509証明書検証. boolean coreValidity; try { coreValidity = signature.validate(valContext); } catch (XMLSignatureException e) { if (e.getCause().getClass() == new KeySelectorException().getClass()) { throw new TokenDsigException(e.getCause().getMessage(), e.getCause()); } throw new TokenDsigException(e.getMessage(), e); } // http://www.w3.org/TR/xmldsig-core/#sec-CoreValidation // Check core validation status. if (!coreValidity) { // シグネチャ検証 boolean isDsigValid; try { isDsigValid = signature.getSignatureValue().validate(valContext); } catch (XMLSignatureException e) { throw new TokenDsigException(e.getMessage(), e); } if (!isDsigValid) { throw new TokenDsigException("Failed signature validation"); } // リファレンス検証 Iterator i = signature.getSignedInfo().getReferences().iterator(); for (int j = 0; i.hasNext(); j++) { boolean refValid; try { refValid = ((Reference) i.next()).validate(valContext); } catch (XMLSignatureException e) { throw new TokenDsigException(e.getMessage(), e); } if (!refValid) { throw new TokenDsigException("Failed to validate reference [" + j + "]"); } } throw new TokenDsigException("Signature failed core validation. unkwnon reason."); } return new TransCellAccessToken(id, dt.getMillis(), issuer.getTextContent(), subjectNameID.getTextContent(), target, roles, schema); } catch (UnsupportedEncodingException e) { throw new TokenParseException(e.getMessage(), e); } catch (SAXException e) { throw new TokenParseException(e.getMessage(), e); } catch (IOException e) { throw new TokenParseException(e.getMessage(), e); } } @Override public String getTarget() { return this.target; } @Override public String getId() { return this.id; } /** * X509の設定をする. * @param privateKeyFileName 秘密鍵ファイル名 * @param certificateFileName 証明書ファイル名 * @param rootCertificateFileNames ルート証明書ファイル名 * @throws IOException IOException * @throws NoSuchAlgorithmException NoSuchAlgorithmException * @throws InvalidKeySpecException InvalidKeySpecException * @throws CertificateException CertificateException */ public static void configureX509(String privateKeyFileName, String certificateFileName, String[] rootCertificateFileNames) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, CertificateException { xmlSignatureFactory = XMLSignatureFactory.getInstance("DOM"); // Read RootCA Certificate x509RootCertificateFileNames = new ArrayList<String>(); if (rootCertificateFileNames != null) { for (String fileName : rootCertificateFileNames) { x509RootCertificateFileNames.add(fileName); } } // Read Private Key InputStream is = null; if (privateKeyFileName == null) { is = TransCellAccessToken.class.getClassLoader().getResourceAsStream( X509KeySelector.DEFAULT_SERVER_KEY_PATH); } else { is = new FileInputStream(privateKeyFileName); } PEMReader privateKeyPemReader = new PEMReader(is); byte[] privateKeyDerBytes = privateKeyPemReader.getDerBytes(); PKCS1EncodedKeySpec keySpecRSAPrivateKey = new PKCS1EncodedKeySpec(privateKeyDerBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); privKey = keyFactory.generatePrivate(keySpecRSAPrivateKey.getKeySpec()); // Read Certificate if (certificateFileName == null) { is = TransCellAccessToken.class.getClassLoader().getResourceAsStream( X509KeySelector.DEFAULT_SERVER_CRT_PATH); } else { is = new FileInputStream(certificateFileName); } PEMReader serverCertificatePemReader; serverCertificatePemReader = new PEMReader(is); byte[] serverCertificateBytesCert = serverCertificatePemReader.getDerBytes(); CertificateFactory cf = CertificateFactory.getInstance(X509KeySelector.X509KEY_TYPE); x509Certificate = (X509Certificate) cf.generateCertificate( new ByteArrayInputStream(serverCertificateBytesCert)); // Create the KeyInfo containing the X509Data KeyInfoFactory keyInfoFactory = xmlSignatureFactory.getKeyInfoFactory(); List x509Content = new ArrayList(); x509Content.add(x509Certificate.getSubjectX500Principal().getName()); x509Content.add(x509Certificate); X509Data xd = keyInfoFactory.newX509Data(x509Content); keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(xd)); // http://java.sun.com/developer/technicalArticles/xml/dig_signature_api/ } @Override public String getExtCellUrl() { return this.getIssuer(); } @Override public List<Role> getRoleList() { return this.getRoles(); } }