diff --git a/README.md b/README.md index ad97627..4b68b59 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ go-saml ====== -[![Build Status](https://travis-ci.org/RobotsAndPencils/go-saml.svg?branch=master)](https://travis-ci.org/RobotsAndPencils/go-saml) - A just good enough SAML client library written in Go. This library is by no means complete and has been developed to solve several specific integration efforts. However, it's a start, and it would be great to see it evolve into a more fleshed out implemention. @@ -11,7 +9,7 @@ Inspired by the early work of [Matt Baird](https://github.com/mattbaird/gosaml). The library supports: -* generating signed/unsigned AuthnRequests +* generating signed AuthnRequests * validating signed AuthnRequests * generating service provider metadata * generating signed Responses @@ -42,7 +40,11 @@ sp := saml.ServiceProviderSettings{ IDPSSOURL: "http://idp/saml2", IDPSSODescriptorURL: "http://idp/issuer", IDPPublicCertPath: "idpcert.crt", - SPSignRequest: "true", + Id: "entityID", // can be SP hostname without slash + DisplayName: "name", //maybe it will be displayed in IDP login page + Description: "desc" //maybe it will be displayed in IDP login page + SPSignRequest: true, + IDPSignResponse: true, AssertionConsumerServiceURL: "http://localhost:8000/saml_consume", } sp.Init() @@ -106,6 +108,18 @@ response = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return } + //If is a encrypted response need decode + if response.IsEncrypted(){ + err = response.Decrypt(sp.PrivateKeyPath) + if err != nil { + httpcommon.SendBadRequest(w, "SAMLResponse parse: "+err.Error()) + return + } + } + // Print plain xml + //fmt.Printf(response.String()) + + //... } ``` diff --git a/authnrequest.go b/authnrequest.go index 3c826a6..000de8b 100644 --- a/authnrequest.go +++ b/authnrequest.go @@ -84,7 +84,9 @@ func (r *AuthnRequest) Validate(publicCertPath string) error { func (s *ServiceProviderSettings) GetAuthnRequest() *AuthnRequest { r := NewAuthnRequest() r.AssertionConsumerServiceURL = s.AssertionConsumerServiceURL - r.Issuer.Url = s.IDPSSODescriptorURL + r.Destination = s.IDPSSOURL + //r.Issuer.Url = s.IDPSSODescriptorURL + r.Issuer.Url = s.Id r.Signature.KeyInfo.X509Data.X509Certificate.Cert = s.PublicCert() return r @@ -116,6 +118,7 @@ func NewAuthnRequest() *AuthnRequest { ID: id, ProtocolBinding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", Version: "2.0", + Destination: "", // caller must populate ar.AppSettings.Destination, AssertionConsumerServiceURL: "", // caller must populate ar.AppSettings.AssertionConsumerServiceURL, Issuer: Issuer{ XMLName: xml.Name{ diff --git a/authnresponse.go b/authnresponse.go index 52f1f5a..442444e 100644 --- a/authnresponse.go +++ b/authnresponse.go @@ -4,9 +4,8 @@ import ( "encoding/base64" "encoding/xml" "errors" + "github.com/diego-araujo/go-saml/util" "time" - - "github.com/RobotsAndPencils/go-saml/util" ) func ParseCompressedEncodedResponse(b64ResponseXML string) (*Response, error) { @@ -31,6 +30,7 @@ func ParseCompressedEncodedResponse(b64ResponseXML string) (*Response, error) { func ParseEncodedResponse(b64ResponseXML string) (*Response, error) { response := Response{} bytesXML, err := base64.StdEncoding.DecodeString(b64ResponseXML) + //dst := string(bytesXML[:]) if err != nil { return nil, err } @@ -38,14 +38,78 @@ func ParseEncodedResponse(b64ResponseXML string) (*Response, error) { if err != nil { return nil, err } - + //fmt.Printf("%+v\n", response) // There is a bug with XML namespaces in Go that's causing XML attributes with colons to not be roundtrip // marshal and unmarshaled so we'll keep the original string around for validation. response.originalString = string(bytesXML) - // fmt.Println(response.originalString) return &response, nil } +func (r *Response) IsEncrypted() bool { + + //Test if exits EncryptedAssertion tag + if r.EncryptedAssertion.EncryptedData.EncryptionMethod.Algorithm == "" { + return false + } else { + return true + } +} + +func (r *Response) Decrypt(privateKeyPath string) error { + s := r.originalString + + if r.IsEncrypted() == false { + return errors.New("missing EncryptedAssertion tag on SAML Response, is encrypted?") + + } + plainXML, err := DecryptResponse(s, privateKeyPath) + if err != nil { + return err + } + err = xml.Unmarshal([]byte(plainXML), &r) + if err != nil { + return err + } + + r.originalString = plainXML + return nil +} + +func (r *Response) ValidateResponseSignature(s *ServiceProviderSettings) error { + + assertion, err := r.getAssertion() + if err != nil { + return err + } + + if len(assertion.Signature.SignatureValue.Value) == 0 { + return errors.New("no signature") + } + + err = VerifyResponseSignature(r.originalString, s.IDPPublicCertPath) + if err != nil { + return err + } + + return nil +} + +func (r *Response) getAssertion() (Assertion, error) { + + assertion := Assertion{} + + if r.IsEncrypted() { + assertion = r.EncryptedAssertion.Assertion + } else { + assertion = r.Assertion + } + + if len(assertion.ID) == 0 { + return assertion, errors.New("no Assertions") + } + return assertion, nil +} + func (r *Response) Validate(s *ServiceProviderSettings) error { if r.Version != "2.0" { return errors.New("unsupported SAML Version") @@ -55,33 +119,35 @@ func (r *Response) Validate(s *ServiceProviderSettings) error { return errors.New("missing ID attribute on SAML Response") } - if len(r.Assertion.ID) == 0 { - return errors.New("no Assertions") + assertion, err := r.getAssertion() + if err != nil { + return err } - if len(r.Signature.SignatureValue.Value) == 0 { - return errors.New("no signature") + if assertion.Subject.SubjectConfirmation.Method != "urn:oasis:names:tc:SAML:2.0:cm:bearer" { + return errors.New("assertion method exception") + } + + if assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient != s.AssertionConsumerServiceURL { + return errors.New("subject recipient mismatch, expected: " + s.AssertionConsumerServiceURL + " not " + assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient) } if r.Destination != s.AssertionConsumerServiceURL { return errors.New("destination mismath expected: " + s.AssertionConsumerServiceURL + " not " + r.Destination) } - if r.Assertion.Subject.SubjectConfirmation.Method != "urn:oasis:names:tc:SAML:2.0:cm:bearer" { - return errors.New("assertion method exception") - } + return nil +} - if r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient != s.AssertionConsumerServiceURL { - return errors.New("subject recipient mismatch, expected: " + s.AssertionConsumerServiceURL + " not " + r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient) - } +func (r *Response) ValidateExpiredConfirmation(s *ServiceProviderSettings) error { - err := VerifyResponseSignature(r.originalString, s.IDPPublicCertPath) + assertion, err := r.getAssertion() if err != nil { return err } //CHECK TIMES - expires := r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.NotOnOrAfter + expires := assertion.Subject.SubjectConfirmation.SubjectConfirmationData.NotOnOrAfter notOnOrAfter, e := time.Parse(time.RFC3339, expires) if e != nil { return e @@ -92,7 +158,6 @@ func (r *Response) Validate(s *ServiceProviderSettings) error { return nil } - func NewSignedResponse() *Response { return &Response{ XMLName: xml.Name{ @@ -277,6 +342,10 @@ func (r *Response) String() (string, error) { return string(b), nil } +func (r *Response) OriginalString() string { + return r.originalString +} + func (r *Response) SignedString(privateKeyPath string) (string, error) { s, err := r.String() if err != nil { @@ -307,7 +376,15 @@ func (r *Response) CompressedEncodedSignedString(privateKeyPath string) (string, // GetAttribute by Name or by FriendlyName. Return blank string if not found func (r *Response) GetAttribute(name string) string { - for _, attr := range r.Assertion.AttributeStatement.Attributes { + attrStatement := AttributeStatement{} + + if r.IsEncrypted() { + attrStatement = r.EncryptedAssertion.Assertion.AttributeStatement + } else { + attrStatement = r.Assertion.AttributeStatement + } + + for _, attr := range attrStatement.Attributes { if attr.Name == name || attr.FriendlyName == name { return attr.AttributeValue.Value } diff --git a/iDPEntityDescriptor.go b/iDPEntityDescriptor.go index afc4a83..211306c 100644 --- a/iDPEntityDescriptor.go +++ b/iDPEntityDescriptor.go @@ -13,7 +13,7 @@ func (s *ServiceProviderSettings) GetEntityDescriptor() (string, error) { DS: "http://www.w3.org/2000/09/xmldsig#", XMLNS: "urn:oasis:names:tc:SAML:2.0:metadata", MD: "urn:oasis:names:tc:SAML:2.0:metadata", - EntityId: s.AssertionConsumerServiceURL, + EntityId: s.Id, Extensions: Extensions{ XMLName: xml.Name{ @@ -22,9 +22,50 @@ func (s *ServiceProviderSettings) GetEntityDescriptor() (string, error) { Alg: "urn:oasis:names:tc:SAML:metadata:algsupport", MDAttr: "urn:oasis:names:tc:SAML:metadata:attribute", MDRPI: "urn:oasis:names:tc:SAML:metadata:rpi", + + UIInfo: UIInfo{ + XMLName: xml.Name{ + Local: "mdui:UIInfo", + }, + MDUI: "urn:oasis:names:tc:SAML:metadata:ui", + DisplayName: UIDisplayName{ + Lang: "en", + Value: "", + }, + Description: UIDescription{ + Lang: "en", + Value: "", + }, + }, }, SPSSODescriptor: SPSSODescriptor{ ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol", + AuthnRequestsSigned: fmt.Sprintf("%t", s.SPSignRequest), + WantAssertionsSigned: fmt.Sprintf("%t", s.IDPSignResponse), + + Extensions: Extensions{ + XMLName: xml.Name{ + Local: "md:Extensions", + }, + Alg: "urn:oasis:names:tc:SAML:metadata:algsupport", + MDAttr: "urn:oasis:names:tc:SAML:metadata:attribute", + MDRPI: "urn:oasis:names:tc:SAML:metadata:rpi", + + UIInfo: UIInfo{ + XMLName: xml.Name{ + Local: "mdui:UIInfo", + }, + MDUI: "urn:oasis:names:tc:SAML:metadata:ui", + DisplayName: UIDisplayName{ + Value: s.DisplayName, + Lang: "en", + }, + Description: UIDescription{ + Lang: "en", + Value: s.Description, + }, + }, + }, SigningKeyDescriptor: KeyDescriptor{ XMLName: xml.Name{ Local: "md:KeyDescriptor", @@ -86,15 +127,16 @@ func (s *ServiceProviderSettings) GetEntityDescriptor() (string, error) { Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", Location: s.AssertionConsumerServiceURL, Index: "0", + Default: true, }, - AssertionConsumerService{ - XMLName: xml.Name{ - Local: "md:AssertionConsumerService", - }, - Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", - Location: s.AssertionConsumerServiceURL, - Index: "1", - }, + // AssertionConsumerService{ + // XMLName: xml.Name{ + // Local: "md:AssertionConsumerService", + // }, + // Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", + // Location: s.AssertionConsumerServiceURL, + // Index: "1", + // }, }, }, } diff --git a/saml.go b/saml.go index 4ba8f64..5a4c3ce 100644 --- a/saml.go +++ b/saml.go @@ -1,6 +1,7 @@ package saml -import "github.com/RobotsAndPencils/go-saml/util" +import "github.com/diego-araujo/go-saml/util" +import stderrors "errors" // ServiceProviderSettings provides settings to configure server acting as a SAML Service Provider. // Expect only one IDP per SP in this configuration. If you need to configure multipe IDPs for an SP @@ -10,16 +11,25 @@ type ServiceProviderSettings struct { PrivateKeyPath string IDPSSOURL string IDPSSODescriptorURL string + DisplayName string + Description string IDPPublicCertPath string AssertionConsumerServiceURL string - SPSignRequest bool - - hasInit bool - publicCert string - privateKey string - iDPPublicCert string + Id string + SPSignRequest bool + IDPSignResponse bool + hasInit bool + publicCert string + privateKey string + iDPPublicCert string } +var ( + ErrPrivkey = stderrors.New("error load private key") + ErrSpPubCert = stderrors.New("error load SP publicCert") + ErrIdpPubCert = stderrors.New("error load IDP publicCert") +) + type IdentityProviderSettings struct { } @@ -29,43 +39,34 @@ func (s *ServiceProviderSettings) Init() (err error) { } s.hasInit = true - if s.SPSignRequest { + if s.SPSignRequest { s.publicCert, err = util.LoadCertificate(s.PublicCertPath) if err != nil { - panic(err) + return ErrSpPubCert } s.privateKey, err = util.LoadCertificate(s.PrivateKeyPath) if err != nil { - panic(err) + return ErrPrivkey } } s.iDPPublicCert, err = util.LoadCertificate(s.IDPPublicCertPath) if err != nil { - panic(err) + return ErrIdpPubCert } return nil } func (s *ServiceProviderSettings) PublicCert() string { - if !s.hasInit { - panic("Must call ServiceProviderSettings.Init() first") - } return s.publicCert } func (s *ServiceProviderSettings) PrivateKey() string { - if !s.hasInit { - panic("Must call ServiceProviderSettings.Init() first") - } return s.privateKey } func (s *ServiceProviderSettings) IDPPublicCert() string { - if !s.hasInit { - panic("Must call ServiceProviderSettings.Init() first") - } return s.iDPPublicCert } diff --git a/types.go b/types.go index d32b091..b02ffb5 100644 --- a/types.go +++ b/types.go @@ -8,6 +8,7 @@ type AuthnRequest struct { SAML string `xml:"xmlns:saml,attr"` SAMLSIG string `xml:"xmlns:samlsig,attr"` ID string `xml:"ID,attr"` + Destination string `xml:"Destination,attr"` Version string `xml:"Version,attr"` ProtocolBinding string `xml:"ProtocolBinding,attr"` AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"` @@ -23,7 +24,7 @@ type AuthnRequest struct { type Issuer struct { XMLName xml.Name - SAML string `xml:"xmlns:saml,attr"` + SAML string `xml:"xmlns:saml2,attr"` Url string `xml:",innerxml"` } @@ -52,6 +53,7 @@ type Signature struct { SignedInfo SignedInfo SignatureValue SignatureValue KeyInfo KeyInfo + DS string `xml:"xmlns:dsig,attr"` } type SignedInfo struct { @@ -68,9 +70,13 @@ type SignatureValue struct { type KeyInfo struct { XMLName xml.Name - X509Data X509Data `xml:",innerxml"` + X509Data X509Data } +type KeyInfoMain struct { + XMLName xml.Name `xml:"KeyInfo"` + EncryptedKey EncryptedKey `xml:"EncryptedKey,omitempty"` +} type CanonicalizationMethod struct { XMLName xml.Name Algorithm string `xml:"Algorithm,attr"` @@ -91,7 +97,7 @@ type SamlsigReference struct { type X509Data struct { XMLName xml.Name - X509Certificate X509Certificate `xml:",innerxml"` + X509Certificate X509Certificate } type Transforms struct { @@ -130,21 +136,43 @@ type EntityDescriptor struct { } type Extensions struct { - XMLName xml.Name - Alg string `xml:"xmlns:alg,attr"` - MDAttr string `xml:"xmlns:mdattr,attr"` - MDRPI string `xml:"xmlns:mdrpi,attr"` + XMLName xml.Name + Alg string `xml:"xmlns:alg,attr"` + MDAttr string `xml:"xmlns:mdattr,attr"` + MDRPI string `xml:"xmlns:mdrpi,attr"` + EntityAttributes string `xml:"EntityAttributes,omitempty"` + UIInfo UIInfo +} + +type UIInfo struct { + XMLName xml.Name + DisplayName UIDisplayName + MDUI string `xml:"xmlns:mdui,attr"` + Description UIDescription +} - EntityAttributes string `xml:"EntityAttributes"` +type UIDisplayName struct { + XMLName xml.Name `xml:"mdui:DisplayName"` + Lang string `xml:"xml:lang,attr,omitempty"` + Value string `xml:",innerxml"` +} + +type UIDescription struct { + XMLName xml.Name `xml:"mdui:Description"` + Lang string `xml:"xml:lang,attr,omitempty"` + Value string `xml:",innerxml"` } type SPSSODescriptor struct { XMLName xml.Name ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + AuthnRequestsSigned string `xml:"AuthnRequestsSigned,attr"` + WantAssertionsSigned string `xml:"WantAssertionsSigned,attr"` SigningKeyDescriptor KeyDescriptor EncryptionKeyDescriptor KeyDescriptor // SingleLogoutService SingleLogoutService `xml:"SingleLogoutService"` AssertionConsumerServices []AssertionConsumerService + Extensions Extensions `xml:"Extensions"` } type EntityAttributes struct { @@ -173,11 +201,12 @@ type AssertionConsumerService struct { Binding string `xml:"Binding,attr"` Location string `xml:"Location,attr"` Index string `xml:"index,attr"` + Default bool `xml:"isDefault,attr,omitempty"` } type Response struct { XMLName xml.Name - SAMLP string `xml:"xmlns:samlp,attr"` + SAMLP string `xml:"xmlns:saml2p,attr"` SAML string `xml:"xmlns:saml,attr"` SAMLSIG string `xml:"xmlns:samlsig,attr"` Destination string `xml:"Destination,attr"` @@ -186,12 +215,44 @@ type Response struct { IssueInstant string `xml:"IssueInstant,attr"` InResponseTo string `xml:"InResponseTo,attr"` - Assertion Assertion `xml:"Assertion"` - Signature Signature `xml:"Signature"` - Issuer Issuer `xml:"Issuer"` - Status Status `xml:"Status"` + EncryptedAssertion EncryptedAssertion `xml:"EncryptedAssertion,omitempty"` + Assertion Assertion `xml:"Assertion,omitempty"` + Issuer Issuer `xml:"Issuer"` + Status Status `xml:"Status"` + Signature Signature `xml:"Signature"` + originalString string +} + +type EncryptedAssertion struct { + XMLName xml.Name + EncryptedData EncryptedData + Assertion Assertion `xml:"Assertion"` +} + +type EncryptedData struct { + XMLName xml.Name + EncryptionMethod EncryptionMethod + KeyInfo KeyInfoMain `xml:"KeyInfo"` + CipherData CipherData +} + +type EncryptionMethod struct { + XMLName xml.Name + Algorithm string `xml:"Algorithm,attr"` + DigestMethod DigestMethod +} - originalString string +type EncryptedKey struct { + XMLName xml.Name + ID string `xml:"Id,attr"` + EncryptionMethod EncryptionMethod + KeyInfo KeyInfo + CipherData CipherData +} + +type CipherData struct { + XMLName xml.Name + CipherValue string `xml:"CipherValue"` } type Assertion struct { @@ -206,6 +267,7 @@ type Assertion struct { Subject Subject Conditions Conditions AttributeStatement AttributeStatement + Signature Signature } type Conditions struct { diff --git a/xmlsec.go b/xmlsec.go index 484a940..15a0dc6 100644 --- a/xmlsec.go +++ b/xmlsec.go @@ -9,7 +9,8 @@ import ( ) const ( - xmlResponseID = "urn:oasis:names:tc:SAML:2.0:protocol:Response" + //xmlResponseID = "urn:oasis:names:tc:SAML:2.0:protocol:Response" + xmlResponseID = "urn:oasis:names:tc:SAML:2.0:assertion:Assertion" xmlRequestID = "urn:oasis:names:tc:SAML:2.0:protocol:AuthnRequest" ) @@ -27,6 +28,24 @@ func SignResponse(xml string, privateKeyPath string) (string, error) { return sign(xml, privateKeyPath, xmlResponseID) } +// DecryptResponse decrypt a SAML 2.0 xml using his private key +func DecryptResponse(xml string, privateKeyPath string) (string, error) { + return decrypt(xml, privateKeyPath) +} + +func decrypt(xml string, privateKeyPath string) (string, error) { + + tempfile, err := ioutil.TempFile(os.TempDir(), "saml-resp") + ioutil.WriteFile(tempfile.Name(), []byte(xml), 0644) + plainXML, err := exec.Command("xmlsec1", "--decrypt", "--privkey-pem", privateKeyPath, tempfile.Name()).Output() + defer deleteTempFile(tempfile.Name()) + if err != nil { + return "", errors.New(err.Error() + " : " + string(plainXML)) + } + + return strings.Trim(string(plainXML), "\n"), nil +} + func sign(xml string, privateKeyPath string, id string) (string, error) { samlXmlsecInput, err := ioutil.TempFile(os.TempDir(), "tmpgs") @@ -89,9 +108,11 @@ func verify(xml string, publicCertPath string, id string) error { defer deleteTempFile(samlXmlsecInput.Name()) //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() + cmd := exec.Command("xmlsec1", "--verify", "--pubkey-cert-pem", publicCertPath, "--id-attr:ID", id, samlXmlsecInput.Name()) + output, err := cmd.CombinedOutput() + if err != nil { - return errors.New("error verifing signature: " + err.Error()) + return errors.New("error verifing signature: " + string(output)) } return nil }