Test-SmtpSaslAuthBlob.psm1
|
<#
.Synopsis Diagnostic module for SMTP SASL Auth Blob .Description Utility module for testing SMTP SASL Auth Blob for common issues and misconfigurations. Validates SASL XOAUTH2 authentication blobs and their embedded OAuth tokens. .Parameter EncodedAuthBlob Base64 encoded SASL XOAUTH2 auth blob to test. Expected format when decoded: user=username^Aauth=Bearer token^A^A Where ^A represents the ASCII control character 0x01 .Parameter RbacEnabledApplication Switch to indicate if RBAC enforcement is enabled for application authentication. .Example # Test an auth blob Test-SmtpSaslAuthBlob -EncodedAuthBlob 'dXNlcj1zb21ldXNlckBleGFtcGxlLmNvbQFhdXRoPUJlYXJlciB5YTI5LnZGOWRmdDRxbVRjMk52YjNSbGNrQmhkSFJoZG1semRHRXVZMjl0Q2cBAQ==' -Verbose AuthBlobUserName : someuser@example.com AuthBlobToken : eyJ0eXAiOiJKV1QiLCJub25jZSI6IlItWW... OAuthTokenAudience : https://outlook.office.com OAuthTokenScopes : SMTP.Send OAuthTokenRoles : OAuthTokenUpn : someuser@example.com OAuthTokenExpirationUtc : 01/01/1970 12:00:00 AM ApplicationId : 9954180a-16f4-4683-aaaaaaaaaaaa AppDisplayName : My OAuth SMTP Application IsAuthBlobValid : True IsAuthTokenValid : True .Notes The SASL XOAUTH2 mechanism format specification: base64("user=" + userName + "^Aauth=Bearer " + accessToken + "^A^A") Where ^A is the ASCII control character 0x01 (SOH - Start of Heading) #> using module .\Utils.psm1 # Constants for SASL blob validation $script:SASL_CONTROL_CHAR = [char]0x01 $script:SASL_USER_PREFIX = "user=" $script:SASL_AUTH_PREFIX = "auth=" $script:SASL_BEARER_PREFIX = "Bearer " $script:SASL_USER_PREFIX_LENGTH = $script:SASL_USER_PREFIX.Length $script:SASL_AUTH_PREFIX_LENGTH = $script:SASL_AUTH_PREFIX.Length $script:SASL_BEARER_PREFIX_LENGTH = $script:SASL_BEARER_PREFIX.Length # Flag if RBAC enforcement is enabled for application authentication $script:RbacEnabled = $false function Test-SmtpSaslAuthBlob { [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0)] [string]$EncodedAuthBlob, [Parameter(Mandatory = $false)] [switch]$RbacEnabledApplication ) [bool]$script:RbacEnabled = $RbacEnabledApplication if ($script:RbacEnabled) { Write-Verbose "RBAC enforcement for application authentication is enabled." } # Initialize result object $result = [PSCustomObject]@{ AuthBlobUserName = $null AuthBlobToken = $null OAuthTokenAudience = $null OAuthTokenScopes = $null OAuthTokenRoles = $null OAuthTokenUpn = $null OAuthTokenExpirationUtc = $null ApplicationId = $null AppDisplayName = $null IsAuthBlobValid = $false IsAuthTokenValid = $false } # Validate input parameter if ([string]::IsNullOrEmpty($EncodedAuthBlob)) { Write-Error "AuthBlob is null or empty. Please supply a value to test." -ErrorAction Stop } try { # Decode and validate the SASL blob structure $decodedAuthBlob = ConvertFrom-EncodedSaslBlob -EncodedAuthBlob $EncodedAuthBlob $parsedBlob = ConvertFrom-SaslAuthBlob -DecodedBlob $decodedAuthBlob # Extract user and token information $result.AuthBlobUserName = $parsedBlob.UserName $result.AuthBlobToken = $parsedBlob.AccessToken # Validate the OAuth token Test-AccessTokenStructure -AccessToken $result.AuthBlobToken -Result $result -UserName $result.AuthBlobUserName # Mark blob as valid if we reach here $result.IsAuthBlobValid = $true Write-Verbose "SASL auth blob validation completed successfully" return $result } catch { Write-Error $_.Exception.Message -ErrorAction Stop } } function ConvertFrom-EncodedSaslBlob { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$EncodedAuthBlob ) try { $decodedBlob = DecodeBase64Value($EncodedAuthBlob) Write-Verbose "Successfully decoded SASL auth blob" return $decodedBlob } catch { Write-Verbose "Failed to decode auth blob: $EncodedAuthBlob" throw "AuthBlob is not a valid base64 encoded string. Check your input and try again." } } function ConvertFrom-SaslAuthBlob { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param( [Parameter(Mandatory = $true)] [string]$DecodedBlob ) Write-Verbose "Parsing SASL auth blob structure" # Get key indices in the blob $userIndex = $DecodedBlob.IndexOf($script:SASL_USER_PREFIX) $authIndex = $DecodedBlob.IndexOf($script:SASL_AUTH_PREFIX) $bearerIndex = $DecodedBlob.IndexOf($script:SASL_BEARER_PREFIX) # Validate SASL blob structure $charArray = $DecodedBlob.ToCharArray() # Validate 'user=' is at the beginning if ($userIndex -ne 0) { Write-Verbose "Invalid SASL blob. Expected 'user=' at index 0. Found at index: $userIndex" throw "SASL auth blob is incorrectly formatted. 'user=' parameter not found in correct position." } # Validate 'auth=' is present if ($authIndex -eq -1) { Write-Verbose "Invalid SASL blob. 'auth=' not found in blob." throw "SASL auth blob is incorrectly formatted. 'auth=' parameter not found in blob." } # Validate 'Bearer ' is present after 'auth=' if ($bearerIndex -eq -1 -or $bearerIndex -lt $authIndex) { Write-Verbose "Invalid SASL blob. 'Bearer ' not found or not in correct position. Bearer index: $bearerIndex" throw "SASL auth blob is incorrectly formatted. 'Bearer ' not found in expected position." } # Validate control character before 'auth=' if ($authIndex -eq 0 -or $charArray[$authIndex - 1] -ne $script:SASL_CONTROL_CHAR) { if ($authIndex -gt 0) { $actualChar = GetCharHexValue($charArray[$authIndex - 1]) Write-Verbose "Invalid SASL blob. Expected control character 0x01 before 'auth=' but found: 0x$actualChar" } else { Write-Verbose "Invalid SASL blob. Control character 0x01 not found before 'auth='." } throw "SASL auth blob is incorrectly formatted. Missing control character (0x01) before 'auth='." } # Validate control characters at the end (should be ^A^A) if ($charArray.Length -lt 2 -or $charArray[-1] -ne $script:SASL_CONTROL_CHAR -or $charArray[-2] -ne $script:SASL_CONTROL_CHAR) { $endChars = if ($charArray.Length -ge 2) { "0x$(GetCharHexValue($charArray[-2]))0x$(GetCharHexValue($charArray[-1]))" } else { "insufficient length" } Write-Verbose "Invalid SASL blob. Expected control characters 0x01 0x01 at end but found: $endChars" throw "SASL auth blob is incorrectly formatted. Missing control characters (0x01 0x01) at the end." } # Extract username: from after "user=" to before the control character preceding "auth=" try { $startPos = $userIndex + $script:SASL_USER_PREFIX_LENGTH $length = $authIndex - 1 - $startPos # -1 to exclude the control character if ($length -le 0) { throw "Invalid username length calculated: $length" } $userName = $DecodedBlob.Substring($startPos, $length) if ([string]::IsNullOrEmpty($userName)) { throw "Username is empty" } Write-Verbose "Extracted username: $userName" } catch { Write-Verbose "Failed to extract username from SASL blob" throw "AuthBlob does not contain a valid username" } # Extract token: from after "auth=Bearer " to before the final control characters try { $startPos = $bearerIndex + $script:SASL_BEARER_PREFIX_LENGTH $length = $DecodedBlob.Length - 2 - $startPos # -2 to exclude the final ^A^A if ($length -le 0) { throw "Invalid token length calculated: $length" } $accessToken = $DecodedBlob.Substring($startPos, $length) if ([string]::IsNullOrEmpty($accessToken)) { throw "Access token is empty" } Write-Verbose "Extracted access token (length: $($accessToken.Length))" } catch { Write-Verbose "Failed to extract access token from SASL blob" throw "AuthBlob does not contain a valid access token" } return @{ UserName = $userName AccessToken = $accessToken } } # OAuth token validation constants $script:EXPECTED_AUDIENCES = @( "https://outlook.office365.com", "https://outlook.office.com", "https://outlook.office365.us" ) $script:REQUIRED_DELEGATED_SCOPE = "SMTP.Send" $script:REQUIRED_APPLICATION_ROLE = "SMTP.SendAsApp" function Test-AccessTokenStructure { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$AccessToken, [Parameter(Mandatory = $true)] [PSCustomObject]$Result, [Parameter(Mandatory = $true)] [string]$UserName ) if ([string]::IsNullOrEmpty($AccessToken)) { Write-Verbose "Access token is null or empty. Skipping token verification." $Result.IsAuthTokenValid = $false return } try { # Parse and decode the JWT token $tokenParts = $AccessToken.Split(".") # Validate JWT structure (header.payload.signature) if ($tokenParts.Count -ne 3 -or [string]::IsNullOrEmpty($tokenParts[0]) -or [string]::IsNullOrEmpty($tokenParts[1])) { throw "Invalid JWT token structure. Expected 3 parts (header.payload.signature)" } # Decode and parse token claims try { # Validate header can be decoded DecodeBase64Value($tokenParts[0]) | Out-Null # Decode payload and convert from JSON $decodedPayload = DecodeBase64Value($tokenParts[1]) $tokenClaims = ConvertFrom-Json $decodedPayload Write-Verbose "Successfully decoded JWT token claims" } catch { throw "Failed to decode JWT token or token payload is not valid JSON: $($_.Exception.Message)" } # Populate result with token information $Result.OAuthTokenAudience = $tokenClaims.aud $Result.OAuthTokenUpn = $tokenClaims.upn $Result.OAuthTokenScopes = $tokenClaims.scp $Result.OAuthTokenRoles = $tokenClaims.roles $Result.ApplicationId = $tokenClaims.appid $Result.AppDisplayName = $tokenClaims.app_displayname # Convert Unix timestamp to UTC DateTime if ($tokenClaims.exp) { try { $Result.OAuthTokenExpirationUtc = [System.DateTimeOffset]::FromUnixTimeSeconds($tokenClaims.exp).UtcDateTime } catch { Write-Verbose "Failed to parse token expiration time: $($tokenClaims.exp)" $Result.OAuthTokenExpirationUtc = $null } } # Validate token claims $isTokenValid = Test-TokenClaim -TokenClaims $tokenClaims -UserName $UserName $Result.IsAuthTokenValid = $isTokenValid } catch { Write-Verbose "OAuth token validation failed: $($_.Exception.Message)" throw "Token in auth blob is invalid: $($_.Exception.Message)" } } function Test-TokenClaim { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [PSCustomObject]$TokenClaims, [Parameter(Mandatory = $true)] [string]$UserName ) $validationResults = @() # Test audience claim if ($TokenClaims.aud -notin $script:EXPECTED_AUDIENCES) { Write-Verbose "Invalid audience claim. Expected one of: '$($script:EXPECTED_AUDIENCES -join "', '")' but found: '$($TokenClaims.aud)'" Write-Warning "Authentication token contains an invalid audience claim for SMTP Client Submission." $validationResults += $false } else { Write-Verbose "Token audience validation passed: $($TokenClaims.aud)" $validationResults += $true } # Test expiration if (-not $TokenClaims.exp) { Write-Verbose "Token does not contain expiration claim" Write-Warning "Authentication token does not contain expiration information." $validationResults += $false } else { try { $expirationTime = [System.DateTimeOffset]::FromUnixTimeSeconds($TokenClaims.exp).UtcDateTime $currentTime = (Get-Date).ToUniversalTime() if ($currentTime -gt $expirationTime) { Write-Verbose "Token has expired. Expiration: '$expirationTime', Current: '$currentTime'" Write-Warning "Authentication token has expired." $validationResults += $false } else { Write-Verbose "Token expiration validation passed. Expires: $expirationTime" $validationResults += $true } } catch { Write-Verbose "Failed to validate token expiration: $($_.Exception.Message)" Write-Warning "Unable to validate token expiration time." $validationResults += $false } } # Determine authentication type and test permissions $isApplicationAuth = [string]::IsNullOrEmpty($TokenClaims.upn) if ($isApplicationAuth) { Write-Verbose "Application authentication detected. UPN claim not found in token." Write-Output "Application authentication detected." # Only time empty roles are allowed is in RBAC enabled applications if (-not $script:RbacEnabled -and [string]::IsNullOrEmpty($TokenClaims.roles)) { Write-Verbose "Roles claim is null or empty" Write-Warning "Required permission for SMTP Client Submission not found in token." $validationResults += $false } else { # Check if roles is null before splitting if ([string]::IsNullOrEmpty($TokenClaims.roles)) { $roles = @() } else { $roles = $TokenClaims.roles.Split(' ') } # Check that no SMTP roles are present in RBAC enabled applications if ($script:RbacEnabled -and $script:REQUIRED_APPLICATION_ROLE -in $roles) { Write-Verbose "Unexpected SMTP roles found in RBAC enabled application." Write-Warning "RBAC enabled application should not have direct SMTP permissions assigned." $validationResults += $false } elseif ($script:RbacEnabled -eq $false -and $script:REQUIRED_APPLICATION_ROLE -notin $roles) { Write-Verbose "Required role missing. Expected: '$script:REQUIRED_APPLICATION_ROLE', Found: '$($TokenClaims.roles)'" Write-Warning "Required permission for SMTP Client Submission not found in token." $validationResults += $false } else { Write-Verbose "Application permissions validation passed" $validationResults += $true } } } else { Write-Output "Delegated authentication detected" $isValid = $true # Validate UPN matches auth blob username if ($TokenClaims.upn -ne $UserName) { Write-Verbose "UPN mismatch. Token UPN: '$($TokenClaims.upn)', Auth blob username: '$UserName'" Write-Warning "UPN in authentication token and AuthBlob do not match." $isValid = $false } # Validate required scope if ([string]::IsNullOrEmpty($TokenClaims.scp)) { Write-Verbose "Scopes claim is null or empty" Write-Warning "Required permission for SMTP Client Submission not found in token." $isValid = $false } else { $scopes = $TokenClaims.scp.Split(' ') if ($script:REQUIRED_DELEGATED_SCOPE -notin $scopes) { Write-Verbose "Required scope missing. Expected: '$script:REQUIRED_DELEGATED_SCOPE', Found: '$($TokenClaims.scp)'" Write-Warning "Required permission for SMTP Client Submission not found in token." $isValid = $false } else { Write-Verbose "Delegated permissions validation passed" } } $validationResults += $isValid } # Return true only if all validations passed return ($validationResults -notcontains $false) } # SIG # Begin signature block # MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUvLt3CyWVFi98zGotiQ8KE1ZU # RWuggiFMMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0B # AQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk # IElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw # ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz # 7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS # 5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7 # bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfI # SKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jH # trHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14 # Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2 # h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt # 6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPR # iQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ER # ElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4K # Jpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAd # BgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SS # y4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAk # BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAC # hjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURS # b290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRV # HSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyh # hyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO # 0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo # 8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++h # UD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5x # aiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMIIGsDCCBJig # AwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG # EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl # cnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEw # NDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UE # ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg # Q29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7 # xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAk # ZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6 # mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4Q # vRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSl # rzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7 # CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiD # NipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy7 # 40hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110 # YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOF # ECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLM # bDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYE # FGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/n # upiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3Bggr # BgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNv # bTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lD # ZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2Ny # bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0g # BBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2N # CHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6 # XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9 # oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjV # Gv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gz # yE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m # 6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG # 1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsTo # FpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TO # HnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampA # MEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPt # hkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGtDCCBJygAwIBAgIQDcesVwX/ # IZkuQEMiDDpJhjANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UE # ChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYD # VQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjUwNTA3MDAwMDAwWhcN # MzgwMTE0MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs # IEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5n # IFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A # MIICCgKCAgEAtHgx0wqYQXK+PEbAHKx126NGaHS0URedTa2NDZS1mZaDLFTtQ2oR # jzUXMmxCqvkbsDpz4aH+qbxeLho8I6jY3xL1IusLopuW2qftJYJaDNs1+JH7Z+Qd # SKWM06qchUP+AbdJgMQB3h2DZ0Mal5kYp77jYMVQXSZH++0trj6Ao+xh/AS7sQRu # QL37QXbDhAktVJMQbzIBHYJBYgzWIjk8eDrYhXDEpKk7RdoX0M980EpLtlrNyHw0 # Xm+nt5pnYJU3Gmq6bNMI1I7Gb5IBZK4ivbVCiZv7PNBYqHEpNVWC2ZQ8BbfnFRQV # ESYOszFI2Wv82wnJRfN20VRS3hpLgIR4hjzL0hpoYGk81coWJ+KdPvMvaB0WkE/2 # qHxJ0ucS638ZxqU14lDnki7CcoKCz6eum5A19WZQHkqUJfdkDjHkccpL6uoG8pbF # 0LJAQQZxst7VvwDDjAmSFTUms+wV/FbWBqi7fTJnjq3hj0XbQcd8hjj/q8d6ylgx # CZSKi17yVp2NL+cnT6Toy+rN+nM8M7LnLqCrO2JP3oW//1sfuZDKiDEb1AQ8es9X # r/u6bDTnYCTKIsDq1BtmXUqEG1NqzJKS4kOmxkYp2WyODi7vQTCBZtVFJfVZ3j7O # gWmnhFr4yUozZtqgPrHRVHhGNKlYzyjlroPxul+bgIspzOwbtmsgY1MCAwEAAaOC # AV0wggFZMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO9vU0rp5AZ8esri # kFb2L9RJ7MtOMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1Ud # DwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkw # JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcw # AoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJv # b3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwB # BAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQAXzvsWgBz+Bz0RdnEw # vb4LyLU0pn/N0IfFiBowf0/Dm1wGc/Do7oVMY2mhXZXjDNJQa8j00DNqhCT3t+s8 # G0iP5kvN2n7Jd2E4/iEIUBO41P5F448rSYJ59Ib61eoalhnd6ywFLerycvZTAz40 # y8S4F3/a+Z1jEMK/DMm/axFSgoR8n6c3nuZB9BfBwAQYK9FHaoq2e26MHvVY9gCD # A/JYsq7pGdogP8HRtrYfctSLANEBfHU16r3J05qX3kId+ZOczgj5kjatVB+NdADV # ZKON/gnZruMvNYY2o1f4MXRJDMdTSlOLh0HCn2cQLwQCqjFbqrXuvTPSegOOzr4E # Wj7PtspIHBldNE2K9i697cvaiIo2p61Ed2p8xMJb82Yosn0z4y25xUbI7GIN/TpV # fHIqQ6Ku/qjTY6hc3hsXMrS+U0yy+GWqAXam4ToWd2UQ1KYT70kZjE4YtL8Pbzg0 # c1ugMZyZZd/BdHLiRu7hAWE6bTEm4XYRkA6Tl4KSFLFk43esaUeqGkH/wyW4N7Oi # gizwJWeukcyIPbAvjSabnf7+Pu0VrFgoiovRDiyx3zEdmcif/sYQsfch28bZeUz2 # rtY/9TCA6TD8dC3JE3rYkrhLULy7Dc90G6e8BlqmyIjlgp2+VqsS9/wQD7yFylIz # 0scmbKvFoW2jNrbM1pD2T7m3XDCCBu0wggTVoAMCAQICEAqA7xhLjfEFgtHEdqeV # dGgwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lD # ZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFt # cGluZyBSU0E0MDk2IFNIQTI1NiAyMDI1IENBMTAeFw0yNTA2MDQwMDAwMDBaFw0z # NjA5MDMyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgU0hBMjU2IFJTQTQwOTYgVGltZXN0YW1w # IFJlc3BvbmRlciAyMDI1IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC # AQDQRqwtEsae0OquYFazK1e6b1H/hnAKAd/KN8wZQjBjMqiZ3xTWcfsLwOvRxUwX # cGx8AUjni6bz52fGTfr6PHRNv6T7zsf1Y/E3IU8kgNkeECqVQ+3bzWYesFtkepEr # vUSbf+EIYLkrLKd6qJnuzK8Vcn0DvbDMemQFoxQ2Dsw4vEjoT1FpS54dNApZfKY6 # 1HAldytxNM89PZXUP/5wWWURK+IfxiOg8W9lKMqzdIo7VA1R0V3Zp3DjjANwqAf4 # lEkTlCDQ0/fKJLKLkzGBTpx6EYevvOi7XOc4zyh1uSqgr6UnbksIcFJqLbkIXIPb # cNmA98Oskkkrvt6lPAw/p4oDSRZreiwB7x9ykrjS6GS3NR39iTTFS+ENTqW8m6TH # uOmHHjQNC3zbJ6nJ6SXiLSvw4Smz8U07hqF+8CTXaETkVWz0dVVZw7knh1WZXOLH # gDvundrAtuvz0D3T+dYaNcwafsVCGZKUhQPL1naFKBy1p6llN3QgshRta6Eq4B40 # h5avMcpi54wm0i2ePZD5pPIssoszQyF4//3DoK2O65Uck5Wggn8O2klETsJ7u8xE # ehGifgJYi+6I03UuT1j7FnrqVrOzaQoVJOeeStPeldYRNMmSF3voIgMFtNGh86w3 # ISHNm0IaadCKCkUe2LnwJKa8TIlwCUNVwppwn4D3/Pt5pwIDAQABo4IBlTCCAZEw # DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU5Dv88jHt/f3X85FxYxlQQ89hjOgwHwYD # VR0jBBgwFoAU729TSunkBnx6yuKQVvYv1Ensy04wDgYDVR0PAQH/BAQDAgeAMBYG # A1UdJQEB/wQMMAoGCCsGAQUFBwMIMIGVBggrBgEFBQcBAQSBiDCBhTAkBggrBgEF # BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMF0GCCsGAQUFBzAChlFodHRw # Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3Rh # bXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5jcnQwXwYDVR0fBFgwVjBUoFKgUIZO # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0 # YW1waW5nUlNBNDA5NlNIQTI1NjIwMjVDQTEuY3JsMCAGA1UdIAQZMBcwCAYGZ4EM # AQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAZSqt8RwnBLmuYEHs # 0QhEnmNAciH45PYiT9s1i6UKtW+FERp8FgXRGQ/YAavXzWjZhY+hIfP2JkQ38U+w # tJPBVBajYfrbIYG+Dui4I4PCvHpQuPqFgqp1PzC/ZRX4pvP/ciZmUnthfAEP1HSh # TrY+2DE5qjzvZs7JIIgt0GCFD9ktx0LxxtRQ7vllKluHWiKk6FxRPyUPxAAYH2Vy # 1lNM4kzekd8oEARzFAWgeW3az2xejEWLNN4eKGxDJ8WDl/FQUSntbjZ80FU3i54t # px5F/0Kr15zW/mJAxZMVBrTE2oi0fcI8VMbtoRAmaaslNXdCG1+lqvP4FbrQ6IwS # BXkZagHLhFU9HCrG/syTRLLhAezu/3Lr00GrJzPQFnCEH1Y58678IgmfORBPC1JK # kYaEt2OdDh4GmO0/5cHelAK2/gTlQJINqDr6JfwyYHXSd+V08X1JUPvB4ILfJdmL # +66Gp3CSBXG6IwXMZUXBhtCyIaehr0XkBoDIGMUG1dUtwq1qmcwbdUfcSYCn+Own # cVUXf53VJUNOaMWMts0VlRYxe5nK+At+DI96HAlXHAL5SlfYxJ7La54i71McVWRP # 66bW+yERNpbJCjyCYG2j+bdpxo/1Cy4uPcU3AWVPGrbn5PhDBf3Froguzzhk++am # i+r3Qrx5bIbY3TVzgiFI7Gq3zWcwggdaMIIFQqADAgECAhANw2Fva20K+rkvK+/+ # Bxk+MA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdp # Q2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNp # Z25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwHhcNMjUwNzE1MDAwMDAwWhcN # MjgwNzE2MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEOMAwGA1UECBMFVGV4YXMxDzAN # BgNVBAcTBklydmluZzEYMBYGA1UEChMPUmljaGFyZCBGYWphcmRvMRgwFgYDVQQD # Ew9SaWNoYXJkIEZhamFyZG8wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC # AQDsnVRBsMdpPCQMc05ysOVukiJC6uzFiamN/MuOMMicR/oTyBZa9o1+l0PV8uBJ # 8v5yu/eDmZkwCuJRhL1QA/T5FrJ/gwwI3YtHOWBG1QDOXsg8qiekMsaFgdYWAjCe # nEriC25YZE3I6mYNbWinJ/9/gsmP8EZCRGFbFtXfZO70c+83P2Q7TlS6R/Jt9ZgM # d7ML6F6jvvvxSR7lJ2D+7tyiNE45r/eWjL0tYAXaKaKpnnZbMW+At/vYjRqN10CE # OSnhQeuvTZTTiy4E/gjS6dN8gvregoO/9bAVofkDuRh6s+UvZtsp+yYyFpHTPbd6 # xEzwSwplT4/CYIVvaFEZ7mD0m3iIqR6RrdQYsdwB1dRKzJDgpxisUuAnpXRFUF8U # NHM17/mXQPf8wwJHnleVJbnecQ8ltiVvsMKi1lveru7T8b5EeSdt1vwMzUxe6tS2 # qWeYPvBvToOc0SyrmKPvLYq3Ut6fveK5g/XRXdqj6F2JRNvelnP+gg+DIgctoKTU # 7U8G07YIaJtO/lCIpvJXgNd0VD4iIZkhKMAbW95k4ovBhNJ9LZJ1mKF21i1MetFU # 6vk4T2kE8oSCzAH6gFyZ+SXhGzyzcOwhv/tBhYAiwwA7keSotpqgbnhnQST0XEFP # 9eQP1zMrpuGDNZMIxZFImD2dgNdP+X7e8zH8PJ6HxTwt/QIDAQABo4ICAzCCAf8w # HwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYEFJflaBtr # VOU8mYho6v2pIvOs2kN9MD4GA1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUH # AgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMCB4Aw # EwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDov # L2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdS # U0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4 # NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw # OTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IC # AQBcJngbL8FgxIfCimjUkh+HAPlxzbWJW3brq7D3RHe9wfV5ItOrui6Op64vKVg5 # T5lyzZrOAzQM6GPXBBoWWVLfdSscLsI/NvLMx6eW81My+xFUgRnFSp/IQ/DwQNUf # bthZJNN8us+urwPCoE7pinMHVOx4K5OtV9tBuu9qrqvti1IlAueOkkPZB9sKlN+a # Q2e7o7IaEDeQ8cf9NXNlwUU5Npt4A15AyY/Rs4UZ0VA+V5+Jg12fxcP7GzonxUoB # kNdeX6o9MnOSu0uyoahFqU6xdqhYt6rp9Jm4DCyTnOH0PhCOc2o4Yw+kyLCT89G9 # eh141zP3S3enNXUqZX1Y/oYHAk0yv1o5ANKmpLD6oHMYmD0MurprwX9Fs36mIllp # ElxPpgnVZK6RvJjucrM9+Ohd1wTaQWLFJ/P1ma2gwZqucHebrd5m+8aABh//jtAs # bU6cu5hQm/+Ip5jKD8BJE5xiZNr++fOokE6/x38MMgqtPSj29KNOwcAUAV73aY+x # CKC6uRBoMaKHXVdN8r2f4/8a6dA26PaEHLY0v/PeWYUxCfiyCgMcqhFUkhP7HvsX # 3/XxfRLB+FM2n1MkfECTc2C17mAb1+Gmt0yW6gd141JLzJAfogQxYt3FoJEcht7x # /yvg+sWOkZtILz1ndlgt2eo/aQL/Z17OYxdPdo4rSXD8GTGCBkgwggZEAgEBMH0w # aTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQD # EzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4 # NCAyMDIxIENBMQIQDcNhb2ttCvq5Lyvv/gcZPjAJBgUrDgMCGgUAoHgwGAYKKwYB # BAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAc # BgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUc3aB # VqHST3bmd1kOI1nMqHG+AlkwDQYJKoZIhvcNAQEBBQAEggIAjJjx0lEhmd6IzTe9 # 3y9n3r0K7U1VmWX/dEGZE8ZnaTXbgYT89Ld3hcsYijum1CgckQttWmLvsyoWFzis # pGGcS/9d38/GKhdVDl+d5MxX/yveG0imyu/jGwc37VoSHnCovrSKhZZg3q6If1k/ # QiUjcF8842K0dB3lOnptpuU3JQH5nzyLI6IxsHZVtrTzBxHLA1U7N+e920HwhSL2 # y++IRNBRTyqA4dLi/v+HVglcUwPoehEXx/Mh934ECJmFQQ3XHvlsDdChMyELzhWx # 60TeXYTksxQyLZltarEnI6C4fSwi0GYW8P6d2hVMpoHK6yW7NzMCR04ie9PqUYEE # qxBKxVqb6Ai7IkEGWWp23bAmP4zs/FCgEGeMkNivtUGr/akt5zDp9urcQyJO3AiM # xIHtrXODlIgGd+HzZfoYupJwjfEfawG5T09lJdIoYfhKoXM75odFsIxgmvKMytCq # gS69jQcXrkwxFzpuencyYb0PtwiCzWl0HWuOh/Y3ZNDmhRiwcJ2rS99UQPtCydh1 # 5YNNdwxuApH6tVxCXqiaJ0rHg1923hHYac/e+1wzVNeurJqiE+T2b5+yYGMU+KwS # LMCLU4wnoTZzXd/q9v0oKB1nbFsYr+NYPwUVqUUYsy3tSKEGY7ohMe/uQ3yFygPK # w+7lecal0OVXC18nNe1rMP5ic5ChggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8C # AQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUA # oGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjUx # MTE4MjAxMjUxWjAvBgkqhkiG9w0BCQQxIgQg5ENX/7bdkQFJtljZax3CSYHtxZcy # 99wI+H3BZYSoy4YwDQYJKoZIhvcNAQEBBQAEggIAelD/C6sz0L47hl+R5/I1qGaM # jeWjCeibDcaeE8SubITSwA1bzTdA1LhJPbhhe4KAmVYOPWzWxL+GcKf+p/Ul4dtR # 3PtyUQ98Rg4vtg3BsWRR/2wb0UJB60E3UdoRgBrey+0z52pVNU1FtE3LV5jmjIb7 # D+xJW5+Ua8KjOp/mix41yP2pYNSuwyPrEmhgK1lDqsfzsxU6UcqvBWBx4bPaqsQh # q5zQW5WA61KocGWmJCIkcVjr7eQ4QVhXNvnjsNQ7DIja+LZjAzTf7keSLdPjO8Yz # cc3b1gVKCWbX/QrTPB9y6ZHrPjEt8VSjwKJi1ivtkHYC47WjYFBUES4FETRR/yKz # KmZmXKVqHpf0x0mgo5hM11Iwoz45UqvdoF9G4MWH5cyelyX2P6ZZXk2f4rnTHhW0 # n/PfaryeWYN+XDOuIz0qkz+jJafr7KuZWrQk4IC1Gp/PxxFp0p83iPVmtcJkN/50 # BVvHNoERqxWC3zgJwUFEk/+Zg/dq4JOhtvDeIxE7CKGunqXqODFhzH/dc5zvpPB8 # eCJurXddm69pjHQhHG/Ld9FUAh42dCz7+Ojo3M+6zT6Gdv3dmCKtm0/QnH/3HsYy # RcbrpX/GqDnXjdubSqIqazPl3cVXdGjU0GIibIx8cbqwciIDM869sqVuVmrlENhg # 5pJIUZg+KP/KXW4Gj5U= # SIG # End signature block |