-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
added secret types rfc #2859
added secret types rfc #2859
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
- Feature Name: Secret Types | ||
- Start Date: 2020-01-23 | ||
- RFC PR: | ||
- Rust Issue: | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
The goal is to provide primitive data types that can be used to traffic in transient secrets in code that are important to not accidentally leak. These new primitives would be used in conjunction with an in-progress LLVM RFC to ensure important security invariants like secret-independent runtime are maintained throughout the compilation process. Note that we explicitly do not want secret_isize and secret_usize, because we do not want to index based on secrets. | ||
|
||
- secret_i8 | ||
- secret_i16 | ||
- secret_i32 | ||
- secret_i64 | ||
- secret_i128 | ||
- secret_u8 | ||
- secret_u16 | ||
- secret_u32 | ||
- secret_u64 | ||
- secret_u128 | ||
- secret_bool | ||
|
||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
Applications deal with sensitive data all the time to varying degrees of success. Sensitive data is not limited to information like cryptographic codes and keys, but could also be data like passwords or PII such as social security numbers. Accidental secret leakage is a challenge for both programmers, who might mix secret and public data inadvertently, and compilers, which might use optimizations that reveal secrets via side-channels. | ||
|
||
Writing cryptographic and other security critical code in high-level languages like Rust is attractive for numerous reasons. High level languages are generally more readable and accessible to developers and reviewers, leading to higher quality, more secure code. It also allows the integration of cryptographic code with the rest of an application without requiring the use of any FFI. We are also typically motivated to have a reference implementation for algorithms that is portable to architectures that may not be supported by highly optimized assembly implementations. However, writing data invariant code in high level languages is difficult due to compiler optimizations. For this reason, having compiler support for a data type that is resistant to timing side channel attacks is desirable. | ||
|
||
Timing side channel attacks are a particular threat in a post spectre world [1]. Side channels are primarily used to attack secrets that are | ||
long lived | ||
- Extremely valuable if compromised | ||
- Each bit compromised provides incremental value | ||
- Confidentiality of compromise is desirable | ||
Therefore, it’s important to use data-invariant programming for secrets. For this reason, secret types would only allow data invariant operations at all levels of compilation. | ||
|
||
Additionally, secret types serve as an indicator to programmers that this information should be treated with care, and not mixed with non-secret data, an invariant that would be enforced by the type system. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just some more detail on this: it's important not to mix secrets and untrusted input in a context such as compressed content. Otherwise CRIME-like (https://en.wikipedia.org/wiki/CRIME) attacks can be used to retrieve the secret information. |
||
|
||
[1] *Cryptographic Software in a Post-Spectre World.* Chandler Carruth. RWC 2020. Recording forthcoming. | ||
|
||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
Secret integer types are a type of restricted integer primitive. In particular, non-constant time operations like division are prohibited. Printing of secret integers directly is also prohibited--they must first be declassified into non-secret integers. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are these primitive types as opposed to e.g. exposed intrinsics on normal integer types for which wrappers can be built around? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using wrapper type means you can't write There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, If the RFC desires to insist on primitives rather than library types, I would expect a very extensive section documenting why that would be necessary; as it stands, adding a new primitive has a much higher bar than adding a new library type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think that would be completely acceptable, documenting the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this interact with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this work with existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No, I would assume secret types behave more like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, that is understandable, so it means it is explicitly leaving optimization on the table for security purposes. Maybe we can add this to the drawbacks such that we mention that we explicitly prevent some optimization for security purposes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't call that a "drawback" so much as the actual goal (a.k.a. "that's not a bug, that's a feature") When integrated with hypothetical secret-type-aware LLVM or Cranelift (etc) backend, the purpose of using secret types is to ensure they are, at all layers of processing, optimization, and codegen, explicitly opted-out of anything which would introduce any form of data-dependent timing variability. Anything that special cased and branched on a When writing constant time code, if you really wanted the equivalent of an |
||
|
||
These integers are intended to form a basis for the creation of other secret types, which can be built on top of them, such as secret strings for storing passwords in them. | ||
|
||
Comparison of secret integer types is allowed, but the algorithm is independent of the secrets. Comparison of secret types must return a secret_bool, which cannot be branched on. | ||
|
||
Example: comparison | ||
``` | ||
let x : secret_i8 = 6; | ||
Let y : secret_i8 = 10; | ||
if (x < y) { // compiler error: cannot branch on secret bool | ||
... | ||
} | ||
``` | ||
Example: declassification | ||
``` | ||
let x : secret_i8 = 6; | ||
let y : secret_i8 = 10; | ||
println!((x ^ y).declassify()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explicitly copy the secret type to a public type. As an example, think of a ciphertext, which depends on a secret key and a secret message and is thus secret, being declassified before being sent over a channel. |
||
``` | ||
|
||
Since indexing vectors and arrays is only possible using `usize,` you will be prevented from indexing into a vector or array with a secret integer. Error messages will derive naturally from the type system. | ||
|
||
|
||
Secret types will likely be a more advanced topic when teaching Rust. | ||
|
||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please explain the rationale for including these specific operations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was mentioned in the guide-level explanation:
It might still be useful to re-iterate here that these are the specific operations that we know are possible to implement in constant-time. |
||
For each fixed-size integer type: | ||
Implement the following methods: | ||
- From_be | ||
- From_le | ||
- From_be_bytes | ||
- From_le_bytes | ||
- From_ne_bytes | ||
- Is_positive | ||
- Is_negative | ||
- Leading_zeros | ||
- Min_value | ||
- Max_value | ||
- Overflowing_add | ||
- Overflowing_sub | ||
- Overflowing_mul | ||
- Overflowing_neg | ||
- Overflowing_shl | ||
- Overflowiing_shr | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. *overflowing |
||
- Overflowing_pow | ||
- Pow | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is never constant time I believe. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't seem to understand why this cannot be constant time. Can you elaborate? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is implemented using a loop and if's: https://doc.rust-lang.org/stable/src/core/num/mod.rs.html#3667 pub fn pow(self, mut exp: u32) -> Self {
let mut base = self;
let mut acc = 1;
while exp > 1 {
if (exp & 1) == 1 {
acc = acc * base;
}
exp /= 2;
base = base * base;
}
// Deal with the final bit of the exponent separately, since
// squaring the base afterwards is not necessary and may cause a
// needless overflow.
if exp == 1 {
acc = acc * base;
}
acc
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I see what you mean. I misread this as "cannot be implemented in constant-time". In any case, we could There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need more code for constant time, like https://github.com/dalek-cryptography/curve25519-dalek/blob/master/src/backend/serial/u64/field.rs#L444 but maybe anyone using secret data already does this themselves. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would allow |
||
- Reverse_bits | ||
- Rotate_left | ||
- Rotate_right | ||
- Saturating_add | ||
- Saturating_sub | ||
- Saturating_neg | ||
- Saturating_mul | ||
- Saturating_pow | ||
- Signum | ||
- Swap_bytes | ||
- To_be | ||
- To_le | ||
- To_be_bytes | ||
- To_le_bytes | ||
- To_ne_bytes | ||
- Trailing_zeros | ||
- Wrapping_add | ||
- Wrapping_sub | ||
- Wrapping_mul | ||
- Wrapping_neg | ||
- Wrapping_shl | ||
- Wrapping_shr | ||
- Wrapping_pow | ||
|
||
|
||
Implement the following traits | ||
Secret integers may only be combined with other secret integers and the result will be a secret type | ||
- Add | ||
- AddAssign | ||
- BitAnd | ||
- BitAndAssign | ||
- BitOr | ||
- BitOrAssign | ||
- BitXor | ||
- BitXorAssign | ||
- Clone | ||
- Copy | ||
- Default | ||
- Drop | ||
- Mul | ||
- MulAssign | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is often not constant time on the hardware level, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Add and Mul I don't know enough to say in all cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should have said that I was talking about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mul, and thus MulAssign, are not constant-time on ARM Cortex or Power/PowerPC. Many implementations use smaller multipliers in HW and then stitch the partial products together over multiple clock cycles. Most such implementations allow some of the clock cycles to be skipped for certain data. The most common is early termination if either input is zero. The next most common early termination case is if the upper half of either input is zero. The ARM Cortex family multipliers have an even more complex scheme; it appears to be undocumented, but testing indicates that the Cortex multipliers early terminate whenever the upper half of either input has a Hamming weight less than 2 or more than 14 (for 32x32 multiply). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Many architectures have a constant-time means of converting a status bit (usually carry) to an all-zero or all-one mask. On those architectures conditional if/then/else logic can be implemented by xor/masked-and/xor selection to run in constant time. The LLVM IR has the needed primitive, but LLVM usually realizes that primitive as a varying-execution-time conditional branch. That conversion would need to be suppressed for conditional expressions involving these secret types. Among other things, that should enable constant-time for Saturating_add and Saturating_sub. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The constant-time behaviour of multiplication can indeed be a problem on some CPUs, although it's very often safe. Much more information is at https://www.bearssl.org/ctmul.html However, multiplication is extremely useful! Omitting it at one layer likely means that people will simulate it one layer up. (I.e. people will implement multiplication in Rust code using addition etc.) LLVM knows the CPU target, can know the behaviour of mul on that target, and is likely best placed to substitute a constant-time replacement when needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if you're using a CPU which e.g. short-circuits on multiply-by-zero or multiply-by-one, it's basically broken for cryptographic purposes (e.g. the aforementioned PPC32) I have no expectation of Rust or LLVM to try to autodetect these CPUs or apply some sort of countermeasure, but would rather explicitly call out CPU-level assumptions about how e.g. multiplication must work, hopefully as a standard set of boilerplate which can be copied-and-pasted into Rust libraries which rely on those set of assumptions. |
||
- Neg | ||
- Not | ||
- Shl | ||
- ShlAssign | ||
- Shr | ||
- ShrAssign | ||
- Sub | ||
- SubAssign | ||
- Sub | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think these should only be implemented when the size is at most the native pointer size to eliminate most non constant time implementations. |
||
|
||
|
||
For secret_bool, implement the following methods: | ||
- n/a | ||
And the following traits | ||
- BitAnd | ||
- BitAndAssign | ||
- BitOr | ||
- BitOrAssign | ||
- BitXor | ||
- BitXorAssign | ||
- Clone | ||
- Copy | ||
- Default | ||
- Not | ||
|
||
We will also need to define a trait Classify<T> for T and method declassify. Classify will take a non secret integer or boolean and return a secret integer or boolean. `declassify` will consume a secret integer returning a non-secret integer. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would make for a more readable proposal if you specified the trait definitions, and the other operations in ```rust blocks. |
||
|
||
We will need to define new comparison traits that run in constant time and return a secret_bool: | ||
- SecretEq | ||
- SecretOrd | ||
For reference, see the subtle crate [2] . | ||
|
||
[2] https://docs.rs/subtle/2.2.2/subtle/ | ||
|
||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
Because secret integers prohibit the use of certain operations and compiler optimizations, there will be a performance impact on code that uses them. However, this is intentional. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is vague. Please be as specific as possible regarding what optimizations or categories of optimizations would be prohibited, at what granularity they would be prohibited, and what the impacts of those prohibitions would be. Please also, without referring to LLVM, specify how these prohibitions are justified in terms of a specification. If guarantees (with stability promises and all that, as opposed to best effort constructs) are sought after, such a specification should be operational (in the sense of operational semantics and abstract machines). At the moment, I do not see how this can be specified in terms of an abstract machine / interpreter like Miri. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I imagine one could re-use some ideas from this paper and more generally from the IFC (information flow control) literature. The guarantee should probably be some form of non-interference -- program executions that only differ in secrets must not be observably different, or so. One hard problem is defining what exactly the possible observations are; that can and should (IMO) be done in an operational way (a "trace of observations" that e.g. Miri could print, or so). A type-based approach certainly sounds like a good start for me; this feels much better than ad-hoc attributes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the paper reference; that's a good start. Rather than just enumerate possible observations, we can also enumerate observation modalities that are out of scope. My list of the latter includes
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you made a very good point for why we need to precisely list what we do consider an observation -- everything not on that list (and that will be all sorts of weird stuff, like your two items) is not protected. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can generally put all microarchitectural covert channels out-of-scope, with the possible caveat that writing constant-time code is the best known defense against them. That said I think compiling a list of more general architecture properties is a good idea. An example of one is: the underlying architecture is assumed to be able to perform integer multiplication in constant time (e.g. doesn't short-circuit on multiply-by-0-or-1) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't think such a strong assumption is required. Once the information that inputs to a multiplication are secret is forwarded to the compiler, it's the compiler's job to generate constant-time code. If there is a constant-time multiplier in the target micro-architecture, that's easy. If not, what the compiler will need are constant-time runtime libraries to use for arithmetic on secret integers. The critical job for the language to take care of is to forward the information about secrecy in the first place. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Even across the same microarchitecture there are CPUs that do-or-do-not perform multiply in constant time, e.g. the PowerPC 7xx CPUs used by Apple perform constant time multiplication, but many other PPC32 CPUs do not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that there is a difference between different microarchitectures (i.e., implementations of some architecture) with only some having variable-time arithmetic instructions. The situation is even worse if you take future implementations of some architecture into account, that suddenly introduce new variable-time behavior. Compilers will need a new hardware-software contract to know what subset of instructions is safe to use (see https://ts.data61.csiro.au/publications/csiro_full_text/Ge_YH_18.pdf). However, to me this all seems like lower-level issues: what can be done on language-level is to ensure that some operations are avoided on secret data that a compiler cannot possibly protect (branching, addressing) and for all other operations leave it to the compiler and hardware to guarantee constant-time behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice find! Here's a more recent one: https://arxiv.org/abs/1901.08338. One of the authors of that paper also contributed to this recent attack, which shows that Intel's attempt at ad-hoc patching is useless: https://cacheoutattack.com/ RIDL is another good attack, well written paper: https://mdsattacks.com/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Co-author of said paper here. Cryptographic constant-time security is indeed usually stated as a non-interference property. This works by taking the operational semantics of the considered language, and instrumenting it by adding "leakages" or "observations" to step taken by instructions that can be compiled to some sort of jump or memory accesses. A program is then said to be secure if for two executions that differ only on secret inputs, then the leakages are equal. As for impacted optimizations, surprisingly, the only thing impacted during our work on CompCert was the generation of builtin code that was not branchless, so it might be safe to consider those optimizations implemented in CompCert to not be impacted for llvm too: tailcall, inlining, CSE, constant propagation, deadcode elimination. That remains a lot less than the optimizations implemented in llvm though, I assume... |
||
|
||
|
||
# Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives | ||
|
||
The main alternative to this design is to handle this via crates such as secret_integers [3]. However, without compiler integration, the compiler could optimize out the guarantees that have been claimed at the source level. For example, the compiler could use non constant time instructions such as divide. Using these primitives correctly from a high-level language will typically require careful management of memory allocation, flagging relevant memory as containing secret for the operating system, hardware, or other system level components. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could not find what guarantees were claimed at the source level in this RFC. |
||
Other alternatives would require developers to use handwritten assembly to ensure that only constant time operations are used. | ||
|
||
[3] https://docs.rs/secret_integers/0.1.5/secret_integers/ | ||
|
||
|
||
# Prior art | ||
[prior-art]: #prior-art | ||
- https://docs.rs/secret_integers/0.1.5/secret_integers/ | ||
- https://docs.rs/subtle/2.2.2/subtle/ | ||
|
||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
Out of scope: Memory zeroing (beyond implementing the drop trait) and register spilling | ||
|
||
There is an in progress RFC for LLVM that will be required for the secret type guarantees to be fully guaranteed throughout the compilation process. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about Cranelift? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If Cranelift were to add support for this, the earlier the better. It's going to be a monumental effort for LLVM, not because the work is complicated - it's simple and mundane, but there is going to be a lot of it because it touches every layer of code generation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also note that in as much as Cranelift is intended to be a backend for a WASM runtime, there are parallel efforts to add similar features to WASM, like CT-WASM: https://github.com/PLSysSec/ct-wasm Ideally if this does end up finding its way into WASM proper, Cranelift could use a common implementation for both Rust secret integers and WASM secret integers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great. This should be elaborated upon in the text. And specifically, I think guarantees are sought (which I am skeptical of, see above), I think the same guarantees must be given in Cranelift as well before this can become stable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc https://github.com/bytecodealliance/cranelift/issues/1327 (not exactly the same, but for the same purpose) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bjorn3 aah nice! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think for this a special type may be better though to also prevent any branches, non constant time multiplication and other ways of leaking it. |
||
|
||
# Future possibilities | ||
[future-possibilities]: #future-possibilities | ||
|
||
This RFC describes the basic primitive types required for secret types in Rust. The intention is for crates to build additional secret types---for example a SecretString type---on top of these primitives. | ||
|
||
Register spilling is a problem, and may require additional work to store secret values separately from non-secret values. One of the easy things to do with spectre is to create a timing side channel for stale data on the stack, meaning that it’s important to zero sensitive data from the stack. Reliably zeroing sensitive data is a difficult problem. We leave memory zeroing as future work. | ||
|
||
Future possibilities also include creating compiler errors that specify why public and secret primitives cannot be mixed instead of returning a simple type error. | ||
|
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 LLVM RFC should be linked