-
Notifications
You must be signed in to change notification settings - Fork 10
Enhancement/Decoding X509Certificates with unsupported X509SignatureAlgorithm #312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Conversation
please amend the changelog. I'll review later |
/** | ||
* Consumes all remaining children | ||
* */ | ||
fun consumeRemainingChildren() { index = children.size } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but that just advances the index and discards everythin?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When decoding an unsupported X509SignatureAlgorithm, we don’t need to consume the ASN.1 structure; instead, we just store it as a value. This causes the ASN.1 trailing exception to occur. ConsumeRemainingChildren() serves as a workaround for calling nextChild() repeatedly until the structure is fully parsed. Therefore, this function is called only for algorithms that we don’t know how to parse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get it. we're not doing any decoding operation on the ASN.1 element in question, so we don't need to touch the element or its child nodes at all, so there shouldn't be any checks about training elements anyhow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't feel this function should even be a thing. it is just ignoring content and thus misleading. If (at all) it should be discardRemainignChildren
, but I'd need convincing of a use case for that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you check the doDecode function in X509SignatureAlgorithm:
override fun doDecode(src: Asn1Sequence): X509SignatureAlgorithm = runRethrowing {
when (val oid = (src.nextChild() as Asn1Primitive).readOid()) {
KnownOIDs.rsaPSS -> parsePssParams(src)
else -> {
val alg = fromOid(oid)
if (alg is RSA) {
val tag = src.nextChild().tag
if (tag != Asn1Element.Tag.NULL)
throw Asn1TagMismatchException(Asn1Element.Tag.NULL, tag, "RSA Params not allowed.")
}
alg ?: Other(oid, src).also { src.consumeRemainingChildren() }
}
}
}
This is where I use that function. For DSA alg, the same handling as for RSA is required—parsing the next child, which should have the NULL tag to indicate that there are no parameters. If I don't do that, or if I don't call consumeRemainingChildren(), there will be trailing children left, which will cause an error.
The reason for me not to do the same thing as for RSA is that this is the case for specific algorithm and since we don't support it, I assume that we shouldn't implement parsing specifics for that sole alg.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the NULL-tag-to-indicate-last-child something that is specified generically for any algorithm in the RFC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY DEFINED BY algorithm OPTIONAL }
"The contents of the optional parameters
field will vary according to the algorithm identified." RFC-4.1.1.2
In the RSA signature algorithm, there should always be a NULL tag, whereas for EC algorithms, only the OID should be present without any additional parameters. Also, if I understood the RFC correctly, DSA shouldn't have any additional parameters either. In my opinion, the consumeRemainingChildren()
function makes sense because the "parameters" field in the ASN.1 structure is OPTIONAL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, I then agree that for unknown algorithms, any parameters are permitted. They should still be consumed and represented as a list/sequence/whatever, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated X509SignatureAlgorithm to store list of parameters as Asn1Elements, allowing me to remove discussed consumeRemainingChildren()" function and just parse remaining children as parameters
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt
Outdated
Show resolved
Hide resolved
…with the base class; add runtime registration mechanism for signature algorithms
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good, but I think we can push this further and encapsulate the newly introduced pattern into an interface. Let's wait for comments form @iaik-jheher, before actually doing it.
@@ -204,6 +257,6 @@ object X509SignatureAlgorithmSerializer : KSerializer<X509SignatureAlgorithm> { | |||
|
|||
override fun deserialize(decoder: Decoder): X509SignatureAlgorithm { | |||
val decoded = decoder.decodeString() | |||
return X509SignatureAlgorithm.entries.first { it.name == decoded } | |||
return X509SignatureAlgorithm.registeredAlgorithms.first { it.name == decoded } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good thing I have a PR going that gets rid of this ugly hack ;-). fine here, of course
@@ -171,7 +199,7 @@ constructor( | |||
RelativeDistinguishedName.decodeFromTlv(it as Asn1Set) | |||
} | |||
|
|||
val cryptoPublicKey = CryptoPublicKey.decodeFromTlv(src.nextChild() as Asn1Sequence) | |||
val rawKey = src.nextChild() as Asn1Sequence |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a shorthand .asSequence
but this is just cosmetic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually use that shorthand, but since the rest of the function use as Asn1Something
I followed the pattern to keep it consistent.
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt
Show resolved
Hide resolved
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt
Show resolved
Hide resolved
private val _registeredAlgorithms = MutableStateFlow( | ||
setOf( | ||
ES256, ES384, ES512, | ||
PS256, PS384, PS512, | ||
RS1, RS256, RS384, RS512 | ||
) | ||
) | ||
val registeredAlgorithms: Set<X509SignatureAlgorithm> | ||
get() = _registeredAlgorithms.value | ||
|
||
@Throws(Asn1OidException::class) | ||
private fun fromOid(oid: ObjectIdentifier) = catching { entries.first { it.oid == oid } }.getOrElse { | ||
throw Asn1OidException("Unsupported OID: $oid", oid) | ||
fun register(algorithm: X509SignatureAlgorithm) { | ||
_registeredAlgorithms.update { it + algorithm} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need this exact pattern in many other places in the near future. I think we should seize the opportunity and introduce an interface (with default implementations of methods, where possible) that encapsulates this functionality. @iaik-jheher do you agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, I use a set to store registered algorithms, but in the interface, it would make more sense to use a map. For example, in certificate validation, I use a Map with the OID as the key to register new X509Extensions. It would also be easier here to use a map with the OID as the key and access values directly by OID instead of using the fromOid method. However, that wasn’t possible because RSA-PSS algorithms use the same OID regardless of the number of bits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Introduced Registry interface in ac90ceb. Tried to make it as generic as possible by storing a Map of registered items, with the ability for subclasses to define how the key is extracted from each item. Usually that can be OID (for example X509CertificateExtension in #297 ) but in the X509SignatureAlgorithm case that wasn't possible since RSA-PSS use the same OID regardless of the number of bits.
…to core data classes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
except for the open discussion point, this looks exactly as it is supposed to. Let's try to finish this on Monday
* key extraction from the item using the [extractKey] function. | ||
*/ | ||
abstract class MutableRegistry<K, T>( | ||
private val extractKey: ((T) -> K)? = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not limited to a key, but a generic transformation, so I'd rename this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is something like transform
good enough or is it too generic, or should I use something that still indicates it is used to transform an item into a key (keySelector
, itemToKey
)?
@@ -15,28 +28,18 @@ import kotlinx.serialization.encoding.Decoder | |||
import kotlinx.serialization.encoding.Encoder | |||
|
|||
@Serializable(with = X509SignatureAlgorithmSerializer::class) | |||
enum class X509SignatureAlgorithm( | |||
open class X509SignatureAlgorithm private constructor( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the constructor should be public to allow for constructing some "unknown" signature algorithms, if needed for passing them along without accessing them
* This abstract class allows runtime extension of the registry and optionally supports automatic | ||
* key extraction from the item using the [extractKey] function. | ||
*/ | ||
abstract class MutableRegistry<K, T>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ideally, this should be an interface with default implementations of functions. Of course, then the encapsulation with registeredItems
wont work. I think we need to discuss this offline with @iaik-jheher, as he will be implementing more advanced registration and mapping, so for now, something simple and functional is fine, but we should probably make those interfaces internal anyway until we settle on something.
val RS384 = RSA(KnownOIDs.sha384WithRSAEncryption, "RS384") | ||
val RS512 = RSA(KnownOIDs.sha512WithRSAEncryption, "RS512") | ||
|
||
override val _registeredItems = MutableStateFlow( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like how readable this is by overrding theregisterdItems, but this should probably somehow be encapsulated, so that we don't need to access a member that starts with an underscore. but also here: part of an offline discussion we need to have
ac90ceb
to
708b79e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpicks
val RS384 = RSA(KnownOIDs.sha384WithRSAEncryption, "RS384") | ||
val RS512 = RSA(KnownOIDs.sha512WithRSAEncryption, "RS512") | ||
|
||
val registeredAlgorithms = setOf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We called such constructs entries in other locations
val registeredAlgorithms = setOf( | |
val entries = setOf( |
) | ||
|
||
private fun fromOid(oid: ObjectIdentifier): X509SignatureAlgorithm? = | ||
registeredAlgorithms.firstOrNull { it.oid == oid } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
registeredAlgorithms.firstOrNull { it.oid == oid } | |
entries.firstOrNull { it.oid == oid } |
@@ -204,6 +250,6 @@ object X509SignatureAlgorithmSerializer : KSerializer<X509SignatureAlgorithm> { | |||
|
|||
override fun deserialize(decoder: Decoder): X509SignatureAlgorithm { | |||
val decoded = decoder.decodeString() | |||
return X509SignatureAlgorithm.entries.first { it.name == decoded } | |||
return X509SignatureAlgorithm.registeredAlgorithms.first { it.name == decoded } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return X509SignatureAlgorithm.registeredAlgorithms.first { it.name == decoded } | |
return X509SignatureAlgorithm.entries.first { it.name == decoded } |
@@ -11,7 +11,7 @@ infix fun <T> KmmResult<T>.shouldSucceedWith(b: T) : T = | |||
(this.getOrThrow() shouldBe b) | |||
class X509ConversionTests : FreeSpec({ | |||
"X509 -> Alg -> X509 is stable" - { | |||
withData(X509SignatureAlgorithm.entries) { | |||
withData(X509SignatureAlgorithm.registeredAlgorithms) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
withData(X509SignatureAlgorithm.registeredAlgorithms) { | |
withData(X509SignatureAlgorithm.entries) { |
Uh oh!
There was an error while loading. Please reload this page.