• Noser.com
facebook
linkedin
twitter
youtube
  • NOSERmobile
    • Android
    • HTML 5
    • Hybrid Apps
    • iOS
    • Windows Phone
  • NOSERembedded
    • Medizintechnik
  • NOSERprojektmanagement
  • NOSERtesting
  • NOSERlifecycle
    • .NET Allgemein
    • Application Lifecylce Management
    • Apps
    • Architektur
    • ASP.NET
    • Azure
    • Cleancode
    • Cloud
    • Silverlight
    • Visual Studio / Team Foundation Server
    • Windows 8
    • Windows Presentation Foundation
  • NOSERinnovation
    • Big Data
    • Cloud
    • IoT
    • Operations Research
    • Augmented Reality
    • RFID, NFC, Bluetooth LE

gRPC Tutorial Teil 6: JWT Client Credentials

23. Januar 2020
Erik Stroeken
3
client credential flow, gRPC, identity server, jwt, oauth

In diesem Blog wird erläutert, wie die Authentifizierung und Autorisierung mit JSON Web Token (JWT) funktioniert. Der einfachste OAuth2-Flow, der Client Credential Flow, wird anhand eines Beispiels mit einem gRPC-Client / Server und einem minimalistischen Tokenserver erläutert.

JSON Web Token (JWT)

JSON Web Token (JWT) ist ein auf JSON basiertes und nach RFC 7519 genormtes Access-Token und wird zur Authentifizierung und Autorisierung eines Benutzers verwendet. Die Informationen im Token können überprüft und als vertrauenswürdig eingestuft werden, da sie digital signiert sind. JWTs können symmetrisch, mit einem geheimen Schlüssel (‘HS256’: HMAC mit SHA-256), oder asymmetrisch mit einem Public- / Private-Schlüsselpaar mit RSA (‘RS256’: RSA Signatur mit SHA-256) signiert werden (‘RS256’).

Man sollte sich bewusst sein, dass ein JWT nicht verschlüsselt ist und daher immer über eine sichere Verbindung gesendet werden sollte.

Ein JWT besteht aus drei Teilen: Header, Payload und Signatur. Alle drei Teile sind Base64 kodiert und durch einen Punkt getrennt.

Header.Payload.Signatur

Beispiel eines JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Dekodiert vom Base64 Format ergibt das Beispiel oben:

{“alg”:”HS256″,”typ”:”JWT”}.{“sub”:”1234567890″,”name”:”John Doe”,”iat”:1516239022}.[Binäre Signatur]

 

JWT Header

Der Header hat üblicherweise nur zwei Felder, wovon das Feld ‘typ’  auf ‘JWT’ gesetzt ist. Das Feld ‘alg’ beschreibt, welche Verschlüsselungsmethode für die Signierung zum Einsatz kommt: ‘HS256’ für symmetrische Verschlüsselung oder ‘RS256’ (RSA mit SHA-256) für asymmetrische Verschlüsselung.
Beispiel:

{
  "alg": "HS256",
  "typ": "JWT"
}

JWT Payload

Die Payload-Sektion enthält die Claims, d.h. die zu übermittelnden Informationen.
Beispiel:

{
  "iat": 1579623173,
  "jti": "de84fbc9-e47e-43f2-82ec-f03b05416450",
  "exp": 1579624973,
  "iss": "https://localhost:5001",
  "aud": "DemoService"
}

Einige oft verwendete Claims:

iss Issuer der Aussteller des Tokens
sub Subject definiert für wen oder was die Claims getätigt werden
aud Audience die Zieldomäne, für die das Token ausgestellt wurde, bei uns die API
exp Expiration Time Das Ablaufdatum des Tokens in Unixzeit, also der Anzahl der Sekunden seit 1970-01-01T00:00:00Z (‘Epoch time’)
nbf Not Before Die Unixzeit, ab der das Token gültig ist
iat Issued At Die Unixzeit, zu der das Token ausgestellt wurde
jti JWT ID Eindeutige ID die Mehrfachverwendung des Tokens verhindert

 

JWT Signatur

Die Signatur wird dadurch erzeugt, dass Header und Payload im Base64 kodierten und durch einen Punkt getrennten Format mit der spezifizierten Hashmethode gehashed wird:

var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
var hash = HMACSHA256(encodedString, secret);

Online JWT validieren/dekodieren: jwt.io

Die Seite jwt.io stellt den Inhalt eines JWT dar, das man einfach im linken Fenster (‘Encoded’) einfügen kann. Wenn man den Schlüssel besitzt, kann man diesen im Feld ‘Decoded / VERIFY SIGNATURE’ einfügen. Die Seite zeigt dann, ob die Signatur gültig ist.

Noser Engineering AG - gRPC - Client Credential Flow - jwt.io

jwt.io

JWT generieren und validieren/dekodieren

Mit dem NuGet-Paket ‘Microsoft.IdentityModels.Tokens.JWT’ kann man JWT generieren und validieren/dekodieren.

Beispiel:

