Repository: gopcua/opcua
Pull request #204 (2019-05-18 03:44:03)

allow username auth without providing a client certificate
Remove the restriction that all non-None asymmetric security policies
need to have both RSA keys present.  This allows for using the
uapolicy package to encrypt usernames for authorization, but
since decryption is never required, a private key is not necessary.

See #190 for discussion



Patch:
diff --git a/examples/crypto/crypto.go b/examples/crypto/crypto.go
index 86abf6e..ff6342c 100644
--- a/examples/crypto/crypto.go
+++ b/examples/crypto/crypto.go
@@ -138,7 +138,7 @@ func clientOptsFromFlags(endpoints []*ua.EndpointDescription) []opcua.Option {
 	case strings.HasPrefix(*policy, ua.SecurityPolicyURIPrefix):
 		secPolicy = *policy
 		*policy = ""
-	case *policy == "Basic128Rsa15" || *policy == "Basic256" || *policy == "Basic256Sha256":
+	case *policy == "None" || *policy == "Basic128Rsa15" || *policy == "Basic256" || *policy == "Basic256Sha256" || *policy == "Aes128_Sha256_RsaOaep" || *policy == "Aes256_Sha256_RsaPss":
 		secPolicy = ua.SecurityPolicyURIPrefix + *policy
 		*policy = ""
 	default:
diff --git a/uapolicy/crypto_pkcs1v15.go b/uapolicy/crypto_pkcs1v15.go
index c0f0114..df58d00 100644
--- a/uapolicy/crypto_pkcs1v15.go
+++ b/uapolicy/crypto_pkcs1v15.go
@@ -8,6 +8,8 @@ import (
 	// Force compilation of required hashing algorithms, although we don't directly use the packages
 	_ "crypto/sha1"
 	_ "crypto/sha256"
+
+	"github.com/gopcua/opcua/ua"
 )
 
 const PKCS1v15MinPadding = 11
@@ -19,6 +21,10 @@ type PKCS1v15 struct {
 }
 
 func (c *PKCS1v15) Decrypt(src []byte) ([]byte, error) {
+	if c.PrivateKey == nil {
+		return nil, ua.StatusBadSecurityChecksFailed
+	}
+
 	rng := rand.Reader
 
 	var plaintext []byte
@@ -47,6 +53,10 @@ func (c *PKCS1v15) Decrypt(src []byte) ([]byte, error) {
 }
 
 func (c *PKCS1v15) Encrypt(src []byte) ([]byte, error) {
+	if c.PublicKey == nil {
+		return nil, ua.StatusBadSecurityChecksFailed
+	}
+
 	rng := rand.Reader
 
 	var ciphertext []byte
@@ -74,6 +84,10 @@ func (c *PKCS1v15) Encrypt(src []byte) ([]byte, error) {
 }
 
 func (s *PKCS1v15) Signature(msg []byte) ([]byte, error) {
+	if s.PrivateKey == nil {
+		return nil, ua.StatusBadSecurityChecksFailed
+	}
+
 	rng := rand.Reader
 
 	h := s.Hash.New()
@@ -86,6 +100,10 @@ func (s *PKCS1v15) Signature(msg []byte) ([]byte, error) {
 }
 
 func (s *PKCS1v15) Verify(msg, signature []byte) error {
+	if s.PublicKey == nil {
+		return ua.StatusBadSecurityChecksFailed
+	}
+
 	h := s.Hash.New()
 	if _, err := h.Write(msg); err != nil {
 		return err
diff --git a/uapolicy/crypto_rsaoaep.go b/uapolicy/crypto_rsaoaep.go
index 1f23ced..971ff15 100644
--- a/uapolicy/crypto_rsaoaep.go
+++ b/uapolicy/crypto_rsaoaep.go
@@ -8,6 +8,8 @@ import (
 	// Force compilation of required hashing algorithms, although we don't directly use the packages
 	_ "crypto/sha1"
 	_ "crypto/sha256"
+
+	"github.com/gopcua/opcua/ua"
 )
 
 // messageLen = (keyLenBits / 8) - 2*(hashLenBits / 8) - 2
@@ -25,6 +27,10 @@ type RSAOAEP struct {
 }
 
 func (a *RSAOAEP) Decrypt(src []byte) ([]byte, error) {
+	if a.PrivateKey == nil {
+		return nil, ua.StatusBadSecurityChecksFailed
+	}
+
 	rng := rand.Reader
 
 	var plaintext []byte
@@ -53,6 +59,10 @@ func (a *RSAOAEP) Decrypt(src []byte) ([]byte, error) {
 }
 
 func (a *RSAOAEP) Encrypt(src []byte) ([]byte, error) {
+	if a.PublicKey == nil {
+		return nil, ua.StatusBadSecurityChecksFailed
+	}
+
 	rng := rand.Reader
 
 	var ciphertext []byte
diff --git a/uapolicy/crypto_rsapss.go b/uapolicy/crypto_rsapss.go
index 86e3f17..04a0b86 100644
--- a/uapolicy/crypto_rsapss.go
+++ b/uapolicy/crypto_rsapss.go
@@ -8,6 +8,8 @@ import (
 	// Force compilation of required hashing algorithms, although we don't directly use the packages
 	_ "crypto/sha1"
 	_ "crypto/sha256"
+
+	"github.com/gopcua/opcua/ua"
 )
 
 type RSAPSS struct {
@@ -17,6 +19,10 @@ type RSAPSS struct {
 }
 
 func (s *RSAPSS) Signature(msg []byte) ([]byte, error) {
+	if s.PrivateKey == nil {
+		return nil, ua.StatusBadSecurityChecksFailed
+	}
+
 	rng := rand.Reader
 
 	h := s.Hash.New()
@@ -29,6 +35,10 @@ func (s *RSAPSS) Signature(msg []byte) ([]byte, error) {
 }
 
 func (s *RSAPSS) Verify(msg, signature []byte) error {
+	if s.PublicKey == nil {
+		return ua.StatusBadSecurityChecksFailed
+	}
+
 	h := s.Hash.New()
 	if _, err := h.Write(msg); err != nil {
 		return err
diff --git a/uapolicy/policyAes128Sha256RsaOaep.go b/uapolicy/policyAes128Sha256RsaOaep.go
index ec6ead7..078bab8 100644
--- a/uapolicy/policyAes128Sha256RsaOaep.go
+++ b/uapolicy/policyAes128Sha256RsaOaep.go
@@ -94,15 +94,24 @@ func newAes128Sha256RsaOaepAsymmetric(localKey *rsa.PrivateKey, remoteKey *rsa.P
 		return nil, errors.New(msg)
 	}
 
+	var localKeySize, remoteKeySize int
+	if localKey != nil {
+		localKeySize = localKey.PublicKey.Size()
+	}
+
+	if remoteKey != nil {
+		remoteKeySize = remoteKey.Size()
+	}
+
 	return &EncryptionAlgorithm{
-		blockSize:           remoteKey.Size(),
-		plainttextBlockSize: remoteKey.Size() - RSAOAEPMinPaddingSHA1,
+		blockSize:           remoteKeySize,
+		plainttextBlockSize: remoteKeySize - RSAOAEPMinPaddingSHA1,
 		encrypt:             &RSAOAEP{Hash: crypto.SHA1, PublicKey: remoteKey},    // RSA-OAEP-SHA1
 		decrypt:             &RSAOAEP{Hash: crypto.SHA1, PrivateKey: localKey},    // RSA-OAEP-SHA1
 		signature:           &PKCS1v15{Hash: crypto.SHA256, PrivateKey: localKey}, // RSA-PKCS15-SHA2-256
 		verifySignature:     &PKCS1v15{Hash: crypto.SHA256, PublicKey: remoteKey}, // RSA-PKCS15-SHA2-256
 		nonceLength:         nonceLength,
-		signatureLength:     localKey.PublicKey.Size(),
+		signatureLength:     localKeySize,
 		encryptionURI:       "http://opcfoundation.org/ua/security/rsa-oaep-sha1",
 		signatureURI:        "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
 	}, nil
diff --git a/uapolicy/policyAes256Sha256RsaPss.go b/uapolicy/policyAes256Sha256RsaPss.go
index 8180a22..f027db9 100644
--- a/uapolicy/policyAes256Sha256RsaPss.go
+++ b/uapolicy/policyAes256Sha256RsaPss.go
@@ -98,15 +98,24 @@ func newAes256Sha256RsaPssAsymmetric(localKey *rsa.PrivateKey, remoteKey *rsa.Pu
 		return nil, errors.New(msg)
 	}
 
+	var localKeySize, remoteKeySize int
+	if localKey != nil {
+		localKeySize = localKey.PublicKey.Size()
+	}
+
+	if remoteKey != nil {
+		remoteKeySize = remoteKey.Size()
+	}
+
 	return &EncryptionAlgorithm{
-		blockSize:           remoteKey.Size(),
-		plainttextBlockSize: remoteKey.Size() - RSAOAEPMinPaddingSHA256,
+		blockSize:           remoteKeySize,
+		plainttextBlockSize: remoteKeySize - RSAOAEPMinPaddingSHA256,
 		encrypt:             &RSAOAEP{Hash: crypto.SHA256, PublicKey: remoteKey}, // RSA-OAEP-SHA256
 		decrypt:             &RSAOAEP{Hash: crypto.SHA256, PrivateKey: localKey}, // RSA-OAEP-SHA256
 		signature:           &RSAPSS{Hash: crypto.SHA256, PrivateKey: localKey},  // RSA-PSS-SHA2-256
 		verifySignature:     &RSAPSS{Hash: crypto.SHA256, PublicKey: remoteKey},  // RSA-PSS-SHA2-256
 		nonceLength:         nonceLength,
-		signatureLength:     localKey.PublicKey.Size(),
+		signatureLength:     localKeySize,
 		encryptionURI:       "http://opcfoundation.org/UA/security/rsa-oaep-sha2-256",
 		signatureURI:        "http://opcfoundation.org/UA/security/rsa-pss-sha2-256",
 	}, nil
diff --git a/uapolicy/policyBasic128Rsa15.go b/uapolicy/policyBasic128Rsa15.go
index 38399d3..9c306ae 100644
--- a/uapolicy/policyBasic128Rsa15.go
+++ b/uapolicy/policyBasic128Rsa15.go
@@ -83,15 +83,24 @@ func newBasic128Rsa15Asymmetric(localKey *rsa.PrivateKey, remoteKey *rsa.PublicK
 		return nil, errors.New(msg)
 	}
 
+	var localKeySize, remoteKeySize int
+	if localKey != nil {
+		localKeySize = localKey.PublicKey.Size()
+	}
+
+	if remoteKey != nil {
+		remoteKeySize = remoteKey.Size()
+	}
+
 	return &EncryptionAlgorithm{
-		blockSize:           remoteKey.Size(),
-		plainttextBlockSize: remoteKey.Size() - PKCS1v15MinPadding,
+		blockSize:           remoteKeySize,
+		plainttextBlockSize: remoteKeySize - PKCS1v15MinPadding,
 		encrypt:             &PKCS1v15{PublicKey: remoteKey},                    // RSA-SHA15+KWRSA15
 		decrypt:             &PKCS1v15{PrivateKey: localKey},                    // RSA-SHA15+KWRSA15
 		signature:           &PKCS1v15{Hash: crypto.SHA1, PrivateKey: localKey}, // RSA-SHA1
 		verifySignature:     &PKCS1v15{Hash: crypto.SHA1, PublicKey: remoteKey}, // RSA-SHA1
 		nonceLength:         nonceLength,
-		signatureLength:     localKey.PublicKey.Size(),
+		signatureLength:     localKeySize,
 		encryptionURI:       "http://www.w3.org/2001/04/xmlenc#rsa-1_5",
 		signatureURI:        "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
 	}, nil
diff --git a/uapolicy/policyBasic256.go b/uapolicy/policyBasic256.go
index 0db076c..de3966b 100644
--- a/uapolicy/policyBasic256.go
+++ b/uapolicy/policyBasic256.go
@@ -82,15 +82,24 @@ func newBasic256Asymmetric(localKey *rsa.PrivateKey, remoteKey *rsa.PublicKey) (
 		return nil, errors.New(msg)
 	}
 
+	var localKeySize, remoteKeySize int
+	if localKey != nil {
+		localKeySize = localKey.PublicKey.Size()
+	}
+
+	if remoteKey != nil {
+		remoteKeySize = remoteKey.Size()
+	}
+
 	return &EncryptionAlgorithm{
-		blockSize:           remoteKey.Size(),
-		plainttextBlockSize: remoteKey.Size() - RSAOAEPMinPaddingSHA1,
+		blockSize:           remoteKeySize,
+		plainttextBlockSize: remoteKeySize - RSAOAEPMinPaddingSHA1,
 		encrypt:             &RSAOAEP{Hash: crypto.SHA1, PublicKey: remoteKey},  // RSA-OAEP
 		decrypt:             &RSAOAEP{Hash: crypto.SHA1, PrivateKey: localKey},  // RSA-OAEP
 		signature:           &PKCS1v15{Hash: crypto.SHA1, PrivateKey: localKey}, // RSA-SHA1
 		verifySignature:     &PKCS1v15{Hash: crypto.SHA1, PublicKey: remoteKey}, // RSA-SHA1
 		nonceLength:         nonceLength,
-		signatureLength:     localKey.PublicKey.Size(),
+		signatureLength:     localKeySize,
 		encryptionURI:       "http://www.w3.org/2001/04/xmlenc#rsa-oaep",
 		signatureURI:        "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
 	}, nil
diff --git a/uapolicy/policyBasic256Sha256.go b/uapolicy/policyBasic256Sha256.go
index 3c56084..2f94861 100644
--- a/uapolicy/policyBasic256Sha256.go
+++ b/uapolicy/policyBasic256Sha256.go
@@ -95,16 +95,24 @@ func newBasic256Rsa256Asymmetric(localKey *rsa.PrivateKey, remoteKey *rsa.Public
 		msg := fmt.Sprintf("remote key size should be %d-%d bytes, got %d bytes", minAsymmetricKeyLength, maxAsymmetricKeyLength, remoteKey.Size())
 		return nil, errors.New(msg)
 	}
+	var localKeySize, remoteKeySize int
+	if localKey != nil {
+		localKeySize = localKey.PublicKey.Size()
+	}
+
+	if remoteKey != nil {
+		remoteKeySize = remoteKey.Size()
+	}
 
 	return &EncryptionAlgorithm{
-		blockSize:           remoteKey.Size(),
-		plainttextBlockSize: remoteKey.Size() - RSAOAEPMinPaddingSHA1,
+		blockSize:           remoteKeySize,
+		plainttextBlockSize: remoteKeySize - RSAOAEPMinPaddingSHA1,
 		encrypt:             &RSAOAEP{Hash: crypto.SHA1, PublicKey: remoteKey},    // RSA-OAEP
 		decrypt:             &RSAOAEP{Hash: crypto.SHA1, PrivateKey: localKey},    // RSA-OAEP
 		signature:           &PKCS1v15{Hash: crypto.SHA256, PrivateKey: localKey}, // RSA-PKCS15-SHA2-256
 		verifySignature:     &PKCS1v15{Hash: crypto.SHA256, PublicKey: remoteKey}, // RSA-PKCS15-SHA2-256
 		nonceLength:         nonceLength,
-		signatureLength:     localKey.PublicKey.Size(),
+		signatureLength:     localKeySize,
 		encryptionURI:       "http://www.w3.org/2001/04/xmlenc#rsa-oaep",
 		signatureURI:        "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
 	}, nil
diff --git a/uapolicy/securitypolicy.go b/uapolicy/securitypolicy.go
index 52d32ee..8c8c648 100644
--- a/uapolicy/securitypolicy.go
+++ b/uapolicy/securitypolicy.go
@@ -34,10 +34,6 @@ func Asymmetric(uri string, localKey *rsa.PrivateKey, remoteKey *rsa.PublicKey)
 		return nil, fmt.Errorf("unsupported security policy %s", uri)
 	}
 
-	if uri != ua.SecurityPolicyURINone && (localKey == nil || remoteKey == nil) {
-		return nil, errors.New("invalid asymmetric security policy config: both keys required")
-	}
-
 	return p.asymmetric(localKey, remoteKey)
 }
 


Test patch:
diff --git a/uapolicy/securitypolicy_test.go b/uapolicy/securitypolicy_test.go
index be26549..6b35d9d 100644
--- a/uapolicy/securitypolicy_test.go
+++ b/uapolicy/securitypolicy_test.go
@@ -235,6 +235,86 @@ func TestEncryptionAlgorithms(t *testing.T) {
 	}
 }
 
+func TestMissingKey(t *testing.T) {
+	payload := make([]byte, 5000)
+	_, err := rand.Read(payload)
+	if err != nil {
+		t.Fatalf("could not generate random payload")
+	}
+
+	payloadRef := make([]byte, len(payload))
+	copy(payloadRef, payload)
+
+	key, err := generatePrivateKey(2048)
+	if err != nil {
+		t.Fatalf("Unable to generate private key\n")
+	}
+
+	for uri, p := range policies {
+		if uri == ua.SecurityPolicyURINone {
+			continue
+		}
+
+		t.Run(uri, func(t *testing.T) {
+			encryptOnly, err := p.asymmetric(nil, &key.PublicKey)
+			if err != nil {
+				t.Fatalf("failed to create encrypt-only asymmetric algorithms: %s", err)
+			}
+
+			decryptOnly, err := p.asymmetric(key, nil)
+			if err != nil {
+				t.Fatalf("failed to create decrypt-only asymmetric algorithms: %s", err)
+			}
+
+			ciphertext, err := encryptOnly.Encrypt(payload)
+			if err != nil {
+				t.Fatalf("failed to encrypt with encrypt-only policy: %s", err)
+			}
+
+			signature, err := decryptOnly.Signature(payload)
+			if err != nil {
+				t.Fatalf("decrypt-only algorithm failed to generate signature: %s", err)
+			}
+
+			err = encryptOnly.VerifySignature(payload, signature)
+			if err != nil {
+				t.Fatalf("failed to verify signature with encrypt-only algorithm: %s", err)
+			}
+
+			plaintext, err := decryptOnly.Decrypt(ciphertext)
+			if err != nil {
+				t.Fatalf("failed to decrypt with decrypt-only algorithm: %s", err)
+			}
+
+			if got, want := plaintext, payloadRef; !bytes.Equal(got, want) {
+				t.Errorf("decryption failed:\ngot %#v want %#v\n", got, want)
+			}
+
+			_, err = encryptOnly.Decrypt(ciphertext)
+			if err == nil {
+				t.Fatal("encrypt-only algorithm decrypted block without error - should be impossible")
+			}
+
+			_, err = encryptOnly.Signature(payload)
+			if err == nil {
+				t.Fatalf("encrypt-only algorithm generated a signature without error - should be impossible")
+			}
+
+			_, err = decryptOnly.Encrypt(payload)
+			if err == nil {
+				t.Fatal("decrypt-only algorithm encrypted block without error - should be impossible")
+			}
+
+			err = decryptOnly.VerifySignature(payload, signature)
+			if err == nil {
+				t.Fatalf("decrypt-only algorithm verified a signature without error - should be impossible")
+			}
+
+		})
+
+	}
+}
+
 func TestZeroStruct(t *testing.T) {
 	defer func() {
 		if r := recover(); r != nil {