Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DID <> TLS integration example #516

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions example/tls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Secure TLS Communication with Self-Signed Certificates and DID Integration

## Overview

This Go application showcases secure communication over a network using TLS (Transport Layer Security) with dynamically generated self-signed X.509 certificates. It integrates Decentralized Identifiers (DIDs) for unique identity representation, leveraging the SSI (Self-Sovereign Identity) SDK for cryptographic operations and DID resolution. The application consists of a TLS server and a TLS client, demonstrating encrypted message exchange.


## TL;DR

Run this example by doing `go run main.go` from this folder. The expected result is below:
```
$ go run main.go
Server: Listening on localhost:8443
Client: Verifying peer certificate
Server: Received 'Hello from Client'
Client: Received 'Hello from Server'
```

## Key Features

- **TLS Server and Client Implementation**: Establishes a simple TLS server and client that securely exchange messages over the network.
- **Dynamic Self-Signed Certificate Generation**: Automatically generates RSA private keys and the corresponding self-signed X.509 certificates, incorporating DIDs into the certificates' subject fields.
- **DID-Based Identity**: Utilizes DIDs to uniquely identify server and client entities, converting RSA public keys into JWK (JSON Web Key) format and further into DID-based JWKs for enhanced identity management.
- **Encrypted Communication**: Ensures all communications between the server and client are encrypted and authenticated through TLS, offering privacy and data integrity.

> [!NOTE]
> Private keys can easily be switched to any supported key type.

> [!NOTE]
> Other did methods besides JWK can be easily supported by adding additional resolvers.

## Functionality

### Initialization

- Upon execution, the application initializes both the TLS server and client in separate goroutines, enabling concurrent operation.
- The server listens on `localhost:8443`, ready to accept client connections.

### RSA Keys and Certificate Generation

- Both server and client generate their RSA private keys and use them to create self-signed certificates. The certificates embed DIDs in their common names, facilitating secure TLS connections with identity verification.

### Conversion to JWK and DID

- The application converts public RSA keys to JWK format, subsequently transforming them into DID-based JWKs using functionalities provided by the `ssi-sdk`.
- The conversion process links TLS identities (as represented by the certificates) with decentralized identities (DIDs), showcasing a modern approach to identity management.

### Certificate Verification

During the TLS handshake, the client verifies the server's certificate against a list of trusted Certificate Authorities (CAs). Since the application uses self-signed certificates, the following custom verification logic is applied:

- **InsecureSkipVerify**: Set to `true` to bypass the default certificate verification process, and overrides `VerifyPeerCertificate` as shown below.
- **VerifyPeerCertificate**: A custom callback function provided to the TLS client's configuration. It implements additional verification checks on the peer's certificate.

### Custom Verification Logic

The custom verification logic includes:

1. **Certificate Parsing**: Parses the peer's certificate to extract critical information, including the subject's Common Name (CN), which contains the DID.
1. **DID Resolution**: Utilizes the SSI SDK to resolve the DID embedded in the certificate's CN to a public key. This step simulates the process of fetching the corresponding public key from a decentralized identity document.
1. **Public Key Matching**: Compares the resolved public key against the public key embedded in the certificate to ensure they match. This confirms that the certificate is indeed associated with the DID it claims to represent.
1. **Integrity Check**: Verifies that the certificate's signature is valid and that it has not been tampered with. This ensures the integrity of the certificate and the authenticity of its issuer (in this case, self-issued).


### Secure Message Exchange

- Following the establishment of a secure TLS channel, the client sends a greeting message to the server, which responds accordingly.
- The exchange demonstrates the application's capability to facilitate secure, encrypted communications over TLS.

## Dependencies

- **Go Standard Libraries (`crypto/x509`, `crypto/tls`, etc.)**: Used for cryptographic functions and TLS communication.
- **SSI SDK (`github.com/TBD54566975/ssi-sdk/crypto/jwx`, `github.com/TBD54566975/ssi-sdk/did/jwk`, etc.)**: Utilized for DID to JWK conversion and cryptographic operations.

