signedxml.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. // Package signedxml transforms and validates signedxml documents
  2. package signedxml
  3. import (
  4. "crypto"
  5. "crypto/x509"
  6. "encoding/base64"
  7. "encoding/pem"
  8. "errors"
  9. "fmt"
  10. "log"
  11. "os"
  12. "strings"
  13. "github.com/beevik/etree"
  14. )
  15. var logger = log.New(os.Stdout, "DEBUG-SIGNEDXML: ", log.Ldate|log.Ltime|log.Lshortfile)
  16. func init() {
  17. hashAlgorithms = map[string]crypto.Hash{
  18. "http://www.w3.org/2001/04/xmldsig-more#md5": crypto.MD5,
  19. "http://www.w3.org/2000/09/xmldsig#sha1": crypto.SHA1,
  20. "http://www.w3.org/2001/04/xmldsig-more#sha224": crypto.SHA224,
  21. "http://www.w3.org/2001/04/xmlenc#sha256": crypto.SHA256,
  22. "http://www.w3.org/2001/04/xmldsig-more#sha384": crypto.SHA384,
  23. "http://www.w3.org/2001/04/xmlenc#sha512": crypto.SHA512,
  24. "http://www.w3.org/2001/04/xmlenc#ripemd160": crypto.RIPEMD160,
  25. }
  26. signatureAlgorithms = map[string]x509.SignatureAlgorithm{
  27. "http://www.w3.org/2001/04/xmldsig-more#rsa-md2": x509.MD2WithRSA,
  28. "http://www.w3.org/2001/04/xmldsig-more#rsa-md5": x509.MD5WithRSA,
  29. "http://www.w3.org/2000/09/xmldsig#rsa-sha1": x509.SHA1WithRSA,
  30. "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": x509.SHA256WithRSA,
  31. "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384": x509.SHA384WithRSA,
  32. "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": x509.SHA512WithRSA,
  33. "http://www.w3.org/2000/09/xmldsig#dsa-sha1": x509.DSAWithSHA1,
  34. "http://www.w3.org/2000/09/xmldsig#dsa-sha256": x509.DSAWithSHA256,
  35. "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1": x509.ECDSAWithSHA1,
  36. "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256": x509.ECDSAWithSHA256,
  37. "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384": x509.ECDSAWithSHA384,
  38. "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512": x509.ECDSAWithSHA512,
  39. }
  40. CanonicalizationAlgorithms = map[string]CanonicalizationAlgorithm{
  41. "http://www.w3.org/2000/09/xmldsig#enveloped-signature": EnvelopedSignature{},
  42. "http://www.w3.org/2001/10/xml-exc-c14n#": ExclusiveCanonicalization{},
  43. "http://www.w3.org/2001/10/xml-exc-c14n#WithComments": ExclusiveCanonicalization{WithComments: true},
  44. }
  45. }
  46. // CanonicalizationAlgorithm defines an interface for processing an XML
  47. // document into a standard format.
  48. //
  49. // If any child elements are in the Transform node, the entire transform node
  50. // will be passed to the Process method through the transformXML parameter as an
  51. // XML string. This is necessary for transforms that need additional processing
  52. // data, like XPath (http://www.w3.org/TR/xmldsig-core/#sec-XPath). If there are
  53. // no child elements in Transform (or CanonicalizationMethod), then an empty
  54. // string will be passed through.
  55. type CanonicalizationAlgorithm interface {
  56. Process(inputXML string, transformXML string) (outputXML string, err error)
  57. }
  58. // CanonicalizationAlgorithms maps the CanonicalizationMethod or
  59. // Transform Algorithm URIs to a type that implements the
  60. // CanonicalizationAlgorithm interface.
  61. //
  62. // Implementations are provided for the following transforms:
  63. // http://www.w3.org/2001/10/xml-exc-c14n# (ExclusiveCanonicalization)
  64. // http://www.w3.org/2001/10/xml-exc-c14n#WithComments (ExclusiveCanonicalizationWithComments)
  65. // http://www.w3.org/2000/09/xmldsig#enveloped-signature (EnvelopedSignature)
  66. //
  67. // Custom implementations can be added to the map
  68. var CanonicalizationAlgorithms map[string]CanonicalizationAlgorithm
  69. var hashAlgorithms map[string]crypto.Hash
  70. var signatureAlgorithms map[string]x509.SignatureAlgorithm
  71. // signatureData provides options for verifying a signed XML document
  72. type signatureData struct {
  73. xml *etree.Document
  74. signature *etree.Element
  75. signedInfo *etree.Element
  76. sigValue string
  77. sigAlgorithm x509.SignatureAlgorithm
  78. canonAlgorithm CanonicalizationAlgorithm
  79. refIDAttribute string
  80. }
  81. // SetSignature can be used to assign an external signature for the XML doc
  82. // that Validator will verify
  83. func (s *signatureData) SetSignature(sig string) error {
  84. doc := etree.NewDocument()
  85. err := doc.ReadFromString(sig)
  86. s.signature = doc.Root()
  87. return err
  88. }
  89. func (s *signatureData) parseEnvelopedSignature() error {
  90. sig := s.xml.FindElement(".//Signature")
  91. if sig != nil {
  92. s.signature = sig
  93. } else {
  94. return errors.New("signedxml: Unable to find a unique signature element " +
  95. "in the xml document. The signature must either be enveloped in the " +
  96. "xml doc or externally assigned to Validator.SetSignature")
  97. }
  98. return nil
  99. }
  100. func (s *signatureData) parseSignedInfo() error {
  101. s.signedInfo = nil
  102. s.signedInfo = s.signature.SelectElement("SignedInfo")
  103. if s.signedInfo == nil {
  104. return errors.New("signedxml: unable to find SignedInfo element")
  105. }
  106. // move the Signature level namespace down to SignedInfo so that the signature
  107. // value will match up
  108. if s.signedInfo.Space != "" {
  109. attr := s.signature.SelectAttr(s.signedInfo.Space)
  110. if attr != nil {
  111. s.signedInfo.Attr = []etree.Attr{*attr}
  112. }
  113. } else {
  114. attr := s.signature.SelectAttr("xmlns")
  115. if attr != nil {
  116. s.signedInfo.Attr = []etree.Attr{*attr}
  117. }
  118. }
  119. // Copy SignedInfo xmlns: into itself if it does not exist and is defined as a root attribute
  120. root := s.xml.Root()
  121. if root != nil {
  122. sigNS := root.SelectAttr("xmlns:" + s.signedInfo.Space)
  123. if sigNS != nil {
  124. if s.signedInfo.SelectAttr("xmlns:"+s.signedInfo.Space) == nil {
  125. s.signedInfo.CreateAttr("xmlns:"+s.signedInfo.Space, sigNS.Value)
  126. }
  127. }
  128. }
  129. return nil
  130. }
  131. func (s *signatureData) parseSigValue() error {
  132. s.sigValue = ""
  133. sigValueElement := s.signature.SelectElement("SignatureValue")
  134. if sigValueElement != nil {
  135. s.sigValue = sigValueElement.Text()
  136. return nil
  137. }
  138. return errors.New("signedxml: unable to find SignatureValue")
  139. }
  140. func (s *signatureData) parseSigAlgorithm() error {
  141. s.sigAlgorithm = x509.UnknownSignatureAlgorithm
  142. sigMethod := s.signedInfo.SelectElement("SignatureMethod")
  143. var sigAlgoURI string
  144. if sigMethod == nil {
  145. return errors.New("signedxml: Unable to find SignatureMethod element")
  146. }
  147. sigAlgoURI = sigMethod.SelectAttrValue("Algorithm", "")
  148. if sigAlgoURI == "" {
  149. return errors.New("signedxml: Unable to find Algorithm in " +
  150. "SignatureMethod element")
  151. }
  152. sigAlgo, ok := signatureAlgorithms[sigAlgoURI]
  153. if ok {
  154. s.sigAlgorithm = sigAlgo
  155. return nil
  156. }
  157. return errors.New("signedxml: Unsupported Algorithm " + sigAlgoURI + " in " +
  158. "SignatureMethod")
  159. }
  160. func (s *signatureData) parseCanonAlgorithm() error {
  161. s.canonAlgorithm = nil
  162. canonMethod := s.signedInfo.SelectElement("CanonicalizationMethod")
  163. var canonAlgoURI string
  164. if canonMethod == nil {
  165. return errors.New("signedxml: Unable to find CanonicalizationMethod element")
  166. }
  167. canonAlgoURI = canonMethod.SelectAttrValue("Algorithm", "")
  168. if canonAlgoURI == "" {
  169. return errors.New("signedxml: Unable to find Algorithm in " +
  170. "CanonicalizationMethod element")
  171. }
  172. canonAlgo, ok := CanonicalizationAlgorithms[canonAlgoURI]
  173. if ok {
  174. s.canonAlgorithm = canonAlgo
  175. return nil
  176. }
  177. return errors.New("signedxml: Unsupported Algorithm " + canonAlgoURI + " in " +
  178. "CanonicalizationMethod")
  179. }
  180. func (s *signatureData) getReferencedXML(reference *etree.Element, inputDoc *etree.Document) (outputDoc *etree.Document, err error) {
  181. uri := reference.SelectAttrValue("URI", "")
  182. uri = strings.Replace(uri, "#", "", 1)
  183. // populate doc with the referenced xml from the Reference URI
  184. if uri == "" {
  185. outputDoc = inputDoc
  186. } else {
  187. refIDAttribute := "ID"
  188. if s.refIDAttribute != "" {
  189. refIDAttribute = s.refIDAttribute
  190. }
  191. path := fmt.Sprintf(".//[@%s='%s']", refIDAttribute, uri)
  192. e := inputDoc.FindElement(path)
  193. if e != nil {
  194. outputDoc = etree.NewDocument()
  195. outputDoc.SetRoot(e.Copy())
  196. } else {
  197. // SAML v1.1 Assertions use AssertionID
  198. path := fmt.Sprintf(".//[@AssertionID='%s']", uri)
  199. e := inputDoc.FindElement(path)
  200. if e != nil {
  201. outputDoc = etree.NewDocument()
  202. outputDoc.SetRoot(e.Copy())
  203. }
  204. }
  205. }
  206. if outputDoc == nil {
  207. return nil, errors.New("signedxml: unable to find refereced xml")
  208. }
  209. return outputDoc, nil
  210. }
  211. func getCertFromPEMString(pemString string) (*x509.Certificate, error) {
  212. pubkey := fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----",
  213. pemString)
  214. pemBlock, _ := pem.Decode([]byte(pubkey))
  215. if pemBlock == nil {
  216. return &x509.Certificate{}, errors.New("Could not parse Public Key PEM")
  217. }
  218. if pemBlock.Type != "PUBLIC KEY" {
  219. return &x509.Certificate{}, errors.New("Found wrong key type")
  220. }
  221. cert, err := x509.ParseCertificate(pemBlock.Bytes)
  222. return cert, err
  223. }
  224. func processTransform(transform *etree.Element,
  225. docIn *etree.Document) (docOut *etree.Document, err error) {
  226. transformAlgoURI := transform.SelectAttrValue("Algorithm", "")
  227. if transformAlgoURI == "" {
  228. return nil, errors.New("signedxml: unable to find Algorithm in Transform")
  229. }
  230. transformAlgo, ok := CanonicalizationAlgorithms[transformAlgoURI]
  231. if !ok {
  232. return nil, fmt.Errorf("signedxml: unable to find matching transform"+
  233. "algorithm for %s in CanonicalizationAlgorithms", transformAlgoURI)
  234. }
  235. var transformContent string
  236. if transform.ChildElements() != nil {
  237. tDoc := etree.NewDocument()
  238. tDoc.SetRoot(transform.Copy())
  239. transformContent, err = tDoc.WriteToString()
  240. if err != nil {
  241. return nil, err
  242. }
  243. }
  244. docString, err := docIn.WriteToString()
  245. if err != nil {
  246. return nil, err
  247. }
  248. docString, err = transformAlgo.Process(docString, transformContent)
  249. if err != nil {
  250. return nil, err
  251. }
  252. docOut = etree.NewDocument()
  253. docOut.ReadFromString(docString)
  254. return docOut, nil
  255. }
  256. func calculateHash(reference *etree.Element, doc *etree.Document) (string, error) {
  257. digestMethodElement := reference.SelectElement("DigestMethod")
  258. if digestMethodElement == nil {
  259. return "", errors.New("signedxml: unable to find DigestMethod")
  260. }
  261. digestMethodURI := digestMethodElement.SelectAttrValue("Algorithm", "")
  262. if digestMethodURI == "" {
  263. return "", errors.New("signedxml: unable to find Algorithm in DigestMethod")
  264. }
  265. digestAlgo, ok := hashAlgorithms[digestMethodURI]
  266. if !ok {
  267. return "", fmt.Errorf("signedxml: unable to find matching hash"+
  268. "algorithm for %s in hashAlgorithms", digestMethodURI)
  269. }
  270. doc.WriteSettings.CanonicalEndTags = true
  271. doc.WriteSettings.CanonicalText = true
  272. doc.WriteSettings.CanonicalAttrVal = true
  273. h := digestAlgo.New()
  274. docBytes, err := doc.WriteToBytes()
  275. if err != nil {
  276. return "", err
  277. }
  278. // ioutil.WriteFile("C:/Temp/SignedXML/Suspect.xml", docBytes, 0644)
  279. // s, _ := doc.WriteToString()
  280. // logger.Println(s)
  281. h.Write(docBytes)
  282. d := h.Sum(nil)
  283. calculatedValue := base64.StdEncoding.EncodeToString(d)
  284. return calculatedValue, nil
  285. }