legal name validator for doorman node registration (#532)

This commit is contained in:
Patrick Kuo 2017-04-24 21:16:00 +08:00 committed by GitHub
parent 9425b7c927
commit 87dd99d968
3 changed files with 235 additions and 1 deletions

View File

@ -0,0 +1,101 @@
@file:JvmName("LegalNameValidator")
package net.corda.core.utilities
import java.lang.Character.UnicodeScript.*
import java.text.Normalizer
import java.util.regex.Pattern
import javax.security.auth.x500.X500Principal
/**
* The validation function will validate the input string using the following rules:
* - No blacklisted words like "node", "server".
* - Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and character confusability attacks.
* - Should start with a capital letter.
* - No commas or equals signs.
* - No dollars or quote marks, we might need to relax the quote mark constraint in future to handle Irish company names.
*/
fun validateLegalName(normalizedLegalName: String) {
rules.forEach { it.validate(normalizedLegalName) }
}
/**
* The normalize function will trim the input string, replace any multiple spaces with a single space,
* and normalize the string according to NFKC normalization form.
*/
fun normaliseLegalName(legalName: String): String {
val trimmedLegalName = legalName.trim().replace(Regex("\\s+"), " ")
return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC)
}
private val rules: List<Rule<String>> = listOf(
UnicodeNormalizationRule(),
CharacterRule(',', '=', '$', '"', '\'', '\\'),
WordRule("node", "server"),
LengthRule(maxLength = 255),
// TODO: Implement confusable character detection if we add more scripts.
UnicodeRangeRule(LATIN, COMMON, INHERITED),
CapitalLetterRule(),
X500NameRule()
)
private class UnicodeNormalizationRule : Rule<String> {
override fun validate(legalName: String) {
require(legalName == normaliseLegalName(legalName)) { "Legal name must be normalized. Please use 'normaliseLegalName' to normalize the legal name before validation." }
}
}
private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeScript) : Rule<String> {
private val pattern = supportScripts.map { "\\p{Is$it}" }.joinToString(separator = "", prefix = "[", postfix = "]*").let { Pattern.compile(it) }
override fun validate(legalName: String) {
require(pattern.matcher(legalName).matches()) {
val illegalChars = legalName.replace(pattern.toRegex(), "").toSet()
if (illegalChars.size > 1) {
"Illegal characters $illegalChars in \"$legalName\"."
} else {
"Illegal character $illegalChars in \"$legalName\"."
}
}
}
}
private class CharacterRule(vararg val bannedChars: Char) : Rule<String> {
override fun validate(legalName: String) {
bannedChars.forEach {
require(!legalName.contains(it, true)) { "Illegal character: $it" }
}
}
}
private class WordRule(vararg val bannedWords: String) : Rule<String> {
override fun validate(legalName: String) {
bannedWords.forEach {
require(!legalName.contains(it, ignoreCase = true)) { "Illegal word: $it" }
}
}
}
private class LengthRule(val maxLength: Int) : Rule<String> {
override fun validate(legalName: String) {
require(legalName.length <= maxLength) { "Legal name longer then $maxLength characters." }
}
}
private class CapitalLetterRule : Rule<String> {
override fun validate(legalName: String) {
val capitalizedLegalName = legalName.split(" ").map(String::capitalize).joinToString(" ")
require(legalName == capitalizedLegalName) { "Legal name should be capitalized. i.e. '$capitalizedLegalName'" }
}
}
private class X500NameRule : Rule<String> {
override fun validate(legalName: String) {
// This will throw IllegalArgumentException if the name does not comply with X500 name format.
X500Principal("CN=$legalName")
}
}
private interface Rule<in T> {
fun validate(legalName: T)
}

View File

