使用最新SDK在Azure应用服务上安装TLS证书时出现问题

k7fdbhmy  于 2023-05-23  发布在  其他
关注(0)|答案(1)|浏览(255)
  • (注意:VB.NET或C#答案都可以。)*

使用new SDK安装上载的证书时遇到问题。
我使用REST API进行上传,因为新的SDK似乎没有办法完成这一部分。显然,使用旧的,即将被弃用的版本上传是可能的,如here所示,但时间无情地向前迈进,我们必须跟上。
问题是,当我用新的网站数据推送更新时,我得到了一个404:
状态:404(Not Found)-找不到证书370 A4097 E85476 AE 65545702 B410866882 AD 4541。
指纹和pfx证书上的指纹吻合,所以我们知道那部分是有效的。
我上传的时候收到了202,所以那部分也正常。但我本以为证书会出现在传送门里,但事实并非如此。这有点奇怪。
下面是我的完整代码,但为了简洁起见,这里是相关部分:

上传

oApiCert = New Certificate With {.Location = oWebSiteData.Location.DisplayName}
oApiCert.Properties.CanonicalName = sCanonicalName
oApiCert.Properties.ServerFarmId = oWebSiteData.AppServicePlanId.ToString
oApiCert.Properties.Password = My.Resources.PfxPassword
oApiCert.Properties.PfxBlob = oX509Cert.Export(X509ContentType.Pfx, My.Resources.PfxPassword)
oApiCert.Properties.HostNames.AddRange(oX509Cert.SanHostNames)

sJsonContent = JsonConvert.SerializeObject(oApiCert, JsonHelper.DefaultSerializationSettings)
oContent = New StringContent(sJsonContent, Encoding.UTF8, "application/json")

oClient = New HttpClient
oClient.DefaultRequestHeaders.Add("Authorization", "Bearer " & oToken.Token)
oClient.BaseAddress = New Uri(My.Resources.ManagementEndpoint)

oResponse = Await oClient.PutAsync(sUrl, oContent)
oResponse.EnsureSuccessStatusCode()

更新

oSslStates = oWebSiteData.HostNameSslStates

oHostNames = oHostNames.Intersect(oWebSiteData.HostNames).ToList
oHostNames.ForEach(Sub(HostName)
                     oSslState = oSslStates.FirstOrDefault(Function(SslState) SslState.Name = HostName)

                     If oSslState Is Nothing Then
                       oSslState = New HostNameSslState With {.Name = HostName}
                       oSslStates.Add(oSslState)
                     End If

                     oSslState.Thumbprint = BinaryData.FromObjectAsJson(oX509Cert.Thumbprint)
                     oSslState.SslState = HostNameBindingSslState.SniEnabled
                     oSslState.ToUpdate = True
                   End Sub)

oSitePatch = New SitePatchInfo

For Each oState In oSslStates
  If oState.ToUpdate Then
    oSitePatch.HostNameSslStates.Add(oState)
  End If
Next

oWebSiteResource.Update(oSitePatch) ' <-- 404 here '

这段代码是从LetsEncrypt-SiteExtension包移植的,可以在这里找到。该库使用旧的SDK。
如果有其他可行的方法,我不一定非得用这种方法。问题是SDK的文档和代码示例少得可怜。
如何使用新的SDK在Azure应用服务上上载和安装第三方TLS证书?有人在这方面取得了成功吗?

完整编码

Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Net.Http
Imports System.Security.Cryptography.X509Certificates
Imports System.Text
Imports System.Threading.Tasks
Imports Azure
Imports Azure.Core
Imports Azure.Identity
Imports Azure.ResourceManager
Imports Azure.ResourceManager.AppService
Imports Azure.ResourceManager.AppService.Models
Imports Azure.ResourceManager.Resources
Imports Matrix.Common
Imports Newtonsoft.Json
Imports Nito.AsyncEx

Friend Module Program
  Friend Sub Main()
    AsyncContext.Run(Async Function()
                       Await UploadCert()
                     End Function)
  End Sub


  Private Async Function UploadCert() As Task
    Dim oWebSiteResponse As Response(Of WebSiteResource)
    Dim oWebSiteResource As WebSiteResource
    Dim oResourceGroup As ResourceGroupResource
    Dim sCanonicalName As String
    Dim oSubscription As SubscriptionResource
    Dim oWebSiteData As WebSiteData
    Dim sJsonContent As String
    Dim oCredential As ClientSecretCredential
    Dim oSslStates As IList(Of HostNameSslState)
    Dim oHostNames As List(Of String)
    Dim aHostNames As String()
    Dim oSitePatch As SitePatchInfo
    Dim oArmClient As ArmClient
    Dim oResponse As HttpResponseMessage
    Dim oX509Cert As X509Certificate2
    Dim oSslState As HostNameSslState
    Dim aScopeUrl As String()
    Dim sCertName As String
    Dim oContext As TokenRequestContext
    Dim oContent As StringContent
    Dim oApiCert As Certificate
    Dim oClient As HttpClient
    Dim oToken As AccessToken
    Dim oUrl As List(Of String)
    Dim sUrl As String

    oX509Cert = Utils.GetCertificate(My.Resources.CertFriendlyName)
    aHostNames = My.Resources.HostNames.Split(",", StringSplitOptions.RemoveEmptyEntries)
    oHostNames = New List(Of String)(aHostNames)
    sCanonicalName = oHostNames.First
    sCertName = $"{sCanonicalName}-{oX509Cert.Thumbprint}"

    oUrl = New List(Of String) From {
      "subscriptions",
      My.Resources.SubscriptionId,
      "resourceGroups",
      My.Resources.ResourceGroup,
      "providers",
      "Microsoft.Web",
      "certificates",
      sCertName
    }

    sUrl = String.Join("/", oUrl)
    sUrl = $"/{sUrl}?api-version=2022-03-01"

    oCredential = New ClientSecretCredential(My.Resources.TenantId, My.Resources.ClientId, My.Resources.ClientSecret)
    oArmClient = New ArmClient(oCredential)
    oSubscription = Await oArmClient.GetDefaultSubscriptionAsync
    oResourceGroup = Await oSubscription.GetResourceGroups.GetAsync(My.Resources.ResourceGroup)
    oWebSiteResponse = Await oResourceGroup.GetWebSiteAsync(My.Resources.AppServiceName)
    oWebSiteResource = oWebSiteResponse.Value
    oWebSiteData = oWebSiteResource.Data
    aScopeUrl = New String() {$"{My.Resources.ManagementEndpoint}.default"}
    oContext = New TokenRequestContext(aScopeUrl)
    oToken = Await oCredential.GetTokenAsync(oContext)

    oApiCert = New Certificate With {.Location = oWebSiteData.Location.DisplayName}
    oApiCert.Properties.CanonicalName = sCanonicalName
    oApiCert.Properties.ServerFarmId = oWebSiteData.AppServicePlanId.ToString
    oApiCert.Properties.Password = My.Resources.PfxPassword
    oApiCert.Properties.PfxBlob = oX509Cert.Export(X509ContentType.Pfx, My.Resources.PfxPassword)
    oApiCert.Properties.HostNames.AddRange(oX509Cert.SanHostNames)

    sJsonContent = JsonConvert.SerializeObject(oApiCert, JsonHelper.DefaultSerializationSettings)
    oContent = New StringContent(sJsonContent, Encoding.UTF8, "application/json")

    oClient = New HttpClient
    oClient.DefaultRequestHeaders.Add("Authorization", "Bearer " & oToken.Token)
    oClient.BaseAddress = New Uri(My.Resources.ManagementEndpoint)

    oResponse = Await oClient.PutAsync(sUrl, oContent)
    oResponse.EnsureSuccessStatusCode()

    oSslStates = oWebSiteData.HostNameSslStates

    oHostNames = oHostNames.Intersect(oWebSiteData.HostNames).ToList
    oHostNames.ForEach(Sub(HostName)
                         oSslState = oSslStates.FirstOrDefault(Function(SslState) SslState.Name = HostName)

                         If oSslState Is Nothing Then
                           oSslState = New HostNameSslState With {.Name = HostName}
                           oSslStates.Add(oSslState)
                         End If

                         oSslState.Thumbprint = BinaryData.FromObjectAsJson(oX509Cert.Thumbprint)
                         oSslState.SslState = HostNameBindingSslState.SniEnabled
                         oSslState.ToUpdate = True
                       End Sub)

    oSitePatch = New SitePatchInfo

    For Each oState In oSslStates
      If oState.ToUpdate Then
        oSitePatch.HostNameSslStates.Add(oState)
      End If
    Next

    oWebSiteResource.Update(oSitePatch) ' <-- 404 here '
  End Function
End Module
uelo1irk

uelo1irk1#

天啊......那真是一次冒险!
好吧,在经历了很多痛苦之后,我终于找到了答案。(旁注:很高兴知道与Create/Update Certificate REST API等效的SDK,所以我还在寻找...)
技巧(变通方案?)是先将证书上传到密钥库,然后从那里将其部署到我们的应用程序服务。
一旦cert在保险库中,我们接下来要做的就是在PUT调用的JSON主体(不区分大小写)中提供四个值:

  1. .Location * 例如美国东部 *
  2. .Properties.KeyVaultSecretName * 保管库中证书的名称 *
  3. .Properties.ServerFarmId * 即AppService.AppServicePlanId.ToString*
  4. .Properties.KeyVaultId * 即KeyVault.Value.Id*
    就是这样没有字节数组,没有密码,仅此而已。
    除了一件事。。我们必须在Vault上授予适当的访问策略:
  • 我们的服务宗旨= CERTIFICATE IMPORT
  • Microsoft Azure应用服务主体= SECRET GET

第一个用于将证书上传到Vault,第二个用于部署。
Microsoft Azure App Service主体的资源标识符为abfa0a7c-a6b6-4736-8310-5855508787cd,但Azure Gov云环境除外,在Azure Gov云环境中,资源标识符为6a02c803-dafd-4136-b4c3-5a6f318b4714
就像这样:

像这样上传到vault:

Dim oCertClient As CertificateClient
Dim oVaultUri As Uri
Dim oOptions As ImportCertificateOptions
Dim aPfxCert As Byte()

oVaultUri = New Uri($"https://{My.Resources.VaultName}.vault.azure.net")
oCertClient = New CertificateClient(oVaultUri, Credential)

aPfxCert = X509Cert.Export(X509ContentType.Pfx, My.Resources.PfxPassword)
oOptions = New ImportCertificateOptions(CertName, aPfxCert) With {.Password = My.Resources.PfxPassword}

Await oCertClient.ImportCertificateAsync(oOptions)

现在我们可以将其部署到我们的App Service:

oApiCert = New Upload.Certificate With {.Location = WebSiteResource.Data.Location.DisplayName}
oApiCert.Properties.KeyVaultSecretName = CertName
oApiCert.Properties.ServerFarmId = WebSiteResource.Data.AppServicePlanId.ToString
oApiCert.Properties.KeyVaultId = oKeyVaultResponse.Value.Id.ToString

aScopeUrls = New String() {$"{My.Resources.ManagementEndpoint}.default"}
oContext = New TokenRequestContext(aScopeUrls)
oToken = Await Credential.GetTokenAsync(oContext)

oHttpClient = New HttpClient
oHttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {oToken.Token}")
oHttpClient.BaseAddress = New Uri(My.Resources.ManagementEndpoint)

  ...

sJsonContent = JsonConvert.SerializeObject(oApiCert, JsonHelper.DefaultSerializationSettings)
oContent = New StringContent(sJsonContent, Encoding.UTF8, "application/json")

oHttpResponse = Await oHttpClient.PutAsync(sUrl, oContent)
oHttpResponse.EnsureSuccessStatusCode()

当我偶然发现这个seven-year-old blog post时,我发现了这一切(为多年来保留有价值的内容而欢呼三声)。另外,这里有一个QuickStart Template的概念。
关于这篇文章有一点值得注意:它声称,一个不受保护的PFX是必要的,这工作。然而,在我的测试中,我遇到了相反的情况:至少对我来说,它与密码保护的PFX一起工作得很好。我不打算花时间去找出这种差异我就跟着它走保护好我的PFXs那就这样吧YMMV
如果它可能会帮助别人,这里是完整的工作代码:

Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Net.Http
Imports System.Security.Cryptography.X509Certificates
Imports System.Text
Imports System.Threading.Tasks
Imports Azure
Imports Azure.Core
Imports Azure.Identity
Imports Azure.ResourceManager
Imports Azure.ResourceManager.AppService
Imports Azure.ResourceManager.AppService.Models
Imports Azure.ResourceManager.KeyVault
Imports Azure.ResourceManager.Resources
Imports Azure.Security.KeyVault.Certificates
Imports Matrix.Common
Imports Newtonsoft.Json
Imports Nito.AsyncEx

Friend Module Program
  Friend Sub Main()
    AsyncContext.Run(Async Function()
                       Await InstallNewCert()
                     End Function)
  End Sub

  Private Async Function InstallNewCert() As Task
    Dim oWebSiteResponse As Response(Of WebSiteResource)
    Dim oResourceGroup As ResourceGroupResource
    Dim oSubscription As SubscriptionResource
    Dim oCredential As ClientSecretCredential
    Dim oArmClient As ArmClient
    Dim oX509Cert As X509Certificate2
    Dim sCertName As String

    oCredential = New ClientSecretCredential(My.Resources.TenantId, My.Resources.ClientId, My.Resources.ClientSecret)
    oX509Cert = Utils.GetCertificate(My.Resources.CertFriendlyName)

    sCertName = oX509Cert.SanHostNames.Where(Function(HostName)
                                               Return _
                                                 Not HostName.Contains("*"c) AndAlso
                                                 HostName.Occurs("."c) = 1
                                             End Function).First

    sCertName = sCertName.Split("."c).First
    sCertName = $"{sCertName}-{Guid.NewGuid}"

    oArmClient = New ArmClient(oCredential)
    oSubscription = Await oArmClient.GetDefaultSubscriptionAsync
    oResourceGroup = Await oSubscription.GetResourceGroups.GetAsync(My.Resources.SecondaryResourceGroup)
    oWebSiteResponse = Await oResourceGroup.GetWebSiteAsync(My.Resources.AppServiceName)

    Await UploadCertToVault(oCredential, oX509Cert, sCertName)
    Await DeployCertFromVault(oSubscription, oCredential, oWebSiteResponse.Value, sCertName)
    Await UpdateBindings(oWebSiteResponse.Value, oX509Cert)
  End Function

  Private Async Function UploadCertToVault(
      Credential As ClientSecretCredential,
      X509Cert As X509Certificate2,
      CertName As String) As Task

    Dim oCertClient As CertificateClient
    Dim oVaultUri As Uri
    Dim oOptions As ImportCertificateOptions
    Dim aPfxCert As Byte()

    oVaultUri = New Uri($"https://{My.Resources.VaultName}.vault.azure.net")
    oCertClient = New CertificateClient(oVaultUri, Credential)

    aPfxCert = X509Cert.Export(X509ContentType.Pfx, My.Resources.PfxPassword)
    oOptions = New ImportCertificateOptions(CertName, aPfxCert) With {.Password = My.Resources.PfxPassword}

    Await oCertClient.ImportCertificateAsync(oOptions)
  End Function

  Private Async Function DeployCertFromVault(
      Subscription As SubscriptionResource,
      Credential As ClientSecretCredential,
      WebSiteResource As WebSiteResource,
      CertName As String) As Task

    Dim oKeyVaultResponse As Response(Of KeyVaultResource)
    Dim oResourceGroup As ResourceGroupResource
    Dim oHttpResponse As HttpResponseMessage
    Dim sJsonContent As String
    Dim oHttpClient As HttpClient
    Dim aScopeUrls As String()
    Dim oContext As TokenRequestContext
    Dim oApiCert As Upload.Certificate
    Dim oContent As StringContent
    Dim oToken As AccessToken
    Dim oUrl As List(Of String)
    Dim sUrl As String

    oResourceGroup = Await Subscription.GetResourceGroups.GetAsync(My.Resources.PrimaryResourceGroup)
    oKeyVaultResponse = Await oResourceGroup.GetKeyVaultAsync(My.Resources.VaultName)

    oApiCert = New Upload.Certificate With {.Location = WebSiteResource.Data.Location.DisplayName}
    oApiCert.Properties.KeyVaultSecretName = CertName
    oApiCert.Properties.ServerFarmId = WebSiteResource.Data.AppServicePlanId.ToString
    oApiCert.Properties.KeyVaultId = oKeyVaultResponse.Value.Id.ToString

    aScopeUrls = New String() {$"{My.Resources.ManagementEndpoint}.default"}
    oContext = New TokenRequestContext(aScopeUrls)
    oToken = Await Credential.GetTokenAsync(oContext)

    oHttpClient = New HttpClient
    oHttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {oToken.Token}")
    oHttpClient.BaseAddress = New Uri(My.Resources.ManagementEndpoint)

    oUrl = New List(Of String) From {
      "subscriptions",
      My.Resources.SubscriptionId,
      "resourceGroups",
      My.Resources.PrimaryResourceGroup,
      "providers",
      "Microsoft.Web",
      "certificates",
      CertName
    }

    sUrl = String.Join("/", oUrl)
    sUrl = $"/{sUrl}?api-version=2022-03-01"

    sJsonContent = JsonConvert.SerializeObject(oApiCert, JsonHelper.DefaultSerializationSettings)
    oContent = New StringContent(sJsonContent, Encoding.UTF8, "application/json")

    oHttpResponse = Await oHttpClient.PutAsync(sUrl, oContent)
    oHttpResponse.EnsureSuccessStatusCode()
  End Function

  Private Async Function UpdateBindings(
      WebSiteResource As WebSiteResource,
      X509Cert As X509Certificate2) As Task

    Dim oSslStates As IList(Of HostNameSslState)
    Dim oSitePatch As SitePatchInfo
    Dim oSslState As HostNameSslState

    oSslStates = WebSiteResource.Data.HostNameSslStates
    oSitePatch = New SitePatchInfo

    With X509Cert.SanHostNames.Intersect(WebSiteResource.Data.HostNames).ToList
      .ForEach(Sub(HostName)
                 oSslState = oSslStates.FirstOrDefault(Function(SslState) SslState.Name = HostName)

                 If oSslState Is Nothing Then
                   oSslState = New HostNameSslState With {.Name = HostName}
                   oSslStates.Add(oSslState)
                 End If

                 oSslState.Thumbprint = BinaryData.FromObjectAsJson(X509Cert.Thumbprint)
                 oSslState.SslState = HostNameBindingSslState.SniEnabled
                 oSslState.ToUpdate = True
               End Sub)
    End With

    For Each oState In oSslStates
      If oState.ToUpdate Then
        oSitePatch.HostNameSslStates.Add(oState)
      End If
    Next

    Await WebSiteResource.UpdateAsync(oSitePatch)
  End Function
End Module

相关问题