-
Notifications
You must be signed in to change notification settings - Fork 146
der, x509-cert: fix handling of negative integers #831
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
Conversation
(I can also confirm that these changes allow me to successfully parse and re-encode all 447 X.509 certs in the Windows trust list 🙂) |
x509-cert/src/serial_number.rs
Outdated
pub fn new(bytes: &[u8]) -> Result<Self> { | ||
let inner = Int::new(bytes)?; | ||
let inner = Uint::new(bytes)?; | ||
|
||
if inner.len() > SerialNumber::MAX_LEN { | ||
return Err(ErrorKind::Overlength.into()); | ||
} | ||
|
||
Ok(Self { inner }) | ||
Ok(Self { | ||
inner: inner.into(), | ||
}) |
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.
With this change, users will no longer be able to directly build a new negative serial number: the only way they'll be able to end up with a SerialNumber
with a negative internal representation is by loading an existing one.
I very strongly think signed integers should just use the 2's complement representation and not be any sort of enum which seems to be adding a lot of incidental complexity. 2's complement already solves the problem of "how do we store an integer which can be negative or positive" without resorting to an enum. The With that said, another approach is making I'm happy to take a crack at this if you're having trouble. |
That makes sense, thanks! I agree about the complexity (and I'm not happy with how big this change is). I think the thing that tripped me up is over-thinking RFC 5280's length constraint on serial numbers, when really it's ambiguous: the RFC limits serial number values to 20 octets, but doesn't say whether that's 20 with the zero-extension byte or not. It looks like this also caused some confusion in Go's The current Does that make sense? |
Yeah sure. The existing In parallel I can look at composing |
Sounds good, I can make those changes this afternoon/evening. |
This reverts commit 130f52c.
#[inline] | ||
pub(super) fn encoded_len(bytes: &[u8]) -> Result<Length> { | ||
pub(super) fn negative_encoded_len(bytes: &[u8]) -> Result<Length> { |
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.
N.B.: I renamed this to emphasize what this function is actually doing, as currently implemented: it doesn't calculate the encoded length of any signed integer represented as bytes, but only negative signed integers. Passing a positive signed integer here would result in the wrong length, since we only trim leading ones, not zeroes.
(I confirmed that the one remaining callsite for this only calls it on negative integers, and removed the other misleading callsites.)
x509-cert/src/serial_number.rs
Outdated
pub fn new(bytes: &[u8]) -> Result<Self> { | ||
let inner = Int::new(bytes)?; | ||
|
||
if inner.len() > SerialNumber::MAX_LEN { | ||
let inner = Uint::new(bytes)?; | ||
|
||
// The user might give us a 20 byte unsigned integer with a high MSB, | ||
// which we'd then encode with 21 octets to preserve the sign bit. | ||
// RFC 5280 is ambiguous about whether this is valid, so we limit | ||
// `SerialNumber` *encodings* tp 20 bytes or fewer while permitting | ||
// `SerialNumber` *decodings* to have up to 21 bytes below. | ||
if inner.value_len()? > SerialNumber::MAX_LEN { | ||
return Err(ErrorKind::Overlength.into()); | ||
} | ||
|
||
Ok(Self { inner }) | ||
Ok(Self { | ||
inner: inner.into(), | ||
}) | ||
} |
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.
This ensures that we never create a new negative serial number, but the way it does that is potentially surprising because of how the Int::new
API (unchanged) works. In particular, Int::new
assumes that its input is positive, and encodes it correctly to enforce this assumption.
I've updated the tests below to show this behavior.
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.
(This arguably violates POLA, since we're changing the serial number given to us by the user. But it's how the current Int::new
API behaves, and I didn't want to make changes to that before putting something initial up here 🙂)
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.
curious: what does POLA stands for?
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.
Principle of least astonishment
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.
Yep! Sorry for the acronym abuse.
By POLA in this case I meant that users might consider it astonishing that the serial they provide doesn't end up in the cert, even if the result is technically more correct.
@woodruffw can you rebase? |
Yep, can do this afternoon! |
Okay, should be up-to-date again. It looks like a bunch of |
Thanks! |
This is my first attempt at a fix for the problem in #826.
I tried going with a
signed: bool
field at first, but ran into a lot of friction: I ended up having to essentially duplicate all ofuint.rs
inint.rs
. So I scrapped that, and went with @baloo's suggestion of an enum:IntRef
now varies overSintRef
andUintRef
, and provides a uniform interface by forwarding to each.Advantages to this approach:
int.rs
to accommodate these changes, mostly around refusing to accept positive numbers and making sure that we handle the sign byte correctly.SintRef
(now poorly named) can be used to guarantee construction of a negative integer, similar to howUintRef
can only construct positive integers.Disadvantages:
Sint
andSintRef
are poorly named, since they only represent negative integers (and zero). I'm open to naming suggestions here; I left them this way just to get an initial PR up.Int::Unsigned(0) != Int::Signed(0)
. This relates to the first problem, and I can fix it as part of namingSint
something better (likeNegative
) and forbidding zero by construction.Notably, that second disadvantage would still exist with the
signed: bool
approach, since care would need to be taken to ensure a canonicalsigned: false
value when buildingInt(0)
.cc @baloo
Fixes #826.