/*
* Copyright 2001-2005 Internet2
*
* 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 gov.nih.nci.cagrid.opensaml;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.Key;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import org.apache.xml.security.algorithms.MessageDigestAlgorithm;
import org.apache.xml.security.c14n.Canonicalizer;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.keys.KeyInfo;
import org.apache.xml.security.keys.content.X509Data;
import org.apache.xml.security.signature.Reference;
import org.apache.xml.security.signature.SignedInfo;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.apache.xml.security.transforms.params.InclusiveNamespaces;
/**
* Abstract base class for all SAML objects that can be signed
*
* @author Scott Cantor
* @created March 25, 2002
*/
public abstract class SAMLSignedObject extends SAMLObject implements Cloneable
{
private XMLSignature sig = null;
private boolean sig_from_parse = false;
/**
* Debugging aid to access the internal XML Signature implementation
*
* @return Returns a Java object corresponding to the native class
* used by the underlying XML Signature implementation to represent
* a signature. Be careful using this method, unless you're debugging
* or know what you're doing.
*/
public Object getNativeSignature() {
return sig;
}
/**
* Gets the ID of the signed object
*
* @return The XML ID
*/
public abstract String getId();
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLObject#fromDOM(Element e)
*/
public void fromDOM(Element e) throws SAMLException {
super.fromDOM(e);
// Locate the Signature beneath the root.
Element n = XML.getFirstChildElement(e, XML.XMLSIG_NS, "Signature");
if (n!=null) {
try {
sig=new XMLSignature((Element)n,"");
sig_from_parse = true;
}
catch (XMLSecurityException ex) {
throw new InvalidCryptoException("SAMLSignedObject.fromDOM() detected an XML security exception: " + ex.getMessage(),ex);
}
}
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLObject#toDOM(boolean xmlns)
*/
public Node toDOM(boolean xmlns) throws SAMLException {
if (root == null) {
// The purpose of the override is to reuse the document used to create
// the signature, if we have one.
if (sig != null)
return toDOM(sig.getDocument(),xmlns);
}
// If no signature, just let the base class handle it.
return super.toDOM(xmlns);
}
/**
* Places the signature into the object's DOM to prepare for signing<p>
*
* Must be overridden by subclass that knows where to place it</p>
* @throws SAMLException Thrown if an error occurs while placing the signature
*/
protected abstract void insertSignature() throws SAMLException;
/**
* Get the DOM element containing the signature
*
* @return The ds:Signature element of a signature
*/
protected Element getSignatureElement()
{
return (sig!=null) ? sig.getElement() : null;
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLObject#setDirty()
*/
protected void setDirty(boolean flag) {
if (flag)
unsign();
super.setDirty(flag);
}
/**
* Remove the signature and turn this into an unsigned object.
* Modifying an object after signing will automatically unsign it.
*/
public void unsign() {
if (sig != null && sig.getElement().getParentNode() != null)
sig.getElement().getParentNode().removeChild(sig.getElement());
sig = null;
}
/**
* Sign the SAML object according to the input parameters, using a default
* digest algorithm.
*
* @param sigalg The XML signature algorithm to apply
* @param k The secret or private key to sign the resulting digest
* @param certs The public key certificate(s) to embed in the object, if any
* @throws SAMLException Thrown if an error occurs while constructing the signature
*/
public void sign(String sigalg, Key k, Collection certs)
throws SAMLException {
sign(sigalg,null,k,certs);
}
/**
* Sign the SAML object according to the input parameters
*
* @param sigalg The XML signature algorithm to apply
* @param digalg The digest algorithm to apply
* @param k The secret or private key to sign the resulting digest
* @param certs The public key certificate(s) to embed in the object, if any
* @throws SAMLException Thrown if an error occurs while constructing the signature
*/
public void sign(String sigalg, String digalg, Key k, Collection certs)
throws SAMLException
{
unsign();
// Generate the DOM if not already built, and anchor the DOM in the document.
toDOM();
plantRoot();
try
{
// Build the empty signature.
sig=new XMLSignature(root.getOwnerDocument(),"",sigalg,Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
// Have the object place it in the proper place.
insertSignature();
Transforms transforms = new Transforms(sig.getDocument());
transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
transforms.item(1).getElement().appendChild(
new InclusiveNamespaces(root.getOwnerDocument(),config.getProperty("gov.nih.nci.cagrid.opensaml.inclusive-namespace-prefixes")).getElement()
);
if (config.getBooleanProperty("gov.nih.nci.cagrid.opensaml.compatibility-mode"))
sig.addDocument("",transforms,(digalg!=null) ? digalg : MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA1);
else
sig.addDocument("#" + getId(),transforms,(digalg!=null) ? digalg : MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA1);
// Add any X.509 certificates provided.
X509Data x509 = new X509Data(root.getOwnerDocument());
if (certs!=null)
{
int count = 0;
Iterator i=certs.iterator();
while (i.hasNext())
{
Object cert=i.next();
if (cert instanceof X509Certificate) {
if (!i.hasNext() && count > 0) {
// Last (but not only) cert in chain. Only add if it's not self-signed.
if (((X509Certificate)cert).getSubjectDN().equals(((X509Certificate)cert).getIssuerDN()))
break;
}
x509.addCertificate((X509Certificate)cert);
}
count++;
}
}
if (x509.lengthCertificate()>0)
{
KeyInfo keyinfo = new KeyInfo(root.getOwnerDocument());
keyinfo.add(x509);
sig.getElement().appendChild(keyinfo.getElement());
}
// Finally, sign the thing.
sig.sign(k);
}
catch (XMLSecurityException e)
{
unsign();
throw new InvalidCryptoException("SAMLSignedObject.sign() detected an XML security exception: " + e.getMessage(),e);
}
}
/**
* Verifies the signature using only the keying material included within it
*
* @throws SAMLException Thrown if the signature is invalid or if an error occurs
*/
public void verify()
throws SAMLException
{
verify((Key)null);
}
/**
* Verifies the signature using the keying material provided
*
* @param cert A public key certificate to use in verifying the signature
* @throws SAMLException Thrown if the signature is invalid or if an error occurs
*/
public void verify(Certificate cert)
throws SAMLException
{
verify(cert.getPublicKey());
}
/**
* Verifies the signature using the keying material provided
*
* @param k A secret or public key to use in verifying the signature
* @throws SAMLException Thrown if the signature is invalid or if an error occurs
*/
public void verify(Key k)
throws SAMLException
{
if (!isSigned())
throw new InvalidCryptoException("SAMLSignedObject.verify() can't verify unsigned object");
try
{
// Validate the signature content by checking for specific Transforms.
boolean valid=false;
SignedInfo si=sig.getSignedInfo();
if (si.getLength()==1)
{
Reference ref = si.item(0);
if (ref.getURI() == null || ref.getURI().equals("") || ref.getURI().equals("#" + getId()))
{
Transforms trans = ref.getTransforms();
for (int i=0; i < trans.getLength(); i++)
{
if (trans.item(i).getURI().equals(Transforms.TRANSFORM_ENVELOPED_SIGNATURE))
valid = true;
else if (!trans.item(i).getURI().equals(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS))
{
valid = false;
break;
}
}
}
}
if (!valid)
throw new InvalidCryptoException("SAMLSignedObject.verify() detected an invalid signature profile");
// If k is null, try and find a key inside the signature.
if (k == null)
{
if (sig_from_parse)
k=sig.getKeyInfo().getPublicKey();
else
{
// This is really, ugly, but when the signature hasn't been fully built from a DOM,
// none of the interesting bits of keying material are reachable via the API.
// We have to serialize out the KeyInfo piece, and reparse it.
ByteArrayOutputStream out = new ByteArrayOutputStream();
Canonicalizer c = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS);
out.write(c.canonicalizeSubtree(sig.getElement().getLastChild()));
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
KeyInfo temp = new KeyInfo(XML.parserPool.parse(in).getDocumentElement(),"");
k=temp.getPublicKey();
}
}
if (!sig.checkSignatureValue(k))
throw new InvalidCryptoException("SAMLSignedObject.verify() failed to validate signature value");
}
catch (XMLSecurityException e)
{
throw new InvalidCryptoException("SAMLSignedObject.verify() detected an XML security exception: " + e.getMessage(),e);
}
catch (java.io.IOException e)
{
throw new InvalidCryptoException("SAMLSignedObject.verify() detected an I/O exception: " + e.getMessage(),e);
}
catch (SAXException e)
{
throw new InvalidCryptoException("SAMLSignedObject.verify() detected a XML parsing exception: " + e.getMessage(),e);
}
}
/**
* Returns an iterator over the X.509 certificates included in the signature, if any
*
* @return Provides access to the certificates
* @throws SAMLException Thrown if the signature is missing
*/
public Iterator getX509Certificates() throws SAMLException
{
if (isSigned()) {
KeyInfo ki=sig.getKeyInfo();
if (ki!=null && ki.containsX509Data()) {
try {
for (int i=0; i<ki.lengthX509Data(); i++) {
X509Data x509 = ki.itemX509Data(i);
if (x509.containsCertificate()) {
ArrayList certs=new ArrayList(x509.lengthCertificate());
for (int j=0; j<x509.lengthCertificate(); j++)
certs.add(x509.itemCertificate(j).getX509Certificate());
return certs.iterator();
}
}
}
catch (XMLSecurityException e) {
throw new InvalidCryptoException("SAMLSignedObject.getX509Certificates() detected an XML security exception: " + e.getMessage(),e);
}
}
throw new InvalidCryptoException("SAMLSignedObject.getX509Certificates() can't find any X.509 certificates in signature");
}
throw new InvalidCryptoException("SAMLSignedObject.getX509Certificates() can't examine unsigned object");
}
/**
* Returns the signing algorithm identifier from the signature
*
* @return The algorithm identifier
* @throws SAMLException Thrown if the signature is missing
*/
public String getSignatureAlgorithm()
throws SAMLException
{
if (isSigned())
return sig.getSignedInfo().getSignatureMethodURI();
throw new InvalidCryptoException("SAMLSignedObject.getSignatureAlgorithm() can't examine unsigned object");
}
/**
* Returns the digest algorithm identifier from the signature
*
* @return The algorithm identifier
* @throws SAMLException Thrown if the signature is missing
*/
public String getDigestAlgorithm()
throws SAMLException
{
if (isSigned()) {
SignedInfo si=sig.getSignedInfo();
if (si.getLength()==1) {
Reference ref;
try {
ref = si.item(0);
return ref.getMessageDigestAlgorithm().getAlgorithmURI();
}
catch (XMLSecurityException e) {
throw new InvalidCryptoException("SAMLSignedObject.getDigestAlgorithm() detected an XML security exception: " + e.getMessage(),e);
}
}
}
throw new InvalidCryptoException("SAMLSignedObject.getDigestAlgorithm() can't examine unsigned or improperly signed object");
}
/**
* Returns true iff the object contains a signature
*
* @return true iff the object contains a signature
*/
public boolean isSigned()
{
return (sig!=null);
}
/**
* Copies a SAML object such that no dependencies exist between the original
* and the copy.
*
* @return The new object
* @see java.lang.Object#clone()
*/
protected Object clone()
throws CloneNotSupportedException
{
SAMLSignedObject dup=(SAMLSignedObject)super.clone();
// Clear the signature before returning the copy.
dup.sig = null;
return dup;
}
}