forked from RobotsAndPencils/go-saml
-
Notifications
You must be signed in to change notification settings - Fork 1
/
xmlsec.go
164 lines (141 loc) · 5.35 KB
/
xmlsec.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package saml
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
)
const (
xmlResponseID = "urn:oasis:names:tc:SAML:2.0:protocol:Response"
xmlRequestID = "urn:oasis:names:tc:SAML:2.0:protocol:AuthnRequest"
xmlAssertionID = "urn:oasis:names:tc:SAML:2.0:assertion:Assertion"
)
// SignRequest sign a SAML 2.0 AuthnRequest
// `privateKeyPath` must be a path on the filesystem, xmlsec1 is run out of process
// through `exec`
func SignRequest(xml string, privateKeyPath string) (string, error) {
return sign(xml, privateKeyPath, xmlRequestID)
}
// SignLogoutRequest returns the signature to a SAML 2.0 LogoutRequest
// `privateKeyPath` must be a path on the filesystem, openssl is run out of process
// through `exec`
func SignLogoutRequest(xml string, privateKeyPath string) (string, error) {
samlXmlsecInput, err := ioutil.TempFile(os.TempDir(), "tmpgs")
if err != nil {
return "", err
}
defer deleteTempFile(samlXmlsecInput.Name())
samlXmlsecInput.WriteString("<?xml version='1.0' encoding='UTF-8'?>\n")
samlXmlsecInput.WriteString(xml)
samlXmlsecInput.Close()
samlXmlsecOutput, err := ioutil.TempFile(os.TempDir(), "tmpgs")
if err != nil {
return "", err
}
defer deleteTempFile(samlXmlsecOutput.Name())
samlXmlsecOutput.Close()
output, err := exec.Command(
"openssl",
"dgst",
"-sha1",
"-sign", privateKeyPath,
"-out", samlXmlsecOutput.Name(), samlXmlsecInput.Name()).CombinedOutput()
if err != nil {
return "", errors.New(err.Error() + " : " + string(output))
}
signature, err := ioutil.ReadFile(samlXmlsecOutput.Name())
if err != nil {
return "", err
}
return string(signature), nil
}
// SignResponse sign a SAML 2.0 Response
// `privateKeyPath` must be a path on the filesystem, xmlsec1 is run out of process
// through `exec`
func SignResponse(xml string, privateKeyPath string) (string, error) {
return sign(xml, privateKeyPath, xmlResponseID)
}
func sign(xml string, privateKeyPath string, id string) (string, error) {
samlXmlsecInput, err := ioutil.TempFile(os.TempDir(), "tmpgs")
if err != nil {
return "", err
}
defer deleteTempFile(samlXmlsecInput.Name())
samlXmlsecInput.WriteString("<?xml version='1.0' encoding='UTF-8'?>\n")
samlXmlsecInput.WriteString(xml)
samlXmlsecInput.Close()
samlXmlsecOutput, err := ioutil.TempFile(os.TempDir(), "tmpgs")
if err != nil {
return "", err
}
defer deleteTempFile(samlXmlsecOutput.Name())
samlXmlsecOutput.Close()
// fmt.Println("xmlsec1", "--sign", "--privkey-pem", privateKeyPath,
// "--id-attr:ID", id,
// "--output", samlXmlsecOutput.Name(), samlXmlsecInput.Name())
output, err := exec.Command("xmlsec1", "--sign", "--privkey-pem", privateKeyPath,
"--id-attr:ID", id,
"--output", samlXmlsecOutput.Name(), samlXmlsecInput.Name()).CombinedOutput()
if err != nil {
return "", errors.New(err.Error() + " : " + string(output))
}
samlSignedRequest, err := ioutil.ReadFile(samlXmlsecOutput.Name())
if err != nil {
return "", err
}
samlSignedRequestXML := strings.Trim(string(samlSignedRequest), "\n")
return samlSignedRequestXML, nil
}
// VerifyResponseSignature verify signature of a SAML 2.0 Response document
// `publicCertPath` must be a path on the filesystem, xmlsec1 is run out of process
// through `exec`
func VerifyResponseSignature(xml string, publicCertPath string) error {
return verify(xml, publicCertPath, xmlResponseID)
}
// VerifyAssertionSignature verify signature of a SAML 2.0 Assertion block
// `publicCertPath` must be a path on the filesystem, xmlsec1 is run out of process
// through `exec`
func VerifyAssertionSignature(xml string, publicCertPath string) error {
return verify(xml, publicCertPath, xmlAssertionID)
}
// VerifyRequestSignature verify signature of a SAML 2.0 AuthnRequest document
// `publicCertPath` must be a path on the filesystem, xmlsec1 is run out of process
// through `exec`
func VerifyRequestSignature(xml string, publicCertPath string) error {
return verify(xml, publicCertPath, xmlRequestID)
}
func verify(xml string, publicCertPath string, id string) error {
//Write saml to
samlXmlsecInput, err := ioutil.TempFile(os.TempDir(), "tmpgs")
if err != nil {
return err
}
samlXmlsecInput.WriteString(xml)
samlXmlsecInput.Close()
defer deleteTempFile(samlXmlsecInput.Name())
// This performs a very basic defence agains XML Signature wrapping attacks.
// There should be exactly one occurrence of the "Response" / "Assertion" tag in a SAML response payload
for _, token := range []string{"Response","Assertion",} {
responses, err := exec.Command("sh", "-c", "grep -oiE '<([^: ]*:)?"+token+" [^>]*>' "+samlXmlsecInput.Name()+" | wc -l | awk '{print $1}'").CombinedOutput()
if err != nil {
return err
}
if !bytes.Equal(responses, []byte{'1', 10}) {
return fmt.Errorf("error validating response: incorrect number of '%s' in request: got '%v'",token,responses)
}
}
//fmt.Println("xmlsec1", "--verify", "--pubkey-cert-pem", publicCertPath, "--id-attr:ID", id, samlXmlsecInput.Name())
_, err = exec.Command("xmlsec1", "--verify", "--pubkey-cert-pem", publicCertPath, "--id-attr:ID", id, samlXmlsecInput.Name()).CombinedOutput()
if err != nil {
return errors.New("error verifying signature: " + err.Error())
}
return nil
}
// deleteTempFile remove a file and ignore error
// Intended to be called in a defer after the creation of a temp file to ensure cleanup
func deleteTempFile(filename string) {
_ = os.Remove(filename)
}