Skip to content

Commit

Permalink
string-increase + clientname
Browse files Browse the repository at this point in the history
  • Loading branch information
led0nk committed Feb 5, 2025
1 parent c615e12 commit 40af3d5
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 43 deletions.
Binary file removed .DS_Store
Binary file not shown.
20 changes: 12 additions & 8 deletions opcua_plugin/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,31 @@ func (g *OPCUAInput) GetOPCUAClientOptions(selectedEndpoint *ua.EndpointDescript
opts = append(opts, opcua.AuthUsername(g.Username, g.Password))
}

// Generate certificates if we don't connect without Security
// Generate certificates if we connect with Security
if selectedEndpoint.SecurityPolicyURI != ua.SecurityPolicyURINone {
if g.cachedTLSCertificate == nil {
if g.CertificateSeed == "" {
// Generate an 8-character random string if no 'certificateSeed'
// Generate an 64-character random string if no 'certificateSeed'
// provided by the user.
g.CertificateSeed = randomString(8)
g.CertificateSeed = randomString(64)
g.Log.Infof(
"The client certificate was generated randomly upon startup and will "+
"change on every restart. To use the current dynamically generated "+
"certificate as a fixed certificate, copy the following configuration "+
"snippet into your config: 'certificateSeed: \"%s\"'", g.CertificateSeed)

}

// Just use a random String to make the appearance in "trusted clients"
// more "unique". So the user is able to recognize the client in the
// servers UI.
clientNameUID := randomString(8)

clientName := "urn:benthos-umh:client-predefined-" + clientNameUID
certPEM, keyPEM, err := GenerateCertWithMode(clientName,
certPEM, keyPEM, clientName, err := GenerateCertWithMode(
24*time.Hour*365*10,
g.SecurityMode,
g.SecurityPolicy,
g.CertificateSeed,
clientNameUID)
)
if err != nil {
g.Log.Errorf("Failed to generate certificate: %v", err)
return nil, err
Expand All @@ -73,7 +77,7 @@ func (g *OPCUAInput) GetOPCUAClientOptions(selectedEndpoint *ua.EndpointDescript
g.cachedTLSCertificate = &cert
g.Log.Infof("The clients certificate was created, to use an encrypted connection "+
"please proceed to the OPC-UA Server Configuration and trust either all clients "+
"or the clients certificate with the Application-URI: '%s", clientName)
"or the clients certificate with the client-name: '%s'", clientName)
}

pk, ok := g.cachedTLSCertificate.PrivateKey.(*rsa.PrivateKey)
Expand Down
85 changes: 50 additions & 35 deletions opcua_plugin/generate_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ import (
"net"
"net/url"
"os"
"strconv"
"strings"
"time"
)

// We create an implementation for io.Reader here since we don't want to
// randomly generate our private key but instead want to seed it.

type seededReader struct {
src *mrand.Rand
}
Expand All @@ -48,24 +52,28 @@ func (r *seededReader) Read(p []byte) (int, error) {
// usage, taking into account the security mode (Sign vs SignAndEncrypt) and
// the desired security policy (Basic128Rsa15, Basic256, Basic256Sha256, etc.).
//
// - host: CommonName & DNS/IP/URI entries to include
// - validFor: Certificate validity duration
// - securityMode: The OPC UA message security mode (None, Sign, SignAndEncrypt)
// - policy: The OPC UA security policy (e.g., "Basic128Rsa15", "Basic256", "Basic256Sha256")
// - host: CommonName & DNS/IP/URI entries to include
// - validFor: Certificate validity duration
// - securityMode: The OPC UA message security mode (None, Sign, SignAndEncrypt)
// - securityPolicy: The OPC UA security policy (e.g., "Basic128Rsa15", "Basic256", "Basic256Sha256")
// - seedString: The seed which is used to create the client certificate
func GenerateCertWithMode(
host string,
validFor time.Duration,
securityMode string,
securityPolicy string,
seedString string,
clientNameUID string,
) (certPEM, keyPEM []byte, err error) {
var rsaBits int
) (certPEM, keyPEM []byte, clientName string, err error) {
var (
rsaBits int
signatureAlgorithm x509.SignatureAlgorithm
)

if len(host) == 0 {
return nil, nil, fmt.Errorf("missing required host parameter")
}
var signatureAlgorithm x509.SignatureAlgorithm
h := fnv.New64a()
h.Write([]byte(seedString))
seed := int64(h.Sum64())
clientUID := strconv.FormatInt(seed, 10)

host := "urn:benthos-umh:client-predefined-" + clientUID[0:7]

switch securityPolicy {
case "Basic256Rsa256":
Expand All @@ -87,18 +95,10 @@ func GenerateCertWithMode(
signatureAlgorithm = x509.SHA256WithRSA
}

h := fnv.New64a()
h.Write([]byte(seedString))
seed := int64(h.Sum64())

// Create a custom io.Reader to ensure we don't use random Numbers to create
// the private key, but instead use the 'certificateSeed'.
seededReader := newSeededReader(seed)

// Generate RSA private key
priv, err := rsa.GenerateKey(seededReader, rsaBits)
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
return nil, nil, "", fmt.Errorf("failed to generate private key: %w", err)
}

notBefore := time.Now()
Expand All @@ -113,21 +113,22 @@ func GenerateCertWithMode(
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate serial number: %w", err)
return nil, nil, "", fmt.Errorf("failed to generate serial number: %w", err)
}

// Prepare the certificate template
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
// Modify as appropriate for your org
CommonName: "benthos-umh-predefined-" + clientNameUID,
// Modified by using the first 8 characters of the "hashed" seed
CommonName: "benthos-umh-predefined-" + clientUID[0:7],
Organization: []string{"UMH"},
},
NotBefore: notBefore,
NotAfter: notAfter,
BasicConstraintsValid: true,
SignatureAlgorithm: signatureAlgorithm,
IsCA: false,

// ExtKeyUsage: Both server & client auth for OPC UA usage
ExtKeyUsage: []x509.ExtKeyUsage{
Expand All @@ -136,7 +137,9 @@ func GenerateCertWithMode(
},
}

// Fill in IPAddresses, DNSNames, URIs from the host string (comma-separated)
clientName = template.Subject.CommonName

// Fill in IPAddresses, DNSNames, URIs from the ApplicationURI string (comma-separated)
hosts := strings.Split(host, ",")
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
Expand All @@ -152,35 +155,47 @@ func GenerateCertWithMode(
// Decide on the key usage bits based on security mode
switch securityMode {
case "Sign":
// For Sign-only, we need DigitalSignature
template.KeyUsage = x509.KeyUsageDigitalSignature
// For Sign-only, we need DigitalSignature and ContentCommitment("NonRepudiation")
// meaning the certificate can be used to sign data or communications that
// the signer later cannot deny having signed it.
template.KeyUsage = x509.KeyUsageDigitalSignature |
x509.KeyUsageContentCommitment
case "SignAndEncrypt":
// For Sign and Encrypt, we need KeyEncipherment + DigitalSignature
template.KeyUsage = x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment
// For Sign and Encrypt, we need KeyEncipherment, DigitalSignature,
// DataEncipherement, ContentCommitment and CertSign
template.KeyUsage = x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature |
x509.KeyUsageDataEncipherment |
x509.KeyUsageContentCommitment |
x509.KeyUsageCertSign
default:
// e.g. fallback for SecurityMode 'None'
template.KeyUsage = x509.KeyUsageDigitalSignature
}

// Create a custom io.Reader to ensure we don't use random Numbers to create
// the server certificate, but instead use the 'certificateSeed'.
seededReader := newSeededReader(seed)

// Actually create the certificate
derBytes, err := x509.CreateCertificate(
//NOTE: We could also seed the certificate itself, but at least 1 component
// should stay random, either the certificate or the priv.key
rand.Reader,
// Use the seededReader to seed the server certificate. We could also seed
// the private key, but at least 1 of them should be randomly created.
seededReader,
&template,
&template, // self-signed
publicKey(priv),
priv,
)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
return nil, nil, "", fmt.Errorf("failed to create certificate: %w", err)
}

// PEM-encode the results
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
keyPEM = pem.EncodeToMemory(pemBlockForKey(priv))

return certPEM, keyPEM, nil
return certPEM, keyPEM, clientName, nil
}

func publicKey(priv interface{}) interface{} {
Expand Down

0 comments on commit 40af3d5

Please sign in to comment.