points by grantjpowell 3 years ago

Great callout, I haven't had my coffee yet. Here is a version that better shows what I intended

    pub struct Secret<T>(T);

    impl<T> Secret<T> {
        pub fn map<U>(&self, func: impl FnOnce(&T) -> U) -> Secret<U> {
            Secret(func(&self.0))
        }
    }
    
    impl<T: AsRef<[u8]>> PartialEq<&[u8]> for Secret<T> {
        fn eq(&self, other: &&[u8]) -> bool {
            constant_time_eq(self.0.as_ref(), other)
        }
    }

    /* Some other file */

    use secret::Secret;
    
    // Translated from the example
    fn check_mac<T: AsRef<[u8]>>(mac_secret: Secret<T>, message: &[u8], mac: &[u8]) -> bool {
        // This returns a new Secret<[u8; 32]>
        let computed_mac = mac_secret.map(|secret| hmac_sha_256(secret.as_ref(), message));

        // This uses the `constant_time_eq` impl from above
        computed_mac == mac
    }


I think the interesting part of the example is what you _can't_ do in the other file. It's pretty hard to misuse because the return type of `Secret::map` is a new `Secret`, the only way to do `==` on a `Secret<T>` uses a constant time compare.

I guess my main point is that when you have a instead of having to add new things at the language _level_, if I have something as powerful as the rust type system I can implement the same functionality in not much of code.

mumblemumble 3 years ago

That covers the one case in the example, but the language goes even further than that in ensuring constant-time processing of secrets, including ensuring speculative execution in the CPU won't expose the data to timing attacks.

I don't know enough about the subject to really evaluate this in detail, but I am more than willing to at least entertain the notion that the problem space is thorny enough that a language-level solution really can do some things that can't be as effectively accomplished with a library solution. Even in a language with a strong compiler like Rust.

Rune also has an interesting approach to pointer safety that's significantly different from Rust's: https://github.com/google/rune/blob/main/doc/index.md#runes-...

yccs27 3 years ago

It all gets more complicated when you want to pass more than one secret parameter, or the function already returns a Secret - now you need a monad. The key feature seems to be that the code does not need 'map' or anything, the secrecy flag is propagated regardless.

  • grantjpowell 3 years ago

    > now you need a Monad

    "need a Monad" sounds scary but in practice it looks like this

        impl<T> Secret<T> {
            pub fn map<U>(&self, func: impl FnOnce(&T) -> U) -> Secret<U> {
                Secret(func(&self.0))
            }
    
            pub fn flat_map<U>(&self, func: impl FnOnce(&T) -> Secret<U>) -> Secret<U> {
                func(&self.0)
            }
        }
    

    If you need an escape hatch for something more complicated, you could provide an api to that

        impl<T> Secret<T> {
            pub unsafe fn reveal(&self) -> &T {
                &self.0
            }
        }
    • yccs27 3 years ago

      My point, which I didn’t express well there, was about the call side: How do you call

          func(param1: A, param2: B) -> C
      

      with both parameters being secrets (secret1: Secret<A>, secret2: Secret<B>)? In Rust, it‘s

          flat_map(secret1, |param1|
            map(secret2, |param2|
              func(param1, param2)
            )
          )
      

      or with do_notation at least a bit cleaner

          do! {
            param1 <- secret1;
            param2 <- secret2;
            Secret(func(param1, param2));
          }
      

      whereas Rune manages it with

          func(secret1, secret2)