public string CreateJwtToken()
{
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(DateTime.Now).ToString(), ClaimValueTypes.Integer64),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    };

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967"));
    var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    var jwtSecurityToken = new JwtSecurityToken(
        "https://localhost:5001",
        "DemoService",
        claims,
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: signingCredentials);
    return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}

public ClaimsPrincipal ValidateDecodeJwtToken(string token)
{
    var validationParams = new TokenValidationParameters()
    {
        ValidIssuer = "https://localhost:5001",
        ValidAudience = "DemoService",
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes("D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967"))
    };
    var handler = new JwtSecurityTokenHandler();
    SecurityToken securityToken;
   return handler.ValidateToken(token, validationParams, out securityToken);
}

OAuth 2.0

OAuth 2.0 ist ein offenes Protokoll, welches eine standardisierte, sichere API-Autorisierung für Desktop-, Web- und Mobile-Anwendungen erlaubt. Die erste Version OAuth 1.0 wurde in 2007 veröffentlicht, die aktuelle Version OAuth 2.0 in 2010.

Der Ressourcenbesitzer kann mit Hilfe dieses Protokolls einer Anwendung (Client) den Zugriff auf seine Daten zulassen (Autorisierung), die von einem anderen Dienst (Resourcen-Server) bereitgestellt werden, ohne geheime Details seiner Zugangsberechtigung (Authentifizierung) dem Client preiszugeben. Der Ressourcenbesitzer kann so Dritten gestatten, in seinem Namen einen Dienst zu benutzen. Typischerweise wird dabei die Übermittlung von Passwörtern an Dritte vermieden.

OAuth 2.0 ist zu vergleichen mit einem Parkservice: der Ressourcenbesitzer gibt das Auto (die Ressource) an den Parkservice (Client) weiter. Er gibt dem Parkservice den Parkservice-Schlüssel. Moderne amerikanische Autos haben zwei Arten von Schlüsseln: einen Hauptschlüssel und einen Parkservice-Schlüssel mit eingeschränkten Möglichkeiten (z. B. eingeschränkte Kilometer, Kofferraum kann nicht geöffnet werden). In der Computerwelt ist der Hauptschlüssel Ihr Benutzername mit Kennwort (mit vollem Zugriff). Bei OAuth2 geht es darum, Clients einen Schlüssel für Ihre Ressourcen mit beschränktem Zugriff (das JWT-Token) zu geben, ohne den Hauptschlüssel herausgeben zu müssen.

In OAuth 2.0 gibt es 4 Arten von ‘Flows’. Die einfachste Art, der Client Credential Flow, wird in diesem Blog behandelt.

Client Credential Flow

Beim Client Credential Flow meldet der Client sich beim Identity-Server mit seinem ‘Secret’ (z.B. Benutzername und Passwort) an. Der Identity-Server authentifiziert den Benutzer und gibt ein JWT zurück. Dieses JWT enthält u.a. Zugriffsberechtigungen (z.B. Resourcen und Rolle) und ist signiert mit einem symmetrischen Key (‘HS256’) oder dem privaten Schlüssel eines Public-/Private-Key Paares (‘RS256’).

Der Client sendet jetzt bei jeder Resourcen-Server-Anfrage das JWT mit; entweder im HTTP-Header oder in der URL. Der Resourcen-Server validiert das Token mit Hilfe der Signatur und dem privaten symmetrischen Schlüssel (ist beim Resourcen-Server bekannt). Bei einer RS256 Signatur holt der Resourcen-Server über die Issuer-URL im JWT beim Identity-Server den Public-Key zur Validierung ab. Wenn die Validierung des JWTs in Ordnung ist, darf der Client auf die Resourcen zugreifen.

Beispiel: Client Credential Flow

Mit diesem Beispiel ‘RpcJwtClientCredentials’ wird die einfachste Art von OAuth 2.0, der Client Credential Flow, demonstriert. Der Quellcode kann hier als ZIP-Datei heruntergeladen werden.

Weil das JWT nicht verschlüsselt ist, muss die Kommunikation zwingend über eine gesicherte Verbindung laufen. Die Projekte im Beispiel benutzen den Kestrel-Server mit gesicherter Verbindung wie beschrieben in ‘gRPC Tutorial Teil 4: HTTP/2 über HTTPS’.

Obwohl dieses Beispiel mit einem gRPC-Server arbeitet, kann der gleiche Code auch zur Absicherung eines REST-Servers verwendet werden.

Die Solution hat 3 Teilnehmer:

  1. Der Client, welcher auf die Resourcen zugreifen möchte
  2. Der Identity-Server, der im Namen des Resourcenbesitzers ein JWT generiert
  3. Der Resourcen-Server, welcher die Resourcen bereitstellt.

 

Identity-Server

Im Identity-Server wird mit CreateToken() der Benutzername und das Passwort validiert. Wenn der Benutzer bekannt ist und das Passwort stimmt, wird ein JWT erstellt und zurückgegeben.

Hier ist der Code der IdentityController-Klasse:

[Route("~/", Name = "default")]
[ApiController]
public class IdentityController : ControllerBase
{
    private readonly IServerUtil _serverUtil;
    private readonly ILogger<IdentityController> _logger;

