Skip to content

Commit

Permalink
Add support for signed authnRequest (crewjam#296)
Browse files Browse the repository at this point in the history
* Fixes handling signed response with encrypted assertions

When the response is signed, the verification must happen before the assertion is decrypted since the encrypted XML is used in the signature digest.
The response signature is sufficient unless the assertion is also signed in which case both must be valid.

* Add support for signed authnRequests
  • Loading branch information
ricardofandrade authored and seongwoohong committed Dec 18, 2020
1 parent 7296b3b commit a0fe4eb
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 11 deletions.
3 changes: 2 additions & 1 deletion example/trivial/trivial.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func main() {
}

rootURL, _ := url.Parse("http://localhost:8000")
idpMetadataURL, _ := url.Parse("https://www.testshib.org/metadata/testshib-providers.xml")
idpMetadataURL, _ := url.Parse("https://samltest.id/saml/idp")

idpMetadata, err := samlsp.FetchMetadata(
context.Background(),
Expand All @@ -42,6 +42,7 @@ func main() {
IDPMetadata: idpMetadata,
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
Certificate: keyPair.Leaf,
SignRequest: true,
})
if err != nil {
panic(err) // TODO handle error
Expand Down
7 changes: 7 additions & 0 deletions samlsp/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"crypto/rsa"
"crypto/x509"
dsig "github.com/russellhaering/goxmldsig"
"net/http"
"net/url"
"time"
Expand All @@ -22,6 +23,7 @@ type Options struct {
Intermediates []*x509.Certificate
AllowIDPInitiated bool
IDPMetadata *saml.EntityDescriptor
SignRequest bool
ForceAuthn bool // TODO(ross): this should be *bool

// The following fields exist <= 0.3.0, but are superceded by the new
Expand Down Expand Up @@ -125,6 +127,10 @@ func DefaultServiceProvider(opts Options) saml.ServiceProvider {
if opts.ForceAuthn {
forceAuthn = &opts.ForceAuthn
}
signatureMethod := dsig.RSASHA1SignatureMethod
if !opts.SignRequest {
signatureMethod = ""
}

return saml.ServiceProvider{
EntityID: opts.EntityID,
Expand All @@ -136,6 +142,7 @@ func DefaultServiceProvider(opts Options) saml.ServiceProvider {
SloURL: *sloURL,
IDPMetadata: opts.IDPMetadata,
ForceAuthn: forceAuthn,
SignatureMethod: signatureMethod,
AllowIDPInitiated: opts.AllowIDPInitiated,
}
}
Expand Down
62 changes: 55 additions & 7 deletions service_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"compress/flate"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/xml"
Expand Down Expand Up @@ -101,6 +102,9 @@ type ServiceProvider struct {
// SignatureVerifier, if non-nil, allows you to implement an alternative way
// to verify signatures.
SignatureVerifier SignatureVerifier

// SignatureMethod, if non-empty, authentication requests will be signed
SignatureMethod string
}

// MaxIssueDelay is the longest allowed time between when a SAML assertion is
Expand All @@ -126,7 +130,7 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
validDuration = sp.MetadataValidDuration
}

authnRequestsSigned := false
authnRequestsSigned := len(sp.SignatureMethod) > 0
wantAssertionsSigned := true
validUntil := TimeNow().Add(validDuration)

Expand All @@ -137,12 +141,6 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
certBytes = append(certBytes, intermediate.Raw...)
}
keyDescriptors = []KeyDescriptor{
{
Use: "signing",
KeyInfo: KeyInfo{
Certificate: base64.StdEncoding.EncodeToString(certBytes),
},
},
{
Use: "encryption",
KeyInfo: KeyInfo{
Expand All @@ -156,6 +154,14 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
},
},
}
if len(sp.SignatureMethod) > 0 {
keyDescriptors = append(keyDescriptors, KeyDescriptor{
Use: "signing",
KeyInfo: KeyInfo{
Certificate: base64.StdEncoding.EncodeToString(certBytes),
},
})
}
}

return &EntityDescriptor{
Expand Down Expand Up @@ -330,9 +336,51 @@ func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string) (*AuthnReque
},
ForceAuthn: sp.ForceAuthn,
}
if len(sp.SignatureMethod) > 0 {
if err := sp.SignAuthnRequest(&req); err != nil {
return nil, err
}
}
return &req, nil
}

