package net.deterlab.abac;
import java.io.*;
import java.math.*;
import java.text.*;
import java.util.*;
import java.security.*;
import java.security.cert.*;
import javax.security.auth.x500.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import javax.xml.crypto.*;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.*;
import javax.xml.crypto.dsig.keyinfo.*;
import javax.xml.crypto.dsig.spec.*;
import org.xml.sax.*;
import org.w3c.dom.*;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.x509.*;
import org.bouncycastle.x509.util.*;
import org.bouncycastle.openssl.*;
/**
* An ABAC credential, with or without an underlying certificate that
* represents it. These are edges in proof garphs and can be constructed from
* their constituent Roles.
* @author ISI ABAC team
* @version 1.4
*/
public class GENICredential extends Credential implements Comparable {
/** The signed XML representing this credential */
protected Document doc;
/**
* X509KeySelector implementation from
* http://www.outsourcingi.com/art-271-Programming-With-the-Java-XML-Digital-Signature-API.html
* .
*
* Straight ahead extraction of a key from an X509 cert, but it is their
* code and hard to improve on.
*/
protected class X509KeySelector extends KeySelector {
public KeySelectorResult select(KeyInfo keyInfo,
KeySelector.Purpose purpose,
AlgorithmMethod method,
XMLCryptoContext context)
throws KeySelectorException {
Iterator ki = keyInfo.getContent().iterator();
while (ki.hasNext()) {
XMLStructure info = (XMLStructure) ki.next();
if (!(info instanceof X509Data))
continue;
X509Data x509Data = (X509Data) info;
Iterator xi = x509Data.getContent().iterator();
while (xi.hasNext()) {
Object o = xi.next();
if (!(o instanceof X509Certificate))
continue;
final PublicKey key = ((X509Certificate)o).getPublicKey();
// Make sure the algorithm is compatible
// with the method.
if (algEquals(method.getAlgorithm(), key.getAlgorithm())) {
return new KeySelectorResult() {
public Key getKey() { return key; }
};
}
}
}
throw new KeySelectorException("No key found!");
}
boolean algEquals(String algURI, String algName) {
if ((algName.equalsIgnoreCase("DSA") &&
algURI.equalsIgnoreCase(SignatureMethod.DSA_SHA1)) ||
(algName.equalsIgnoreCase("RSA") &&
algURI.equalsIgnoreCase(SignatureMethod.RSA_SHA1))) {
return true;
} else {
return false;
}
}
}
/**
* Create an empty Credential.
*/
public GENICredential() {
m_head = m_tail = null;
doc = null;
id = null;
}
/**
* Create a credential from a head and tail role. This credential has no
* underlying certificate, and cannot be exported or used in real proofs.
* make_cert can create a certificate for a credential initialized this
* way.
* @param head the Role at the head of the credential
* @param tail the Role at the tail of the credential
*/
public GENICredential(Role head, Role tail) {
m_head = head;
m_tail = tail;
doc = null;
id = null;
}
/**
* Do the credential extraction from an input stream. This parses the
* certificate from the input stream and saves it. The contents must be
* validated and parsed later.
* @param stream the InputStream to read the certificate from.
* @throws IOException if the stream is unparsable
*/
protected void read_certificate(InputStream stream)
throws IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = null;
if ( dbf == null )
throw new IOException("Cannot get DocumentBuilderFactory!?");
try {
/* Note this setting is required to find the properly
* namespace-scoped signature block in the credential.
*/
dbf.setNamespaceAware(true);
if ( (db = dbf.newDocumentBuilder()) == null )
throw new IOException("Cannot get DocumentBuilder!?");
doc = db.parse(stream);
doc.normalizeDocument();
}
catch (IllegalArgumentException ie) {
throw new IOException("null stream", ie);
}
catch (SAXException se) {
throw new IOException(se.getMessage(), se);
}
catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* Walk the document and set all instances of an xml:id attribute to be ID
* attributes in terms of the java libraries. This is needed for the
* signature code to find signed subsections. The "xml:id" is treated as
* both a namespace free ID with a colon and as an "id" identifier in the
* "xml" namespace so we don't miss it.
* @param n the root of the document to mark
*/
static protected void setIDAttrs(Node n) {
if ( n.getNodeType() == Node.ELEMENT_NODE) {
Element e = (Element) n;
String id = e.getAttribute("xml:id");
if ( id != null && id.length() > 0 )
e.setIdAttribute("xml:id", true);
id = e.getAttributeNS("xml", "id");
if ( id != null && id.length() > 0 )
e.setIdAttributeNS("xml", "id", true);
}
for (Node nn = n.getFirstChild(); nn != null; nn = nn.getNextSibling())
setIDAttrs(nn);
}
/**
* Return the child of Node n that has the given name, if there is one.
* @param n a Node to search
* @param name the name to search for
* @return a Node with the name, may be null
*/
static protected Node getChildByName(Node n, String name) {
if (name == null ) return null;
for (Node nn = n.getFirstChild(); nn != null; nn = nn.getNextSibling())
if ( nn.getNodeType() == Node.ELEMENT_NODE &&
name.equals(nn.getNodeName())) return nn;
return null;
}
/**
* Find the X509Certificate in the Signature element and convert it to an
* ABAC identity. This assumes a KeyInfo in that section holding an
* X509Data section containing the certificate in an X509Certificate
* section. Any of that not being found will cause a failure. If the
* Identity cannot be created a Gignature exception is thrown.
* @param n a Node pointing to a Signature section
* @return an Identity constructed frm the X509 Certificate
* @throws MissingIssuerException if the Identity cannot be created from the
* certificate.
*/
static protected Identity getIdentity(Node n)
throws MissingIssuerException {
Identity rv = null;
Node nn = getChildByName(n, "KeyInfo");
String certStr = null;
if ( nn == null ) return null;
if ( ( nn = getChildByName(nn, "X509Data")) == null ) return null;
if ( ( nn = getChildByName(nn, "X509Certificate")) == null ) return null;
if ( ( certStr = nn.getTextContent()) == null ) return null;
try {
certStr = "-----BEGIN CERTIFICATE-----\n" +
certStr +
"\n-----END CERTIFICATE-----";
return new Identity(new StringReader(certStr));
}
catch (Exception e) {
throw new MissingIssuerException(e.getMessage(), e);
}
}
/**
* Initialize a credential from parsed certificate. Validiate it against
* the given identities and parse out the roles. Note that catching
* java.security.GeneralSecurityException catches most of the exceptions
* this throws.
* @param ids a Collection of Identities to use in validating the cert
* @throws CertInvalidException if the stream is unparsable
* @throws MissingIssuerException if none of the Identities can validate the
* certificate
* @throws BadSignatureException if the signature check fails
*/
protected void init(Collection ids)
throws ABACException {
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS,
"Signature");
DOMValidateContext valContext = null;
XMLSignature signature = null;
try {
if (nl.getLength() == 0)
throw new CertInvalidException("Cannot find Signature element");
setIDAttrs(doc);
valContext = new DOMValidateContext(new X509KeySelector(),
nl.item(0));
if ( valContext == null )
throw new ABACException("No validation context!?");
signature = fac.unmarshalXMLSignature(valContext);
if (signature == null)
throw new BadSignatureException("Cannot unmarshal signature");
if (!signature.validate(valContext))
throw new BadSignatureException("bad signature");
load_roles();
id = getIdentity(nl.item(0));
if ( !ids.contains(id) ) ids.add(id);
if (!id.getKeyID().equals(m_head.principal()))
throw new MissingIssuerException("Issuer ID and left hand " +
"side principal disagree");
}
catch (ABACException ae) {
throw ae;
}
catch (Exception e) {
throw new BadSignatureException(e.getMessage(), e);
}
}
/**
* Parse a credential from an InputStream and initialize the role from it.
* Combine read_credential(stream) and init(ids). Note that catching
* java.security.GeneralSecurityException catches most of the exceptions
* this throws.
* @param stream the InputStream to read the certificate from.
* @param ids a Collection of Identities to use in validating the cert
* @throws CertInvalidException if the stream is unparsable
* @throws MissingIssuerException if none of the Identities can validate the
* certificate
* @throws BadSignatureException if the signature check fails
*/
protected void init(InputStream stream, Collection ids)
throws ABACException {
try {
read_certificate(stream);
}
catch (IOException e) {
throw new CertInvalidException("Cannot parse cert", e);
}
if (doc == null) throw new CertInvalidException("Unknown Format");
init(ids);
}
/**
* Create a credential from an attribute cert in a file. Throws an
* exception if the cert file can't be opened or if there's a format
* problem with the cert. Note that catching
* java.security.GeneralSecurityException catches most of the exceptions
* this throws.
* @param filename a String containing the filename to read
* @param ids a Collection of Identities to use in validating the cert
* @throws CertInvalidException if the stream is unparsable
* @throws MissingIssuerException if none of the Identities can validate the
* certificate
* @throws BadSignatureException if the signature check fails
*/
GENICredential(String filename, Collection ids)
throws ABACException {
try {
init(new FileInputStream(filename), ids);
}
catch (FileNotFoundException e) {
throw new CertInvalidException("Bad filename", e);
}
}
/**
* Create a credential from an attribute cert in a file. Throws an
* exception if the cert file can't be opened or if there's a format
* problem with the cert. Note that catching
* java.security.GeneralSecurityException catches most of the exceptions
* this throws.
* @param file the File to read
* @param ids a Collection of Identities to use in validating the cert
* @throws CertInvalidException if the stream is unparsable
* @throws MissingIssuerException if none of the Identities can validate the
* certificate
* @throws BadSignatureException if the signature check fails
*/
GENICredential(File file, Collection ids)
throws ABACException {
try {
init(new FileInputStream(file), ids);
}
catch (FileNotFoundException e) {
throw new CertInvalidException("Bad filename", e);
}
}
/**
* Create a credential from an InputStream. Throws an exception if the
* stream can't be parsed or if there's a format problem with the cert.
* Note that catching java.security.GeneralSecurityException catches most
* of the exceptions this throws.
* @param s the InputStream to read
* @param ids a Collection of Identities to use in validating the cert
* @throws CertInvalidException if the stream is unparsable
* @throws MissingIssuerException if none of the Identities can validate the
* certificate
* @throws BadSignatureException if the signature check fails
*/
GENICredential(InputStream s, Collection ids)
throws ABACException { init(s, ids); }
/**
* Encode the abac credential's XML. This is straight line code that
* directly builds the credential.
* @return a Node, the place to attach signatures
* @throws ABACException if any of the XML construction fails
*/
protected Node make_rt0_content() throws ABACException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = null;
SimpleDateFormat df = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss'Z'");
StringBuffer expBuf = new StringBuffer();
/* This is a weirdness to cope with the GENI format. They have a naked
* xml:id specifier without any namespace declarations in the
* credential. Notice that this is the opposite of the setting used in
* init to read a credential. */
dbf.setNamespaceAware(false);
if ( dbf == null )
throw new ABACException("Cannot get DocumentBuilderFactory!?");
try {
db = dbf.newDocumentBuilder();
}
catch (ParserConfigurationException pe) {
throw new ABACException("Cannot get DocumentBuilder!?" +
pe.getMessage(), pe);
}
doc = db.newDocument();
if ( doc == null )
throw new ABACException("No Document");
Element root = doc.createElement("signed-credential");
Element cred = doc.createElement("credential");
Element sig = doc.createElement("signatures");
Element e = doc.createElement("type");
Node text = doc.createTextNode("abac");
doc.appendChild(root);
cred.setAttribute("xml:id", "ref0");
/* So that the signing code can find the section to sign */
cred.setIdAttribute("xml:id", true);
root.appendChild(cred);
e.appendChild(text);
cred.appendChild(e);
/* XXX: pass in valid time */
df.format(new Date(System.currentTimeMillis()
+ (3600L * 1000L * 24L * 365L)), expBuf,
new FieldPosition(0));
e = doc.createElement("expires");
text = doc.createTextNode(expBuf.toString());
e.appendChild(text);
cred.appendChild(e);
e = doc.createElement("version");
text = doc.createTextNode("1.0");
e.appendChild(text);
cred.appendChild(e);
e = doc.createElement("rt0");
text = doc.createTextNode(m_head + "<-" + m_tail);
e.appendChild(text);
cred.appendChild(e);
root.appendChild(sig);
return sig;
}
/**
* Create a certificate from this credential issued by the given identity.
* This is the signed XML ABAC credential.
* @param i the Identity that will issue the certificate
* @throws ABACException if xml creation fails
* @throws MissingIssuerException if the issuer is bad
* @throws BadSignatureException if the signature creation fails
*/
public void make_cert(Identity i)
throws ABACException {
X509Certificate cert = i.getCertificate();
KeyPair kp = i.getKeyPair();
PublicKey pubKey = null;
PrivateKey privKey = null;
if ( cert == null )
throw new MissingIssuerException("No credential in identity?");
if ( kp == null )
throw new MissingIssuerException("No keypair in identity?");
pubKey = kp.getPublic();
if ((privKey = kp.getPrivate()) == null )
throw new MissingIssuerException("No private ket in identity");
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
Reference ref = null;
SignedInfo si = null;
KeyInfoFactory kif = fac.getKeyInfoFactory();
/* The