    public IdentityController(
        ILogger<IdentityController> logger,
        IServerUtil serverUtil)
    {
        _serverUtil = serverUtil;
        _logger = logger;
    }

    [HttpGet]
    public string CreateToken(string username, string password)
    {
        string token = null;
        if (!_serverUtil.IsUserValid(username, password))
        {
            _logger.LogError($"Invalid User ('{username}') or password supplied.");
        }
        else
        {
            token = _serverUtil.CreateJwtToken(username, password);
            _logger.LogInformation($"Generated toke for User '{username}'. Token {token}");
        }
        return token;
    }
}

Die Datei appsettings.json enthält die Werte zur Generierung des JWTs und zur Verschlüsselung der Verbindung (‘Certificate’, siehe ‘gRPC Tutorial Teil 4: HTTP/2 über HTTPS’).

{
…
    "JwtToken": {
        "Issuer": "https://localhost:5001",
        "Key": "D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967",
        "Audience": "DemoService"
    },
    "Certificate": {
        "File": "server.pfx",
        "Password": "P@ssw0rd"
    }
}

Die Schnittstelle IServerUtil definiert die Methoden zur Validierung und JWT Generierung:

public interface IServerUtil
{
    string CreateJwtToken(string username, string password);
    bool IsUserValid(string username, string password);
}

Hier die Implementierung:

public class ServerUtil : IServerUtil
{
    private readonly IConfiguration _config;
    private readonly ILogger<ServerUtil> _logger;

    public ServerUtil(IConfiguration config, ILogger<ServerUtil> logger)
    {
        _config = config;
        _logger = logger;
    }

    public string CreateJwtToken(string username, string password)
    {
        string token = null;
        try
        {
            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.UniqueName, username)
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JwtToken:Key"]));
            var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwtSecurityToken = new JwtSecurityToken(
                _config["JwtToken:Issuer"],
                _config["JwtToken:Audience"],
                claims,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: signingCredentials);
            token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
        }
        catch (Exception e)
        {
            _logger.LogError(e.ToString());
        }
        return token;
    }

    public bool IsUserValid(string username, string password)
    {
        // Dummy implementation: validate user credentials here
        return username=="Demo" && password=="P@ssw0rd";
    }
}

Die Validierung der User Credentials passiert in IsUserValid() und ist nicht implementiert. Nur der Benutzer ‘Demo’ mit Passwort ‘P@ssw0rd’ ist berechtigt, auf den Resourcen-Server zuzugreifen.

Das JWT wird mit der Funktionalität aus dem NuGet ‘System.IdentityModel.Tokens.Jwt’ für die Resource ‘DemoService’ erstellt (‘Audience’). Für Demozwecke werden ein paar Claims im Token festgelegt, welche durch den Resourcen-Server validiert werden können. Signiert wird mit dem symmetrischen Schlüssel ‘Key’ definiert in appsettings.json.

In Startup.cs wird die Implementation von IServerUtil am IoC-Container hinzugefügt:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IServerUtil, ServerUtil>();
    services.AddControllers();
}

Nach dem Starten des Identity-Servers kann folgendermassen ein JWT abgeholt werden:

Noser Engineering AG - gRPC - Client Credential Flow - Abrufen JWT beim IdentityServer

Abrufen JWT beim IdentityServer

Der Output vom Identity-Server sieht folgendermassen aus:

Noser Engineering AG - gRPC - Client Credential Flow - IdentityServer Ausgabe

IdentityServer Ausgabe

Wenn man jetzt in jwt.io das JWT zusammen mit dem privaten Schlüssel einfügt, wird folgendes angezeigt:

Noser Engineering AG - gRPC - Client Credential Flow - JWT vom IdentityServer

JWT vom IdentityServer

Resourcen-Server

Im Resourcen-Server wird der Service wie bei REST mit dem Attribut [Authorize] abgesichert.

In DemoServiceRpc.cs befindet sich die Implementation des gRPC Demo-Service:

[Authorize]
public class DemoServiceRpc : DemoService.DemoServiceBase
{
    public override Task<NameMessage> ExchangeNames(NameMessage request, ServerCallContext context)
    {
        NameMessage result = new NameMessage
        {
            Name = "Hello from Server"
        };
        return Task.FromResult(result);
    }
}

Damit die Signatur und die einzelnen Claims kontrolliert werden können, muss der Resourcen-Server den symmetrischen Schlüssel und die zu erwarteten Inhalte der Claims kennen.

Diese werden in appsettings.json definiert. Auch hier wieder die Sektion ‘Certificate’ zum Konfigurieren des Kestrel-Servers für eine gesicherte Verbindung.

Der relevante Teil von appsettings.json:

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information",
            "grpc": "Debug",
            "JwtBearerAuthentication": "Debug"
        }
    },
    "AllowedHosts": "*",
    "JwtBearerAuth": {
        "Audience": "DemoService",
        "Issuer": "https://localhost:5001",
        "Key": "D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967"
    },
    "Certificate": {
        "File": "server.pfx",
        "Password": "P@ssw0rd"
    }
}

