Skip to content

Enhancement: GeneralName support#353

Merged
JesusMcCloud merged 43 commits intoa-sit-plus:nextfrom
StjepanovicSrdjan:enhancement/generalName
Oct 30, 2025
Merged

Enhancement: GeneralName support#353
JesusMcCloud merged 43 commits intoa-sit-plus:nextfrom
StjepanovicSrdjan:enhancement/generalName

Conversation

@StjepanovicSrdjan
Copy link
Collaborator

  • Refactored AlternativeNames for SAN/IAN extraction
  • Removed detailed parsing of individual name types; now delegates decoding to GeneralName and subclasses

Copy link
Collaborator

@JesusMcCloud JesusMcCloud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though these are "just" the data classes, they do contain logic that check if an how constraints are fulfilled. These need comprehensive tests. I'm sure there stuff out there and you can probably integrate some from your large cert-validation PR into here

Copy link
Collaborator

@JesusMcCloud JesusMcCloud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the validation logic need further offline discussion.

return@decodeRethrowing when (oid) {
CommonName.OID -> str.fold(onSuccess = { CommonName(it) }, onFailure = { CommonName(asn1String) })
Country.OID -> str.fold(onSuccess = { Country(it) }, onFailure = { Country(asn1String) })
EmailAddress.OID -> str.fold(onSuccess = { EmailAddress(it) }, onFailure = { EmailAddress(asn1String) })
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't all of these enforce validation, since their default constructors are called?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, fixed this legacy code in bd53929

@StjepanovicSrdjan StjepanovicSrdjan force-pushed the enhancement/generalName branch 2 times, most recently from 82b093c to 09eecc5 Compare October 1, 2025 08:32
Comment on lines 75 to 90
private fun splitFirstUnescaped(input: String, delimiter: Char): List<String> {
val sb = StringBuilder()
var escaped = false
for ((i, c) in input.withIndex()) {
when {
escaped -> {
sb.append('\\').append(c) // preserve the escape
escaped = false
}
c == '\\' -> escaped = true
c == delimiter -> return listOf(sb.toString(), input.substring(i + 1))
else -> sb.append(c)
}
}
return listOf(sb.toString())
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we rely on some pre-existing functionality for that? I know serialization has it (think: escaping/unescaping string is virtually any test-based format) but I would expect this to be buried somewhere inside kotlin's stdlib, or any of the dependencies we already use here. When in doublt use Android studio, as it gives you free Tokens for Gemini-powered AI-assistance

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did refactor the code to use regex, but still didn't find something pre-existing. It is now much cleaner, but not sure if it's satisfactory yet. ae61ba7

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 94 to 122
private fun splitRespectingEscapeAndQuotes(input: String, delimiter: Char): List<String> {
val parts = mutableListOf<String>()
val sb = StringBuilder()
var escaped = false
var inQuotes = false

input.forEach { c ->
when {
escaped -> {
sb.append('\\').append(c) // preserve escape
escaped = false
}
c == '\\' -> escaped = true
c == '"' -> {
sb.append(c)
inQuotes = !inQuotes
}
c == delimiter && !inQuotes -> {
parts.add(sb.toString())
sb.clear()
}
else -> sb.append(c)
}
}

parts.add(sb.toString().trim())
return parts
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Collaborator

@JesusMcCloud JesusMcCloud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think i caught the last loose ends in this cycle.

import at.asitplus.test.FreeSpec
import io.kotest.matchers.shouldBe

class GeneralNamesConstrainsTest : FreeSpec ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we test all those against JCA / BouncyCastle in addition to your tests?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are based on the BouncyCastle tests

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then please add a comment to the source of the test

import at.asitplus.test.FreeSpec
import io.kotest.matchers.shouldBe

class X500NameParsingTest : FreeSpec ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also test those against bouncy castle / JCA?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are based on the BouncyCastle tests

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, please link to the original BC tests in a comment

Copy link
Collaborator

@JesusMcCloud JesusMcCloud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicks and one open point for an offline elaboration

data class RelativeDistinguishedName(val attrsAndValues: List<AttributeTypeAndValue>) : Asn1Encodable<Asn1Set> {
class RelativeDistinguishedName private constructor(
val attrsAndValues: List<AttributeTypeAndValue>,
performValidation: Boolean = false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd omit the default value, to prevent even the possibility of accidentally calling the wrong c'tor

Comment on lines +197 to +199
constructor(str: Asn1String) : this(Asn1Primitive(str.tag, str.value.encodeToByteArray())) {
if (isValid == false) throw Asn1Exception("Invalid Organization!")
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seeing this pattern everywhere: can't we pull that up as a protected c'tor in the base class?

Comment on lines 75 to 90
private fun splitFirstUnescaped(input: String, delimiter: Char): List<String> {
val sb = StringBuilder()
var escaped = false
for ((i, c) in input.withIndex()) {
when {
escaped -> {
sb.append('\\').append(c) // preserve the escape
escaped = false
}
c == '\\' -> escaped = true
c == delimiter -> return listOf(sb.toString(), input.substring(i + 1))
else -> sb.append(c)
}
}
return listOf(sb.toString())
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import at.asitplus.test.FreeSpec
import io.kotest.matchers.shouldBe

class GeneralNamesConstrainsTest : FreeSpec ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then please add a comment to the source of the test

import at.asitplus.test.FreeSpec
import io.kotest.matchers.shouldBe

class X500NameParsingTest : FreeSpec ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, please link to the original BC tests in a comment

@JesusMcCloud JesusMcCloud changed the base branch from development to next October 30, 2025 08:41
StjepanovicSrdjan and others added 27 commits October 30, 2025 09:54
@JesusMcCloud JesusMcCloud force-pushed the enhancement/generalName branch from a3c4079 to 7db7e92 Compare October 30, 2025 09:19
@JesusMcCloud JesusMcCloud merged commit 35625fa into a-sit-plus:next Oct 30, 2025
6 checks passed
JesusMcCloud added a commit that referenced this pull request Oct 31, 2025
* Refactor AlternativeNames to use GeneralName

* add X500Name and refactor X509Cert to use this class as subject and issuer

---------

Co-authored-by: Bernd Prünster <bernd.pruenster@a-sit.at>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants