2010-03-09 6 views
12

Il mio team sta sviluppando un numero di plug-in WPF per un'applicazione client spessa di terze parti. I plug-in WPF utilizzano WCF per utilizzare servizi Web pubblicati da numerosi servizi TIBCO. L'applicazione thick client mantiene un archivio dati centrale separato e utilizza un'API proprietaria per accedere all'archivio dati. I plug-in thick client e WPF devono essere distribuiti su 10.000 workstation. Il nostro cliente desidera mantenere il certificato utilizzato dal thick client nell'archivio dati centrale in modo che non debbano preoccuparsi di riemettere il certificato (il ciclo di ri-emissione corrente richiede circa 3 mesi) e avere anche l'opportunità di autorizzare l'uso del certificato. L'architettura proposta offre una forma di segreto/autenticazione condivisa tra l'archivio dati centrale ei servizi TIBCO.Certificati WCF senza certificato Memorizza

Anche se non sono necessariamente d'accordo con l'architettura proposta, il nostro team non è in grado di cambiarlo e deve lavorare con ciò che è stato fornito.

Fondamentalmente il nostro cliente ci vuole per costruire nei nostri plug-in WPF un meccanismo che recupera il certificato dall'archivio dati centrale (che sarà consentito o negato in base ai ruoli in tale archivio dati) in memoria quindi utilizzare il certificato per creando la connessione SSL ai servizi TIBCO. Non è consentito l'utilizzo dell'archivio certificati della macchina locale e la versione in memoria deve essere eliminata alla fine di ogni sessione.

Quindi la domanda è qualcuno sa se è possibile passare un certificato in memoria a un servizio WCF (.NET 3.5) per la crittografia del livello di trasporto SSL?

Nota: Avevo fatto una domanda simile (here) ma da allora l'ho cancellata e ho chiesto nuovamente con ulteriori informazioni.

risposta

13

È possibile. Facciamo qualcosa di simile con Autenticazione del certificato reciproco - il certificato del servizio e in alcuni casi il certificato del cliente viene prelevato da un'autorità centrale come parte di un meccanismo di auto-scoperta/single-sign-on.

Non è del tutto chiaro in quale contesto verrà utilizzato il certificato, ma in tutti i casi ciò che è necessario è definire il proprio comportamento e comportamento derivante dal particolare comportamento/elemento nello spazio dei nomi System.ServiceModel.Description che accetta il certificato. Per il momento suppongo che si tratti di una credenziale del cliente. In primo luogo si deve scrivere il comportamento, che più o meno così:

public class MyCredentials : ClientCredentials 
{ 
    public override void ApplyClientBehavior(ServiceEndpoint endpoint, 
     ClientRuntime behavior) 
    { 
     // Assuming GetCertificateFromNetwork retrieves from CDS 
     ClientCertificate.Certificate = GetCertificateFromNetwork(); 
    } 

    protected override ClientCredentials CloneCore() 
    { 
     // ... 
    } 
} 

Ora è necessario creare un elemento che può andare nella configurazione XML:

public class MyCredentialsExtensionElement : ClientCredentialsElement 
{ 
    protected override object CreateBehavior() 
    { 
     return new MyCredentials(); 
    } 

    public override Type BehaviorType 
    { 
     get { return typeof(MyCredentials); } 
    } 

    // Snip other overrides like Properties 
} 

Dopo questo si può aggiungere il criterio al tuo WCF config:

<behaviors> 
    <endpointBehaviors> 
     <behavior name="MyEndpointBehavior"> 
      <myCredentials/> 
     </behavior> 
    </endpointBehaviors> 
</behaviors> 

Edit: quasi dimenticato di citare, è necessario registrare l'estensione:

<system.serviceModel> 
    <extensions> 
     <behaviorExtensions> 
      <add name="myCredentials" 
       type="MyAssembly.MyCredentialsExtensionElement, MyAssembly, 
         Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> 
     </behaviorExtensions> 
    </extensions> 
</system.serviceModel> 

Spero che questo aiuti. Se hai bisogno di maggiori dettagli sulla disposizione di tutte queste classi e su cosa succede dietro le quinte, prova a leggere Extending WCF with Custom Behaviors.

+0

Grazie mille per il suggerimento. Ti farò sapere come vado. – Kane

+0

Mi dispiace chiedere così tardi. Perché assegnare direttamente il certificato client anziché chiamare 'SetCertificate()'? C'è una differenza? – ashes999

+0