Die Magie wird in Startup.cs konfiguriert. Mit AddAuthentication wird JWT-Bearer Authentication konfiguriert durch Zuweisen von ‘Bearer’ an DefaultAuthenticateScheme und DefaultChallengeScheme. Die Signatur wird validiert mit dem symmetrischen Schlüssel (‘IssuerSigningKey’) und die Claims ‘Issuer’ und ‘Audience’ werden verglichen mit den Werten wie in appsettings.json konfiguriert.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(cfg =>
            {
                // Simply holds "Bearer"
                cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = Configuration["JwtBearerAuth:Audience"],
                    ValidIssuer = Configuration["JwtBearerAuth:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtBearerAuth:Key"]))
                };
                options.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        _logger.LogDebug($"Exception: {c.Exception}");
                        return Task.CompletedTask;
                    }
                };
            });
        services.AddAuthorization();
        services.AddControllers();
        services.AddGrpc(opt => { opt.EnableDetailedErrors = true; });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger("JwtBearerAuthentication");
…
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<DemoServiceRpc>();
            endpoints.MapControllers();
        });
    }
}

Wenn die Validierung des JWTs fehlschlägt werden standardmässig keine weiteren Informationen ausgegeben. Durch Installieren eines Eventhandlers auf ‘OnAuthenticationFailed()’ in der Sektion ‘options.Events’, kann man schnell die Ursache der Validierungsprobleme ausfindig machen.

Der Client

Bevor der gRPC-Client erstellt wird, wird ein JWT Token beim Identity-Server angefragt. Der Code befindet sich in worker.cs:

private async Task<bool> GetToken()
{
    if (string.IsNullOrEmpty(_token) == false)
    {
        return true;
    }
    try
    {
        using var identityClient = new HttpClient();
        string identityUri = $"{_config["IdentityServer:Authority"]}?username={_config["Client:Username"]}&password={_config["Client:Password"]}";
        _token = await identityClient.GetStringAsync(identityUri).ConfigureAwait(false);
    }
    catch (Exception e)
    {
        _logger.LogCritical(e, "Exception while getting JWT");
        return false;
    }
    return true;
}

Das JWT wird bei jeder Anfrage im Header mitgesendet. Dazu wird das HTTP-Headerfeld ‘Authorization’ auf ‘Bearer [JWT]’ gesetzt.

...
_logger.LogInformation("Calling 'ExchangeNames()' at: {time}", DateTimeOffset.Now);
NameMessage nameMessage = new NameMessage
{
    Name = $"{Assembly.GetExecutingAssembly().FullName}"
};
string tokenValue = $"Bearer {_token}";
var metadata = new Metadata()
{
    { "Authorization", tokenValue }
};
CallOptions callOptions = new CallOptions(metadata);
var result = await _client.ExchangeNamesAsync(nameMessage, callOptions);
_logger.LogInformation($"Server answered with {result.Name}");
...

Wenn beispielsweise der Inhalt des JWT unterwegs manipuliert wird, generiert der Client folgende Ausgabe:

Noser Engineering AG - gRPC - Client Credential Flow - Unauthenticated Client

Unauthenticated Client

Dank des OnAuthenticationFailed Handlers im Resourcen-Server findet man jetzt einfach den Grund des Problems heraus:

Noser Engineering AG - gRPC - Client Credential Flow - Unauthenticated Client Server-Ausgabe

Unauthenticated Client Server-Ausgabe

Fazit

Obwohl der einfachste OAuth2 Flow, der Client Credential Flow, mit symmetrischem Schlüssel verwendet wird, zeigt dieser Blog, dass die Absicherung mit JWT nicht ganz trivial ist. Im nächsten Blog wird der IdentityServer4 verwendet, immer noch mit Client Credential Flow, aber mit Public-/Private-Key Authentifizierung und Rollen.

← Voriger Post
Nächster Post →

gRPC Tutorial Teil 4: HTTP/2 über HTTPS

21. Januar 2020
Erik Stroeken
3
gesicherte verbindung, gRPC, https, Kestrel, Zertifikat

gRPC benutzt HTTP/2, welches 2015 veröffentlicht wurde und binäre Kommunikation unterstützt. Moderne Browser unterstützen HTTP/2 ausschliesslich über eine gesicherte Verbindung. Ein gRPC-Client kann dagegen problemlos über eine ungesicherte Verbindung mit einem gRPC-Server kommunizieren. Für Autorisierungstechniken wie z.B. JWT Bearer Tokens (siehe Blog über JWT Client Credentials) muss die Verbindung zwingend gesichert sein. In diesem Blog wird demonstriert, wie man den Kestrel-Server für eine gesicherter Verbindung konfiguriert. Obwohl dieser Blog als Grundlage für einen späteren gRPC-Blog dient, funktioniert das Absichern genau gleich wie für eine REST-Schnittstelle oder eine normale Internetseite.

Basisdefinitionen

