/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cxf.rs.security.saml.sso;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
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.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
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.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.w3c.dom.Document;
import org.apache.cxf.staxutils.W3CDOMStreamWriter;
import org.apache.wss4j.common.util.DOM2Writer;
import org.apache.xml.security.stax.impl.util.IDGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MetadataWriter {
private static final Logger LOG = LoggerFactory.getLogger(MetadataWriter.class);
private static final XMLSignatureFactory XML_SIGNATURE_FACTORY = XMLSignatureFactory.getInstance("DOM");
//CHECKSTYLE:OFF
public Document getMetaData(
String serviceURL,
String assertionConsumerServiceURL,
String logoutURL,
Key signingKey,
X509Certificate signingCert,
boolean wantRequestsSigned
) throws Exception {
W3CDOMStreamWriter writer = new W3CDOMStreamWriter();
writer.writeStartDocument(StandardCharsets.UTF_8.name(), "1.0");
String referenceID = IDGenerator.generateID("_");
writer.writeStartElement("md", "EntityDescriptor", SSOConstants.SAML2_METADATA_NS);
writer.writeAttribute("ID", referenceID);
writer.writeAttribute("entityID", serviceURL);
writer.writeNamespace("md", SSOConstants.SAML2_METADATA_NS);
writer.writeNamespace("wsa", SSOConstants.WS_ADDRESSING_NS);
writer.writeNamespace("xsi", SSOConstants.SCHEMA_INSTANCE_NS);
writeSAMLMetadata(writer, assertionConsumerServiceURL, logoutURL, signingCert, wantRequestsSigned);
writer.writeEndElement(); // EntityDescriptor
writer.writeEndDocument();
writer.close();
if (LOG.isDebugEnabled()) {
String out = DOM2Writer.nodeToString(writer.getDocument());
LOG.debug("***************** unsigned ****************");
LOG.debug(out);
LOG.debug("***************** unsigned ****************");
}
Document doc = writer.getDocument();
if (signingKey != null) {
return signMetaInfo(signingCert, signingKey, doc, referenceID);
}
return doc;
}
private void writeSAMLMetadata(
XMLStreamWriter writer,
String assertionConsumerServiceURL,
String logoutURL,
X509Certificate signingCert,
boolean wantRequestsSigned
) throws XMLStreamException, MalformedURLException, CertificateEncodingException {
writer.writeStartElement("md", "SPSSODescriptor", SSOConstants.SAML2_METADATA_NS);
writer.writeAttribute("AuthnRequestsSigned", Boolean.toString(wantRequestsSigned));
writer.writeAttribute("WantAssertionsSigned", "true");
writer.writeAttribute("protocolSupportEnumeration", "urn:oasis:names:tc:SAML:2.0:protocol");
if (logoutURL != null) {
writer.writeStartElement("md", "SingleLogoutService", SSOConstants.SAML2_METADATA_NS);
writer.writeAttribute("Location", logoutURL);
writer.writeAttribute("Binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
writer.writeEndElement(); // SingleLogoutService
}
writer.writeStartElement("md", "AssertionConsumerService", SSOConstants.SAML2_METADATA_NS);
writer.writeAttribute("Location", assertionConsumerServiceURL);
writer.writeAttribute("index", "0");
writer.writeAttribute("isDefault", "true");
writer.writeAttribute("Binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
writer.writeEndElement(); // AssertionConsumerService
writer.writeStartElement("md", "AssertionConsumerService", SSOConstants.SAML2_METADATA_NS);
writer.writeAttribute("Location", assertionConsumerServiceURL);
writer.writeAttribute("index", "1");
writer.writeAttribute("Binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-REDIRECT");
writer.writeEndElement(); // AssertionConsumerService
/*
if (protocol.getClaimTypesRequested() != null && !protocol.getClaimTypesRequested().isEmpty()) {
writer.writeStartElement("md", "AttributeConsumingService", SSOConstants.SAML2_METADATA_NS);
writer.writeAttribute("index", "0");
writer.writeStartElement("md", "ServiceName", SSOConstants.SAML2_METADATA_NS);
writer.writeAttribute("xml:lang", "en");
writer.writeCharacters(config.getName());
writer.writeEndElement(); // ServiceName
for (Claim claim : protocol.getClaimTypesRequested()) {
writer.writeStartElement("md", "RequestedAttribute", SSOConstants.SAML2_METADATA_NS);
writer.writeAttribute("isRequired", Boolean.toString(claim.isOptional()));
writer.writeAttribute("Name", claim.getType());
writer.writeAttribute("NameFormat",
"urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified");
writer.writeEndElement(); // RequestedAttribute
}
writer.writeEndElement(); // AttributeConsumingService
}
*/
if (signingCert != null) {
writer.writeStartElement("md", "KeyDescriptor", SSOConstants.SAML2_METADATA_NS);
writer.writeAttribute("use", "signing");
writer.writeStartElement("ds", "KeyInfo", "http://www.w3.org/2000/09/xmldsig#");
writer.writeNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
writer.writeStartElement("ds", "X509Data", "http://www.w3.org/2000/09/xmldsig#");
writer.writeStartElement("ds", "X509Certificate", "http://www.w3.org/2000/09/xmldsig#");
// Write the Base-64 encoded certificate
byte data[] = signingCert.getEncoded();
String encodedCertificate = Base64.getMimeEncoder().encodeToString(data);
writer.writeCharacters(encodedCertificate);
writer.writeEndElement(); // X509Certificate
writer.writeEndElement(); // X509Data
writer.writeEndElement(); // KeyInfo
writer.writeEndElement(); // KeyDescriptor
}
writer.writeEndElement(); // SPSSODescriptor
}
private static Document signMetaInfo(X509Certificate signingCert, Key signingKey,
Document doc, String referenceID
) throws Exception {
String signatureMethod = null;
if ("SHA1withDSA".equals(signingCert.getSigAlgName())) {
signatureMethod = SignatureMethod.DSA_SHA1;
} else if ("SHA1withRSA".equals(signingCert.getSigAlgName())) {
signatureMethod = SignatureMethod.RSA_SHA1;
} else if ("SHA256withRSA".equals(signingCert.getSigAlgName())) {
signatureMethod = SignatureMethod.RSA_SHA1;
} else {
LOG.error("Unsupported signature method: " + signingCert.getSigAlgName());
throw new RuntimeException("Unsupported signature method: " + signingCert.getSigAlgName());
}
List<Transform> transformList = new ArrayList<>();
transformList.add(XML_SIGNATURE_FACTORY.newTransform(Transform.ENVELOPED, (TransformParameterSpec)null));
transformList.add(XML_SIGNATURE_FACTORY.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE,
(C14NMethodParameterSpec)null));
// Create a Reference to the enveloped document (in this case,
// you are signing the whole document, so a URI of "" signifies
// that, and also specify the SHA1 digest algorithm and
// the ENVELOPED Transform.
Reference ref =
XML_SIGNATURE_FACTORY.newReference("#" + referenceID,
XML_SIGNATURE_FACTORY.newDigestMethod(DigestMethod.SHA1, null),
transformList,
null, null);
// Create the SignedInfo.
SignedInfo si =
XML_SIGNATURE_FACTORY.newSignedInfo(
XML_SIGNATURE_FACTORY.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE,
(C14NMethodParameterSpec)null),
XML_SIGNATURE_FACTORY.newSignatureMethod(signatureMethod, null),
Collections.singletonList(ref));
// Create the KeyInfo containing the X509Data.
KeyInfoFactory kif = XML_SIGNATURE_FACTORY.getKeyInfoFactory();
List<Object> x509Content = new ArrayList<>();
x509Content.add(signingCert.getSubjectX500Principal().getName());
x509Content.add(signingCert);
X509Data xd = kif.newX509Data(x509Content);
KeyInfo ki = kif.newKeyInfo(Collections.singletonList(xd));
// Create a DOMSignContext and specify the RSA PrivateKey and
// location of the resulting XMLSignature's parent element.
//DOMSignContext dsc = new DOMSignContext(keyEntry.getPrivateKey(), doc.getDocumentElement());
DOMSignContext dsc = new DOMSignContext(signingKey, doc.getDocumentElement());
dsc.setIdAttributeNS(doc.getDocumentElement(), null, "ID");
dsc.setNextSibling(doc.getDocumentElement().getFirstChild());
// Create the XMLSignature, but don't sign it yet.
XMLSignature signature = XML_SIGNATURE_FACTORY.newXMLSignature(si, ki);
// Marshal, generate, and sign the enveloped signature.
signature.sign(dsc);
// Output the resulting document.
return doc;
}
}