-
Notifications
You must be signed in to change notification settings - Fork 73
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
secrecy: rework the SecretBox
type
#1140
Conversation
@tarcieri : any comments on this? |
secrecy/src/boxed.rs
Outdated
pub fn new(ctr: impl FnOnce() -> S) -> Self { | ||
let mut data = ctr(); | ||
let secret = Self { | ||
inner_secret: Box::new(data.clone()), | ||
}; | ||
data.zeroize(); | ||
secret | ||
} |
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.
My first thought looking at this is that there has to be another way and that this approach is suboptimal.
I am unfortunately quite aware of how tricky it is to ensure values are constructed on the heap, but this approach seems like it would make ensured inlining more difficult and force the value to be stack allocated, then moved to the heap, whereas with proper inlining I believe it's possible to avoid that initial stack allocation.
It's been awhile since I've checked that in godbolt though, and I think work on placement-by-return has largely stalled (and the box
keyword removed).
Without placement-by-return though, I think this will leave a copy on the stack (i.e. from ctr
's original stack frame) unless LLVM decides to inline ctr
, so this feels like something of a fraught endeavor.
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.
A simpler alternative is to accept Box<T>
as an argument.
I think a complex constructor like this needs to be informed by and justified by the generated assembly on popular platforms. If it leaves any copies on the stack it defeats the point (and potentially the inliner).
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 am unfortunately quite aware of how tricky it is to ensure values are constructed on the heap, but this approach seems like it would make ensured inlining more difficult and force the value to be stack allocated, then moved to the heap, whereas with proper inlining I believe it's possible to avoid that initial stack allocation.
I don't think it is supported in Rust currently. It's called "placement new", and currently only exists as a proposal. At the moment we have to accept the fact the we have to construct the value on stack, and all we can do is at least contain it within a small closure and zeroize it. (Edit: sorry, just realized that you alluded to the same feature).
Without placement-by-return though, I think this will leave a copy on the stack
Not according to my tests.
A simpler alternative is to accept Box as an argument.
That is certainly a good idea as an additional constructor, but it's not always possible to already have a Box
.
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 think it is supported in Rust currently. It's called "placement new", and currently only exists as a proposal.
You just linked to the placement-by-return proposal I was referring to in my comments.
But without placement-by-return, you're going to make a copy when ctr
returns a value, which defeats the whole point.
The only way to eliminate the copy is by eliminating the intermediate stack frame, i.e. inlining, which it seems like this approach will always defeat.
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 other words, this constructor needlessly forces a stack allocation of a value, then copies it to the heap.
I think we should avoid making copies as much as possible.
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 according to my tests.
What tests did you do?
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.
You just linked to the placement-by-return proposal I was referring to in my comments.
Yes, sorry again, I knew this feature as "placement new", and evidently it got renamed. Either way, it's not in Rust right now, and won't be for a while.
But without placement-by-return, you're going to make a copy when ctr returns a value, which defeats the whole point.
I need the stack frame to get access to the value to zeroize it. ctr
passes the ownership of the value, so as far as I understand, there's no additional copy being made within ctr
, it's allocated on the stack such that when ctr
returns, it's in the outer frame's stack.
Without the placement new you have to have a stack value. It's currently unavoidable, there's no inlining possible that will place it directly in the heap. Best thing we can do is to zeroize it.
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 other words, this constructor needlessly forces a stack allocation of a value, then copies it to the heap.
Well, either you have a Box::new(T::default())
and then mutate it, or you construct it on the stack. The former is not always possible or convenient, and the latter seems to be safe with the constructor I provided.
What tests did you do?
Creating Secret
values in a function and inspecting the stack with psm
and read-process-memory
I linked in the description. I'm thinking on how to make it into an autotest, since it's a little flaky.
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 think your proposed constructor would need a lot of testing on different platforms to determine it accomplishes the desired goals, yes. Can we hold off on it for now?
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 can if we must, but I'll probably still have to use it in my own code since I cannot create a random crypto_bigint::Uint
or k256::Scalar
directly in a Box
.
The rest of this PR is great and the direction I wanted to go with If you can provide a simpler constructor, e.g. one which accepts |
Any comments on the rest of the listed things in the starting post? Or should they be left for other PRs? |
|
Yes, but what if I want a
I was thinking that it would simplify using secret values in generic context, but I see your point.
If we remove everything but |
Okay, your
I think we should get rid of all of the types besides
|
Also,
That's not a problem, we can have a separate |
@fjarri but it's currently the only constructor, leading to a de facto If you had You can then add a second constructor that does your |
Oh yes, I have no problems with having a constructor that takes |
A |
This potentially seems like a better pattern worth supporting, i.e. a constructor which handles the allocation using pub fn init_with(initializer: impl FnOnce(&mut S)) -> Self
where
S: Default |
Removed all the stuff, updated the PR description accordingly. |
Any new on this? |
I'd be happy to merge it if it weren't still marked a draft |
Currently
SecretBox = Secret<Box<S>>
is not particularly convenient to use:Secret<S>
requiresS: Zeroize
, but if one uses a boxed third-party type, it is impossible toimpl Zeroize
for it.Zeroize
is implemented forBox<[T]>
, but not forBox<T>
.This PR redefines
SecretBox
as a struct containingBox<S>
, and impls the same traits for it thatSecret
had.Changes:
SecretBox
.Secret
cannot be created safely, or even used safely by mutating the contents within the secret (the secret lingers on the stack).SecretVec/String/Bytes
may be safe if mutation is prohibited, but that just transfers the burden of creating them safely to the user. I suppose we can add them back with a safety note.DebugSecret
removed,SecretBox
just implsDebug
now.ExposeSecretMut
added. Not sure about that one, maybe we don't need it as long asSecretBox::new_with_mut()
is available.SecretBox
can be created: from an existingBox
, by mutating the contents (new_with_mut()
), from infallible constructor (new_with_ctr
), from fallible constructor (new_with_fallible_ctr
). The names may be changed. The latter method we need for theDeserialize
impl. I added a safety note to the constructor method indicating that the first two methods are preferable when possible.Testing notes:
I spent some time trying to introspect the memory to check that no secret data is left on the stack. I used https://docs.rs/psm and https://docs.rs/read-process-memory to check it. It seems that
Secret::new(X::new())
does leave the contents ofX::new()
on the stack, butSecretBox::new(|| X::new())
(the best I could come up with without resorting tounsafe
) doesn't. It may be affected by the nature ofX::new()
and Rust version/optimization flags though, but I couldn't come up with a better test.It may even be made into an automatic test, if we pick some "safe" memory window to look into without risking a segfault.