diff --git a/.gitignore b/.gitignore index 0d8c5ba..077c171 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,8 @@ example/example .vscode +/.idea/.gitignore +/.idea/dbnavigator.xml +/.idea/modules.xml +/.idea/spid-go.iml +/.idea/vcs.xml diff --git a/example/service.go b/example/service.go index ded89f2..dd5f80c 100644 --- a/example/service.go +++ b/example/service.go @@ -43,6 +43,11 @@ func main() { Attributes: []string{"fiscalNumber", "name", "familyName", "dateOfBirth"}, }, }, + Organization: spidsaml.SPOrganization{ + OrganizationName: "Example Srl", + OrganizationDisplayName: "Example S.r.l.", + OrganizationURL: "https://www.example-sp.it", + }, } // Load Identity Providers from their XML metadata diff --git a/example/sp.pem b/example/sp.pem index e5c0847..1bc67eb 100644 --- a/example/sp.pem +++ b/example/sp.pem @@ -1,16 +1,17 @@ -----BEGIN CERTIFICATE----- -MIICljCCAX4CCQDP2/0y0d0/dzANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJJ -VDAeFw0xODA2MjgwOTM2MDRaFw0xOTA2MjgwOTM2MDRaMA0xCzAJBgNVBAYTAklU -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo4i2HsG/+Qm3qe0gqwEO -h4wlBYK181WCn3RTuyFNQh6mdn6pLdv/dYXY22zER2ufY227FFto3vqtSdT306lp -IoVuXKBAVoWwcrzO0dadM4dyHX7KRmWSDT51GBqkP6Hj1UoUywbXp5q9GXP5uVL8 -U93caT11VZaalHhEjKxtSYJHDP7ZP/2k9p54JgVIonMF0DJVhx0smPZ3QdHX+my/ -JNevsnuXTZIyyu0KjcXlflKSldngVDjv9D6cGE4wGOa5Vz5M+z4tjKnJtfj/xacI -Wcj/4Ukuu6CDyQ8+YNCaE9YjitRmdi5ZqTDOoGKonmlbhCcfqPeRvGwfWXJcVR+q -UwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBVO9pGMnJ3X5D6ny/32TM4fvFesdRB -tSTnivkdssvn4o8u6570XZIpz2AFQ9eltREbobAqYuWrXIr+1x5aACsReFSjusSM -Nb9dUCwZbpcId53WQdGikXVkLwgRw9LfYSr73EfeUIIc9R5HCbR5p2piDzw9cNpR -9wGhhL64g1zhy7O7bdWCXZ4cg9in9N2fCMTjdNpUvG4ZiToRdUqvuMDF4gsQJOwV -wmN6BxQFloyODpdf1XoTk9dEqPqFO5B8h+DY/26JV8QYUPKzGUJkr24GxjFj5dly -d8++oQDBEz0WVC0uRl5nGj+MDAO0DmHNeaS05gWQpp/KpykPzKyQw7cg ------END CERTIFICATE----- \ No newline at end of file +MIICoTCCAYkCFF5EIcofizvlSV+cMzaM458OR9HgMA0GCSqGSIb3DQEBCwUAMA0x +CzAJBgNVBAYTAklUMB4XDTIxMDQxNTEzNTM0OFoXDTMxMDQxMzEzNTM0OFowDTEL +MAkGA1UEBhMCSVQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCjiLYe +wb/5Cbep7SCrAQ6HjCUFgrXzVYKfdFO7IU1CHqZ2fqkt2/91hdjbbMRHa59jbbsU +W2je+q1J1PfTqWkihW5coEBWhbByvM7R1p0zh3IdfspGZZINPnUYGqQ/oePVShTL +Btenmr0Zc/m5UvxT3dxpPXVVlpqUeESMrG1JgkcM/tk//aT2nngmBUiicwXQMlWH +HSyY9ndB0df6bL8k16+ye5dNkjLK7QqNxeV+UpKV2eBUOO/0PpwYTjAY5rlXPkz7 +Pi2Mqcm1+P/FpwhZyP/hSS67oIPJDz5g0JoT1iOK1GZ2LlmpMM6gYqieaVuEJx+o +95G8bB9ZclxVH6pTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAH2fN9TBKtzcfmaT +GQ/hAmTur2BAU9vbMRJizgpQR9hSO7ty1FSVfS/rzTGOKr/oRfVI3SaTjo+ZYUjc +rVMw3gn/3JfJGTnZmZ6f1yqu0Cm7a7wbvyqWQxbCZzXEqHG13xYq64aYP/aKV8eS +zs1VTw7ShhfhrSYNlQWlr5OUMTgy6WN7ENAr8aCLCKyj+CR4et3hGyo66WPmKGuJ +eD97BYxP/No0S47Z6hUO+W8AgF1Qp52oAMQQ6N6EBVtViT34XMhpK1/7RnmTYusc +gj13zX2qZGt+HpOEOLqA2N/5PtLfEenxPO6vs6ZEPwngelpOQST8St2DmpSBwRjN +PObavq0= +-----END CERTIFICATE----- diff --git a/spidsaml/authnrequest_out.go b/spidsaml/authnrequest_out.go index baba20d..2603577 100644 --- a/spidsaml/authnrequest_out.go +++ b/spidsaml/authnrequest_out.go @@ -23,7 +23,7 @@ func (sp *SP) NewAuthnRequest(idp *IDP) *AuthnRequest { req := new(AuthnRequest) req.SP = sp req.IDP = idp - req.ID = generateMessageID() + req.ID = GenerateRandomID() req.AcsIndex = -1 req.AttrIndex = -1 req.Level = 1 @@ -32,6 +32,7 @@ func (sp *SP) NewAuthnRequest(idp *IDP) *AuthnRequest { } // XML generates the XML representation of this AuthnRequest +// Applicato il bugfix di saml:Issuer func (authnreq *AuthnRequest) XML(binding SAMLBinding) []byte { var signatureTemplate string if binding == HTTPPost { @@ -71,26 +72,24 @@ func (authnreq *AuthnRequest) XML(binding SAMLBinding) []byte { ForceAuthn="{{ if gt .Level 1 }}true{{ else }}false{{ end }}"> - - {{ .SP.EntityID }} - + {{ .SP.EntityID }} {{ .SignatureTemplate }} - - https://www.spid.gov.it/SpidL{{ .Level }} - + https://www.spid.gov.it/SpidL{{ .Level }} ` t := template.Must(template.New("req").Parse(tmpl)) var metadata bytes.Buffer - t.Execute(&metadata, data) + + if t.Execute(&metadata, data) != nil { + return nil + } + return metadata.Bytes() } diff --git a/spidsaml/button.go b/spidsaml/button.go deleted file mode 100644 index 1fa47b6..0000000 --- a/spidsaml/button.go +++ /dev/null @@ -1,28 +0,0 @@ -package spidsaml - -import ( - "bytes" - "fmt" - "html/template" - "net/url" -) - -const tmplButton = ` -{{ range $entityID, $url := . }} -

