Skip to content

Commit

Permalink
Add explicit tests for XSW (crewjam#338)
Browse files Browse the repository at this point in the history
XML Signature Wrapping attacks are unfortunately still very common
in SAML implementations. crewjam/saml is not vulnerable to any XSW
attacks as goxmldsig and this library's use of goxmldsig are safe.

This commit adds a number of tests against common XSW attacks, so
that these can serve as verification of the current safe state,
prevent future regressions in crewjam/saml and detect possible
future regressions in goxmldsig

The numbering of the permutations of the XSW attack follows that
of https://github.com/CompassSecurity/SAMLRaider and a visual
depiction is available in
https://github.com/CompassSecurity/SAMLRaider/blob/5b9eace70e88d0af17b86c26c2cad1178b08c7d0/src/main/resources/xswlist.png
  • Loading branch information
jkakavas authored Mar 25, 2021
1 parent 0301c03 commit 83bd6bf
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 0 deletions.
257 changes: 257 additions & 0 deletions service_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,263 @@ func TestSPInvalidAssertions(t *testing.T) {
assert.Check(t, err)
}

func TestXswPermutationOneIsRejected(t *testing.T) {
test := NewServiceProviderTest(t)
idpMetadata := golden.Get(t, "TestSPCanHandleOneloginResponse_IDPMetadata")
respStr := golden.Get(t, "TestXswPermutationOneIsRejected_response")
TimeNow = func() time.Time {
rv, _ := time.Parse("Mon Jan 2 15:04:05 UTC 2006", "Tue Jan 5 17:53:12 UTC 2016")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())

s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"),
AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"),
IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.Check(t, err)

req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", string(respStr))
_, err = s.ParseResponse(&req, []string{"id-d40c15c104b52691eccf0a2a5c8a15595be75423"})
assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr,
"cannot validate signature on Response: Missing signature referencing the top-level element"))
}

func TestXswPermutationTwoIsRejected(t *testing.T) {
test := NewServiceProviderTest(t)
idpMetadata := golden.Get(t, "TestSPCanHandleOneloginResponse_IDPMetadata")
respStr := golden.Get(t, "TestXswPermutationTwoIsRejected_response")
TimeNow = func() time.Time {
rv, _ := time.Parse("Mon Jan 2 15:04:05 UTC 2006", "Tue Jan 5 17:53:12 UTC 2016")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())

s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/metadata"),
AcsURL: mustParseURL("https://29ee6d2e.ngrok.io/saml/acs"),
IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.Check(t, err)

req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", string(respStr))
_, err = s.ParseResponse(&req, []string{"id-d40c15c104b52691eccf0a2a5c8a15595be75423"})
assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr,
"cannot validate signature on Response: Missing signature referencing the top-level element"))
}

func TestXswPermutationThreeIsRejected(t *testing.T) {
test := NewServiceProviderTest(t)
idpMetadata := golden.Get(t, "TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata")
respStr := golden.Get(t, "TestXswPermutationThreeIsRejected_response")
TimeNow = func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T01:02:59Z")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())

s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("http://sp.example.com/demo1/metadata.php"),
AcsURL: mustParseURL("http://sp.example.com/demo1/index.php?acs"),
IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.Check(t, err)

req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", string(respStr))
_, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"})
// Because this permutation contains an unsigned assertion as child of the response
assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr,
"either the Response or Assertion must be signed"))
}

func TestXswPermutationFourIsRejected(t *testing.T) {
test := NewServiceProviderTest(t)
idpMetadata := golden.Get(t, "TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata")
respStr := golden.Get(t, "TestXswPermutationFourIsRejected_response")
TimeNow = func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T01:02:59Z")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())

s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("http://sp.example.com/demo1/metadata.php"),
AcsURL: mustParseURL("http://sp.example.com/demo1/index.php?acs"),
IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.Check(t, err)

req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", string(respStr))
_, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"})
// Because this permutation contains an unsigned assertion as child of the response
assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr,
"either the Response or Assertion must be signed"))
}

