Skip to content
This repository has been archived by the owner on Jun 6, 2023. It is now read-only.

Use signedxml to validate signatures #20

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ init:
go get github.com/nu7hatch/gouuid
go get github.com/kardianos/osext
go get github.com/stretchr/testify/assert
go get github.com/ma314smith/signedxml

vet: init
@echo "$(OK_COLOR)==> Go Vetting$(NO_COLOR)"
Expand Down
4 changes: 2 additions & 2 deletions authnrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (r *AuthnRequest) Validate(publicCertPath string) error {

// TODO more validation

err := VerifyRequestSignature(r.originalString, publicCertPath)
err := Verify(r.originalString, publicCertPath)
if err != nil {
return err
}
Expand Down Expand Up @@ -241,7 +241,7 @@ func (r *AuthnRequest) SignedString(privateKeyPath string) (string, error) {
return "", err
}

return SignRequest(s, privateKeyPath)
return Sign(s, privateKeyPath)
}

// GetAuthnRequestURL generate a URL for the AuthnRequest to the IdP with the SAMLRequst parameter encoded
Expand Down
2 changes: 1 addition & 1 deletion authnrequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestGetSignedRequest(t *testing.T) {
assert.NoError(err)
assert.NotEmpty(signedXML)

err = VerifyRequestSignature(signedXML, sp.PublicCertPath)
err = Verify(signedXML, sp.PublicCertPath)
assert.NoError(err)
}

Expand Down
4 changes: 2 additions & 2 deletions authnresponse.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ 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)
err := Verify(r.originalString, s.IDPPublicCertPath)
if err != nil {
return err
}
Expand Down Expand Up @@ -285,7 +285,7 @@ func (r *Response) SignedString(privateKeyPath string) (string, error) {
return "", err
}

return SignResponse(s, privateKeyPath)
return Sign(s, privateKeyPath)
}

func (r *Response) EncodedSignedString(privateKeyPath string) (string, error) {
Expand Down
102 changes: 35 additions & 67 deletions xmlsec.go
Original file line number Diff line number Diff line change
@@ -1,103 +1,71 @@
package saml

import (
"crypto/x509"
"encoding/pem"
"errors"
"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"
"github.com/ma314smith/signedxml"
)

// 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)
}

// 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")
// Sign creates a signature for an XML document and returns it
func Sign(xml string, privateKeyPath string) (string, error) {
pemString, err := ioutil.ReadFile(privateKeyPath)
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")
pemBlock, _ := pem.Decode([]byte(pemString))
if pemBlock == nil {
return "", errors.New("Count not parse private key")
}

key, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
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()
signer, err := signedxml.NewSigner(xml)
if err != nil {
return "", errors.New(err.Error() + " : " + string(output))
return "", err
}

samlSignedRequest, err := ioutil.ReadFile(samlXmlsecOutput.Name())
samlSignedRequestXML, err := signer.Sign(key)
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)
}
// Verify validates the signature of an XML document
func Verify(xml string, publicCertPath string) error {
pemString, err := ioutil.ReadFile(publicCertPath)
if err != nil {
return err
}

// 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)
}
pemBlock, _ := pem.Decode([]byte(pemString))
if pemBlock == nil {
return errors.New("Could not parse certificate")
}

func verify(xml string, publicCertPath string, id string) error {
//Write saml to
samlXmlsecInput, err := ioutil.TempFile(os.TempDir(), "tmpgs")
cert, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
return err
}

samlXmlsecInput.WriteString(xml)
samlXmlsecInput.Close()
defer deleteTempFile(samlXmlsecInput.Name())
validator, err := signedxml.NewValidator(xml)
if err != nil {
return err
}

validator.Certificates = append(validator.Certificates, *cert)

//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()
err = validator.Validate()
if err != nil {
return errors.New("error verifing signature: " + err.Error())
return err
}
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)
}
8 changes: 4 additions & 4 deletions xmlsec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ func TestRequest(t *testing.T) {
assert.NoError(err)
xmlAuthnRequest := string(b)

signedXml, err := SignRequest(xmlAuthnRequest, "./default.key")
signedXml, err := Sign(xmlAuthnRequest, "./default.key")
assert.NoError(err)
assert.NotEmpty(signedXml)

err = VerifyRequestSignature(signedXml, "./default.crt")
err = Verify(signedXml, "./default.crt")
assert.NoError(err)
}

Expand All @@ -42,10 +42,10 @@ func TestResponse(t *testing.T) {
assert.NoError(err)
xmlResponse := string(b)

signedXml, err := SignResponse(xmlResponse, "./default.key")
signedXml, err := Sign(xmlResponse, "./default.key")
assert.NoError(err)
assert.NotEmpty(signedXml)

err = VerifyRequestSignature(signedXml, "./default.crt")
err = Verify(signedXml, "./default.crt")
assert.NoError(err)
}