mirror of
https://github.com/corda/corda.git
synced 2025-02-18 16:40:55 +00:00
legal name validator for doorman node registration (#532)
This commit is contained in:
parent
9425b7c927
commit
87dd99d968
@ -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)
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
-------------------------
|
-------------------------
|
||||||
|
Loading…
x
Reference in New Issue
Block a user