func TestXswPermutationFiveIsRejected(t *testing.T) {
test := NewServiceProviderTest(t)
idpMetadata := golden.Get(t, "TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata")
respStr := golden.Get(t, "TestXswPermutationFiveIsRejected_response")
TimeNow = func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T01:02:59Z")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())

s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("http://sp.example.com/demo1/metadata.php"),
AcsURL: mustParseURL("http://sp.example.com/demo1/index.php?acs"),
IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.Check(t, err)

req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", string(respStr))
_, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"})
assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr,
"cannot validate signature on Response: Missing signature referencing the top-level element"))
}

func TestXswPermutationSixIsRejected(t *testing.T) {
test := NewServiceProviderTest(t)
idpMetadata := golden.Get(t, "TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata")
respStr := golden.Get(t, "TestXswPermutationSixIsRejected_response")
TimeNow = func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T01:02:59Z")
return rv
}
Clock = dsig.NewFakeClockAt(TimeNow())

s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("http://sp.example.com/demo1/metadata.php"),
AcsURL: mustParseURL("http://sp.example.com/demo1/index.php?acs"),
IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.Check(t, err)

req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", string(respStr))
_, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"})
assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr,
"cannot validate signature on Response: Missing signature referencing the top-level element"))
}

func TestXswPermutationSevenIsRejected(t *testing.T) {
test := NewServiceProviderTest(t)
idpMetadata := golden.Get(t, "TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata")
respStr := golden.Get(t, "TestXswPermutationSevenIsRejected_response")
TimeNow = func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T01:02:59Z")
return rv
}
Clock = dsig.NewFakeClockAt(func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T14:12:57Z")
return rv
}())

s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("http://sp.example.com/demo1/metadata.php"),
AcsURL: mustParseURL("http://sp.example.com/demo1/index.php?acs"),
IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.Check(t, err)

req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", string(respStr))
_, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"})
//It's the assertion signature that can't be verified. The error message is generic and always mentions Response
assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr,
"cannot validate signature on Response: Signature could not be verified"))
}

func TestXswPermutationEightIsRejected(t *testing.T) {
test := NewServiceProviderTest(t)
idpMetadata := golden.Get(t, "TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata")
respStr := golden.Get(t, "TestXswPermutationEightIsRejected_response")
TimeNow = func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T01:02:59Z")
return rv
}
Clock = dsig.NewFakeClockAt(func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T14:12:57Z")
return rv
}())

s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("http://sp.example.com/demo1/metadata.php"),
AcsURL: mustParseURL("http://sp.example.com/demo1/index.php?acs"),
IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.Check(t, err)

req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", string(respStr))
_, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"})
//It's the assertion signature that can't be verified. The error message is generic and always mentions Response
assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr,
"cannot validate signature on Response: Signature could not be verified"))
}

func TestXswPermutationNineIsRejected(t *testing.T) {
test := NewServiceProviderTest(t)
idpMetadata := golden.Get(t, "TestServiceProviderCanHandleSignedAssertionsResponse_IDPMetadata")
respStr := golden.Get(t, "TestXswPermutationNineIsRejected_response")
TimeNow = func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T01:02:59Z")
return rv
}
Clock = dsig.NewFakeClockAt(func() time.Time {
rv, _ := time.Parse(timeFormat, "2014-07-17T14:12:57Z")
return rv
}())

s := ServiceProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("http://sp.example.com/demo1/metadata.php"),
AcsURL: mustParseURL("http://sp.example.com/demo1/index.php?acs"),
IDPMetadata: &EntityDescriptor{},
}
err := xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.Check(t, err)