@ ashes999: questo metodo non ha un sovraccarico che richiede un effettivo 'X509Certificate'. È utile solo se il tuo certificato è già in uno dei negozi locali, il che non è esplicitamente il caso qui. – Aaronaught

4

Sono il tipo che ha fatto Kane (il nostro servitore di SO!) A porre la domanda originale. Ho pensato che avrei finalmente creato un account e pubblicato i risultati/risultati/esperienze in merito alla risposta inviata da Aaronaught (quindi qualsiasi credito a lui sopra).

Abbiamo tentato di aggiungere un comportamento personalizzato come suggerito sopra e di impostare behaviourConfiguration sull'elemento di configurazione dell'endpoint per utilizzarlo. Non siamo riusciti a far scattare il codice, quindi abbiamo optato per un approccio programmatico.

Dato che avevamo impostato una classe wrapper per creare un oggetto ClientBase, abbiamo utilizzato le nostre funzioni di creazione esistenti per aggiungere il comportamento dopo aver creato tutte le altre parti del ClientBase.

Abbiamo riscontrato alcuni problemi anche in questo, vale a dire che era già stato definito un comportamento ClientCredentials per l'autenticazione del nostro ClientBase con un nome utente e una password anziché il nostro certificato + nome utente e password. Quindi abbiamo rimosso il comportamento esistente a livello di codice prima di aggiungere il nostro nuovo comportamento basato sul certificato (con il nome utente e la password iniettati) come misura temporanea per il test. Ancora nessun dado, il nostro comportamento era in fase di costruzione e ApplyClientBehavior veniva sparato, ma il servizio stava ancora crollando quando è stato chiamato Invoke (non abbiamo mai ottenuto la vera eccezione a causa di una serie di istruzioni che erano difficili da refactoring).

Abbiamo quindi deciso, invece di rimuovere il comportamento di ClientCredentials esistente, di inserire il nostro certificato in esso prima di lasciare che tutto il processo proceda normalmente. La terza volta è un fascino e tutto funziona e funziona ora.

Vorrei ringraziare Aaronaught (e voterei se possibile!) Per averci messo sulla strada giusta e fornire una risposta ben ponderata e utile.

Ecco un piccolo frammento di codice su e in esecuzione (utilizzando un file .CRT di prova).

 protected override ClientBase<TChannel> CreateClientBase(string endpointConfigurationName) 
    { 
     ClientBase<TChannel> clientBase = new ClientBase<TChannel>(endpointConfigurationName); // Construct yours however you want here 

     // ... 

     ClientCredentials credentials = clientBase.Endpoint.Behaviors.Find<ClientCredentials>(); 

     X509Certificate2 certificate = new X509Certificate2(); 
     byte[] rawCertificateData = File.ReadAllBytes(@"C:\Path\To\YourCert.crt"); 
     certificate.Import(rawCertificateData); 

     credentials.ClientCertificate.Certificate = certificate; 

     return clientBase; 
    } 

Come altra nota a margine, come parte del test abbiamo rimosso tutti i nostri certificati dal computer locale, questo realmente causato un problema utilizzando Fiddler. Fiddler non ha rilevato il nostro certificato client perché era puramente in memoria e non nell'archivio fidato. Se lo avessimo aggiunto al negozio fidato, allora Fiddler ha iniziato a giocare di nuovo bene.

Grazie ancora.

+0

Grazie per la risposta jules :) – Kane

+0

Hmm, molto sconcertante che non si potesse ottenere il comportamento da eseguire. Ti sei dimenticato di configurare l'endpoint con il comportamento? Ad ogni modo, sembra che voi ragazzi abbiate trovato una soluzione, quindi tutto va bene che finisce bene! – Aaronaught

+0

Sì, abbiamo configurato l'endpoint per utilizzare il comportamento, ma è stato possibile attivarlo solo quando lo si aggiungeva in modo programmatico. È stato un po 'bizzarro ma non l'abbiamo approfondito mentre prendevamo un cambio di direzione. –

0

Aaronaught ha avuto l'idea giusta, ma ho dovuto apportare alcune modifiche per farlo funzionare. Segue l'implementazione che ho usato. L'ho aggiunto un po 'di più con la possibilità di ottenere un certificato da una risorsa incorporata.

using System.IO; 
using System.Linq; 
using System.Reflection; 
using System.Security.Cryptography.X509Certificates; 
using System.ServiceModel.Configuration; 
using System.Configuration; 
using System.ServiceModel.Description; 

