diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..ebb1c60
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,35 @@
+name: Go
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build:
+ name: Go Build (CGO)
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ steps:
+ - name: Set up Go 1.x
+ uses: actions/setup-go@v5
+ with:
+ go-version: stable
+ id: go
+
+ - name: Check out code into the Go module directory
+ uses: actions/checkout@v4
+
+ - name: Test (Unix)
+ if: runner.os != 'Windows'
+ run: |
+ make check
+
+ - name: Test (Windows)
+ if: runner.os == 'Windows'
+ run: |
+ go vet ./...
+ go test -v ./...
diff --git a/.gitignore b/.gitignore
index daf913b..6ee6b9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,8 @@ _testmain.go
*.exe
*.test
*.prof
+
+/bin/
+/gitleaks.tar.gz
+/lint-project.sh
+/coverage.txt
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 6ece920..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-language: go
-sudo: false
-
-go:
- - 1.x
- - 1.6
- - 1.7.x
- - master
-
-before_install:
- - go get -v github.com/golang/lint/golint
-
-script:
- - go vet ./...
- - golint ./...
- - go test -cover -v ./...
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..8d5c017
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,52 @@
+## v1.2.2 (Released 2024-06-10)
+
+IMPROVEMENTS
+
+- fix: allow no Transforms when signing
+- fix: removed XML declaration before signature operations
+- examples: run 3.6 in tests
+
+BUILD
+
+- fix(deps): update module github.com/beevik/etree to v1.4.0
+
+## v1.2.1 (Released 2024-03-20)
+
+IMPROVEMENTS
+
+- fix: handle adjacent comments during canonicalization
+- feat: support http://www.w3.org/TR/2001/REC-xml-c14n-20010315
+
+## v1.2.0 (Released 2024-02-19)
+
+ADDITIONS
+
+- signer: add convenience method for creating a signer given an already built etree.Document
+
+IMPROVEMENTS
+
+- fix: support Signature element on the root level
+
+BUILD
+
+- build: use latest stable Go release
+- fix(deps): update module github.com/beevik/etree to v1.3.0
+- fix(deps): update module github.com/smartystreets/goconvey to v1.8.1
+
+## v1.1.1 (Released 2023-06-08)
+
+IMPROVEMENTS
+
+- Preserve CDATA text when signing a document
+- Typo in TestEnvelopedSignatureProcess
+
+## v1.1.0 (Released 2023-05-30)
+
+IMPROVEMENTS
+
+- feat: replace Validate() with ValidateReferences()
+- meta: use moov-io/infra Go linter script in CI
+
+## v1.0.0 (Released 2023-04-21)
+
+This is the first tagged release of the `moov-io/signedxml` package. It was previously released `ma314smith/signedxml` but has been moved over to the Moov.io Github organization.
diff --git a/LICENSE.md b/LICENSE.md
index 538c383..6cb67a3 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2015 Matt Smith
+Copyright (c) 2023 Moov.io
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 9e1c9df..1526ab3 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,23 @@
-## signedxml
+[](https://github.com/moov-io)
-[](https://travis-ci.org/ma314smith/signedxml)
-[](https://godoc.org/github.com/ma314smith/signedxml)
+## moov-io/signedxml
+
+[](https://godoc.org/github.com/moov-io/signedxml)
+[](https://github.com/moov-io/signedxml/actions)
+[](https://codecov.io/gh/moov-io/signedxml)
+[](https://goreportcard.com/report/github.com/moov-io/signedxml)
+[](https://github.com/moov-io/signedxml)
+[](https://raw.githubusercontent.com/moov-io/signedxml/master/LICENSE.md)
+[](https://slack.moov.io/)
+[](https://twitter.com/moov?lang=en)
The signedxml package transforms and validates signed xml documents. The main use case is to support Single Sign On protocols like SAML and WS-Federation.
-Other packages that provide similar functionality rely on C libraries, which makes them difficult to run across platforms without significant configuration. `signedxml` is written in pure go, and can be easily used on any platform.
+Other packages that provide similar functionality rely on C libraries, which makes them difficult to run across platforms without significant configuration. `signedxml` is written in pure go, and can be easily used on any platform. This package was originally created by [Matt Smith](https://github.com/ma314smith) and is in use at Moov Financial.
### Install
-`go get github.com/ma314smith/signedxml`
+`go get github.com/moov-io/signedxml`
### Included Algorithms
@@ -47,7 +55,7 @@ Other packages that provide similar functionality rely on C libraries, which mak
### Examples
#### Validating signed XML
-If your signed xml contains the signature and certificate, then you can just pass in the xml and call `Validate()`.
+If your signed xml contains the signature and certificate, then you can just pass in the xml and call `ValidateReferences()`.
```go
validator, err := signedxml.NewValidator(``)
xml, err = validator.ValidateReferences()
@@ -95,5 +103,31 @@ signedxml.CanonicalizationAlgorithms["http://myTranform"] = NoChangeCanonicaliza
See `envelopedsignature.go` and `exclusivecanonicalization.go` for examples of actual implementations.
-### Contributions
+### Using a custom reference ID attribute
+It is possible to set a custom reference ID attribute for both the signer and the validator. The default value is `"ID"`
+
+Signer example:
+```go
+signer.SetReferenceIDAttribute("customId")
+```
+
+Validator example:
+```go
+validator.SetReferenceIDAttribute("customId")
+```
+
+## Getting help
+
+ channel | info
+ ------- | -------
+Twitter [@moov](https://twitter.com/moov) | You can follow Moov.io's Twitter feed to get updates on our project(s). You can also tweet us questions or just share blogs or stories.
+[GitHub Issue](https://github.com/moov-io/signedxml/issues/new) | If you are able to reproduce a problem please open a GitHub Issue under the specific project that caused the error.
+[moov-io slack](https://slack.moov.io/) | Join our slack channel to have an interactive discussion about the development of the project.
+
+## Contributions
+
Contributions are welcome. Just fork the repo and send a pull request.
+
+## Releated Projects
+
+- [Moov RTP20022](http://github.com/moov-io/rtp20022) implements ISO20022 messages in Go for Real Time Payments (RTP)
diff --git a/canonicalization.go b/canonicalization.go
new file mode 100644
index 0000000..ea77157
--- /dev/null
+++ b/canonicalization.go
@@ -0,0 +1,96 @@
+package signedxml
+
+import (
+ "github.com/beevik/etree"
+ dsig "github.com/russellhaering/goxmldsig"
+)
+
+type c14N10RecCanonicalizer struct {
+ WithComments bool
+}
+
+func (c *c14N10RecCanonicalizer) ProcessElement(inputXML *etree.Element, transformXML string) (outputXML string, err error) {
+ transformedXML, err := c.processElement(inputXML, transformXML)
+ if err != nil {
+ return "", err
+ }
+ return transformedXML, nil
+}
+
+func (c *c14N10RecCanonicalizer) ProcessDocument(doc *etree.Document, transformXML string) (outputXML string, err error) {
+
+ transformedXML, err := c.processElement(doc.Root(), transformXML)
+ if err != nil {
+ return "", err
+ }
+ return transformedXML, nil
+}
+
+func (c c14N10RecCanonicalizer) Process(inputXML string, transformXML string) (outputXML string, err error) {
+ doc := etree.NewDocument()
+ err = doc.ReadFromString(inputXML)
+ if err != nil {
+ return "", err
+ }
+ return c.ProcessDocument(doc, transformXML)
+}
+
+func (c *c14N10RecCanonicalizer) processElement(inputXML *etree.Element, transformXML string) (outputXML string, err error) {
+ var canon dsig.Canonicalizer
+ if c.WithComments {
+ canon = dsig.MakeC14N10WithCommentsCanonicalizer()
+ } else {
+ canon = dsig.MakeC14N10RecCanonicalizer()
+ }
+
+ out, err := canon.Canonicalize(inputXML)
+ if err != nil {
+ return "", err
+ }
+ return string(out), nil
+}
+
+type c14N11Canonicalizer struct {
+ WithComments bool
+}
+
+func (c *c14N11Canonicalizer) ProcessElement(inputXML *etree.Element, transformXML string) (outputXML string, err error) {
+ transformedXML, err := c.processElement(inputXML, transformXML)
+ if err != nil {
+ return "", err
+ }
+ return transformedXML, nil
+}
+
+func (c *c14N11Canonicalizer) ProcessDocument(doc *etree.Document, transformXML string) (outputXML string, err error) {
+
+ transformedXML, err := c.processElement(doc.Root(), transformXML)
+ if err != nil {
+ return "", err
+ }
+ return transformedXML, nil
+}
+
+func (c c14N11Canonicalizer) Process(inputXML string, transformXML string) (outputXML string, err error) {
+ doc := etree.NewDocument()
+ err = doc.ReadFromString(inputXML)
+ if err != nil {
+ return "", err
+ }
+ return c.ProcessDocument(doc, transformXML)
+}
+
+func (c *c14N11Canonicalizer) processElement(inputXML *etree.Element, transformXML string) (outputXML string, err error) {
+ var canon dsig.Canonicalizer
+ if c.WithComments {
+ canon = dsig.MakeC14N11WithCommentsCanonicalizer()
+ } else {
+ canon = dsig.MakeC14N11Canonicalizer()
+ }
+
+ out, err := canon.Canonicalize(inputXML)
+ if err != nil {
+ return "", err
+ }
+ return string(out), nil
+}
diff --git a/envelopedsignature.go b/envelopedsignature.go
index 018a8d3..7c3b53a 100644
--- a/envelopedsignature.go
+++ b/envelopedsignature.go
@@ -2,6 +2,7 @@ package signedxml
import (
"errors"
+ "strings"
"github.com/beevik/etree"
)
@@ -12,24 +13,32 @@ import (
// algorithm
type EnvelopedSignature struct{}
-// Process is called to transfrom the XML using the EnvelopedSignature
-// algorithm
-func (e EnvelopedSignature) Process(inputXML string,
- transformXML string) (outputXML string, err error) {
+// see CanonicalizationAlgorithm.ProcessElement
+func (e EnvelopedSignature) ProcessElement(inputXML *etree.Element, transformXML string) (outputXML string, err error) {
+ transformedXML, err := e.processElement(inputXML.Copy(), transformXML)
+ if err != nil {
+ return "", err
+ }
doc := etree.NewDocument()
- doc.ReadFromString(inputXML)
- sig := doc.FindElement(".//Signature")
- if sig == nil {
- return "", errors.New("signedxml: unable to find Signature node")
+ doc.SetRoot(transformedXML)
+ docString, err := doc.WriteToString()
+ if err != nil {
+ return "", err
}
+ //logger.Println(docString)
+ return docString, nil
+}
- sigParent := sig.Parent()
- elem := sigParent.RemoveChild(sig)
- if elem == nil {
- return "", errors.New("signedxml: unable to remove Signature element")
- }
+// see CanonicalizationAlgorithm.ProcessDocument
+func (e EnvelopedSignature) ProcessDocument(doc *etree.Document,
+ transformXML string) (outputXML string, err error) {
+ transformedRoot, err := e.processElement(doc.Root().Copy(), transformXML)
+ if err != nil {
+ return "", err
+ }
+ doc.SetRoot(transformedRoot)
docString, err := doc.WriteToString()
if err != nil {
return "", err
@@ -37,3 +46,36 @@ func (e EnvelopedSignature) Process(inputXML string,
//logger.Println(docString)
return docString, nil
}
+
+// see CanonicalizationAlgorithm.Process
+func (e EnvelopedSignature) Process(inputXML string, transformXML string) (outputXML string, err error) {
+ doc := etree.NewDocument()
+ err = doc.ReadFromString(inputXML)
+ if err != nil {
+ return "", err
+ }
+ return e.ProcessDocument(doc, transformXML)
+}
+
+func (e EnvelopedSignature) processElement(inputXML *etree.Element, transformXML string) (outputXML *etree.Element, err error) {
+ sig := inputXML.FindElement("//Signature")
+ if sig == nil {
+ // TODO(adam): Why can't ./Signature (or /Signature, or //Signature) find the root Signature element?
+ if strings.EqualFold(inputXML.Tag, "Signature") {
+ sig = inputXML
+ }
+ }
+ if sig == nil {
+ return nil, errors.New("signedxml: unable to find Signature node")
+ }
+
+ sigParent := sig.Parent()
+ if sigParent != nil {
+ elem := sigParent.RemoveChild(sig)
+ if elem == nil {
+ return nil, errors.New("signedxml: unable to remove Signature element")
+ }
+ }
+
+ return inputXML, nil
+}
diff --git a/examples/canonicalization_test.go b/examples/canonicalization_test.go
new file mode 100644
index 0000000..2d20161
--- /dev/null
+++ b/examples/canonicalization_test.go
@@ -0,0 +1,217 @@
+package examples
+
+import (
+ "testing"
+
+ "github.com/moov-io/signedxml"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+var example31Input = `
+
+
+
+
+
+Hello, world!
+
+
+
+
+
+`
+
+var example31Output = `
+Hello, world!
+`
+
+var example31OutputWithComments = `
+Hello, world!
+
+
+`
+
+var example32Input = `
+
+ A B
+
+ A
+
+ B
+ A B
+ C
+
+`
+
+var example32Output = `
+
+ A B
+
+ A
+
+ B
+ A B
+ C
+
+`
+
+var example33Input = `]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+var example33Output = `
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+var example34Input = `
+
+]>
+
+ First line
Second line
+ 2
+ "0" && value<"10" ?"valid":"error"]]>
+ valid
+
+
+`
+
+var example34Output = `
+ First line
+Second line
+ 2
+ value>"0" && value<"10" ?"valid":"error"
+ valid
+
+
+`
+
+// modified to not include DTD processing. still tests for whitespace treated as
+// CDATA
+var example34ModifiedOutput = `
+ First line
+Second line
+ 2
+ value>"0" && value<"10" ?"valid":"error"
+ valid
+
+
+`
+
+var example35Input = `
+
+
+
+
+]>
+
+ &ent1;, &ent2;!
+
+
+`
+
+var example35Output = `
+ Hello, world!
+`
+
+var example36Input = `
+©`
+
+var example36Output = "\u00A9"
+
+var example37Input = `
+
+]>
+
+
+
+
+
+
+`
+
+var example37SubsetExpression = `
+
+(//. | //@* | //namespace::*)
+[
+ self::ietf:e1 or (parent::ietf:e1 and not(self::text() or self::e2))
+ or
+ count(id("E3")|ancestor-or-self::node()) = count(ancestor-or-self::node())
+]`
+
+var example37Output = ``
+
+var exampleGHIssue50Input = ``
+var exampleGHIssue50Output = ``
+
+type exampleXML struct {
+ input string
+ output string
+ withComments bool
+ expression string
+}
+
+// test examples from the spec (www.w3.org/TR/2001/REC-xml-c14n-20010315#Examples)
+func TestCanonicalizationExamples(t *testing.T) {
+ Convey("Given XML Input", t, func() {
+ cases := map[string]exampleXML{
+ "(Example 3.1 w/o Comments)": {input: example31Input, output: example31Output},
+ "(Example 3.1 w/Comments)": {input: example31Input, output: example31OutputWithComments, withComments: true},
+ "(Example 3.2)": {input: example32Input, output: example32Output},
+ // 3.3 is for Canonical NOT ExclusiveCanonical (one of the exceptions here: http://www.w3.org/TR/xml-exc-c14n/#sec-Specification)
+ // "(Example 3.3)": {input: example33Input, output: example33Output},
+ "(Example 3.4)": {input: example34Input, output: example34ModifiedOutput},
+ // "(Example 3.5)": {input: example35Input, output: example35Output},
+ // 3.6 will work, but requires a change to the etree package first:
+ // http://stackoverflow.com/questions/6002619/unmarshal-an-iso-8859-1-xml-input-in-go
+ "(Example 3.6)": {input: example36Input, output: example36Output},
+ // "(Example 3.7)": {input: example37Input, output: example37Output, expression: example37SubsetExpression},
+ "(Example from GitHub Issue #50)": {input: exampleGHIssue50Input, output: exampleGHIssue50Output},
+ }
+ for description, test := range cases {
+ Convey("When transformed "+description, func() {
+ transform := signedxml.ExclusiveCanonicalization{
+ WithComments: test.withComments,
+ }
+ resultXML, err := transform.Process(test.input, "")
+ Convey("Then the resulting XML match the example output", func() {
+ So(err, ShouldBeNil)
+ So(resultXML, ShouldEqual, test.output)
+ })
+ })
+ }
+ })
+}
+
+func TestTODO(t *testing.T) {
+ // The XML specifications cover the following examples, but our library does not successfully transform.
+ t.Logf("Input:\n%s\nOutput:\n%s\n", example33Input, example33Output) // Example 3.3
+ t.Logf("Input:\n%s\nOutput:\n%s\n", example35Input, example35Output) // Example 3.5
+ t.Logf("Input:\n%s\nOutput:\n%s\n", example37Input, example37Output) // Example 3.7
+}
diff --git a/examples/examples.go b/examples/examples_validate.go
similarity index 78%
rename from examples/examples.go
rename to examples/examples_validate.go
index 98196f0..22ec0bf 100644
--- a/examples/examples.go
+++ b/examples/examples_validate.go
@@ -1,14 +1,14 @@
-package main
+package examples
import (
"fmt"
- "io/ioutil"
+ "io"
"os"
- "github.com/ma314smith/signedxml"
+ "github.com/moov-io/signedxml"
)
-func main() {
+func ExampleValidate() {
testValidator()
testExclCanon()
}
@@ -21,15 +21,18 @@ func testValidator() {
}
defer xmlFile.Close()
- xmlBytes, _ := ioutil.ReadAll(xmlFile)
+ xmlBytes, _ := io.ReadAll(xmlFile)
validator, err := signedxml.NewValidator(string(xmlBytes))
if err != nil {
fmt.Printf("Validation Error: %s", err)
} else {
- err = validator.Validate()
+ refs, err := validator.ValidateReferences()
if err != nil {
- fmt.Printf("Validation Error: %s", err)
+ fmt.Printf("Validation Error: %s\n", err)
+ }
+ if len(refs) == 0 {
+ fmt.Println("ERROR: No Validated References")
} else {
fmt.Println("Example Validation Succeeded")
}
@@ -59,7 +62,7 @@ func testExclCanon() {
resultXML, err := transform.Process(input, "")
if err != nil {
- fmt.Printf("Transformation Error: %s", err)
+ fmt.Printf("Transformation Error: %s\n", err)
} else {
if resultXML == output {
fmt.Println("Example Tranformation Succeeded")
diff --git a/exclusivecanonicalization.go b/exclusivecanonicalization.go
index d1c01b8..94e4310 100644
--- a/exclusivecanonicalization.go
+++ b/exclusivecanonicalization.go
@@ -49,22 +49,36 @@ type ExclusiveCanonicalization struct {
namespaces map[string]string
}
-// Process is called to transfrom the XML using the ExclusiveCanonicalization
-// algorithm
-func (e ExclusiveCanonicalization) Process(inputXML string,
+// see CanonicalizationAlgorithm.ProcessElement
+func (e ExclusiveCanonicalization) ProcessElement(inputXML *etree.Element, transformXML string) (outputXML string, err error) {
+ doc := etree.NewDocument()
+ doc.SetRoot(inputXML.Copy())
+ return e.processDocument(doc, transformXML)
+}
+
+// see CanonicalizationAlgorithm.ProcessDocument
+func (e ExclusiveCanonicalization) ProcessDocument(doc *etree.Document,
transformXML string) (outputXML string, err error) {
- e.namespaces = make(map[string]string)
+ return e.processDocument(doc.Copy(), transformXML)
+}
+// see CanonicalizationAlgorithm.Process
+func (e ExclusiveCanonicalization) Process(inputXML string, transformXML string) (outputXML string, err error) {
doc := etree.NewDocument()
- doc.WriteSettings.CanonicalEndTags = true
- doc.WriteSettings.CanonicalText = true
- doc.WriteSettings.CanonicalAttrVal = true
-
err = doc.ReadFromString(inputXML)
if err != nil {
return "", err
}
+ return e.ProcessDocument(doc, transformXML)
+}
+
+func (e ExclusiveCanonicalization) processDocument(doc *etree.Document, transformXML string) (outputXML string, err error) {
+ e.namespaces = make(map[string]string)
+
+ doc.WriteSettings.CanonicalEndTags = true
+ doc.WriteSettings.CanonicalText = true
+ doc.WriteSettings.CanonicalAttrVal = true
e.loadPrefixList(transformXML)
e.processDocLevelNodes(doc)
@@ -145,12 +159,14 @@ func (e ExclusiveCanonicalization) processDocLevelNodes(doc *etree.Document) {
func (e ExclusiveCanonicalization) processRecursive(node *etree.Element,
prefixesInScope []string, defaultNS string) {
- newDefaultNS, newPrefixesInScope :=
- e.renderAttributes(node, prefixesInScope, defaultNS)
+ newDefaultNS, newPrefixesInScope := e.renderAttributes(node, prefixesInScope, defaultNS)
for i := 0; i < len(node.Child); i++ {
child := node.Child[i]
+ oldNamespaces := e.namespaces
+ e.namespaces = copyNamespace(oldNamespaces)
+
switch child := child.(type) {
case *etree.Comment:
if !e.WithComments {
@@ -160,13 +176,12 @@ func (e ExclusiveCanonicalization) processRecursive(node *etree.Element,
case *etree.Element:
e.processRecursive(child, newPrefixesInScope, newDefaultNS)
}
+
+ e.namespaces = oldNamespaces
}
}
-func (e ExclusiveCanonicalization) renderAttributes(node *etree.Element,
- prefixesInScope []string, defaultNS string) (newDefaultNS string,
- newPrefixesInScope []string) {
-
+func (e ExclusiveCanonicalization) renderAttributes(node *etree.Element, prefixesInScope []string, defaultNS string) (newDefaultNS string, newPrefixesInScope []string) {
currentNS := node.SelectAttrValue("xmlns", defaultNS)
elementAttributes := []etree.Attr{}
nsListToRender := make(map[string]string)
@@ -186,9 +201,7 @@ func (e ExclusiveCanonicalization) renderAttributes(node *etree.Element,
prefixesInScope = append(prefixesInScope, node.Space)
}
} else if defaultNS != currentNS {
- newDefaultNS = currentNS
- elementAttributes = append(elementAttributes,
- etree.Attr{Key: "xmlns", Value: currentNS})
+ elementAttributes = append(elementAttributes, etree.Attr{Key: "xmlns", Value: currentNS})
}
for _, attr := range node.Attr {
@@ -207,7 +220,10 @@ func (e ExclusiveCanonicalization) renderAttributes(node *etree.Element,
attr.Space != "xmlns" &&
!contains(prefixesInScope, attr.Space) {
- nsListToRender["xmlns:"+attr.Space] = e.namespaces[attr.Space]
+ if attr.Space != "xml" {
+ nsListToRender["xmlns:"+attr.Space] = e.namespaces[attr.Space]
+ }
+
prefixesInScope = append(prefixesInScope, attr.Space)
}
@@ -303,3 +319,11 @@ func isWhitespace(s string) bool {
}
return true
}
+
+func copyNamespace(namespaces map[string]string) map[string]string {
+ newVersion := map[string]string{}
+ for index, element := range namespaces {
+ newVersion[index] = element
+ }
+ return newVersion
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..46e0070
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,22 @@
+module github.com/moov-io/signedxml
+
+go 1.21.0
+
+toolchain go1.24.0
+
+require (
+ github.com/beevik/etree v1.5.0
+ github.com/russellhaering/goxmldsig v1.4.0
+ github.com/smartystreets/goconvey v1.8.1
+ github.com/stretchr/testify v1.10.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/gopherjs/gopherjs v1.17.2 // indirect
+ github.com/jonboulle/clockwork v0.4.0 // indirect
+ github.com/jtolds/gls v4.20.0+incompatible // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/smarty/assertions v1.15.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..b44b80c
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,53 @@
+github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
+github.com/beevik/etree v1.4.0 h1:oz1UedHRepuY3p4N5OjE0nK1WLCqtzHf25bxplKOHLs=
+github.com/beevik/etree v1.4.0/go.mod h1:cyWiXwGoasx60gHvtnEh5x8+uIjUVnjWqBvEnhnqKDA=
+github.com/beevik/etree v1.4.1 h1:PmQJDDYahBGNKDcpdX8uPy1xRCwoCGVUiW669MEirVI=
+github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
+github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
+github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
+github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
+github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
+github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
+github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys=
+github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
+github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
+github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
+github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
+github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/makefile b/makefile
new file mode 100644
index 0000000..3c87c47
--- /dev/null
+++ b/makefile
@@ -0,0 +1,19 @@
+.PHONY: check
+check:
+ifeq ($(OS),Windows_NT)
+ @echo "Skipping checks on Windows, currently unsupported."
+else
+ @wget -O lint-project.sh https://raw.githubusercontent.com/moov-io/infra/master/go/lint-project.sh
+ @chmod +x ./lint-project.sh
+ COVER_THRESHOLD=70.0 ./lint-project.sh
+endif
+
+.PHONY: clean
+clean:
+ @rm -rf ./bin/ ./tmp/ coverage.txt misspell* staticcheck lint-project.sh
+
+.PHONY: cover-test cover-web
+cover-test:
+ go test -coverprofile=cover.out ./...
+cover-web:
+ go tool cover -html=cover.out
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..d41cc92
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,12 @@
+{
+ "extends": [
+ "config:base"
+ ],
+ "groupName": "all",
+ "packageRules": [
+ {
+ "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
+ "automerge": true
+ }
+ ]
+}
diff --git a/signedxml.go b/signedxml.go
index 8202e4e..de193f2 100644
--- a/signedxml.go
+++ b/signedxml.go
@@ -8,15 +8,12 @@ import (
"encoding/pem"
"errors"
"fmt"
- "log"
- "os"
"strings"
"github.com/beevik/etree"
+ dsig "github.com/russellhaering/goxmldsig"
)
-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,
@@ -48,6 +45,10 @@ func init() {
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315": ExclusiveCanonicalization{},
"http://www.w3.org/2001/10/xml-exc-c14n#": ExclusiveCanonicalization{},
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments": ExclusiveCanonicalization{WithComments: true},
+ dsig.CanonicalXML11AlgorithmId.String(): &c14N11Canonicalizer{},
+ dsig.CanonicalXML11WithCommentsAlgorithmId.String(): &c14N11Canonicalizer{WithComments: true},
+ dsig.CanonicalXML10RecAlgorithmId.String(): &c14N10RecCanonicalizer{},
+ dsig.CanonicalXML10WithCommentsAlgorithmId.String(): &c14N10RecCanonicalizer{WithComments: true},
}
}
@@ -61,6 +62,18 @@ func init() {
// no child elements in Transform (or CanonicalizationMethod), then an empty
// string will be passed through.
type CanonicalizationAlgorithm interface {
+ // ProcessElement is called to transform an XML Element within an XML Document
+ // using the implementing algorithm
+ ProcessElement(inputXML *etree.Element, transformXML string) (outputXML string, err error)
+
+ // ProcessDocument is called to transform an XML Document using the implementing
+ // algorithm.
+ ProcessDocument(doc *etree.Document, transformXML string) (outputXML string, err error)
+
+ // Process is called to transform a string containing XML text using the implementing
+ // algorithm. The inputXML parameter should contain a complete XML Document. It is not
+ // correct to use this function on XML fragments. Retained for backward comparability.
+ // Use ProcessElement or ProcessDocument if possible.
Process(inputXML string, transformXML string) (outputXML string, err error)
}
@@ -69,9 +82,10 @@ type CanonicalizationAlgorithm interface {
// 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)
+//
+// 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
@@ -86,6 +100,7 @@ type signatureData struct {
sigValue string
sigAlgorithm x509.SignatureAlgorithm
canonAlgorithm CanonicalizationAlgorithm
+ refIDAttribute string
}
// SetSignature can be used to assign an external signature for the XML doc
@@ -98,7 +113,7 @@ func (s *signatureData) SetSignature(sig string) error {
}
func (s *signatureData) parseEnvelopedSignature() error {
- sig := s.xml.FindElement(".//Signature")
+ sig := s.xml.FindElement("//Signature")
if sig != nil {
s.signature = sig
} else {
@@ -165,13 +180,19 @@ func (s *signatureData) parseSigAlgorithm() error {
}
sigAlgoURI = sigMethod.SelectAttrValue("Algorithm", "")
+ if sigAlgoURI == "" {
+ return errors.New("signedxml: Unable to find Algorithm in " +
+ "SignatureMethod element")
+ }
+
sigAlgo, ok := signatureAlgorithms[sigAlgoURI]
if ok {
s.sigAlgorithm = sigAlgo
return nil
}
- return errors.New("signedxml: Unable to find Algorithm in SignatureMethod element")
+ return errors.New("signedxml: Unsupported Algorithm " + sigAlgoURI + " in " +
+ "SignatureMethod")
}
func (s *signatureData) parseCanonAlgorithm() error {
@@ -184,24 +205,33 @@ func (s *signatureData) parseCanonAlgorithm() error {
}
canonAlgoURI = canonMethod.SelectAttrValue("Algorithm", "")
+ if canonAlgoURI == "" {
+ return errors.New("signedxml: Unable to find Algorithm in " +
+ "CanonicalizationMethod element")
+ }
+
canonAlgo, ok := CanonicalizationAlgorithms[canonAlgoURI]
if ok {
s.canonAlgorithm = canonAlgo
return nil
}
- return errors.New("signedxml: Unable to find Algorithm in " +
- "CanonicalizationMethod element")
+ return errors.New("signedxml: Unsupported Algorithm " + canonAlgoURI + " in " +
+ "CanonicalizationMethod")
}
-func getReferencedXML(reference *etree.Element, inputDoc *etree.Document) (outputDoc *etree.Document, err error) {
+func (s *signatureData) 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)
+ refIDAttribute := "ID"
+ if s.refIDAttribute != "" {
+ refIDAttribute = s.refIDAttribute
+ }
+ path := fmt.Sprintf(".//[@%s='%s']", refIDAttribute, uri)
e := inputDoc.FindElement(path)
if e != nil {
outputDoc = etree.NewDocument()
@@ -265,12 +295,7 @@ func processTransform(transform *etree.Element,
}
}
- docString, err := docIn.WriteToString()
- if err != nil {
- return nil, err
- }
-
- docString, err = transformAlgo.Process(docString, transformContent)
+ docString, err := transformAlgo.ProcessDocument(docIn, transformContent)
if err != nil {
return nil, err
}
@@ -308,13 +333,39 @@ func calculateHash(reference *etree.Element, doc *etree.Document) (string, error
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
}
+
+// removeXMLDeclaration searches for and removes the XML declaration processing instruction.
+func removeXMLDeclaration(doc *etree.Document) {
+ for _, t := range doc.Child {
+ if p, ok := t.(*etree.ProcInst); ok && p.Target == "xml" {
+ doc.RemoveChild(p)
+ break
+ }
+ }
+
+ // Remove CharData right after removing the XML declaration
+ for _, t2 := range doc.Child {
+ if p2, ok := t2.(*etree.CharData); ok {
+ doc.RemoveChild(p2)
+ }
+ }
+}
+
+// parseXML parses an XML string into an etree.Document and removes the XML declaration if present.
+func parseXML(xml string) (*etree.Document, error) {
+ doc := etree.NewDocument()
+ if err := doc.ReadFromString(xml); err != nil {
+ return nil, err
+ }
+
+ doc.ReadSettings.PreserveCData = true
+ removeXMLDeclaration(doc)
+
+ return doc, nil
+}
diff --git a/signedxml_test.go b/signedxml_test.go
index f68ee06..46f86e9 100644
--- a/signedxml_test.go
+++ b/signedxml_test.go
@@ -2,27 +2,96 @@ package signedxml
import (
"crypto/x509"
+ "encoding/base64"
"encoding/pem"
"fmt"
- "io/ioutil"
+ "io"
"os"
+ "path/filepath"
"testing"
"github.com/beevik/etree"
. "github.com/smartystreets/goconvey/convey"
)
+func TestSignatureLogicWithCLibXMLSec(t *testing.T) {
+ pemString, _ := os.ReadFile("./testdata/rsa.crt")
+ pemBlock, _ := pem.Decode([]byte(pemString))
+ cert, _ := x509.ParseCertificate(pemBlock.Bytes)
+
+ b64Bytes, _ := os.ReadFile("./testdata/rsa.key.b64")
+ pemString, _ = base64.StdEncoding.DecodeString(string(b64Bytes))
+ pemBlock, _ = pem.Decode([]byte(pemString))
+ key, _ := x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
+
+ xmlGeneratedSign, _ := os.ReadFile("./testdata/signature-generate-using-xmlseclib.xml")
+ signedDoc := etree.NewDocument()
+ signedDoc.ReadFromString(string(xmlGeneratedSign))
+ signature := signedDoc.Root()
+ digestValueElement := signature.SelectElement("Signature").SelectElement("SignedInfo").SelectElement("Reference").SelectElement("DigestValue")
+
+ Convey("Given an XML, certificate, and RSA key", t, func() {
+ xml, _ := os.ReadFile("./testdata/doc.xml")
+
+ Convey("When generating the signature", func() {
+ signer, _ := NewSigner(string(xml))
+ xmlStr, err := signer.Sign(key)
+ generatedDigestValue := signer.signedInfo.SelectElement("Reference").SelectElement("DigestValue")
+ Convey("Then no error occurs", func() {
+ So(err, ShouldBeNil)
+ })
+ Convey("And the signature should be valid", func() {
+ validator, _ := NewValidator(xmlStr)
+ validator.Certificates = append(validator.Certificates, *cert)
+ refs, err := validator.ValidateReferences()
+ So(err, ShouldBeNil)
+ So(len(refs), ShouldEqual, 1)
+ })
+ Convey("And signature digest should match with signature generated using xmlsec", func() {
+ So(generatedDigestValue.Text(), ShouldEqual, digestValueElement.Text())
+ })
+ Convey("And signature generated using xmlsec should be valid", func() {
+ validator, _ := NewValidator(string(xmlGeneratedSign))
+ validator.Certificates = append(validator.Certificates, *cert)
+ refs, err := validator.ValidateReferences()
+ So(err, ShouldBeNil)
+ So(len(refs), ShouldEqual, 1)
+ })
+ })
+ })
+}
+
func TestSign(t *testing.T) {
+ pemString, _ := os.ReadFile("./testdata/rsa.crt")
+ pemBlock, _ := pem.Decode([]byte(pemString))
+ cert, _ := x509.ParseCertificate(pemBlock.Bytes)
+
+ b64Bytes, _ := os.ReadFile("./testdata/rsa.key.b64")
+ pemString, _ = base64.StdEncoding.DecodeString(string(b64Bytes))
+ pemBlock, _ = pem.Decode([]byte(pemString))
+ key, _ := x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
+
Convey("Given an XML, certificate, and RSA key", t, func() {
- pemString, _ := ioutil.ReadFile("./testdata/rsa.crt")
- pemBlock, _ := pem.Decode([]byte(pemString))
- cert, _ := x509.ParseCertificate(pemBlock.Bytes)
+ xml, _ := os.ReadFile("./testdata/nosignature.xml")
- pemString, _ = ioutil.ReadFile("./testdata/rsa.key")
- pemBlock, _ = pem.Decode([]byte(pemString))
- key, _ := x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
+ Convey("When generating the signature", func() {
+ signer, _ := NewSigner(string(xml))
+ xmlStr, err := signer.Sign(key)
+ Convey("Then no error occurs", func() {
+ So(err, ShouldBeNil)
+ })
+ Convey("And the signature should be valid", func() {
+ validator, _ := NewValidator(xmlStr)
+ validator.Certificates = append(validator.Certificates, *cert)
+ refs, err := validator.ValidateReferences()
+ So(err, ShouldBeNil)
+ So(len(refs), ShouldEqual, 1)
+ })
+ })
+ })
- xml, _ := ioutil.ReadFile("./testdata/nosignature.xml")
+ Convey("Given an XML with http://www.w3.org/TR/2001/REC-xml-c14n-20010315 canonicalization, certificate, and RSA key", t, func() {
+ xml, _ := os.ReadFile("./testdata/nosignature2.xml")
Convey("When generating the signature", func() {
signer, _ := NewSigner(string(xml))
@@ -33,10 +102,69 @@ func TestSign(t *testing.T) {
Convey("And the signature should be valid", func() {
validator, _ := NewValidator(xmlStr)
validator.Certificates = append(validator.Certificates, *cert)
- err := validator.Validate()
+ refs, err := validator.ValidateReferences()
+ So(err, ShouldBeNil)
+ So(len(refs), ShouldEqual, 1)
+ })
+ })
+ })
+
+ Convey("Given an XML with custom referenceIDAttribute, certificate, and RSA key", t, func() {
+ xml, _ := os.ReadFile("./testdata/nosignature-custom-reference-id-attribute.xml")
+
+ Convey("When generating the signature with custom referenceIDAttribute", func() {
+ signer, _ := NewSigner(string(xml))
+ signer.SetReferenceIDAttribute("customId")
+ xmlStr, err := signer.Sign(key)
+ Convey("Then no error occurs", func() {
+ So(err, ShouldBeNil)
+ })
+ Convey("And the signature should be valid", func() {
+ validator, _ := NewValidator(xmlStr)
+ validator.Certificates = append(validator.Certificates, *cert)
+ validator.SetReferenceIDAttribute("customId")
+ refs, err := validator.ValidateReferences()
So(err, ShouldBeNil)
+ So(len(refs), ShouldEqual, 1)
+ })
+ Convey("And the signature should be valid, but validation fail if referenceIDAttribute NOT SET", func() {
+ validator, _ := NewValidator(xmlStr)
+ validator.Certificates = append(validator.Certificates, *cert)
+ refs, err := validator.ValidateReferences()
+ So(err, ShouldNotBeNil)
+ So(len(refs), ShouldEqual, 0)
})
})
+
+ Convey("When generating the signature referenceIDAttribute NOT SET", func() {
+ signer, _ := NewSigner(string(xml))
+ _, err := signer.Sign(key)
+ Convey("Then an error should occur", func() {
+ So(err, ShouldNotBeNil)
+ })
+ })
+
+ })
+
+ Convey("Signature at the Root level, surrounding the Object", t, func() {
+ xml, _ := os.ReadFile(filepath.Join("testdata", "root-level-signature.xml"))
+
+ doc := etree.NewDocument()
+ doc.ReadFromBytes(xml)
+ signature := doc.FindElement("//Signature")
+ t.Logf("signature: %#v", signature)
+
+ signer, _ := NewSigner(string(xml))
+ signer.SetReferenceIDAttribute("Id")
+ xmlStr, err := signer.Sign(key)
+ So(err, ShouldBeNil)
+
+ validator, _ := NewValidator(xmlStr)
+ validator.SetReferenceIDAttribute("Id")
+ validator.Certificates = append(validator.Certificates, *cert)
+ refs, err := validator.ValidateReferences()
+ So(err, ShouldBeNil)
+ So(len(refs), ShouldEqual, 1)
})
}
@@ -53,18 +181,19 @@ func TestValidate(t *testing.T) {
}
for description, test := range cases {
- Convey(fmt.Sprintf("When Validate is called %s", description), func() {
+ Convey("When Validate is called "+description, func() {
xmlFile, err := os.Open(test)
if err != nil {
fmt.Println("Error opening file:", err)
}
defer xmlFile.Close()
- xmlBytes, _ := ioutil.ReadAll(xmlFile)
+ xmlBytes, _ := io.ReadAll(xmlFile)
validator, _ := NewValidator(string(xmlBytes))
- err = validator.Validate()
+ refs, err := validator.ValidateReferences()
Convey("Then no error occurs", func() {
So(err, ShouldBeNil)
So(validator.SigningCert().PublicKey, ShouldNotBeNil)
+ So(len(refs), ShouldEqual, 1)
})
})
}
@@ -73,56 +202,77 @@ func TestValidate(t *testing.T) {
xmlFile, _ := os.Open("./testdata/bbauth-metadata.xml")
sig := `LPHoiAkLmA/TGIuVbgpwlFLXL+ymEBc7TS0fC9/PTQU=d2CXq9GEeDKvMxdpxtTRKQ8TGeSWhJOVPs8LMD0ObeE1t/YGiAm9keorMiki4laxbWqAuOmwHK3qNHogRFgkIYi3fnuFBzMrahXf0n3A5PRXXW1m768Z92GKV09pGuygKUXCtXzwq0seDi6PnzMCJFzFXGQWnum0paa8Oz+6425Sn0zO0fT3ttp3AXeXGyNXwYPYcX1iEMB7klUlyiz2hmn8ngCIbTkru7uIeyPmQ5WD4SS/qQaL4yb3FZibXoe/eRXrbkG1NAJCw9OWw0jsvWncE1rKFaqEMbz21fXSDhh3Ls+p9yVf+xbCrpkT0FMqjTHpNRvccMPZe/hDGrHV7Q==MIIDNzCCAh+gAwIBAgIQQVK+d/vLK4ZNMDk15HGUoTANBgkqhkiG9w0BAQ0FADAoMSYwJAYDVQQDEx1CbGFja2JhdWQgQXV0aGVudGljYXRpb24gMjAyMjAeFw0wMDAxMDEwNDAwMDBaFw0yMjAxMDEwNDAwMDBaMCgxJjAkBgNVBAMTHUJsYWNrYmF1ZCBBdXRoZW50aWNhdGlvbiAyMDIyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArgByjSPVvP4DLf/l7QRz7G7Dhkdns0QjWslnWejHlFIezfkJ4NGPp0+5CRCFYBqAb7DhqyK77Ek5xdzmwgYb1X6GD6UDltWvN5BBFAw69I6/K0WjguFUxk19T7xdc8vTCNAMi+6Ys49O3EBNnI2fiqDoBdMjUTud1F04QY3N2rZWkjMrHV+CnzhoUwqsO/ABWrDbkPzBXdOOIbsKH0k0IP8q2+35pe1y2nxtB9f1fCyCmbUH2HINMHahDmxxanTW5Jy14yD/HSRTFQF9JMTeglomWq5q9VPx0NjsEJR+B5IkRCTf75LoYrrr/fvQm3aummmYPdHauXCBrcm0moX4ywIDAQABo10wWzBZBgNVHQEEUjBQgBDCHOfardZfhltQSbLqsukZoSowKDEmMCQGA1UEAxMdQmxhY2tiYXVkIEF1dGhlbnRpY2F0aW9uIDIwMjKCEEFSvnf7yyuGTTA5NeRxlKEwDQYJKoZIhvcNAQENBQADggEBADrOhfRiynRKGD7EHohpPrltFScJ9+QErYMhEvteqh3C48T99uKgDY8wTqv+PI08QUSZuhmmF2d+W7aRBo3t8ZZepIXCwDaKo/oUp2h5Y9O3vyGDguq5ptgDTmPNYDCwWtdt0TtQYeLtCQTJVbYByWL0eT+KdzQOkAi48cPEOObSc9Biga7LTCcbCVPeJlYzmHDQUhzBt2jcy5BGvmZloI5SsoZvve6ug74qNq8IJMyzJzUp3kRuB0ruKIioSDi1lc783LDT3LSXyIbOGw/vHBEBY4Ax7FK8CqXJ2TsYqVsyo8QypqXDnveLcgK+PNEAhezhxC9hyV8j1I8pfF72ABE=`
defer xmlFile.Close()
- xmlBytes, _ := ioutil.ReadAll(xmlFile)
+ xmlBytes, _ := io.ReadAll(xmlFile)
validator := Validator{}
validator.SetXML(string(xmlBytes))
validator.SetSignature(sig)
- err := validator.Validate()
+ refs, err := validator.ValidateReferences()
Convey("Then no error occurs", func() {
So(err, ShouldBeNil)
So(validator.SigningCert().PublicKey, ShouldNotBeNil)
+ So(len(refs), ShouldEqual, 1)
})
})
Convey("When Validate is called with an external certificate and root xmlns", func() {
xmlFile, _ := os.Open("./testdata/rootxmlns.xml")
- pemString, _ := ioutil.ReadFile("./testdata/rootxmlns.crt")
+ pemString, _ := os.ReadFile("./testdata/rootxmlns.crt")
pemBlock, _ := pem.Decode([]byte(pemString))
cert, _ := x509.ParseCertificate(pemBlock.Bytes)
defer xmlFile.Close()
- xmlBytes, _ := ioutil.ReadAll(xmlFile)
+ xmlBytes, _ := io.ReadAll(xmlFile)
validator := Validator{}
validator.SetXML(string(xmlBytes))
validator.Certificates = append(validator.Certificates, *cert)
- err := validator.Validate()
+ refs, err := validator.ValidateReferences()
Convey("Then no error occurs", func() {
So(err, ShouldBeNil)
So(validator.SigningCert().PublicKey, ShouldNotBeNil)
+ So(len(refs), ShouldEqual, 1)
})
})
})
Convey("Given invalid signed XML", t, func() {
cases := map[string]string{
- "(Changed Content)": "./testdata/invalid-signature-changed content.xml",
+ "(Changed Content)": "./testdata/invalid-signature-changed-content.xml",
"(Non-existing Reference)": "./testdata/invalid-signature-non-existing-reference.xml",
- "(Wrong Sig Value)": "./testdata/invalid-signature-signature-value.xml",
}
+ for description, test := range cases {
+ Convey("When ValidateReferences is called "+description, func() {
+ xmlBytes, err := os.ReadFile(test)
+ if err != nil {
+ fmt.Println("Error reading file:", err)
+ }
+ validator, _ := NewValidator(string(xmlBytes))
+ refs, err := validator.ValidateReferences()
+ Convey("Then an error occurs", func() {
+ So(err, ShouldNotBeNil)
+ So(err.Error(), ShouldContainSubstring, "signedxml:")
+ t.Logf("%v - %d", description, len(refs))
+ So(len(refs), ShouldEqual, 0)
+ })
+ })
+ }
+
+ cases = map[string]string{
+ "(Wrong Sig Value)": "./testdata/invalid-signature-signature-value.xml",
+ }
for description, test := range cases {
- Convey(fmt.Sprintf("When Validate is called %s", description), func() {
- xmlFile, err := os.Open(test)
+ Convey("When ValidateReferences is called "+description, func() {
+ xmlBytes, err := os.ReadFile(test)
if err != nil {
- fmt.Println("Error opening file:", err)
+ fmt.Println("Error reading file:", err)
}
- defer xmlFile.Close()
- xmlBytes, _ := ioutil.ReadAll(xmlFile)
validator, _ := NewValidator(string(xmlBytes))
- err = validator.Validate()
+ refs, err := validator.ValidateReferences()
Convey("Then an error occurs", func() {
So(err, ShouldNotBeNil)
So(err.Error(), ShouldContainSubstring, "signedxml:")
+ t.Logf("%v - %d", description, len(refs))
+ So(len(refs), ShouldEqual, 1)
})
})
}
@@ -132,19 +282,7 @@ func TestValidate(t *testing.T) {
func TestEnvelopedSignatureProcess(t *testing.T) {
Convey("Given a document without a Signature elemement", t, func() {
doc := ""
- Convey("When Process is called", func() {
- envSig := EnvelopedSignature{}
- _, err := envSig.Process(doc, "")
- Convey("Then an error occurs", func() {
- So(err, ShouldNotBeNil)
- So(err.Error(), ShouldContainSubstring, "signedxml:")
- })
- })
- })
-
- Convey("Given an unparented signature element", t, func() {
- doc := ""
- Convey("When Process is called", func() {
+ Convey("When ProcessDocument is called", func() {
envSig := EnvelopedSignature{}
_, err := envSig.Process(doc, "")
Convey("Then an error occurs", func() {
@@ -257,198 +395,3 @@ func TestSignatureDataParsing(t *testing.T) {
})
})
}
-
-var example31Input = `
-
-
-
-
-
-Hello, world!
-
-
-
-
-
-`
-
-var example31Output = `
-Hello, world!
-`
-
-var example31OutputWithComments = `
-Hello, world!
-
-
-`
-
-var example32Input = `
-
- A B
-
- A
-
- B
- A B
- C
-
-`
-
-var example32Output = `
-
- A B
-
- A
-
- B
- A B
- C
-
-`
-
-var example33Input = `]>
-
-
-
-
-
-
-
-
-
-
-
-
-
-`
-
-var example33Output = `
-
-
-
-
-
-
-
-
-
-
-
-
-`
-
-var example34Input = `
-
-]>
-
- First line
Second line
- 2
- "0" && value<"10" ?"valid":"error"]]>
- valid
-
-
-`
-
-var example34Output = `
- First line
-Second line
- 2
- value>"0" && value<"10" ?"valid":"error"
- valid
-
-
-`
-
-// modified to not include DTD processing. still tests for whitespace treated as
-// CDATA
-var example34ModifiedOutput = `
- First line
-Second line
- 2
- value>"0" && value<"10" ?"valid":"error"
- valid
-
-
-`
-
-var example35Input = `
-
-
-
-
-]>
-
- &ent1;, &ent2;!
-
-
-`
-
-var example35Output = `
- Hello, world!
-`
-
-var example36Input = `
-©`
-
-var example36Output = "\u00A9"
-
-var example37Input = `
-
-]>
-
-
-
-
-
-
-`
-
-var example37SubsetExpression = `
-
-(//. | //@* | //namespace::*)
-[
- self::ietf:e1 or (parent::ietf:e1 and not(self::text() or self::e2))
- or
- count(id("E3")|ancestor-or-self::node()) = count(ancestor-or-self::node())
-]`
-
-var example37Output = ``
-
-type exampleXML struct {
- input string
- output string
- withComments bool
- expression string
-}
-
-// test examples from the spec (www.w3.org/TR/2001/REC-xml-c14n-20010315#Examples)
-func TestCanonicalizationExamples(t *testing.T) {
- Convey("Given XML Input", t, func() {
- cases := map[string]exampleXML{
- "(Example 3.1 w/o Comments)": exampleXML{input: example31Input, output: example31Output},
- "(Example 3.1 w/Comments)": exampleXML{input: example31Input, output: example31OutputWithComments, withComments: true},
- "(Example 3.2)": exampleXML{input: example32Input, output: example32Output},
- // 3.3 is for Canonical NOT ExclusiveCanonical (one of the exceptions here: http://www.w3.org/TR/xml-exc-c14n/#sec-Specification)
- //"(Example 3.3)": exampleXML{input: example33Input, output: example33Output},
- "(Example 3.4)": exampleXML{input: example34Input, output: example34ModifiedOutput},
- //"(Example 3.5)": exampleXML{input: example35Input, output: example35Output},
- // 3.6 will work, but requires a change to the etree package first:
- // http://stackoverflow.com/questions/6002619/unmarshal-an-iso-8859-1-xml-input-in-go
- //"(Example 3.6)": exampleXML{input: example36Input, output: example36Output},
- //"(Example 3.7)": exampleXML{input: example37Input, output: example37Output, expression: example37SubsetExpression},
- }
- for description, test := range cases {
- Convey(fmt.Sprintf("When transformed %s", description), func() {
- transform := ExclusiveCanonicalization{WithComments: test.withComments}
- resultXML, err := transform.Process(test.input, "")
- Convey("Then the resulting XML match the example output", func() {
- So(err, ShouldBeNil)
- So(resultXML, ShouldEqual, test.output)
- })
- })
- }
- })
-}
diff --git a/signer.go b/signer.go
index 024f37b..05a33e9 100644
--- a/signer.go
+++ b/signer.go
@@ -17,11 +17,11 @@ func init() {
signingAlgorithms = map[x509.SignatureAlgorithm]cryptoHash{
// MD2 not supported
// x509.MD2WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.MD2},
- x509.MD5WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.MD5},
- x509.SHA1WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA1},
- x509.SHA256WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA256},
- x509.SHA384WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA384},
- x509.SHA512WithRSA: cryptoHash{algorithm: "rsa", hash: crypto.SHA512},
+ x509.MD5WithRSA: {algorithm: "rsa", hash: crypto.MD5},
+ x509.SHA1WithRSA: {algorithm: "rsa", hash: crypto.SHA1},
+ x509.SHA256WithRSA: {algorithm: "rsa", hash: crypto.SHA256},
+ x509.SHA384WithRSA: {algorithm: "rsa", hash: crypto.SHA384},
+ x509.SHA512WithRSA: {algorithm: "rsa", hash: crypto.SHA512},
// DSA not supported
// x509.DSAWithSHA1: cryptoHash{algorithm: "dsa", hash: crypto.SHA1},
// x509.DSAWithSHA256:cryptoHash{algorithm: "dsa", hash: crypto.SHA256},
@@ -46,11 +46,15 @@ type Signer struct {
// NewSigner returns a *Signer for the XML provided
func NewSigner(xml string) (*Signer, error) {
- doc := etree.NewDocument()
- err := doc.ReadFromString(xml)
+ doc, err := parseXML(xml)
if err != nil {
return nil, err
}
+ return NewSignerFromDoc(doc)
+}
+
+// NewSignerFromDoc returns a *Signer for the Document provided
+func NewSignerFromDoc(doc *etree.Document) (*Signer, error) {
s := &Signer{signatureData: signatureData{xml: doc}}
return s, nil
}
@@ -87,12 +91,18 @@ func (s *Signer) Sign(privateKey interface{}) (string, error) {
return xml, nil
}
+// SetReferenceIDAttribute set the referenceIDAttribute
+func (s *Signer) SetReferenceIDAttribute(refIDAttribute string) {
+ s.signatureData.refIDAttribute = refIDAttribute
+}
+
func (s *Signer) setDigest() (err error) {
references := s.signedInfo.FindElements("./Reference")
for _, ref := range references {
doc := s.xml.Copy()
- if transforms := ref.SelectElement("Transforms"); transforms != nil {
+ transforms := ref.SelectElement("Transforms")
+ if transforms != nil {
for _, transform := range transforms.SelectElements("Transform") {
doc, err = processTransform(transform, doc)
if err != nil {
@@ -101,7 +111,7 @@ func (s *Signer) setDigest() (err error) {
}
}
- doc, err := getReferencedXML(ref, doc)
+ doc, err := s.getReferencedXML(ref, doc)
if err != nil {
return err
}
@@ -121,14 +131,7 @@ func (s *Signer) setDigest() (err error) {
}
func (s *Signer) setSignature() error {
- doc := etree.NewDocument()
- doc.SetRoot(s.signedInfo.Copy())
- signedInfo, err := doc.WriteToString()
- if err != nil {
- return err
- }
-
- canonSignedInfo, err := s.canonAlgorithm.Process(signedInfo, "")
+ canonSignedInfo, err := s.canonAlgorithm.ProcessElement(s.signedInfo, "")
if err != nil {
return err
}
diff --git a/testdata/doc.xml b/testdata/doc.xml
new file mode 100644
index 0000000..7e5fdf5
--- /dev/null
+++ b/testdata/doc.xml
@@ -0,0 +1,33 @@
+
+
+
+ Bruce
+ Schneier
+
+ Applied Cryptography
+
+
+ XMLSec
+ http://www.aleksey.com/xmlsec/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testdata/invalid-signature-changed content.xml b/testdata/invalid-signature-changed-content.xml
similarity index 100%
rename from testdata/invalid-signature-changed content.xml
rename to testdata/invalid-signature-changed-content.xml
diff --git a/testdata/invalid-signature-signature-value.xml b/testdata/invalid-signature-signature-value.xml
index 2dccd57..4628e2f 100644
--- a/testdata/invalid-signature-signature-value.xml
+++ b/testdata/invalid-signature-signature-value.xml
@@ -1 +1 @@
-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NameThe mutable display name of the user.SubjectAn immutable, globally unique, non-reusable identifier of the user that is unique to the application for which a token is issued.Given NameFirst name of the user.SurnameLast name of the user.Display NameDisplay name of the user.Nick NameNick name of the user.Authentication InstantThe time (UTC) when the user is authenticated to Windows Azure Active Directory.Authentication MethodThe method that Windows Azure Active Directory uses to authenticate users.ObjectIdentifierPrimary identifier for the user in the directory. Immutable, globally unique, non-reusable.TenantIdIdentifier for the user's tenant.IdentityProviderIdentity provider for the user.EmailEmail address of the user.GroupsGroups of the user.External Access TokenAccess token issued by external identity provider.External Access Token ExpirationUTC expiration time of access token issued by external identity provider.External OpenID 2.0 IdentifierOpenID 2.0 identifier issued by external identity provider.GroupsOverageClaimIssued when number of user's group claims exceeds return limit.Role ClaimRoles that the user or Service Principal is attached toRoleTemplate Id ClaimRole template id of the Built-in Directory Roles that the user is a member ofhttps://login.windows.net/8bd6e98d-e212-4022-b13f-a244fab4c253/wsfedhttps://login.windows.net/8bd6e98d-e212-4022-b13f-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https://sts.windows.net/8bd6e98d-e212-4022-b13f-a244fab4c253/https://login.windows.net/8bd6e98d-e212-4022-b13f-a244fab4c253/wsfedhttps://login.windows.net/8bd6e98d-e212-4022-b13f-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c2lnbmVkeG1sOg==MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs/uPhEf7zVizjfcr/ISGFe9+yUOqwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNGzZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uhbGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrsKsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3/bKkLSuDaKLWSqMhozdhXsIIKvJQ==MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZMIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs/uPhEf7zVizjfcr/ISGFe9+yUOqwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNGzZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uhbGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrsKsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3/bKkLSuDaKLWSqMhozdhXsIIKvJQ==NameThe mutable display name of the user.SubjectAn immutable, globally unique, non-reusable identifier of the user that is unique to the application for which a token is issued.Given NameFirst name of the user.SurnameLast name of the user.Display NameDisplay name of the user.Nick NameNick name of the user.Authentication InstantThe time (UTC) when the user is authenticated to Windows Azure Active Directory.Authentication MethodThe method that Windows Azure Active Directory uses to authenticate users.ObjectIdentifierPrimary identifier for the user in the directory. Immutable, globally unique, non-reusable.TenantIdIdentifier for the user's tenant.IdentityProviderIdentity provider for the user.EmailEmail address of the user.GroupsGroups of the user.External Access TokenAccess token issued by external identity provider.External Access Token ExpirationUTC expiration time of access token issued by external identity provider.External OpenID 2.0 IdentifierOpenID 2.0 identifier issued by external identity provider.GroupsOverageClaimIssued when number of user's group claims exceeds return limit.Role ClaimRoles that the user or Service Principal is attached toRoleTemplate Id ClaimRole template id of the Built-in Directory Roles that the user is a member ofhttps://login.windows.net/8bd6e98d-e212-4022-b13f-a244fab4c253/wsfedhttps://login.windows.net/8bd6e98d-e212-4022-b13f-a244fab4c253/wsfedMIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZMIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs/uPhEf7zVizjfcr/ISGFe9+yUOqwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNGzZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uhbGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrsKsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3/bKkLSuDaKLWSqMhozdhXsIIKvJQ==https://sts.windows.net/8bd6e98d-e212-4022-b13f-a244fab4c253/https://login.windows.net/8bd6e98d-e212-4022-b13f-a244fab4c253/wsfedhttps://login.windows.net/8bd6e98d-e212-4022-b13f-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
diff --git a/testdata/nosignature-custom-reference-id-attribute.xml b/testdata/nosignature-custom-reference-id-attribute.xml
new file mode 100644
index 0000000..c4ce8ee
--- /dev/null
+++ b/testdata/nosignature-custom-reference-id-attribute.xml
@@ -0,0 +1,37 @@
+
+ Test
+
+
+
+ Test
+
+
+
+
+
+
+
+
+
+ asdf
+
+
+ asdf
+
+
+ Test
+
+
+
+
+
+ Test
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified
+
+
+
+
\ No newline at end of file
diff --git a/testdata/nosignature2.xml b/testdata/nosignature2.xml
new file mode 100644
index 0000000..ab618e8
--- /dev/null
+++ b/testdata/nosignature2.xml
@@ -0,0 +1,37 @@
+
+ Test
+
+
+
+ Test
+
+
+
+
+
+
+
+
+
+ asdf
+
+
+ asdf
+
+
+ Test
+
+
+
+
+
+ Test
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified
+
+
+
+
\ No newline at end of file
diff --git a/testdata/root-level-signature.xml b/testdata/root-level-signature.xml
new file mode 100644
index 0000000..44ba6f7
--- /dev/null
+++ b/testdata/root-level-signature.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ testtest
+
+
+
+
diff --git a/testdata/rsa.key b/testdata/rsa.key
deleted file mode 100644
index 2dc9c43..0000000
--- a/testdata/rsa.key
+++ /dev/null
@@ -1,51 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-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
------END RSA PRIVATE KEY-----
diff --git a/testdata/rsa.key.b64 b/testdata/rsa.key.b64
new file mode 100644
index 0000000..e3e9c4c
--- /dev/null
+++ b/testdata/rsa.key.b64
@@ -0,0 +1 @@
+LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBcSsrNVIwRjkrcXJTN05QM1EwVW1ZV2xQTmFjOTlBZ2lTMFdXTjNUVzVST01mZHVGClRNd2FONDJnMXFmd3Y2RGoxRzdTcXlaRnZLdEY2QzltTy9VWmkyN1dWamx6eW1kSEo4d2hLb1RxanVFS0Erc0EKZ09FcDRpN1FxNzVUb1J0OHF1MkdoS21aQkdaMWNxV040U1JOc1NDSWZKSnR5UUNnbVJ2cEVTcTBzdjR5cTRkYgpoVm1pWHRJTHJlbmtVMWkvQTZycDZ0UkdOak9lTlZvYW1HbjhOaVd1cnJPSnJrWVBVSjhUZDZsSjBDR1dGNTBhCmV3cWN3NktVa3JRNnRIUGkyVEtmV0hsUVBDbVVmWTU1a0w4U3F4Z2dXcXp0cm5uVHFaQlJXNlBSWWxDZUY5Ui8KTVpsQVRLY0xlNEJPL01TV0tJbFUwUnk1b0VKdWhqejR4SWR2NjRQZFlRdEVpS0NzTmJYeXhnZHl4K3BNb0c0YwpuNTRCblREV0w0ZXJFTVd5K0RuTlJpNXFOQUZyNlhGOGdnRXQxZk5xc3JZSk0wSXc4ZkV6eDVyMzEzRjdFWEJuCmdBRGhvRDJsdnJjMjR6bU9aWlBDcEQrTUY3VVFoM0dmOGVIeGtHNTQ5UkpYRFlyck9nYTV5UUZPOEF0OU1WemwKcGtkbW9saHJZbWF4eVYyTFBVTklyVjhKVWZkZW8vUWVFSGl0UG5vWDVLZFNQRjhkNVRlakgzcUk2eTY1WWF0WgprK04zdXNTR3FuUjQxM09OS1JOS0RKV0F3VVpyMUptTGMyaEp2L2trc1l4YUJ0YzFHTEpqTmlha0RYWmY2MkN1CmdzL2c3YUc4bXh6bGk3Zk5UVFVJMEhiWWgxV09zTXZ0NWFOeTdrKzV3MEJINWJnaCtYempkTm5rTWNjQ0F3RUEKQVFLQ0FnQWRRVEhHUlFkNWNqMW5YSDJTN1o0bEx2VHlUUjJHZXlmU3BPbDFVQ1VNWHpJbGJBZVhWSmNWWXJ5KwpLVjhXUy9yWDkyOVMvYlVaOEE1NS9pNm4vd0UwcjB3NjZaV2hGOGVNcHZmeHN5SHg0NnAvbGluRXhBc0xXQ25iClB3d2R1bjZRMnM5cm5TdlRDdGZyQk81S0k1SXdsRGVFOHFsSmJSam1WRkJvd2xweXBqdmE0YklaMDNHRUxyc2MKQVZML04zZHcyVkRweXVxVGFGNy85dS9WaFJzTDM0ZUVaRG9GOUJmQkFRQWYrU2I5Q2VudTVLVlAwRE55T2lTTApmYTBMYlRobyttc1FjN3ZLTU16NlBCYkZJUzI4L09Mc2FzWWFUTkpONUtkakwxRitKL2R1V0pLM3pycVpFRWNzCjRWMFBOeWZSaCtSdG5EU2RaeEJGT1BTZkEraHY0OXhDbFc0NnlZc3RRWFF2YUx1UHNzNUFhY2cwU2x0eDFLTFcKTzQvc1pmS1Nqb1Ewdm16aVozU1RrYVhOYjcycUxKVUY4Sm03QXhHaEgwRVkyWjNDK0Z3ekpObnBsTTRNYlJEbgpJeHpxNVFZejVHSzZIWWI5cjZZMjRrdHJGb2xPNkVUNkRJaUdscWJyS1hhajZacFpKYjduN05Cckx3aXZ5aWpVClNSNEh4Zytib2ovdEdJaTRYZmZRcVYvVXowSXZrcWxqVE5yUEJ6dUJoQmw2WFBjRi91c3BjVVJ5a1VVb3dlNXAKTFVlbmNpeVJhYTNjSldWZklDVE1nbWFINGpvSDFiWlZsSjhxOW1BNFdlN0VWbGRLWHdLclRnblRBN2VNL2pwcQpPd21MUXpKRzVQL0Y1eVhjVVEwZmJiek1tY2V6b3RCZXBNWmRZU0p2RjRaeEthWFFjUUtDQVFFQTFmMHNoSVM3Cm90YkN3UFduZWpuWWVpOFpWODRLYThlTEZxbWYxYnlTMjBkMzlJN2d6a2pPQTFCczlETWthNElhdFhQYTJHdUsKNGo3V2FualFUT1F4Y3BqbDYySm1kd3FKK3pRazhTR3ZPcW5oWVByNzBQcnB4dFNYL2JDaW1IRmZUSWo5VklmMwpUVzh6L215N21hSlFIYTlKT2ZuMnFFMWI0My9uNEdLaFZVVFoycU9EMURoaGFwL3JDMUhQZXFKZXBWckIrQng5CllxU21ZMkMwMzdMMm40S2NjWklMRk53RmZaMTd0aFM5dnk4TTZYQU02bVpQeGkrZUJ5VExuQkNER2l4R3RQYUUKUjBkV3orU2dwVjRpN3BQcjgvNkticXFmRGJhNkJpWXBFeTZHdjNNcTU3SnpFSkZQSEppKzhySnRRTzd4TXlBZwprSkcwMG83RXkram5md0tDQVFFQXpiRUtSTUFkL2JoR1NqNDY3ZG4rN1FkWWU0TkV6dzVneklvR1RzWXp4QU0zClZRdERjK0hzc3l5Y3Naa3Y2TmdFdng1Nm51RmVWMWhKanhUMUZhM0xqU3V4Q2l3dnhKTDlUVkZ1dEhrZFJOMTQKYUZsRU1YbDdkM1F4TVJOTVpUOVRtZ0lZejhXQ3dhOS9yUWF6b3ArWExnKzE5U0tsZ244ZWZiaXB5NmdrZU1nRAppb3BvcEY5SFo4MVU4cHpGKzhydys5d0RxY1E1TmlGNy8wZzJ6V2NHY2RqNis0VmYyM0tpNktxSFk5bW1SeHkyCm5jM1Z6NUl0bW9BYlVSQVRiYStzcmlpZkhlV3daV0pYbkU5NEJtUEVnWHVNRWNXN21UMFdDZXg2UmtDRHpMelkKK3RDbVl6QythUUFJbk5pMm1TVUduUGZyOGJpSmY5RVlZWkRqd01hWnVRS0NBUUVBcys0WHVxM3BJRGs0dFF0Wgo4WG9tUGtiUUpuYUhhS3oybE8yNENmNXY5WldZbGJoMTZyNXBDM3hhd05NbjhvWUpjejJoYkl5dzBTQkpKbW5ECnJJUEwyREljVWRvQmttTDROcE5YNUxHUUorR2tNdW1YUjlkVEx1NmZocDg1ZUxrTGlzMDlSaEMxcnhTc1FXaW8KTGJ5L1pOWjQyaGtHZituY00rVGUwbHZja0ZnK1hvdFRVK1M4ZE8xV3M4UHNrNDlueWQ0UWIxRjFWTFpoZWZwYwozQ1FkSDI2eG9RVk44Qkk1UTdCaHoyNk5SZTZJQ3RrKzR2RjRib3FuZEJaS1prVzBGeFBYdUYzNFIvOENUZmhOCjQvN05uSklndXA3elE2NlA0dlljWVFOQlVuR3l0OVJjNVVtMVd0NzBDSFdmU1c4aVVwRVhtd2VPbWtFS0xHb0QKcS9tQWZRS0NBUUFGZ1VsL0VZMDc3eVNpZEF2cnRidm02QjFzSU5WRW5XUGwxWVBacytCS3pMTThhWExDdFRjNgpqR1VkTTdaZWl2bUZpL0ZJTTB2dEZvWFJlSVFlc21vU095c2YxSkxxdGV4bGwxcFpKaXlzWFNrTjBlWExWUzhDCndXOVhtdWFlaHhWYnQ5YW1LT2tZK2ZOMjFBa1d2eUt5cnVUMXpKbVZab1dRWTlMWGlvWi9YUXNMT0MwWXlSd0cKSVM2MjE0OU03OStIUGhveS92ZHhCWEJtSXE2a3pLOFZlUE11ckNFbXBHakx4VlNlVFlMdndFcjY2amxndHM3VgpwYndWYlZLM3lzM2FVMmYreXRqdnZiUU9rT25yY2R3ZWd3bjZwOG9mamN6N013TUt3a3RFdllFcEpWQnVzeTFGCmJpVE0zZGYxUFZKeC9RaXBqWURRbE80TUhtNmFDRGF4QW9JQkFBNDhPOTRwbzRYa2x2SlBNeUdhSE43RGs5SngKemVUMGx1R0c5dW5kUjhEVlI1QlJ5WnFadk9DdXNQQXZCWUFnYjhDVWVTMWlaOUp0YnE1bnZuWFRndW4vZzk1dAp3a2pLWFlSNHZNVnRGWGpJc1owQ3o5dnFMVlJISjNtMm5sWXUrenRpWlVJbFNUQyswWlpVM0xlb1dNaGk0c3VWCkR6TnF4ZWw1YTYyQmloVGlZRW5rN09ocUxSc0ZWeTM4ZHVSanJOOEtXcFVxREdKbmMxWjYrbm9nTWkweCtWT3MKR210QmM0M2xvVFdwSTRVc0RGR2lKSDF2UUlKQXhRbDJETmhucFBGQXo4Y2J3UFNjR1VxVWk2NGlUa0pNbzRVeAp2TjkyOXc4QTBaV21PVTNxVjR3WkhWcXRIN2FSRjhQR082VEJNWVFYT1Q4SkZCQkZUR0FxSURGRzZaND0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K
diff --git a/testdata/signature-generate-using-xmlseclib.xml b/testdata/signature-generate-using-xmlseclib.xml
new file mode 100644
index 0000000..e6b792c
--- /dev/null
+++ b/testdata/signature-generate-using-xmlseclib.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ Bruce
+ Schneier
+
+ Applied Cryptography
+
+
+ XMLSec
+ http://www.aleksey.com/xmlsec/
+
+
+
+
+
+
+
+
+
+
+ kXaaD5Gor8lfNc6eYV31AyF0ekT9H/YY28Oh665mooQ=
+
+
+ Z81G62a0rLOLlgOQyJJN5amwIQ9Wu3vqoCc5CDp8wHz8xVHBGv6MOcaXtzkXOveQ
+KFRuFsxWtlNdxyUyfHzkwHk1BLfzUNbG1BNNjgktJ63leO5w3C4v64/3/HnIPEq1
+eE1uSMO0HDevDrgethWofa2ExxBG2CU8bY4pGzLx95uhBUxk5ilkR1P4tayJp1Va
+U0/roVBZTo8WH+ol15hj5FFV4rjTMPV2kPy9geNsMDZi+N30C/RNHB7n5MScnBC+
+OiCMRmJEcgJpA8/GXCX+y7WFKsVB9p6VO5euC8UUD9dvezvHhMDBwqncjvbnvjOk
+BD58sY6v8LpLIabJUmg3Yt6YjU8dToxv36QrIjsN/m1p5sCsWIMWCdW9MtsR0VGl
+MjkBCQUE70aCUzVbNrtJZaGY+YH9oDvHsLHKzTZusOvYLGCtKwaBcqVEuad/7Nvs
+e7JjYVl6HfyHWZPY6PDt0Q180f+U9QV2C1EdI+r2sbMd+HMft2Jp6Wii3mHZD6hy
+jFr54lztG0x9/X75jwBIaoMv8RtTl8lKfFpBiUc9UrpxKJBOOhMPWiYdwrrAzE1g
+NEHZvU3wbZiFWCAo+4RtCB++pWnoEJ16tuGHKlEIWTJRAnqm4ld1hX0zo7ZVkabD
+ixthaEIn8XmlaL92X5ZAI0P+yFC5kXSDKtLVnf6TgfE=
+
+
+
+
diff --git a/tests/helpers.go b/tests/helpers.go
new file mode 100644
index 0000000..ba2a843
--- /dev/null
+++ b/tests/helpers.go
@@ -0,0 +1,40 @@
+package tests
+
+import (
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/pem"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestCertificate(t *testing.T) *x509.Certificate {
+ pemBytes, err := os.ReadFile(filepath.Join("..", "testdata", "rsa.crt"))
+ require.NoError(t, err)
+
+ pemBlock, _ := pem.Decode(pemBytes)
+
+ cert, err := x509.ParseCertificate(pemBlock.Bytes)
+ require.NoError(t, err)
+
+ return cert
+}
+
+func PrivateKey(t *testing.T) *rsa.PrivateKey {
+ b64Bytes, err := os.ReadFile(filepath.Join("..", "testdata", "rsa.key.b64"))
+ require.NoError(t, err)
+
+ pemString, err := base64.StdEncoding.DecodeString(string(b64Bytes))
+ require.NoError(t, err)
+
+ pemBlock, _ := pem.Decode([]byte(pemString))
+
+ key, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
+ require.NoError(t, err)
+
+ return key
+}
diff --git a/tests/issue55_test.go b/tests/issue55_test.go
new file mode 100644
index 0000000..eb4f15b
--- /dev/null
+++ b/tests/issue55_test.go
@@ -0,0 +1,35 @@
+package tests
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/moov-io/signedxml"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestIssue55(t *testing.T) {
+ xml, err := os.ReadFile(filepath.Join("testdata", "issue55.xml"))
+ require.NoError(t, err)
+
+ signer, err := signedxml.NewSigner(string(xml))
+ require.NoError(t, err)
+
+ // Sign
+ key := PrivateKey(t)
+ xmlStr, err := signer.Sign(key)
+ require.NoError(t, err)
+
+ // Validate
+ validator, err := signedxml.NewValidator(xmlStr)
+ require.NoError(t, err)
+
+ cert := TestCertificate(t)
+ validator.Certificates = append(validator.Certificates, *cert)
+
+ refs, err := validator.ValidateReferences()
+ require.Contains(t, err.Error(), "signedxml: Calculated digest does not match the expected digestvalue of")
+ require.Len(t, refs, 0)
+}
diff --git a/tests/testdata/issue55.xml b/tests/testdata/issue55.xml
new file mode 100644
index 0000000..8802b79
--- /dev/null
+++ b/tests/testdata/issue55.xml
@@ -0,0 +1,50 @@
+
+
+ Hello World!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HIDDEN
+
+
+
+
+
+ 2024-06-04T16:13:09.320+07:00
+
+
+
+
+ kiffFUcHZhaLu0OkrPZ1Ui99V784vgh4zJrJNNn82+XWUfDzz24SKy4GMId/hxDFxiQaak0AdJRWHPtLUgutIA==
+
+
+
+ HIDDEN
+ HIDDEN
+
+
+
+
+
+
+
diff --git a/validator.go b/validator.go
index 4ce56f7..2ca9c22 100644
--- a/validator.go
+++ b/validator.go
@@ -19,8 +19,7 @@ type Validator struct {
// NewValidator returns a *Validator for the XML provided
func NewValidator(xml string) (*Validator, error) {
- doc := etree.NewDocument()
- err := doc.ReadFromString(xml)
+ doc, err := parseXML(xml)
if err != nil {
return nil, err
}
@@ -28,6 +27,11 @@ func NewValidator(xml string) (*Validator, error) {
return v, nil
}
+// SetReferenceIDAttribute set the referenceIDAttribute
+func (v *Validator) SetReferenceIDAttribute(refIDAttribute string) {
+ v.signatureData.refIDAttribute = refIDAttribute
+}
+
// SetXML is used to assign the XML document that the Validator will verify
func (v *Validator) SetXML(xml string) error {
doc := etree.NewDocument()
@@ -43,15 +47,6 @@ func (v *Validator) SigningCert() x509.Certificate {
return v.signingCert
}
-// Validate validates the Reference digest values, and the signature value
-// over the SignedInfo.
-//
-// Deprecated: Use ValidateReferences instead
-func (v *Validator) Validate() error {
- _, err := v.ValidateReferences()
- return err
-}
-
// ValidateReferences validates the Reference digest values, and the signature value
// over the SignedInfo.
//
@@ -59,7 +54,8 @@ func (v *Validator) Validate() error {
// Otherwise, an external signature should be assigned using
// Validator.SetSignature.
//
-// The references returned by this method can be used to verify what was signed.
+// The references returned contain validated XML from the signature and must be used.
+// Callers that ignore the returned references are vulnerable to XML injection.
func (v *Validator) ValidateReferences() ([]string, error) {
if err := v.loadValuesFromXML(); err != nil {
return nil, err
@@ -111,7 +107,9 @@ func (v *Validator) validateReferences() (referenced []*etree.Document, err erro
references := v.signedInfo.FindElements("./Reference")
for _, ref := range references {
doc := v.xml.Copy()
- if transforms := ref.SelectElement("Transforms"); transforms != nil {
+
+ transforms := ref.SelectElement("Transforms")
+ if transforms != nil {
for _, transform := range transforms.SelectElements("Transform") {
doc, err = processTransform(transform, doc)
if err != nil {
@@ -120,7 +118,7 @@ func (v *Validator) validateReferences() (referenced []*etree.Document, err erro
}
}
- doc, err = getReferencedXML(ref, doc)
+ doc, err = v.getReferencedXML(ref, doc)
if err != nil {
return nil, err
}
@@ -147,14 +145,7 @@ func (v *Validator) validateReferences() (referenced []*etree.Document, err erro
}
func (v *Validator) validateSignature() error {
- doc := etree.NewDocument()
- doc.SetRoot(v.signedInfo.Copy())
- signedInfo, err := doc.WriteToString()
- if err != nil {
- return err
- }
-
- canonSignedInfo, err := v.canonAlgorithm.Process(signedInfo, "")
+ canonSignedInfo, err := v.canonAlgorithm.ProcessElement(v.signedInfo, "")
if err != nil {
return err
}