HTTP (Hypertext Transfer Protocol) ist ein Protokoll, das von Clients (z. B. Webbrowsern) verwendet wird, um Ressourcen von Servern (z. B. Webservern) anzufordern.

  • HTTP/1.1 – ist seit 1999 das Äquivalent für HTTP und ist ein text basiertes Protokoll.
  • HTTP/2.0 – wurde am 15. Mai 2015 veröffentlicht und unterstützt binäre Kommunikation, parallele Anfragen über einen TCP-Kanal und Header Kompression.

HTTPS ist eine Methode zum Verschlüsseln von HTTP. Grundsätzlich werden HTTP-Nachrichten in einem verschlüsselten Format unter Verwendung von SSL / TLS verpackt.

  • SSL (Secure Sockets Layer) wurde von Netscape entwickelt und kam 1995 als Version 2.0 auf den Markt. Die POODLE Attacke wurde SSL 2014 zum Verhängnis.
  • TLS (Transport Layer Security) wurde 1999 als Upgrade für SSL 3.0 definiert und ist seit 2006 auf dem Markt.

Eine gesicherte Verbindung läuft heutzutage über TLS. Zur Unterstützung älterer Browser bieten die meisten Server auch SSL an. Leider wird oft immer noch SSL gesagt, wenn eigentlich TLS gemeint ist (z.B. badssl.com).

Verschlüsseln und signieren

Um den Datenverkehr zu verschlüsseln, braucht es einen Schlüssel, welcher den Klartext in einen Geheimtext (Chiffrat) umwandeln kann und wieder zurück. Es gibt zwei grundlegende Verfahren sowie eine Kombination von beiden.

Bei der symmetrischen Verschlüsselung wird derselbe Schlüssel verwendet, um Botschaften zu verschlüsseln und auch wieder zu entschlüsseln. Der Schlüssel muss also sowohl dem Sender als auch dem Empfänger bekannt sein.

Bei der asymmetrischen Verschlüsselung, oder Public-Key-Verschlüsselungsverfahren, gibt es einen öffentlichen und einen privaten Schlüssel.

Daten, die mit dem öffentlichen Schlüssel verschlüsselt wurden, können nur mit dem dazugehörigen privaten Schlüssel entschlüsselt werden.

Verschlüsseln und Signieren mit Public-/Private Key pair

Verschlüsseln und Signieren mit Public-/Private Key pair

Quelle: https://de.wikipedia.org/wiki/asymmetrisches_kryptosystem

Das gleiche Schlüsselpaar wird zum Signieren verwendet. Zum Erstellen einer Signatur wird ein Hashwert aus der zu verschickenden Nachricht gebildet und mit dem privaten Schlüssel signiert. Nachricht und Signatur werden dann zum Empfänger geschickt. Zum Verifizieren der Signatur wird die empfangene Signatur des Hashwertes mit dem öffentlichen Schlüssel geprüft. Ist die Verifizierung erfolgreich, kann davon ausgegangen werden, dass die Nachricht vom Besitzer des privaten Schlüssels stammt und dass die Nachricht bei der Übertragung nicht (zufällig oder absichtlich) manipuliert wurde.

Ein Nachteil der asymmetrischen Verschlüsselung ist der hohe Aufwand für die Ver- und Entschlüsselung, was sich deutlich auf die Geschwindigkeit auswirkt.

Bei der hybriden Verschlüsselung wird mit der asymmetrischen Verschlüsselung die Authentizität des Servers (und optional Clients) überprüft und ein symmetrischer Schlüssel zur Verschlüsselung der nachfolgenden Daten übertragen. Dieser Ablauf findet beim Aufsetzen einer HTTPS-Verbindung statt, auch TLS Handshake genannt.

Das Aufsetzen einer HTTPS-Verbindung

Bevor eine gesicherte Verbindung zustande kommt, wird zwischen Client und Server verhandelt.

Noser Engineering - gRPC - TLS Handshake

TLS Handshake

Quelle: https://www.ibm.com/support

  1. Der Client sendet eine «client hello»-Nachricht, in der u.a. die TLS-Version und die vom Client unterstützten Verschlüsselungsalgorithmen (CipherSuites) aufgelistet sind.
  2. Der Server antwortet mit einer «server hello»-Nachricht mit der gewählten CipherSuite, der Sitzungs-ID und dem Server-Zertifikat.
    Optional: Wenn für den Server ein digitales Zertifikat für die Clientauthentifizierung erforderlich ist, sendet der Server eine Clientzertifikatsanforderung (siehe nächsten Blog über Client-Zertifikat Authentifizierung).
  3. Der Client überprüft das digitale Zertifikat des Servers. Das Hauptziel ist es, sicherzustellen, dass der Server tatsächlich der Server ist, für den ihn der Client hält. Dieser Vorgang ist kompliziert und sprengt den Rahmen dieses Blogs.
  4. Der Client sendet die zufällige Bytefolge, die es sowohl dem Client als auch dem Server ermöglicht, den geheimen Schlüssel zu berechnen, der für die Verschlüsselung der nachfolgenden Nachrichtendaten verwendet werden soll. Die zufällige Bytefolge selbst wird mit dem öffentlichen Schlüssel vom Server-Zertifikat verschlüsselt.
  5. Optional: Wenn der Server eine Clientzertifikatsanforderung gesendet hat, sendet der Client eine zufällige Bytefolge, die mit dem privaten Schlüssel des Clients verschlüsselt wird, zusammen mit dem Client-Zertifikat des Clients.
  6. Optional: Der Server überprüft das Client-Zertifikat.
  7. Der Client sendet dem Server eine «Finished»-Nachricht.
  8. Der Server sendet dem Client eine «Finished»-Nachricht.
  9. Für die Dauer der TLS-Sitzung können jetzt Server und Client Nachrichten austauschen, die symmetrisch mit dem geheimen Schlüssel für gemeinsame Nutzung verschlüsselt sind.

 

