From 5e7e2168795e3aa0f49e921d9aa7eefbbc48432d Mon Sep 17 00:00:00 2001 From: Daniel Dao Date: Fri, 11 Dec 2015 15:46:54 +0000 Subject: [PATCH 1/2] allow idp public cert to be embedded This is useful when we want to load public cert from somewhere other than local filesystem. --- authnrequest_test.go | 47 +++++++++++++++++++++++++++++++++++++++++ saml.go | 29 +++++++++++++++++-------- util/loadCertificate.go | 12 +++++++---- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/authnrequest_test.go b/authnrequest_test.go index 2940080..db230ac 100644 --- a/authnrequest_test.go +++ b/authnrequest_test.go @@ -47,3 +47,50 @@ func TestGetUnsignedRequest(t *testing.T) { assert.NoError(err) assert.NotEmpty(authnRequest) } + +func TestGetUnsignedRequestWithEmbededCert(t *testing.T) { + assert := assert.New(t) + sp := ServiceProviderSettings{ + IDPSSOURL: "http://www.onelogin.net", + IDPSSODescriptorURL: "http://www.onelogin.net", + AssertionConsumerServiceURL: "http://localhost:8000/auth/saml/name", + IDPPublicCertContent: `-----BEGIN CERTIFICATE----- +MIIFYTCCA0mgAwIBAgIJAI1a1evtQYDkMA0GCSqGSIb3DQEBBQUAME8xCzAJBgNV +BAYTAkZSMQ4wDAYDVQQHEwVQYXJpczEOMAwGA1UEChMFRWtpbm8xDzANBgNVBAsT +BkRldk9wczEPMA0GA1UEAxMGZ29zYW1sMB4XDTE1MDcyMDIyNDE1OFoXDTI1MDcx +NzIyNDE1OFowTzELMAkGA1UEBhMCRlIxDjAMBgNVBAcTBVBhcmlzMQ4wDAYDVQQK +EwVFa2lubzEPMA0GA1UECxMGRGV2T3BzMQ8wDQYDVQQDEwZnb3NhbWwwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDoo/DTqWoyJyXR0K+hF4pw4qBkaLL0 +gbbKoiKH+7wvdzHONOoFBfF5NQj02M4JJyeOQ6+hHYV4QjtUG41zMf1XoH/U6Ey/ +oURkuCJJCGhW9AyD+A4WP4YS4Ag/uN7o0P3nuj7hJipefY1Bzmg2n89iHDcpHvwK +TtVWZYdj6Dgbwh9ZH9QiRRRp+GZHXu7nW+VCZM0mE+9qjxK4Mw+KEDD6LIgSOAzR +LWLyUmb2Kwvc++DhwDtIoThVHYoNd4Sk9j6/4B3DmPa83i/1dZKyFaMCDUn7+i6K +hwIWbGfg6uQMM8G6XzF4V5x5agmg8DK24VXs3yb1lOIUczNVq4ZHkApc4jwHWiXn +cab88UnDPG7pVm87whaMghWNwrYAt//QEInExkxjNhWwxNFlelg/8b9fUsdH58Fe +ZiZ+mNnwACXnggmZEE+lUX5Fh8l79bke+dnQbJAhQfi+OhmNlqmc+ouKDPYqk0/I +C9q/3Tg65Ej9Miq918IAvQAVtlwwwp6I5/02Aa5iqZozBTUXYqWE/qXixlpWh2tP +5ljecgGazuw58tGj2+nXS9DA9wVgGUAl4xJFO/s8emna52lSPzwvcr6j+BMifXHr +0WBIEcTbtzXhxUpfC6IC14yfPOf8g4WKKgg1Wq3H4dGiE11y66ceYeh1RZlWXq/J +EtJ1FVLoGq4qLwIDAQABo0AwPjA8BgNVHREENTAzghBsb2dzLmV4YW1wbGUuY29t +ghNtZXRyaWNzLmV4YW1wbGUuY29thwTAqAABhwQKAAAyMA0GCSqGSIb3DQEBBQUA +A4ICAQAcaLdziL6dNZ3lXtm3nsI9ceSVwp2yKfpsswjs524bOsLK97Ucf4hhlh1b +q5hywWbm85N7iuxdpBuhSmeJ94ryFAPDUkhR1Mzcl48c6R8tPbJVhabhbfg+uIHi +4BYUA0olesdsyTOsRHprM4iV+PlKZ85SQT04ZNyaqIDzmNEP7YXDl/Wl3Q0N5E1U +yGfDTBxo07srqrAM2E5X7hN9bwdZX0Hbo/C4q3wgRHAts/wJXXWSSTe1jbIWYXem +EkwAEd01BiMBj1LYK/sJ8s4fONdLxIyKqLUh1Ja46moqpgl5AHuPbqnwPdgGGvEd +iBzz5ppHs0wXFopk+J4rzYRhya6a3BMXiDjg+YOSwFgCysmWmCrxoImmfcQWUZJy +5eMow+hBBiKgT2DxggqVzReN3C7uwsFZLZCsv8+MjvFQz52oEp/GWqFepggFQiRI +K7/QmwcsDdz6zBobZJaJstq3R2mHYkhaVUIOqEuqyD2N7qms8bek7xzq6F9KkYLk +PK/d2Crkxq1bnvM7oO8IsA6vHdTexfZ1SRPf7Mxpg8DMV788qE09BDZ5mLFOkRbw +FY7MHRX6Mz59gfnAcRwK/0HnG6c8EZCJH8jMStzqA0bUjzDiyN2ZgzFkTUA9Cr8j +kq8grtVMsp40mjFnSg/FR+O+rG32D/rbfvNYFCR8wawOcYrGyA== +-----END CERTIFICATE-----`, + } + err := sp.Init() + assert.NoError(err) + + // Construct an AuthnRequest + authnRequest := sp.GetAuthnRequest() + assert.NoError(err) + assert.NotEmpty(authnRequest) +} diff --git a/saml.go b/saml.go index 4ba8f64..c51cdc7 100644 --- a/saml.go +++ b/saml.go @@ -1,6 +1,10 @@ package saml -import "github.com/RobotsAndPencils/go-saml/util" +import ( + "errors" + + "github.com/RobotsAndPencils/go-saml/util" +) // 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,9 +14,10 @@ type ServiceProviderSettings struct { PrivateKeyPath string IDPSSOURL string IDPSSODescriptorURL string + IDPPublicCertContent string IDPPublicCertPath string AssertionConsumerServiceURL string - SPSignRequest bool + SPSignRequest bool hasInit bool publicCert string @@ -27,25 +32,31 @@ func (s *ServiceProviderSettings) Init() (err error) { if s.hasInit { return nil } - s.hasInit = true - if s.SPSignRequest { + if s.SPSignRequest { s.publicCert, err = util.LoadCertificate(s.PublicCertPath) if err != nil { - panic(err) + return err } s.privateKey, err = util.LoadCertificate(s.PrivateKeyPath) if err != nil { - panic(err) + return err } } - s.iDPPublicCert, err = util.LoadCertificate(s.IDPPublicCertPath) - if err != nil { - panic(err) + if s.IDPPublicCertPath != "" { + s.iDPPublicCert, err = util.LoadCertificate(s.IDPPublicCertPath) + if err != nil { + return err + } + } else if s.IDPPublicCertContent != "" { + s.iDPPublicCert = util.SanitizeCertificate(s.IDPPublicCertContent) + } else { + return errors.New("missing idp public cert or its path") } + s.hasInit = true return nil } diff --git a/util/loadCertificate.go b/util/loadCertificate.go index 18d7ef2..2bcd096 100644 --- a/util/loadCertificate.go +++ b/util/loadCertificate.go @@ -6,18 +6,22 @@ import ( "strings" ) +var ( + re = regexp.MustCompile("---(.*)CERTIFICATE(.*)---") +) + // LoadCertificate from file system func LoadCertificate(certPath string) (string, error) { b, err := ioutil.ReadFile(certPath) if err != nil { return "", err } - cert := string(b) + return SanitizeCertificate(string(b)), nil +} - re := regexp.MustCompile("---(.*)CERTIFICATE(.*)---") +func SanitizeCertificate(cert string) string { cert = re.ReplaceAllString(cert, "") cert = strings.Trim(cert, " \n") cert = strings.Replace(cert, "\n", "", -1) - - return cert, nil + return cert } From 96ddd5fd9b2f0f0678830d44a8c2dbc952e1eb31 Mon Sep 17 00:00:00 2001 From: Daniel Dao Date: Mon, 14 Dec 2015 15:59:21 +0000 Subject: [PATCH 2/2] add xmlsec bindings to verify response this add xmlsec binding to libxmlsec1 and libxml2 so we can use that instead of shelling out to an external binary. --- authnresponse.go | 13 ++- xmlsec.go | 27 ++++++ xmlsec/xmlsec.go | 235 +++++++++++++++++++++++++++++++++++++++++++++++ xmlsec_test.go | 14 +++ 4 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 xmlsec/xmlsec.go diff --git a/authnresponse.go b/authnresponse.go index 52f1f5a..42156a9 100644 --- a/authnresponse.go +++ b/authnresponse.go @@ -75,9 +75,16 @@ func (r *Response) Validate(s *ServiceProviderSettings) error { return errors.New("subject recipient mismatch, expected: " + s.AssertionConsumerServiceURL + " not " + r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient) } - err := VerifyResponseSignature(r.originalString, s.IDPPublicCertPath) - if err != nil { - return err + if s.IDPPublicCertContent != "" { + err := VerifyResponseSignatureMem([]byte(r.originalString), []byte(s.IDPPublicCertContent)) + if err != nil { + return err + } + } else { + err := VerifyResponseSignature(r.originalString, s.IDPPublicCertPath) + if err != nil { + return err + } } //CHECK TIMES diff --git a/xmlsec.go b/xmlsec.go index 484a940..c603c2a 100644 --- a/xmlsec.go +++ b/xmlsec.go @@ -6,6 +6,8 @@ import ( "os" "os/exec" "strings" + + "github.com/RobotsAndPencils/go-saml/xmlsec" ) const ( @@ -13,6 +15,19 @@ const ( xmlRequestID = "urn:oasis:names:tc:SAML:2.0:protocol:AuthnRequest" ) +var ( + xmlResponseIDAttr = xmlsec.IDAttr{ + Name: "ID", + NodeName: "Response", + NsHref: "urn:oasis:names:tc:SAML:2.0:protocol", + } + xmlRequestIDAttr = xmlsec.IDAttr{ + Name: "ID", + NodeName: "AuthnRequest", + NsHref: "urn:oasis:names:tc:SAML:2.0:protocol", + } +) + // SignRequest sign a SAML 2.0 AuthnRequest // `privateKeyPath` must be a path on the filesystem, xmlsec1 is run out of process // through `exec` @@ -70,6 +85,12 @@ func VerifyResponseSignature(xml string, publicCertPath string) error { return verify(xml, publicCertPath, xmlResponseID) } +func VerifyResponseSignatureMem(doc []byte, publicCert []byte) error { + return xmlsec.Verify(doc, publicCert, xmlsec.Options{ + IDAttrs: []xmlsec.IDAttr{xmlResponseIDAttr}, + }) +} + // 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` @@ -77,6 +98,12 @@ func VerifyRequestSignature(xml string, publicCertPath string) error { return verify(xml, publicCertPath, xmlRequestID) } +func VerifyRequestSignatureMem(doc []byte, publicCert []byte) error { + return xmlsec.Verify(doc, publicCert, xmlsec.Options{ + IDAttrs: []xmlsec.IDAttr{xmlRequestIDAttr}, + }) +} + func verify(xml string, publicCertPath string, id string) error { //Write saml to samlXmlsecInput, err := ioutil.TempFile(os.TempDir(), "tmpgs") diff --git a/xmlsec/xmlsec.go b/xmlsec/xmlsec.go new file mode 100644 index 0000000..561d04f --- /dev/null +++ b/xmlsec/xmlsec.go @@ -0,0 +1,235 @@ +// depends on libxmlsec1 and libxml2 +package xmlsec + +// #cgo pkg-config: xmlsec1 libxml-2.0 +// +// #include +// #include +// #include +// #include +// #include +// +// #include +// #include +// #include +// #include +// +// // Wrapper for xmlFree because cgo cant access it +// static inline void xmlFreeWrapper(void *p) { +// xmlFree(p); +// } +import "C" + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "unsafe" +) + +var ( + initLock sync.Mutex + initialized uint32 + + ErrVerification = errors.New("verification error") +) + +// https://www.aleksey.com/xmlsec/api/xmlsec-xmldsig.html#XMLSECDSIGSTATUS +const ( + xmlSecDSigStatusUnknown = iota + xmlSecDSigStatusSucceeded + xmlSecDSigStatusInvalid +) + +type Options struct { + // Equivalent to using xmlsec command line utility with "--id-attr" option" + // https://www.aleksey.com/xmlsec/faq.html#section_3_2 + IDAttrs []IDAttr +} + +type IDAttr struct { + Name string + NodeName string + NsHref string +} + +// https://www.aleksey.com/xmlsec/api/xmlsec-notes-init-shutdown.html +// call when initialize the library +func Init() { + if atomic.LoadUint32(&initialized) == 1 { + return + } + C.xmlInitParser() + initLock.Lock() + defer initLock.Unlock() + if initialized == 0 { + defer atomic.StoreUint32(&initialized, 1) + if res := C.xmlSecInit(); res < 0 { + panic("xmlSecInit failed") + } + if res := C.xmlSecCryptoAppInit(nil); res < 0 { + panic("xmlSecCryptoAppInit failed") + } + if rv := C.xmlSecCryptoInit(); rv < 0 { + panic("xmlSecCryptoInit failed") + } + } +} + +// https://www.aleksey.com/xmlsec/api/xmlsec-notes-init-shutdown.html +// calls when shutting down the binary +func Shutdown() { + if atomic.LoadUint32(&initialized) == 0 { + return + } + C.xmlSecCryptoShutdown() // Shutdown xmlsec-crypto library + C.xmlSecCryptoAppShutdown() // Shutdown crypto library + C.xmlSecShutdown() // Shutdown xmlsec library + C.xmlCleanupParser() // Shutdown libxml +} + +// Verify checks that the document is signed with the public certificate. The +// caller needs to make sure that passed slices will not be mutated elsewhere +func Verify(signedDoc []byte, publicCert []byte, opts Options) error { + var ( + cert = (*C.xmlSecByte)(unsafe.Pointer(&publicCert[0])) + certLen = C.xmlSecSize(len(publicCert)) + ) + + // https://www.aleksey.com/xmlsec/api/xmlsec-keysmngr.html#XMLSECKEYSMNGRCREATE + // https://www.aleksey.com/xmlsec/api/xmlsec-keysmngr.html#XMLSECKEYSMNGRDESTROY + km := C.xmlSecKeysMngrCreate() + if km == nil { + return fmt.Errorf("xmlSecKeysMngrCreate failed") + } + defer C.xmlSecKeysMngrDestroy(km) + + // https://www.aleksey.com/xmlsec/api/xmlsec-app.html#XMLSECCRYPTOAPPDEFAULTKEYSMNGRINIT + if errno := C.xmlSecCryptoAppDefaultKeysMngrInit(km); errno < 0 { + return fmt.Errorf("xmlSecCryptoAppDefaultKeysMngrInit failed %d", errno) + } + + // https://www.aleksey.com/xmlsec/api/xmlsec-app.html#XMLSECCRYPTOAPPKEYLOADMEMORY + key := C.xmlSecCryptoAppKeyLoadMemory(cert, certLen, C.xmlSecKeyDataFormatCertPem, nil, nil, nil) + if key == nil { + return errors.New("xmlSecCryptoAppKeyLoadMemory failed") + } + + // https://www.aleksey.com/xmlsec/api/xmlsec-app.html#XMLSECCRYPTOAPPKEYCERTLOADMEMORY + if errno, err := C.xmlSecCryptoAppKeyCertLoadMemory(key, cert, certLen, C.xmlSecKeyDataFormatCertPem); errno < 0 { + C.xmlSecKeyDestroy(key) + return fmt.Errorf("xmlSecCryptoAppKeyCertLoad failed %d %v", errno, err) + } + + // https://www.aleksey.com/xmlsec/api/xmlsec-app.html#XMLSECCRYPTOAPPDEFAULTKEYSMNGRADOPTKEY + if errno := C.xmlSecCryptoAppDefaultKeysMngrAdoptKey(km, key); errno < 0 { + return fmt.Errorf("xmlSecCryptoAppDefaultKeysMngrAdoptKey failed %d", errno) + } + + // https://www.aleksey.com/xmlsec/api/xmlsec-xmldsig.html#XMLSECDSIGCTXCREATE + // https://www.aleksey.com/xmlsec/api/xmlsec-xmldsig.html#XMLSECDSIGCTXDESTROY + ctx := C.xmlSecDSigCtxCreate(km) + if ctx == nil { + return fmt.Errorf("xmlSecDSigCtxCreate failed") + } + defer C.xmlSecDSigCtxDestroy(ctx) + + parsedDoc, err := parseXML(signedDoc, opts) + if err != nil { + return err + } + defer C.xmlFreeDoc(parsedDoc) + + // https://www.aleksey.com/xmlsec/api/xmlsec-xmltree.html#XMLSECFINDNODE + node := C.xmlSecFindNode(C.xmlDocGetRootElement(parsedDoc), + (*C.xmlChar)(unsafe.Pointer(&C.xmlSecNodeSignature)), + (*C.xmlChar)(unsafe.Pointer(&C.xmlSecDSigNs))) + if node == nil { + return errors.New("xmlSecFindNode failed") + } + if errno := C.xmlSecDSigCtxVerify(ctx, node); errno < 0 || ctx.status != xmlSecDSigStatusSucceeded { + return ErrVerification + } + return nil +} + +func parseXML(doc []byte, opts Options) (*C.xmlDoc, error) { + var ( + docPtr = (*C.char)(unsafe.Pointer(&doc[0])) + docLen = C.int(len(doc)) + ) + // http://www.xmlsoft.org/html/libxml-parserInternals.html#xmlCreateMemoryParserCtxt + ctx := C.xmlCreateMemoryParserCtxt(docPtr, docLen) + if ctx == nil { + return nil, errors.New("error creating parser") + } + // this will not free ctx.myDoc + defer C.xmlFreeParserCtxt(ctx) + + if C.xmlParseDocument(ctx) == -1 { + return nil, errors.New("xmlParseDocument failed") + } + if ctx.wellFormed != 1 || ctx.valid != 1 || ctx.myDoc == nil { + return nil, errors.New("xml document is not well formed") + } + for _, attr := range opts.IDAttrs { + err := addIDAttr(C.xmlDocGetRootElement(ctx.myDoc), attr.Name, attr.NodeName, attr.NsHref) + if err != nil { + return nil, err + } + } + return ctx.myDoc, nil +} + +// https://github.com/GNOME/xmlsec/blob/8f78efe126e579041a07e342fe4dbbc38711a414/apps/xmlsec.c#L2740 +// xmlSecAppAddIDAttr(xmlNodePtr node, const xmlChar* attrName, const xmlChar* nodeName, const xmlChar* nsHref)) +func addIDAttr(node *C.xmlNode, attrName, nodeName, nsHref string) error { + var ( + attr, tmpAttr *C.xmlAttr + cur *C.xmlNode + id *C.xmlChar + ) + // process children first because it does not matter much but does simplify code + cur = C.xmlSecGetNextElementNode(node.children) + for { + if cur == nil { + break + } + if err := addIDAttr(cur, attrName, nodeName, nsHref); err != nil { + return err + } + cur = C.xmlSecGetNextElementNode(cur.next) + } + // node name must match + if C.GoString((*C.char)(unsafe.Pointer(node.name))) != nodeName { + return nil + } + // if nsHref is set then it also should match + if nsHref != "" && node.ns != nil && C.GoString((*C.char)(unsafe.Pointer(node.ns.href))) != nsHref { + return nil + } + // the attribute with name equal to attrName should exist + for attr = node.properties; attr != nil; attr = attr.next { + if C.GoString((*C.char)(unsafe.Pointer(attr.name))) == attrName { + break + } + } + if attr == nil { + return nil + } + // if found, the attribute should have some value + id = C.xmlNodeListGetString(node.doc, attr.children, 1) + if id == nil { + return nil + } + defer C.xmlFreeWrapper(unsafe.Pointer(id)) + // check that we dont have the same ID already + tmpAttr = C.xmlGetID(node.doc, id) + if tmpAttr == nil { + C.xmlAddID(nil, node.doc, id, attr) + } else if tmpAttr != attr { + return fmt.Errorf("duplicate ID attribute %s", id) + } + return nil +} diff --git a/xmlsec_test.go b/xmlsec_test.go index 41bedee..723b5f3 100644 --- a/xmlsec_test.go +++ b/xmlsec_test.go @@ -2,12 +2,21 @@ package saml import ( "encoding/xml" + "io/ioutil" + "os" "testing" "github.com/RobotsAndPencils/go-saml/util" + "github.com/RobotsAndPencils/go-saml/xmlsec" "github.com/stretchr/testify/assert" ) +func TestMain(m *testing.M) { + xmlsec.Init() + defer xmlsec.Shutdown() + os.Exit(m.Run()) +} + func TestRequest(t *testing.T) { assert := assert.New(t) cert, err := util.LoadCertificate("./default.crt") @@ -31,6 +40,8 @@ func TestRequest(t *testing.T) { func TestResponse(t *testing.T) { assert := assert.New(t) + data, err := ioutil.ReadFile("./default.crt") + assert.NoError(err) cert, err := util.LoadCertificate("./default.crt") assert.NoError(err) @@ -48,4 +59,7 @@ func TestResponse(t *testing.T) { err = VerifyRequestSignature(signedXml, "./default.crt") assert.NoError(err) + + err = VerifyRequestSignatureMem([]byte(signedXml), data) + assert.NoError(err) }