req := http.Request{PostForm: url.Values{}}
req.PostForm.Set("SAMLResponse", string(respStr))
_, err = s.ParseResponse(&req, []string{"ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"})
//It's the assertion signature that can't be verified. The error message is generic and always mentions Response
assert.Check(t, is.Error(err.(*InvalidResponseError).PrivateErr,
"cannot validate signature on Response: Missing signature referencing the top-level element"))
}

func TestSPRealWorldKeyInfoHasRSAPublicKeyNotX509Cert(t *testing.T) {
// This is a real world SAML response that we observed. It contains <ds:RSAKeyValue> elements
idpMetadata := golden.Get(t, "TestSPRealWorldKeyInfoHasRSAPublicKeyNotX509Cert_idp_metadata")
Expand Down
1 change: 1 addition & 0 deletions testdata/TestXswPermutationEightIsRejected_response
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIERlc3RpbmF0aW9uPSJodHRwOi8vc3AuZXhhbXBsZS5jb20vZGVtbzEvaW5kZXgucGhwP2FjcyIgSUQ9Il84ZThkYzVmNjlhOThjYzRjMWZmMzQyN2U1Y2UzNDYwNmZkNjcyZjkxZTYiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNGZlZTNiMDQ2Mzk1YzRlNzUxMDExZTk3Zjg5MDBiNTI3M2Q1NjY4NSIgSXNzdWVJbnN0YW50PSIyMDE0LTA3LTE3VDAxOjAxOjQ4WiIgVmVyc2lvbj0iMi4wIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIElEPSJwZngwNDY5MDBjNS0wNDIzLTM1Y2ItMmFkYi03MjI4M2JhNWQ4Y2QiIElzc3VlSW5zdGFudD0iMjAxNC0wNy0xN1QwMTowMTo0OFoiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyPmh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz48ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDA0NjkwMGM1LTA0MjMtMzVjYi0yYWRiLTcyMjgzYmE1ZDhjZCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+YmV5ZnFIOXMxUys2bDJHQkhiU2xXOFR4SzZFPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5DSkJMY0pVTm91Q0psY3d5YUtTb1RGdHJUYVJOUWJnWHJFUUdKTmZsdjJkakx0M3J0d2krRzZMd3VQZkQrckF5b3lIbXFyUXlTaVJaZ1lNeWN1bk8vNUQ2R2J5ZVhJVjNrc093Y0YrQXlWZGtrblVpcVN3SDcvOXJkdkVhZmtKcDQ3d1pYKzc4dlFGMDZNcjFnNEpsODByTmNEUncxeE9FdW9QN2pDMjVtMVE9PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDYWpDQ0FkT2dBd0lCQWdJQkFEQU5CZ2txaGtpRzl3MEJBUTBGQURCU01Rc3dDUVlEVlFRR0V3SjFjekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFVk1CTUdBMVVFQ2d3TVQyNWxiRzluYVc0Z1NXNWpNUmN3RlFZRFZRUUREQTV6Y0M1bGVHRnRjR3hsTG1OdmJUQWVGdzB4TkRBM01UY3hOREV5TlRaYUZ3MHhOVEEzTVRjeE5ERXlOVFphTUZJeEN6QUpCZ05WQkFZVEFuVnpNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJVd0V3WURWUVFLREF4UGJtVnNiMmRwYmlCSmJtTXhGekFWQmdOVkJBTU1Ebk53TG1WNFlXMXdiR1V1WTI5dE1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRFp4K09ONElVb0lXeGd1a1RiMXRPaVgzYk1ZellRaXdXUFVOTXArRnE4MnhvTm9nc28yYnlrWkcweWlKbTVvOHp2L3NkNnBHb3VheU1na3gvMkZTT2RjMzZUMGpHYkNIdVJTYnRpYTBQRXpOSVJ0bVZpTXJ0M0Flb1dCaWRSWG1ac3hDTkx3Z0lWNmRuMldwdUU1QXowYkhncFpuUXhUS0ZlazBCTUtVL2Q4d0lEQVFBQm8xQXdUakFkQmdOVkhRNEVGZ1FVR0h4WXFaWXlYN2NUeEtWT0RWZ1p3U1RkQ253d0h3WURWUjBqQkJnd0ZvQVVHSHhZcVpZeVg3Y1R4S1ZPRFZnWndTVGRDbnd3REFZRFZSMFRCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUTBGQUFPQmdRQnlGT2wraE1GSUNiZDNESmZucDJSZ2QvZHF0dHNaRy90eWhJTFd2RXJiaW8vREVlOThtWHBvd2hUa0MwNEVOcHJPeVhpN1piVXFpaWNGODl1QUd5dDFvcWdUVUNEMVZzTGFocUljbXJ6Z3VtTnlUd0xHV28xN1dEQWExL3VzRGhldFdBTWhnekYvQ25mNWVrMG5LMDBtMFlaR3ljNEx6Z0QwQ1JPTUFTVFdOZz09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PE9iamVjdD48c2FtbDpBc3NlcnRpb24geG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiBJRD0icGZ4MDQ2OTAwYzUtMDQyMy0zNWNiLTJhZGItNzIyODNiYTVkOGNkIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDctMTdUMDE6MDE6NDhaIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3NwLmV4YW1wbGUuY29tL2RlbW8xL21ldGFkYXRhLnBocCI+X2NlM2QyOTQ4YjRjZjIwMTQ2ZGVlMGEwYjNkZDZmNjliNmNmODZmNjJkNzwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNGZlZTNiMDQ2Mzk1YzRlNzUxMDExZTk3Zjg5MDBiNTI3M2Q1NjY4NSIgTm90T25PckFmdGVyPSIyMDI0LTAxLTE4VDA2OjIxOjQ4WiIgUmVjaXBpZW50PSJodHRwOi8vc3AuZXhhbXBsZS5jb20vZGVtbzEvaW5kZXgucGhwP2FjcyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTA3LTE3VDAxOjAxOjE4WiIgTm90T25PckFmdGVyPSIyMDI0LTAxLTE4VDA2OjIxOjQ4WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwOi8vc3AuZXhhbXBsZS5jb20vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wNy0xN1QwMTowMTo0OFoiIFNlc3Npb25JbmRleD0iX2JlOTk2N2FiZDkwNGRkY2FlM2MwZWI0MTg5YWRiZTNmNzFlMzI3Y2Y5MyIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyNC0wNy0xN1QwOTowMTo0OFoiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2Vyczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5leGFtcGxlcm9sZTE8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9PYmplY3Q+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3NwLmV4YW1wbGUuY29tL2RlbW8xL21ldGFkYXRhLnBocCI+X2NlM2QyOTQ4YjRjZjIwMTQ2ZGVlMGEwYjNkZDZmNjliNmNmODZmNjJkNzwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNGZlZTNiMDQ2Mzk1YzRlNzUxMDExZTk3Zjg5MDBiNTI3M2Q1NjY4NSIgTm90T25PckFmdGVyPSIyMDI0LTAxLTE4VDA2OjIxOjQ4WiIgUmVjaXBpZW50PSJodHRwOi8vc3AuZXhhbXBsZS5jb20vZGVtbzEvaW5kZXgucGhwP2FjcyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTA3LTE3VDAxOjAxOjE4WiIgTm90T25PckFmdGVyPSIyMDI0LTAxLTE4VDA2OjIxOjQ4WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwOi8vc3AuZXhhbXBsZS5jb20vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wNy0xN1QwMTowMTo0OFoiIFNlc3Npb25JbmRleD0iX2JlOTk2N2FiZDkwNGRkY2FlM2MwZWI0MTg5YWRiZTNmNzFlMzI3Y2Y5MyIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyNC0wNy0xN1QwOTowMTo0OFoiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2Vyczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5leGFtcGxlcm9sZTE8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4K
Loading

0 comments on commit 83bd6bf

Please sign in to comment.