Root- und Server-Zertifikat

Um die HTTPS-Verbindung aufsetzen zu können, braucht es ein Server-Zertifikat. Das Server-Zertifikat befindet sich am Ende der Zertifizierungskette. Am Anfang der Kette befindet sich das Root-Zertifikat, welches verwendet wird, um die Echtheit des Zertifikates und damit auch die Authentizität des Servers zu beweisen.

Ein digitales Client-Zertifikat ist im Grunde eine Datei, die mit einem Passwort geschützt und in eine Client-Anwendung geladen wird (normalerweise als PKCS12-Datei mit der Erweiterung ‘.p12’ oder ‘.pfx’, die Datei kann mit z.B. OpenSSL konvertiert werden in einer .crt/.cer und .key Datei. Letztere Datei enthält den private Key).

Ein digitales Zertifikat enthält relevante Informationen wie eine digitale Signatur, Ablaufdatum, Name des Issuers, Name der CA (Certificate Authority), Sperrstatus (revocation), SSL/TLS-Versionsnummer, Seriennummer und möglicherweise weitere Informationen, die alle nach dem X.509-Standard strukturiert sind.

Selbstsignierte Zertifikate erstellen

Normalerweise werden Zertifikaten von Zertifizierungsstellen (‘certificate authority’ oder ‘certification authority’, kurz CA) ausgestellt (z.B. Verisign, Thawte, etc.). Für unser Beispiel werden wir selber die Zertifikate generieren. Es gibt verschiedene Wege selber Zertifikate zu generieren: Powershell, Online Tools, OpenSSL etc. In diesem Beitrag wird OpenSSL verwendet.

OpenSSL installieren

Die binären Dateien des OpenSSL Tools stehen als ZIP-Datei zur Verfügung auf ‘https://sourceforge.net/projects/openssl‘ oder als Installer auf ‘https://slproweb.com/products/Win32OpenSSL.html‘. Dazu die letzte Version des Installers ‘Win64OpenSSL_Light-1_1_0L.exe’ (oder höher) herunterladen und den Installer ausführen. Wichtig ist, dass ‘openssl.exe’ und ‘openssl.cnf’ sich nach der Installation im Verzeichnis ‘C:\OpenSSL-Win64\bin’ befinden.

Zertifikate generieren

Im Beispiel gibt es einen Unterordner ‘Certs’ in dem alle OpenSSL Tool Input- und Output-Dateien gespeichert werden.

Die Inhalte des Zertifikatfeldes ‘Enhanced Key Usage’ müssen für das OpenSSL Tool in einer externen Datei definiert werden. In diesem Verzeichnis eine Textdatei ‘openssl-ext.cnf’ mit folgendem Inhalt erstellen:

[server_ssl]
extendedKeyUsage = serverAuth

Eine zweite Datei namens ‘create_certs.cmd’ mit folgendem Inhalt erstellen:

set OPENSSL_CONF=c:\OpenSSL-Win64\bin\openssl.cfg   

echo Generate CA key:
c:\OpenSSL-Win64\bin\openssl genrsa -passout pass:P@ssw0rd -des3 -out root.key 4096

echo Generate CA certificate:
c:\OpenSSL-Win64\bin\openssl req -passin pass:P@ssw0rd -new -x509 -days 3650 -key root.key -out root.crt -subj "/C=CH/ST=LU/L=Root D4/O=Noser Engineering AG/OU=www.noser.com/CN=Noser RpcWithCertificates Root"

echo Generate server key:
c:\OpenSSL-Win64\bin\openssl genrsa -passout pass:P@ssw0rd -des3 -out server.key 4096

echo Generate server signing request:
c:\OpenSSL-Win64\bin\openssl req -passin pass:P@ssw0rd -new -key server.key -out server.csr -subj "/C=CH/ST=LU/L=Root D4/O=Noser Engineering AG/OU=www.noser.com/CN=localhost"

echo Self-sign server certificate:
c:\OpenSSL-Win64\bin\openssl x509 -req -passin pass:P@ssw0rd -days 3650 -extensions server_ssl -extfile openssl-ext.cnf  -in server.csr -CA root.crt -CAkey root.key -set_serial 01 -out server.crt

echo Remove passphrase from server key:
c:\OpenSSL-Win64\bin\openssl rsa -passin pass:P@ssw0rd -in server.key -out server.key