## Getting Started

1. Ensure Go is installed on your system and your GOPATH is correctly set up.
2. Clone the repository and navigate to the project directory.
3. Run `go run main.go` to start the server and client.
4. Observe the encrypted message exchange between the server and client in the terminal.

## Security Considerations

This example uses self-signed certificates for simplicity and demonstration purposes. In a production environment, certificates should be obtained from a trusted Certificate Authority (CA) to ensure widespread trust and compatibility. The application exemplifies the integration of TLS security mechanisms with DIDs, suitable for secure communication in various applications, including decentralized systems and IoT devices.

For feedback and contributions, please open an issue or submit a pull request.
270 changes: 270 additions & 0 deletions example/tls/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package main

import (
"context"
gocrypto "crypto"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io"
"log"
"math/big"
"sync"
"time"

"github.com/TBD54566975/ssi-sdk/crypto"
"github.com/TBD54566975/ssi-sdk/crypto/jwx"
"github.com/TBD54566975/ssi-sdk/did/jwk"
"github.com/TBD54566975/ssi-sdk/did/resolution"
)

const (
addr = "localhost:8443"
)

func main() {
var wg sync.WaitGroup

// Start TLS server
wg.Add(1)
go func() {
defer wg.Done()
startTLSServer()
}()

// Give the server a moment to start
wg.Add(1)
go func() {
defer wg.Done()
startTLSClient()
}()

wg.Wait()
}

func startTLSServer() {
_, privateKey, err := crypto.GenerateRSA2048Key()
if err != nil {
panic(err)
}

publicJwk, err := jwx.PublicKeyToPublicKeyJWK(nil, privateKey.Public())
if err != nil {
log.Fatal(err)

Check warning on line 57 in example/tls/main.go

View workflow job for this annotation

GitHub Actions / lint

deep-exit: calls to log.Fatal only in main() or init() functions (revive)
}

didJwk, err := jwk.CreateDIDJWK(*publicJwk)
if err != nil {
log.Fatal(err)

Check warning on line 62 in example/tls/main.go

View workflow job for this annotation

GitHub Actions / lint

deep-exit: calls to log.Fatal only in main() or init() functions (revive)
}

// Define the subject details for the certificate.
subject := pkix.Name{
SerialNumber: "1234",
CommonName: didJwk.String(),
}
certPem, err := GenerateSelfSignedCert(&privateKey, privateKey.Public(), subject)
if err != nil {
panic(err)
}

// Marshal the private key to its ASN.1 PKCS#1 DER encoded form
privateKeyBytes := x509.MarshalPKCS1PrivateKey(&privateKey)

// Create a PEM block with the private key
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: privateKeyBytes,
})

cer, err := tls.X509KeyPair(certPem, privateKeyPEM)
if err != nil {
log.Fatal(err)

Check warning on line 86 in example/tls/main.go

View workflow job for this annotation

GitHub Actions / lint

deep-exit: calls to log.Fatal only in main() or init() functions (revive)
}

config := &tls.Config{Certificates: []tls.Certificate{cer}}
ln, err := tls.Listen("tcp", addr, config)
if err != nil {
log.Fatal(err)
}
defer ln.Close()

fmt.Println("Server: Listening on " + addr)

Check warning on line 96 in example/tls/main.go

View workflow job for this annotation

GitHub Actions / lint

unhandled-error: Unhandled error in call to function fmt.Println (revive)
conn, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
defer conn.Close()

buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
if err != io.EOF {
log.Fatal(err)
}
}
fmt.Printf("Server: Received '%s'\n", string(buffer[:n]))
_, err = conn.Write([]byte("Hello from Server"))
if err != nil {
log.Fatal(err)
}
}