namespace System.ServiceModel.Description 
{ 
    /// <summary> 
    /// Uses a X509 certificate from disk as credentials for the client. 
    /// </summary> 
    public class ClientCertificateCredentialsFromFile : ClientCredentials 
    { 
     public ClientCertificateCredentialsFromFile(CertificateSource certificateSource, string certificateLocation) 
     { 
      if (!Enum.IsDefined(typeof(CertificateSource), certificateSource)) { throw new ArgumentOutOfRangeException(nameof(certificateSource), $"{nameof(certificateSource)} contained an unexpected value."); } 
      if (string.IsNullOrWhiteSpace(certificateLocation)) { throw new ArgumentNullException(nameof(certificateLocation)); } 

      _certificateSource = certificateSource; 
      _certificateLocation = certificateLocation; 

      ClientCertificate.Certificate = certificateSource == CertificateSource.EmbeddedResource ? 
       GetCertificateFromEmbeddedResource(certificateLocation) 
       : GetCertificateFromDisk(certificateLocation); 
     } 

     /// <summary> 
     /// Retrieves a certificate from an embedded resource. 
     /// </summary> 
     /// <param name="certificateLocation">The certificate location and assembly information. Example: The.Namespace.certificate.cer, Assembly.Name</param> 
     /// <returns>A new instance of the embedded certificate.</returns> 
     private static X509Certificate2 GetCertificateFromEmbeddedResource(string certificateLocation) 
     { 
      X509Certificate2 result = null; 

      string[] parts = certificateLocation.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 
      if (parts.Length < 2) { throw new ArgumentException($"{certificateLocation} was expected to have a format of namespace.resource.extension, assemblyName"); } 
      string assemblyName = string.Join(",", parts.Skip(1)); 

      var assembly = Assembly.Load(assemblyName); 
      using (var stream = assembly.GetManifestResourceStream(parts[0])) 
      { 
       var bytes = new byte[stream.Length]; 
       stream.Read(bytes, 0, bytes.Length); 
       result = new X509Certificate2(bytes); 
      } 

      return result; 
     } 

     /// <summary> 
     /// Retrieves a certificate from disk. 
     /// </summary> 
     /// <param name="certificateLocation">The file path to the certificate.</param> 
     /// <returns>A new instance of the certificate from disk</returns> 
     private static X509Certificate2 GetCertificateFromDisk(string certificateLocation) 
     { 
      if (!File.Exists(certificateLocation)) { throw new ArgumentException($"File {certificateLocation} not found."); } 
      return new X509Certificate2(certificateLocation); 
     } 


     /// <summary> 
     /// Used to keep track of the source of the certificate. This is needed when this object is cloned. 
     /// </summary> 
     private readonly CertificateSource _certificateSource; 

     /// <summary> 
     /// Used to keep track of the location of the certificate. This is needed when this object is cloned. 
     /// </summary> 
     private readonly string _certificateLocation; 

     /// <summary> 
     /// Creates a duplicate instance of this object. 
     /// </summary> 
     /// <remarks> 
     /// A new instance of the certificate is created.</remarks> 
     /// <returns>A new instance of <see cref="ClientCertificateCredentialsFromFile"/></returns> 
     protected override ClientCredentials CloneCore() 
     { 
      return new ClientCertificateCredentialsFromFile(_certificateSource, _certificateLocation); 
     } 
    } 
} 


namespace System.ServiceModel.Configuration 
{ 
    /// <summary> 
    /// Configuration element for <see cref="ClientCertificateCredentialsFromFile"/> 
    /// </summary> 
    /// <remarks> 
    /// When configuring the behavior an extension has to be registered first. 
    /// <code> 
    /// <![CDATA[ 
    /// <extensions> 
    ///  <behaviorExtensions> 
    ///   <add name = "clientCertificateCredentialsFromFile" 
    ///    type="System.ServiceModel.Configuration.ClientCertificateCredentialsFromFileElement, Assembly.Name" /> 
    ///  </behaviorExtensions> 
    /// </extensions> 
    /// ]]> 
    /// </code> 
    /// Once the behavior is registered it can be used as follows. 
    /// <code> 
    /// <![CDATA[ 
    /// <behaviors> 
    ///  <endpointBehaviors> 
    ///   <behavior name = "BehaviorConfigurationName" > 
    ///    <clientCertificateCredentialsFromFile fileLocation="C:\certificates\paypal_cert.cer" /> 
    ///   </behavior> 
    ///  </endpointBehaviors> 
    /// </behaviors> 
    /// <client> 
    ///  <endpoint address="https://endpoint.domain.com/path/" behaviorConfiguration="BehaviorConfigurationName" ... /> 
    /// </client> 
    /// ]]> 
    /// </code> 
    /// </remarks> 
    public class ClientCertificateCredentialsFromFileElement : BehaviorExtensionElement 
    { 
     /// <summary> 
     /// Creates a new <see cref="ClientCertificateCredentialsFromFile"/> from this configuration element. 
     /// </summary> 
     /// <returns>The newly configured <see cref="ClientCertificateCredentialsFromFile"/></returns> 
     protected override object CreateBehavior() 
     { 
      return new ClientCertificateCredentialsFromFile(Source, Location); 
     } 