c:\OpenSSL-Win64\bin\openssl pkcs12 -export -in root.crt -inkey root.key -out root.pfx -passout pass:P@ssw0rd -passin pass:P@ssw0rd -name "Noser Engineering self-signed root cert"

c:\OpenSSL-Win64\bin\openssl pkcs12 -export -in server.crt -inkey server.key -out server.pfx -passout pass:P@ssw0rd -name "Noser Engineering self-signed server cert"

pause

Nach dem Ausführen des oberen Skripts hat das Verzeichnis folgenden Inhalt:

Root und Server Zertifikat

Root und Server Zertifikat

Das Skript generiert verschiedene (Zwischen-)Dateien, wovon für uns nur 2 wichtig sind:

  1. root.pfx (friendly name ‘Noser Engineering self-signed root cert’)
  2. server.pfx (friendly name ‘Noser Engineering self-signed server cert’)

Die Datei ‘root.pfx’ enthält das Zertifikat mit ‘Intended Purposes All’ und wird zum Signieren des Server- und Client-Zertifikates verwendet. Das Server-Zertifikat hat ‘Intended Purposes Server Authentication’ und wird später im gRPC Server geladen. Die Zertifikate müssen aber zuerst auf dem Rechner installiert werden.

Installieren der Zertifikate

Da es nicht möglich ist, mit OpenSSL den ‘friendly name’ in einer ‘crt’-Datei zu generieren, verwenden wir die ‘pfx’-Datei zum Installieren der Zertifikate.
Doppelklick auf root.pfx und ‘Local Machine’ wählen.

Importieren Zertifikat

Importieren Zertifikat

Das Passwort einfügen (P@ssw0rd).

Importieren Zertifikat

Importieren Zertifikat: Passwort spezifizieren

Für das Root-Zertifikate den ‘Trusted Root Certification Authorities’ Store auswählen.

Importieren Zertifikat: Store auswählen

Importieren Zertifikat: Trusted Root Store auswählen

Alle Schritte für ‘server.pfx’ wiederholen, mit dem Unterschied, dass ‘Intermediate Certification Authorities’ als Store gewählt werden soll.

Zertifikat Importieren: Intermediate Store auswählen

Zertifikat Importieren: Intermediate Store auswählen

Danach die Microsoft Management Konsole öffnen durch Eingabe von ‘cert’ im Windows Startmenü. Es erscheint ‘Manage computer certificates’ in den Ergebnissen, welches die Konsole öffnet.
Das Root-Zertifikat soll sich jetzt in ‘Trusted Root Certification Authorities’ befinden.

Root Zertifikat im Root Store

Root Zertifikat im Root Store

Und das Server-Zertifikat in ‘Intermediate Certification Authorities’.

Server-Zertifikat in Intermediate Certification Authorities store

Server-Zertifikat in Intermediate Certification Authorities store

gRPC Server Einrichten

In diesem Abschnitt wird der Kestrel-Server für eine HTTPS-Verbindung konfiguriert. Dazu wird das Server-Zertifikat gebraucht, welches gerade generiert und installiert wurde. Das Beispielprojekt ‘RpcOverHTTPSServer’ dient als Grundlage für die nächsten Blogs und kann hier als ZIP-Datei heruntergeladen werden.

Die Datei ‘server.pfx’ muss ins Serverprojekt kopiert und beim Builden ins Ausgabeverzeichnis kopiert werden.

Server.pfx file in server Projekt installieren

Server.pfx file in server Projekt installieren

Passwort und Dateiname werden in appsettings.json vom Serverprojekt in einer neuen Sektion ‘Certificate’ definiert:

{
  "Logging": {
  …
  },
  "Certificate": {
    "File": "server.pfx",
    "Password": "P@ssw0rd"
  }
}

In der program.cs Datei kann jetzt der Kestrel-Server für eine HTTPS-Verbindung konfiguriert werden:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(
                opt =>
                {
                    var config = (IConfiguration)opt.ApplicationServices.GetService(typeof(IConfiguration));
                    var cert = new X509Certificate2(
                        config["Certificate:File"], config["Certificate:Password"]);
                    opt.ConfigureHttpsDefaults(
                        h =>
                        {
                            h.ClientCertificateMode = ClientCertificateMode.NoCertificate;
                            h.CheckCertificateRevocation = false;
                            h.ServerCertificate = cert;
                        });

                });
        });

Der KestrelServerOptions-Parameter von ConfigureKestrel() enthält eine Instanz des DI-Containers, welche gebraucht wird, um die Implementation von IConfiguration zu holen. Mit IConfiguration kann der Dateiname und das Passwort des Zertifikates aus appsettings.json gelesen werden und eine X509-Zertifikat Instanz erstellt werden.

Die HTTPS-Verbindung wird ohne Client-Zertifikat Authentifizierung erstellt (ClientCertificateMode.NoCertificate). Client-Zertifikat Authentifizierung wird im nächsten Blog erklärt und sorgt dafür, dass der Server den Client authentifiziert, bevor der Client auf die Schnittstellen zugreifen darf. Damit wird verhindert, dass unbekannte Clients auf den Server zugreifen.