func startTLSClient() {
// Give the server time to start
time.Sleep(1 * time.Second)

_, privateKey, err := crypto.GenerateRSA2048Key()
if err != nil {
panic(err)
}

publicJwk, err := jwx.PublicKeyToPublicKeyJWK(nil, privateKey.Public())
if err != nil {
log.Fatal(err)
}

didJwk, err := jwk.CreateDIDJWK(*publicJwk)
if err != nil {
log.Fatal(err)
}

// Define the subject details for the certificate.
subject := pkix.Name{
SerialNumber: "5678",
CommonName: didJwk.String(),
}
certPem, err := GenerateSelfSignedCert(&privateKey, privateKey.Public(), subject)
if err != nil {
panic(err)
}

// Marshal the private key to its ASN.1 PKCS#1 DER encoded form
privateKeyBytes := x509.MarshalPKCS1PrivateKey(&privateKey)

// Create a PEM block with the private key
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: privateKeyBytes,
})

cert, err := tls.X509KeyPair(certPem, privateKeyPEM)
if err != nil {
log.Fatal(err)
}

config := &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: true, // Only use this for testing with self-signed certs!

Check failure

Code scanning / CodeQL

Disabled TLS certificate check High

InsecureSkipVerify should not be used in production code.
VerifyPeerCertificate: verifyPeerCertificate,
}

conn, err := tls.Dial("tcp", addr, config)
if err != nil {
log.Fatal(err)
}
defer conn.Close()

_, err = conn.Write([]byte("Hello from Client"))
if err != nil {
log.Fatal(err)
}

buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Client: Received '%s'\n", string(buffer[:n]))
}

func verifyPeerCertificate(certs [][]byte, chains [][]*x509.Certificate) error {
fmt.Println("Client: Verifying peer certificate")

Check warning on line 187 in example/tls/main.go

View workflow job for this annotation

GitHub Actions / lint

unhandled-error: Unhandled error in call to function fmt.Println (revive)

if chains != nil {
return errors.New("verifying peer certificate: chains expected to be nil")
}

cert, err := x509.ParseCertificate(certs[0])
if err != nil {
return errors.New("parsing certificate")
}

opts := x509.VerifyOptions{
Roots: x509.NewCertPool(),
}
opts.Roots.AddCert(cert)

_, err = cert.Verify(opts)
if err != nil {
return errors.New("verifying peer: " + err.Error())
}

if cert.Subject.CommonName != cert.Issuer.CommonName {
return errors.New("common name of subject and issuer aren't equal")
}

fromDid, err := certPublicKeyFromDid(cert.Subject.CommonName, jwk.Resolver{})
if err != nil {
return err
}

jwkFromCert, err := jwx.PublicKeyToPublicKeyJWK(nil, cert.PublicKey)
if err != nil {
return err
}
if *jwkFromCert == *fromDid {
return nil
}

return errors.New("verifying peer certificate failed")
}

func certPublicKeyFromDid(did string, resolver resolution.Resolver) (*jwx.PublicKeyJWK, error) {
// resolve the did
result, err := resolver.Resolve(context.Background(), did)
if err != nil {
return nil, err
}

// Assume there is a verification method, and use that to get the public key of the cert.
return result.Document.VerificationMethod[0].PublicKeyJWK, nil
}

// GenerateSelfSignedCert generates a self-signed X.509 certificate for a given RSA private key and subject details.
// Returns the certificate and any error encountered.
func GenerateSelfSignedCert(privateKey gocrypto.PrivateKey, publicKey gocrypto.PublicKey, subject pkix.Name) ([]byte, error) {
// Set certificate's serial number to a random big integer.
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, err
}

// Prepare certificate template.
certTemplate := x509.Certificate{
SerialNumber: serialNumber,
Subject: subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour), // 1 year validity
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}

// Create the self-signed certificate.
certBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, publicKey, privateKey)
if err != nil {
return nil, errors.New("creating certificate: " + err.Error())
}

// Encode the certificate into PEM format.
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})

return certPEM, nil
}
Loading