@ -0,0 +1,107 @@
package net.corda.core.utilities
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class LegalNameValidatorTest {
@Test
fun `no double spaces`() {
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Test Legal Name")
}
validateLegalName(normaliseLegalName("Test Legal Name"))
}
@Test
fun `no trailing white space`() {
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Test ")
}
}
@Test
fun `no prefixed white space`() {
assertFailsWith(IllegalArgumentException::class) {
validateLegalName(" Test")
}
}
@Test
fun `blacklisted words`() {
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Test Server")
}
}
@Test
fun `blacklisted characters`() {
validateLegalName("Test")
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("\$Test")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("\"Test")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("\'Test")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("=Test")
}
}
@Test
fun `unicode range`() {
validateLegalName("Test A")
assertFailsWith(IllegalArgumentException::class) {
// Greek letter A.
validateLegalName("Test Α")
}
}
@Test
fun `legal name length less then 256 characters`() {
val longLegalName = StringBuilder()
while (longLegalName.length < 255) {
longLegalName.append("A")
}
validateLegalName(longLegalName.toString())
assertFailsWith(IllegalArgumentException::class) {
validateLegalName(longLegalName.append("A").toString())
}
}
@Test
fun `legal name should be capitalized`() {
validateLegalName("Good Legal Name")
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("bad name")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Bad name")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("bad Name")
}
}
@Test
fun `correctly handle whitespaces`() {
assertEquals("Legal Name With Tab", normaliseLegalName("Legal Name With\tTab"))
assertEquals("Legal Name With Unicode Whitespaces", normaliseLegalName("Legal Name\u2004With\u0009Unicode\u0020Whitespaces"))
assertEquals("Legal Name With Line Breaks", normaliseLegalName("Legal Name With\n\rLine\nBreaks"))
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Legal Name With\tTab")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Legal Name\u2004With\u0009Unicode\u0020Whitespaces")
}
assertFailsWith(IllegalArgumentException::class) {
validateLegalName("Legal Name With\n\rLine\nBreaks")
}
}
}

View File

@ -17,7 +17,7 @@ The following information from the node configuration file is needed to generate
:myLegalName: Your company's legal name. e.g. "Mega Corp LLC". This needs to be unique on the network. If another node :myLegalName: Your company's legal name. e.g. "Mega Corp LLC". This needs to be unique on the network. If another node
has already been permissioned with this name then the permissioning server will automatically reject the request. The has already been permissioned with this name then the permissioning server will automatically reject the request. The
request will also be rejected if the name contains a ``=`` or ``,``. request will also be rejected if it violates legal name rules, see `Legal Name Constraints`_ for more information.
.. note:: In a future version the uniqueness requirement will be relaxed to a X.500 name. This will allow differentiation .. note:: In a future version the uniqueness requirement will be relaxed to a X.500 name. This will allow differentiation
between entities with the same name. between entities with the same name.
@ -38,6 +38,32 @@ Once the request has been approved and the certificates downloaded from the serv
This process only is needed when the node connects to the network for the first time, or when the certificate expires. This process only is needed when the node connects to the network for the first time, or when the certificate expires.
Legal Name Constraints
----------------------
The legal name is the unique identifier in the Corda network, so constraints have been set out to prevent encoding attacks and visual spoofing.
The legal name validator (see ``LegalNameValidator.kt``) is used to enforce rules on Corda's legal names, it is intended to be used by the network operator and Corda node during the node registration process.
It has two functions, a function to normalize legal names, and a function to validate legal names.
The normalize function performs the following transformations:
* Remove leading and trailing whitespaces.
* Replace multiple whitespaces with a single space.
* Normalize the string according to `NFKC normalization form <https://en.wikipedia.org/wiki/Unicode_equivalence#Normalization>`_.
The validation function will validate the input string using the following rules:
* No blacklisted words like "node", "server".
* Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and character confusability attacks.
* Should start with a capital letter.
* No commas or equals signs.
* No dollars or quote marks, although we may relax the quote mark constraint in future to handle Irish company names.
Starting the Registration Starting the Registration
------------------------- -------------------------