// Package signedxml transforms and validates signedxml documents package signedxml import ( "crypto" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "fmt" "log" "os" "strings" "github.com/beevik/etree" ) var logger = log.New(os.Stdout, "DEBUG-SIGNEDXML: ", log.Ldate|log.Ltime|log.Lshortfile) func init() { hashAlgorithms = map[string]crypto.Hash{ "http://www.w3.org/2001/04/xmldsig-more#md5": crypto.MD5, "http://www.w3.org/2000/09/xmldsig#sha1": crypto.SHA1, "http://www.w3.org/2001/04/xmldsig-more#sha224": crypto.SHA224, "http://www.w3.org/2001/04/xmlenc#sha256": crypto.SHA256, "http://www.w3.org/2001/04/xmldsig-more#sha384": crypto.SHA384, "http://www.w3.org/2001/04/xmlenc#sha512": crypto.SHA512, "http://www.w3.org/2001/04/xmlenc#ripemd160": crypto.RIPEMD160, } signatureAlgorithms = map[string]x509.SignatureAlgorithm{ "http://www.w3.org/2001/04/xmldsig-more#rsa-md2": x509.MD2WithRSA, "http://www.w3.org/2001/04/xmldsig-more#rsa-md5": x509.MD5WithRSA, "http://www.w3.org/2000/09/xmldsig#rsa-sha1": x509.SHA1WithRSA, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": x509.SHA256WithRSA, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384": x509.SHA384WithRSA, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": x509.SHA512WithRSA, "http://www.w3.org/2000/09/xmldsig#dsa-sha1": x509.DSAWithSHA1, "http://www.w3.org/2000/09/xmldsig#dsa-sha256": x509.DSAWithSHA256, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1": x509.ECDSAWithSHA1, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256": x509.ECDSAWithSHA256, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384": x509.ECDSAWithSHA384, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512": x509.ECDSAWithSHA512, } CanonicalizationAlgorithms = map[string]CanonicalizationAlgorithm{ "http://www.w3.org/2000/09/xmldsig#enveloped-signature": EnvelopedSignature{}, "http://www.w3.org/2001/10/xml-exc-c14n#": ExclusiveCanonicalization{}, "http://www.w3.org/2001/10/xml-exc-c14n#WithComments": ExclusiveCanonicalization{WithComments: true}, } } // CanonicalizationAlgorithm defines an interface for processing an XML // document into a standard format. // // If any child elements are in the Transform node, the entire transform node // will be passed to the Process method through the transformXML parameter as an // XML string. This is necessary for transforms that need additional processing // data, like XPath (http://www.w3.org/TR/xmldsig-core/#sec-XPath). If there are // no child elements in Transform (or CanonicalizationMethod), then an empty // string will be passed through. type CanonicalizationAlgorithm interface { Process(inputXML string, transformXML string) (outputXML string, err error) } // CanonicalizationAlgorithms maps the CanonicalizationMethod or // Transform Algorithm URIs to a type that implements the // CanonicalizationAlgorithm interface. // // Implementations are provided for the following transforms: // http://www.w3.org/2001/10/xml-exc-c14n# (ExclusiveCanonicalization) // http://www.w3.org/2001/10/xml-exc-c14n#WithComments (ExclusiveCanonicalizationWithComments) // http://www.w3.org/2000/09/xmldsig#enveloped-signature (EnvelopedSignature) // // Custom implementations can be added to the map var CanonicalizationAlgorithms map[string]CanonicalizationAlgorithm var hashAlgorithms map[string]crypto.Hash var signatureAlgorithms map[string]x509.SignatureAlgorithm // signatureData provides options for verifying a signed XML document type signatureData struct { xml *etree.Document signature *etree.Element signedInfo *etree.Element sigValue string sigAlgorithm x509.SignatureAlgorithm canonAlgorithm CanonicalizationAlgorithm } // SetSignature can be used to assign an external signature for the XML doc // that Validator will verify func (s *signatureData) SetSignature(sig string) error { doc := etree.NewDocument() err := doc.ReadFromString(sig) s.signature = doc.Root() return err } func (s *signatureData) parseEnvelopedSignature() error { sig := s.xml.FindElement(".//Signature") if sig != nil { s.signature = sig } else { return errors.New("signedxml: Unable to find a unique signature element " + "in the xml document. The signature must either be enveloped in the " + "xml doc or externally assigned to Validator.SetSignature") } return nil } func (s *signatureData) parseSignedInfo() error { s.signedInfo = nil s.signedInfo = s.signature.SelectElement("SignedInfo") if s.signedInfo == nil { return errors.New("signedxml: unable to find SignedInfo element") } // move the Signature level namespace down to SignedInfo so that the signature // value will match up if s.signedInfo.Space != "" { attr := s.signature.SelectAttr(s.signedInfo.Space) if attr != nil { s.signedInfo.Attr = []etree.Attr{*attr} } } else { attr := s.signature.SelectAttr("xmlns") if attr != nil { s.signedInfo.Attr = []etree.Attr{*attr} } } // Copy SignedInfo xmlns: into itself if it does not exist and is defined as a root attribute root := s.xml.Root() if root != nil { sigNS := root.SelectAttr("xmlns:" + s.signedInfo.Space) if sigNS != nil { if s.signedInfo.SelectAttr("xmlns:"+s.signedInfo.Space) == nil { s.signedInfo.CreateAttr("xmlns:"+s.signedInfo.Space, sigNS.Value) } } } return nil } func (s *signatureData) parseSigValue() error { s.sigValue = "" sigValueElement := s.signature.SelectElement("SignatureValue") if sigValueElement != nil { s.sigValue = sigValueElement.Text() return nil } return errors.New("signedxml: unable to find SignatureValue") } func (s *signatureData) parseSigAlgorithm() error { s.sigAlgorithm = x509.UnknownSignatureAlgorithm sigMethod := s.signedInfo.SelectElement("SignatureMethod") var sigAlgoURI string if sigMethod == nil { return errors.New("signedxml: Unable to find SignatureMethod element") } sigAlgoURI = sigMethod.SelectAttrValue("Algorithm", "") sigAlgo, ok := signatureAlgorithms[sigAlgoURI] if ok { s.sigAlgorithm = sigAlgo return nil } return errors.New("signedxml: Unable to find Algorithm in SignatureMethod element") } func (s *signatureData) parseCanonAlgorithm() error { s.canonAlgorithm = nil canonMethod := s.signedInfo.SelectElement("CanonicalizationMethod") var canonAlgoURI string if canonMethod == nil { return errors.New("signedxml: Unable to find CanonicalizationMethod element") } canonAlgoURI = canonMethod.SelectAttrValue("Algorithm", "") canonAlgo, ok := CanonicalizationAlgorithms[canonAlgoURI] if ok { s.canonAlgorithm = canonAlgo return nil } return errors.New("signedxml: Unable to find Algorithm in " + "CanonicalizationMethod element") } func getReferencedXML(reference *etree.Element, inputDoc *etree.Document) (outputDoc *etree.Document, err error) { uri := reference.SelectAttrValue("URI", "") uri = strings.Replace(uri, "#", "", 1) // populate doc with the referenced xml from the Reference URI if uri == "" { outputDoc = inputDoc } else { path := fmt.Sprintf(".//[@ID='%s']", uri) e := inputDoc.FindElement(path) if e != nil { outputDoc = etree.NewDocument() outputDoc.SetRoot(e.Copy()) } else { // SAML v1.1 Assertions use AssertionID path := fmt.Sprintf(".//[@AssertionID='%s']", uri) e := inputDoc.FindElement(path) if e != nil { outputDoc = etree.NewDocument() outputDoc.SetRoot(e.Copy()) } } } if outputDoc == nil { return nil, errors.New("signedxml: unable to find refereced xml") } return outputDoc, nil } func getCertFromPEMString(pemString string) (*x509.Certificate, error) { pubkey := fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", pemString) pemBlock, _ := pem.Decode([]byte(pubkey)) if pemBlock == nil { return &x509.Certificate{}, errors.New("Could not parse Public Key PEM") } if pemBlock.Type != "PUBLIC KEY" { return &x509.Certificate{}, errors.New("Found wrong key type") } cert, err := x509.ParseCertificate(pemBlock.Bytes) return cert, err } func processTransform(transform *etree.Element, docIn *etree.Document) (docOut *etree.Document, err error) { transformAlgoURI := transform.SelectAttrValue("Algorithm", "") if transformAlgoURI == "" { return nil, errors.New("signedxml: unable to find Algorithm in Transform") } transformAlgo, ok := CanonicalizationAlgorithms[transformAlgoURI] if !ok { return nil, fmt.Errorf("signedxml: unable to find matching transform"+ "algorithm for %s in CanonicalizationAlgorithms", transformAlgoURI) } var transformContent string if transform.ChildElements() != nil { tDoc := etree.NewDocument() tDoc.SetRoot(transform.Copy()) transformContent, err = tDoc.WriteToString() if err != nil { return nil, err } } docString, err := docIn.WriteToString() if err != nil { return nil, err } docString, err = transformAlgo.Process(docString, transformContent) if err != nil { return nil, err } docOut = etree.NewDocument() docOut.ReadFromString(docString) return docOut, nil } func calculateHash(reference *etree.Element, doc *etree.Document) (string, error) { digestMethodElement := reference.SelectElement("DigestMethod") if digestMethodElement == nil { return "", errors.New("signedxml: unable to find DigestMethod") } digestMethodURI := digestMethodElement.SelectAttrValue("Algorithm", "") if digestMethodURI == "" { return "", errors.New("signedxml: unable to find Algorithm in DigestMethod") } digestAlgo, ok := hashAlgorithms[digestMethodURI] if !ok { return "", fmt.Errorf("signedxml: unable to find matching hash"+ "algorithm for %s in hashAlgorithms", digestMethodURI) } doc.WriteSettings.CanonicalEndTags = true doc.WriteSettings.CanonicalText = true doc.WriteSettings.CanonicalAttrVal = true h := digestAlgo.New() docBytes, err := doc.WriteToBytes() if err != nil { return "", err } //ioutil.WriteFile("C:/Temp/SignedXML/Suspect.xml", docBytes, 0644) //s, _ := doc.WriteToString() //logger.Println(s) h.Write(docBytes) d := h.Sum(nil) calculatedValue := base64.StdEncoding.EncodeToString(d) return calculatedValue, nil }