     /// <summary> 
     /// Returns <code>typeof(<see cref="ClientCertificateCredentialsFromFile"/>);</code> 
     /// </summary> 
     public override Type BehaviorType 
     { 
      get 
      { 
       return typeof(ClientCertificateCredentialsFromFile); 
      } 
     } 

     /// <summary> 
     /// An attribute used to configure the file location of the certificate to use for the client's credentials. 
     /// </summary> 
     [ConfigurationProperty("location", IsRequired = true)] 
     public string Location 
     { 
      get 
      { 
       return this["location"] as string; 
      } 
      set 
      { 
       this["location"] = value; 
      } 
     } 

     /// <summary> 
     /// An attribute used to configure where the certificate should should be loaded from. 
     /// </summary> 
     [ConfigurationProperty("source", IsRequired = true)] 
     public CertificateSource Source 
     { 
      get 
      { 
       return (CertificateSource)this["source"]; 
      } 
      set 
      { 
       this["source"] = value; 
      } 
     } 
    } 

    /// <summary> 
    /// Used to declare the source of a certificate. 
    /// </summary> 
    public enum CertificateSource 
    { 
     FileOnDisk, 
     EmbeddedResource 
    } 
} 

utilizzando il codice di cui sopra sono riuscito a configurare il mio client come segue

<?xml version="1.0" encoding="utf-8"?> 
<configuration> 
    <system.serviceModel> 
     <extensions> 
      <behaviorExtensions> 
       <add name="clientCertificateCredentialsFromFile" 
        type="System.ServiceModel.Configuration.ClientCertificateCredentialsFromFileElement, My.Project.PayPal" /> 
      </behaviorExtensions> 
     </extensions> 

     <bindings> 
      <basicHttpBinding> 
       <binding name="PayPalAPISoapBinding"> 
        <security mode="Transport"> 
         <transport clientCredentialType="Certificate" /> 
        </security> 
       </binding> 
       <binding name="PayPalAPIAASoapBinding"> 
        <security mode="Transport"> 
         <transport clientCredentialType="Certificate" /> 
        </security> 
       </binding> 
      </basicHttpBinding> 
     </bindings> 
     <behaviors> 
      <endpointBehaviors> 
       <behavior name="PayPalAPICredentialBehavior"> 
        <clientCertificateCredentialsFromFile source="EmbeddedResource" location="My.Project.PayPal.Test.Integration.paypal_cert.cer, My.Project.PayPal.Test.Integration" /> 
       </behavior> 
       <behavior name="PayPalAPIAACredentialBehavior"> 
        <clientCertificateCredentialsFromFile source="EmbeddedResource" location="My.Project.PayPal.Test.Integration.paypal_cert.cer, My.Project.PayPal.Test.Integration" /> 
       </behavior> 
      </endpointBehaviors> 
     </behaviors> 
     <client> 
      <endpoint 
       address="https://api.sandbox.paypal.com/2.0/" 
       behaviorConfiguration="PayPalAPICredentialBehavior" 
       binding="basicHttpBinding" 
       bindingConfiguration="PayPalAPISoapBinding" 
       contract="My.Project.PayPal.Proxy.PayPalAPIInterface" 
       name="PayPalAPI" /> 
      <endpoint 
       address="https://api-aa.sandbox.paypal.com/2.0/" 
       behaviorConfiguration="PayPalAPIAACredentialBehavior" 
       binding="basicHttpBinding" 
       bindingConfiguration="PayPalAPIAASoapBinding" 
       contract="My.Project.PayPal.Proxy.PayPalAPIAAInterface" 
       name="PayPalAPIAA" /> 
     </client> 
    </system.serviceModel> 
</configuration>