// SignAuthnRequest adds the `Signature` element to the `AuthnRequest`.
func (sp *ServiceProvider) SignAuthnRequest(req *AuthnRequest) error {
keyPair := tls.Certificate{
Certificate: [][]byte{sp.Certificate.Raw},
PrivateKey: sp.Key,
Leaf: sp.Certificate,
}
// TODO: add intermediates for SP
//for _, cert := range sp.Intermediates {
// keyPair.Certificate = append(keyPair.Certificate, cert.Raw)
//}
keyStore := dsig.TLSCertKeyStore(keyPair)

if sp.SignatureMethod != dsig.RSASHA1SignatureMethod &&
sp.SignatureMethod != dsig.RSASHA256SignatureMethod &&
sp.SignatureMethod != dsig.RSASHA512SignatureMethod {
return fmt.Errorf("invalid signing method %s", sp.SignatureMethod)
}
signatureMethod := sp.SignatureMethod
signingContext := dsig.NewDefaultSigningContext(keyStore)
signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList)
if err := signingContext.SetSignatureMethod(signatureMethod); err != nil {
return err
}

assertionEl := req.Element()

signedRequestEl, err := signingContext.SignEnveloped(assertionEl)
if err != nil {
return err
}

sigEl := signedRequestEl.Child[len(signedRequestEl.Child)-1]
req.Signature = sigEl.(*etree.Element)
return nil
}

// MakePostAuthenticationRequest creates a SAML authentication request using
// the HTTP-POST binding. It returns HTML text representing an HTML form that
// can be sent presented to a browser to initiate the login process.
Expand Down
99 changes: 96 additions & 3 deletions service_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func TestSPCanSetAuthenticationNameIDFormat(t *testing.T) {
assert.Equal(t, string(EmailAddressNameIDFormat), *req.NameIDPolicy.Format)
}