Login with SPID ({{ $entityID }})

-{{ end }} -` - -// GetButton returns the rendered HTML of the SPID button. -func (sp *SP) GetButton(pattern string) string { - items := make(map[string]string) // entityID: URL - - for entityID := range sp.IDP { - items[entityID] = fmt.Sprintf(pattern, url.QueryEscape(entityID)) - } - - t := template.Must(template.New("button").Parse(tmplButton)) - var button bytes.Buffer - t.Execute(&button, items) - return button.String() -} diff --git a/spidsaml/go.mod b/spidsaml/go.mod index 8c301b7..30651a0 100644 --- a/spidsaml/go.mod +++ b/spidsaml/go.mod @@ -1,7 +1,15 @@ -module github.com/italia/spid-go/spidsaml +module github.com/nicolasvac/spid-go/spidsaml + +go 1.17 require ( - github.com/beevik/etree v1.0.1 - github.com/crewjam/errset v0.0.0-20160219153700-f78d65de925c // indirect + github.com/beevik/etree v1.1.0 github.com/crewjam/go-xmlsec v0.0.0-20170116132012-1aa2f9374afa + github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c +) + +require ( + github.com/crewjam/errset v0.0.0-20160219153700-f78d65de925c // indirect + github.com/smartystreets/goconvey v1.7.2 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/spidsaml/go.sum b/spidsaml/go.sum index 5fc5f07..a12436c 100644 --- a/spidsaml/go.sum +++ b/spidsaml/go.sum @@ -1,6 +1,30 @@ github.com/beevik/etree v1.0.1 h1:lWzdj5v/Pj1X360EV7bUudox5SRipy4qZLjY0rhb0ck= github.com/beevik/etree v1.0.1/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/crewjam/errset v0.0.0-20160219153700-f78d65de925c h1:dCJ9oZ0VgnzJHR5BjkSrwkXA1USu483qlxBd0u29P8s= github.com/crewjam/errset v0.0.0-20160219153700-f78d65de925c/go.mod h1:XhiWL7J86xoqJ8+x2OA+AM2l9skQP2DZ0UOXQYVg7uI= github.com/crewjam/go-xmlsec v0.0.0-20170116132012-1aa2f9374afa h1:P3H7u0mfn9zEniDEKN50Yos1m60AM2PNOcyrInUjRFM= github.com/crewjam/go-xmlsec v0.0.0-20170116132012-1aa2f9374afa/go.mod h1:M9eHnKpImgRwzOFdlFQnbgJRqFwW/eX1cKAVobv03uE= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +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.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c h1:UPJygtyk491bJJ/DnRJFuzcq9Dl9NSeFrJ7VdiRzMxc= +github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c/go.mod h1:KEgVcb43+f5KFUH/x6Vd3NROG0AIL2CuKMrIqYsmx6E= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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= diff --git a/spidsaml/idp.go b/spidsaml/idp.go index 503faaf..6617251 100644 --- a/spidsaml/idp.go +++ b/spidsaml/idp.go @@ -31,9 +31,6 @@ func NewIDPFromXML(xml []byte) *IDP { panic(err) } - // TODO: if metadata is signed, validate /md:EntityDescriptor/dsig:Signature - // against a known CA - idp := new(IDP) idp.EntityID = doc.FindElement("/EntityDescriptor").SelectAttr("entityID").Value @@ -78,6 +75,8 @@ func NewIDPFromXML(xml []byte) *IDP { panic("failed to parse certificate: " + err.Error()) } + idp.XML, _ = doc.WriteToString() + return idp } diff --git a/spidsaml/logoutrequest_in.go b/spidsaml/logoutrequest_in.go index bee13c7..52ca6dc 100644 --- a/spidsaml/logoutrequest_in.go +++ b/spidsaml/logoutrequest_in.go @@ -53,7 +53,7 @@ func (logoutreq *LogoutRequestIn) validate(r *http.Request) error { } } if !knownDestination { - return fmt.Errorf("Invalid Destination: '%s'", destination) + return fmt.Errorf("invalid Destination: '%s'", destination) } return nil diff --git a/spidsaml/logoutrequest_out.go b/spidsaml/logoutrequest_out.go index e015588..4d1fd3f 100644 --- a/spidsaml/logoutrequest_out.go +++ b/spidsaml/logoutrequest_out.go @@ -25,7 +25,7 @@ func (sp *SP) NewLogoutRequest(session *Session) (*LogoutRequestOut, error) { if err != nil { return nil, err } - req.ID = generateMessageID() + req.ID = GenerateRandomID() req.Session = session return req, nil } @@ -78,7 +78,10 @@ func (logoutreq *LogoutRequestOut) XML(binding SAMLBinding) []byte { t := template.Must(template.New("req").Parse(tmpl)) var metadata bytes.Buffer - t.Execute(&metadata, data) + if t.Execute(&metadata, data) != nil { + return nil + } + return metadata.Bytes() } diff --git a/spidsaml/logoutresponse_in.go b/spidsaml/logoutresponse_in.go index c37cb0e..6e00f78 100644 --- a/spidsaml/logoutresponse_in.go +++ b/spidsaml/logoutresponse_in.go @@ -43,7 +43,7 @@ func (logoutres *LogoutResponseIn) validate(r *http.Request, inResponseTo string } if inResponseTo != logoutres.InResponseTo() { - return fmt.Errorf("Invalid InResponseTo: '%s' (expected: '%s')", + return fmt.Errorf("invalid InResponseTo: '%s' (expected: '%s')", logoutres.InResponseTo(), inResponseTo) } @@ -58,7 +58,7 @@ func (logoutres *LogoutResponseIn) validate(r *http.Request, inResponseTo string } } if !knownDestination { - return fmt.Errorf("Invalid Destination: '%s'", destination) + return fmt.Errorf("invalid Destination: '%s'", destination) } return nil diff --git a/spidsaml/logoutresponse_out.go b/spidsaml/logoutresponse_out.go index 29a223b..f0fc285 100644 --- a/spidsaml/logoutresponse_out.go +++ b/spidsaml/logoutresponse_out.go @@ -34,7 +34,7 @@ func (sp *SP) NewLogoutResponse(logoutreq *LogoutRequestIn, status LogoutStatus) if err != nil { return nil, err } - res.ID = generateMessageID() + res.ID = GenerateRandomID() res.InResponseTo = logoutreq.ID() return res, nil } @@ -92,7 +92,11 @@ func (logoutres *LogoutResponseOut) XML(binding SAMLBinding) []byte { t := template.Must(template.New("req").Parse(tmpl)) var metadata bytes.Buffer - t.Execute(&metadata, data) + + if t.Execute(&metadata, data) != nil { + return nil + } + return metadata.Bytes() } diff --git a/spidsaml/messages.go b/spidsaml/messages.go index 7e9df5a..4681541 100644 --- a/spidsaml/messages.go +++ b/spidsaml/messages.go @@ -11,6 +11,7 @@ import ( "encoding/base64" "errors" "fmt" + "github.com/ma314smith/signedxml" "io/ioutil" "net/http" "net/url" @@ -19,7 +20,7 @@ import ( "time" "github.com/beevik/etree" - xmlsec "github.com/crewjam/go-xmlsec" + "github.com/crewjam/go-xmlsec" ) // protocolMessage is the base class for all SAML messages @@ -43,16 +44,6 @@ type inMessage struct { RelayState string } -func generateMessageID() string { - id := make([]byte, 16) - if _, err := rand.Reader.Read(id); err != nil { - panic(err) - } - - // first character must not be a digit - return fmt.Sprintf("_%x", id) -} - func (msg *outMessage) IssueInstant() *time.Time { if msg.issueInstant == nil { t := time.Now().UTC() @@ -70,7 +61,7 @@ func (msg *outMessage) RedirectURL(baseurl string, xml []byte, param string) str w := &bytes.Buffer{} w1 := base64.NewEncoder(base64.StdEncoding, w) w2, _ := flate.NewWriter(w1, 9) - w2.Write([]byte(xml)) + w2.Write(xml) w2.Close() w1.Close() @@ -105,24 +96,29 @@ func (msg *outMessage) RedirectURL(baseurl string, xml []byte, param string) str func (msg *outMessage) PostForm(url string, xml []byte, param string) []byte { // We need to get the name of the root element doc := etree.NewDocument() - doc.ReadFromBytes(xml) - - signedDoc, err := xmlsec.Sign(msg.SP.KeyPEM(), xml, xmlsec.SignatureOptions{ - XMLID: []xmlsec.XMLIDOption{ - { - ElementName: doc.Root().Tag, - ElementNamespace: "", - AttributeName: "ID", - }, - }, - }) + err := doc.ReadFromBytes(xml) + + if err != nil { + panic(err) + } + + completeXML, _ := doc.WriteToString() + + // Sign the Authnrequest + signer, err := signedxml.NewSigner(completeXML) + + if err != nil { + panic(err) + } + + completeXML, err = signer.Sign(msg.SP.Key()) + if err != nil { panic(err) } - //os.Stdout.Write(signedDoc) // encode in base64 - encodedReqBuf := base64.StdEncoding.EncodeToString(signedDoc) + encodedReqBuf := base64.StdEncoding.EncodeToString([]byte(completeXML)) tmpl := template.Must(template.New("saml-post-form").Parse(` @@ -158,23 +154,23 @@ func (msg *outMessage) signatureTemplate() []byte { tmpl := template.Must(template.New("saml-post-form").Parse(` - - - - - - - - - - - - - - - {{ .Cert }} - - + + + + + + + + + + + + + + + {{ .Cert }} + + `)) data := struct { @@ -186,7 +182,10 @@ func (msg *outMessage) signatureTemplate() []byte { } var rv bytes.Buffer - tmpl.Execute(&rv, data) + err := tmpl.Execute(&rv, data) + if err != nil { + return nil + } return rv.Bytes() } diff --git a/spidsaml/spid.go b/spidsaml/spid.go index 9a7fb37..3e60873 100644 --- a/spidsaml/spid.go +++ b/spidsaml/spid.go @@ -7,8 +7,11 @@ import ( "encoding/base64" "encoding/pem" "errors" + "github.com/beevik/etree" "io/ioutil" "text/template" + + "github.com/ma314smith/signedxml" ) // AttributeConsumingService defines, well, an AttributeConsumingService. @@ -26,6 +29,29 @@ const ( HTTPPost SAMLBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" ) +// SPOrganization Organization adds infos about SP +type SPOrganization struct { + OrganizationName string + OrganizationDisplayName string + OrganizationURL string +} + +// SPContactPerson ContactPerson metadata +type SPContactPerson struct { + ContactType string + Company string + EmailAddress string + TelephoneNumber string + Extensions []SPContactPersonExtension +} + +// SPContactPersonExtension extensions for contact person +type SPContactPersonExtension struct { + Tag string + Value string + Extensions []SPContactPersonExtension +} + // SP represents our Service Provider type SP struct { EntityID string @@ -37,6 +63,8 @@ type SP struct { IDP map[string]*IDP _cert *x509.Certificate _key *rsa.PrivateKey + Organization SPOrganization + ContactPersons []SPContactPerson } // Session represents an active SPID session. @@ -76,12 +104,28 @@ func (sp *SP) Key() *rsa.PrivateKey { byteValue, _ := ioutil.ReadFile(sp.KeyFile) block, _ := pem.Decode(byteValue) - if block == nil || block.Type != "RSA PRIVATE KEY" { - panic("failed to parse private key from PEM file") + if block == nil { + panic("failed to parse private key from PEM file " + sp.KeyFile) } var err error - sp._key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + + switch block.Type { + case "RSA PRIVATE KEY": + sp._key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + + case "PRIVATE KEY": + var keyOfSomeType interface{} + keyOfSomeType, err = x509.ParsePKCS8PrivateKey(block.Bytes) + var ok bool + sp._key, ok = keyOfSomeType.(*rsa.PrivateKey) + if !ok { + err = errors.New("file " + sp.KeyFile + " does not contain an RSA private key") + } + default: + err = errors.New("unknown key type " + block.Type) + } + if err != nil { panic(err) } @@ -108,67 +152,189 @@ func (sp *SP) GetIDP(entityID string) (*IDP, error) { } // Metadata generates XML metadata of this Service Provider. -func (sp *SP) Metadata() string { - const tmpl = ` - - - - - - - - {{ .Cert }} - - +func (sp *SP) Metadata(enableSigning bool) string { + const tmpl = ` + + + {{ if .EnableSigning }} + + + + + + + + + + + + + + + + + {{ .Cert }} + + + + {{ end }} + + + + + + + {{ .CertSubject }} + {{ .Cert }} + + - + {{ range $url, $binding := .SingleLogoutServices }} - + Location="{{ $url }}" /> {{ end }} - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient {{ range $index, $url := .AssertionConsumerServices }} - + {{ end }} - + {{ range $index, $attcs := .AttributeConsumingServices }} - + {{ $attcs.ServiceName }} {{ range $attr := $attcs.Attributes }} - + {{ end }} {{ end }} - + + + + {{ .Organization.OrganizationName }} + {{ .Organization.OrganizationDisplayName }} + {{ .Organization.OrganizationURL }} + ` aux := struct { *SP - Cert string + Cert string + RandomRequestID string + CertSubject string + EnableSigning bool }{ sp, base64.StdEncoding.EncodeToString(sp.Cert().Raw), + GenerateRandomID(), // Generate a random ID for each request, + sp.Cert().Subject.String(), + enableSigning, } t := template.Must(template.New("metadata").Parse(tmpl)) var metadata bytes.Buffer - t.Execute(&metadata, aux) - return metadata.String() + // Parse now the template + if t.Execute(&metadata, aux) != nil { + return "" + } + + // Add the contact persons to the XML + completeXML, err := addContactPersons(metadata.String(), sp.ContactPersons) + + if err != nil { + return "" + } + + // If the sign is not enabled, just exit here. + if !enableSigning { + return completeXML + } + + // Sign the XML + signer, err := signedxml.NewSigner(completeXML) + + if err != nil { + return "" + } + + completeXML, err = signer.Sign(sp.Key()) + + if err != nil { + return "" + } + + return completeXML +} + +func addContactPersons(signedXML string, persons []SPContactPerson) (string, error) { + xmlDoc := etree.NewDocument() + + if xmlDoc.ReadFromString(signedXML) != nil { + return "", nil + } + + // Get the basic entity descriptor element + entityDescriptor := xmlDoc.FindElement("EntityDescriptor") + + for _, contactPerson := range persons { + // Create basic contact person element + contactPersonXML := entityDescriptor.CreateElement("md:ContactPerson") + // Add the specified contact type + contactPersonXML.CreateAttr("contactType", contactPerson.ContactType) + + // Add extensions data + contactPersonExtensionsXML := contactPersonXML.CreateElement("md:Extensions") + + addContactPersonExtensions(contactPersonExtensionsXML, contactPerson.Extensions) + + // Add company data + if contactPerson.Company != "" { + contactPersonXML.CreateElement("md:Company").CreateText(contactPerson.Company) + } + + // Add email address data + if contactPerson.EmailAddress != "" { + contactPersonXML.CreateElement("md:EmailAddress").CreateText(contactPerson.EmailAddress) + } + + // Add telephone number data + if contactPerson.TelephoneNumber != "" { + contactPersonXML.CreateElement("md:TelephoneNumber").CreateText(contactPerson.TelephoneNumber) + } + } + + return xmlDoc.WriteToString() +} + +func addContactPersonExtensions(xml *etree.Element, extensions []SPContactPersonExtension) { + + for _, extension := range extensions { + xmlElement := xml.CreateElement(extension.Tag) + + if extension.Value != "" { + xmlElement.CreateText(extension.Value) + } + + if len(extension.Extensions) > 0 { + addContactPersonExtensions(xmlElement, extension.Extensions) + } + } } diff --git a/spidsaml/utils.go b/spidsaml/utils.go new file mode 100644 index 0000000..61e8b75 --- /dev/null +++ b/spidsaml/utils.go @@ -0,0 +1,20 @@ +package spidsaml + +import ( + "crypto/rand" + "math/big" +) + +func GenerateRandomID() string { + const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" + ret := make([]byte, 43) + for i := 0; i < 43; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + if err != nil { + return "" + } + ret[i] = letters[num.Int64()] + } + + return "_" + string(ret) +}