Weil das Zertifikat selbstsigniert ist, kann es nicht durch eine Zertifizierungsinstanz widerrufen werden. Ausserdem fehlt beim Zertifikat die URL, um kontrollieren zu können (OCSP – Online Certificate Status Protocol), ob das Zertifikat widerrufen wurde. Deshalb wird CheckCertificateRevocation auf false gesetzt.

Für lokale Tests gibt es die Datei ‘Properties\launchSettings.json’ welche beschreibt wie der Server gestartet werden soll. Wenn der Server mit ‘dotnet run’ gestartet wird, wird die Konfiguration mit ‘commandName: Project’ genommen. Wichtig ist, dass die applicationUrl auf https://localhost:5001 gesetzt wird und der Standard http://localhost:5000 gelöscht wird.

{
…
    "RpcOverHTTPS": {
      "commandName": "Project",
      "launchBrowser": false,
      "launchUrl": "",
      "applicationUrl": "https://localhost:5001",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Jetzt den Server via Visual Studio 2019 starten oder im Command-Prompt zum Root des Projektes navigieren und dort tippen ‘dotnet run’.

Server starten mit Visual Studio

Server starten mit Visual Studio

Im Ausgabefenster steht jetzt ‘Now listening on: https://localhost:5001’.

Serverausgabe

Serverausgabe

gRPC Client

Der Client ist eine einfache Sache: Man muss nur den Kanal für ‘https://localhost:5001’ erstellen. Im Beispiel ist diese URL ausgelagert in appsettings.json (‘Service:ServiceUrl’).

protected DemoService.DemoServiceClient Client
{
    get
    {
        if (_client == null)
        {
            var handler = new HttpClientHandler();
            var client = new HttpClient(handler);
            var opt = new GrpcChannelOptions
            {
                HttpClient = client,
                LoggerFactory = _loggerFactory
            };
            ChannelBase channel = GrpcChannel.ForAddress(_config["Service:ServiceUrl"], opt);
            _client = new DemoService.DemoServiceClient(channel);
        }
        return _client;
    }
}

Der Client verbindet sich wie vorher mit dem Server, nur jetzt über eine gesicherte Verbindung.

Client-Ausgabe

Client-Ausgabe

Fazit

Standardmässig benutzt gRPC das HTTP/2 Protokoll über eine ungesicherte Verbindung. Dieses Beispiel zeigt, wie der Kestrel-Server für eine sichere Verbindung eingerichtet werden kann. Das Absichern selber funktioniert gleich wie für eine REST-Schnittstelle oder eine normale Internetseite. In einem der nächsten Blogs wird Authentifizierung und Autorisierung mit JWT Tokens erklärt. Wer das Token besitzt kann mit dem Token auf den Server zugreifen. Deshalb ist für JWT Token Authentifizierung eine gesicherte Verbindung eine absolute Voraussetzung.

← Voriger Post
Nächster Post →
‹ Previous12345Next ›Last »

Tag Cloud

.NET android Angular AngularJs Arduino ASP.Net automated testing Azure Big Data C# C++ Cloud continuous integration Elm Embedded Führung gRPC Internet of Things IoT Java Javascript M2M OWASP Projektmanagement protobuf Python Raspberry Pi Reactive Programming REST Scrum Security Softwarequalität SPA Testen testing Testmanagement Teststrategie UX Visual Studio WebAPI windows WPF Xamarin Xamarin.Android Xamarin.Forms

Archive

Current Posts

  • Akzente setzen mit der Android Splash Screen API unter .NET MAUI
  • Do You have Your Personal Space?
  • Automated provisioning with ARM Templates
  • Asynchrone Beobachtungen und Versprechungen in Angular
  • Simplify Your Automated Tests With Fluent Syntax

Last Comments

  • Hans Reinsch bei Der Safety-Plan: Die wichtigsten Antworten mit Checkliste
  • George H. Barbehenn bei Modeling Optocouplers with Spice
  • Noser Blog Touch-Actions in Xamarin.Forms - Noser Blog bei Mach mehr aus Animationen in Xamarin.Forms mit SkiaSharp
  • Noser Blog Focus on the Secure Storage service of Trusted Firmware (TFM) - Noser Blog bei First run of the Trusted Firmware (TFM) application
  • Noser Blog First run of the Trusted Firmware (TFM) application - Noser Blog bei Focus on the Secure Storage service of Trusted Firmware (TFM)

Popular Posts

Xamarin.Android Code Obfuscation

6 Comments

ManuScripts: Wenn jemand eine Reise tut... Funktionale Programmierung mit Elm - Teil 1 - Aufbruch

5 Comments

ManuScripts: Wenn jemand eine Reise tut... Funktionale Programmierung mit Elm - Teil 2 - Kein Picknick

4 Comments

Contact us

  1. Name *
    * Please enter your name
  2. Email *
    * Please enter a valid email address
  3. Message *
    * Please enter message
© 2013 NOSER ENGINEERING AG. All rights reserved. Datenschutz | Cookie-Richtlinie