func TestSPCanProduceMetadata(t *testing.T) {
func TestSPCanProduceMetadataWithEncryptionCert(t *testing.T) {
test := NewServiceProviderTest()
s := ServiceProvider{
Key: test.Key,
Expand All @@ -116,13 +116,43 @@ func TestSPCanProduceMetadata(t *testing.T) {
assert.Equal(t, ""+
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2015-12-03T01:57:09Z\" entityID=\"https://example.com/saml2/metadata\">\n"+
" <SPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2015-12-03T01:57:09Z\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\" AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"true\">\n"+
" <KeyDescriptor use=\"signing\">\n"+
" <KeyDescriptor use=\"encryption\">\n"+
" <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n"+
" <X509Data>\n"+
" <X509Certificate>MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==</X509Certificate>\n"+
" </X509Data>\n"+
" </KeyInfo>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n"+
" </KeyDescriptor>\n"+
" <SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://example.com/saml2/slo\" ResponseLocation=\"https://example.com/saml2/slo\"></SingleLogoutService>\n"+
" <AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://example.com/saml2/acs\" index=\"1\"></AssertionConsumerService>\n"+
" </SPSSODescriptor>\n"+
"</EntityDescriptor>",
string(spMetadata))
}

func TestSPCanProduceMetadataWithBothCerts(t *testing.T) {
test := NewServiceProviderTest()
s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://example.com/saml2/metadata"),
AcsURL: mustParseURL("https://example.com/saml2/acs"),
SloURL: mustParseURL("https://example.com/saml2/slo"),
IDPMetadata: &EntityDescriptor{},
SignatureMethod: "not-empty",
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
assert.NoError(t, err)

spMetadata, err := xml.MarshalIndent(s.Metadata(), "", " ")
assert.NoError(t, err)
assert.Equal(t, ""+
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2015-12-03T01:57:09Z\" entityID=\"https://example.com/saml2/metadata\">\n"+
" <SPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2015-12-03T01:57:09Z\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\" AuthnRequestsSigned=\"true\" WantAssertionsSigned=\"true\">\n"+
" <KeyDescriptor use=\"encryption\">\n"+
" <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n"+
" <X509Data>\n"+
Expand All @@ -134,14 +164,21 @@ func TestSPCanProduceMetadata(t *testing.T) {
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n"+
" <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n"+
" </KeyDescriptor>\n"+
" <KeyDescriptor use=\"signing\">\n"+
" <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n"+
" <X509Data>\n"+
" <X509Certificate>MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==</X509Certificate>\n"+
" </X509Data>\n"+
" </KeyInfo>\n"+
" </KeyDescriptor>\n"+
" <SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://example.com/saml2/slo\" ResponseLocation=\"https://example.com/saml2/slo\"></SingleLogoutService>\n"+
" <AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://example.com/saml2/acs\" index=\"1\"></AssertionConsumerService>\n"+
" </SPSSODescriptor>\n"+
"</EntityDescriptor>",
string(spMetadata))
}

func TestCanProduceMetadataNoSigningKey(t *testing.T) {
func TestCanProduceMetadataNoCerts(t *testing.T) {
test := NewServiceProviderTest()
s := ServiceProvider{
MetadataURL: mustParseURL("https://example.com/saml2/metadata"),
Expand Down Expand Up @@ -248,6 +285,62 @@ func TestSPCanProducePostRequest(t *testing.T) {
string(form))
}

func TestSPCanProduceSignedRequest(t *testing.T) {
test := NewServiceProviderTest()
TimeNow = func() time.Time {
rv, _ := time.Parse("Mon Jan 2 15:04:05.999999999 UTC 2006", "Mon Dec 1 01:31:21.123456789 UTC 2015")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())
s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
IDPMetadata: &EntityDescriptor{},
SignatureMethod: dsig.RSASHA1SignatureMethod,
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
assert.NoError(t, err)

redirectURL, err := s.MakeRedirectAuthenticationRequest("relayState")
assert.NoError(t, err)

decodedRequest, err := testsaml.ParseRedirectRequest(redirectURL)
assert.NoError(t, err)
assert.Equal(t,
"idp.testshib.org",
redirectURL.Host)
assert.Equal(t,
"/idp/profile/SAML2/Redirect/SSO",
redirectURL.Path)
assert.Equal(t,
"<samlp:AuthnRequest xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"id-00020406080a0c0e10121416181a1c1e20222426\" Version=\"2.0\" IssueInstant=\"2015-12-01T01:31:21.123Z\" Destination=\"https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO\" AssertionConsumerServiceURL=\"https://15661444.ngrok.io/saml2/acs\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"><saml:Issuer Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\">https://15661444.ngrok.io/saml2/metadata</saml:Issuer><ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/><ds:SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/><ds:Reference URI=\"#id-00020406080a0c0e10121416181a1c1e20222426\"><ds:Transforms><ds:Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/><ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/></ds:Transforms><ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><ds:DigestValue>XQ5+kdgOf34vpAemZRFalLlzjr0=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>Wtomi/PiWx0bMFlImy5soCrrDbdY4BR2Qb8woGqc8KsVtXAwvl6lfYE2tuoT0YS5ipPLMMsFG8dB1TmLcA+0lnUcqfBiTiiHEwTIo3193RIsoH3STlOmXqBQf9Ax2nRdX+/4HwIYF58lgUzOb+nur+zGL6mYw2xjQBw6YGaX9Cc=</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><samlp:NameIDPolicy Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:transient\" AllowCreate=\"true\"/></samlp:AuthnRequest>",
string(decodedRequest))
}

func TestSPFailToProduceSignedRequestWithBogusSignatureMethod(t *testing.T) {
test := NewServiceProviderTest()
TimeNow = func() time.Time {
rv, _ := time.Parse("Mon Jan 2 15:04:05.999999999 UTC 2006", "Mon Dec 1 01:31:21.123456789 UTC 2015")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())
s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
IDPMetadata: &EntityDescriptor{},
SignatureMethod: "bogus",
}
err := xml.Unmarshal([]byte(test.IDPMetadata), &s.IDPMetadata)
assert.NoError(t, err)

_, err = s.MakeRedirectAuthenticationRequest("relayState")
assert.Errorf(t, err, "invalid signing method bogus")
}

func TestSPCanProducePostLogoutRequest(t *testing.T) {
test := NewServiceProviderTest()
TimeNow = func() time.Time {
Expand Down

0 comments on commit a0fe4eb

Please sign in to comment.