Giter Club home page Giter Club logo

crypto-on-the-edge's Introduction

Private Key Management

The private_key_generator library provides the core functionality of generating private keys from a user-provided ID, and the key_manager library provides functionality of handling API requests using these IDs. The IDs can contain an embedded expiration time and a truncated message authentication code (MAC) to determine probable authenticity. The HMAC can be computed with associated data, which can allow for an ID to only be used by a specific client. The associated data is also used for computing the private key to ensure that it is actually specific to the client, and that another client cannot expect an associated key to work if they don't provide the same associated data.

You might be thinking, "What?! IDs for private keys? How can that be secure???" The IDs behave similarly to having an ID for a locally saved private key. In my use case, I publish some public keys and some related IDs once a month, and set them to expire after a year. Before a client makes their first request, they grab an ECDH public key and an ID, as well as an ECDSA public key and ID, and send the IDs in the request. The server uses the ID to regenerate the private keys using an HKDF that was keyed with a securely generated, uniformly pseudorandom key, verifies the client's signature, then responds to the request, encrypting the data with the generated ECDH key and the client's public key, then signs the data with the expected ECDSA key.

The main differences between saving every private key locally and associating them with an ID, versus publishing and providing clients with these IDs are:

  1. We don't have to save every generated private key
  2. The IDs have a variable-length MAC at the end, so if a client sends a bogus Client ID or key ID, we will (most likely) instantly find out and reject the request.

Features

Aside from the ID MACs and associated IDs, this library also provides the following features:

  • HKDF flexibility: you can use any RustCrypto Digest for the HKDF's digest. Note that if you want to use something like Blake2, you have to use the provided SimpleKeyGenerator because Blake2 does not implement EagerHash.
  • ECC flexibility: you can use most ECDH curves so long as they impl CurveArithmetic and JwkParameters, along with the other main required traits for ECDH. The same is true for ECDSA, although this crate currently only provides functionality for ECDSA, rather than the other signature types.
  • Hash flexibility for signatures: you can specify the hash function that will be used for hashing requests and responses when verifying and signing
  • Time-based salts: MACs and HKDF outputs use unique salts for each "version". The salts are computed using the ChaCha RNG, based on how long the VERSION_LIFETIME is and when the original epoch is set to. Note that using ChaCha20Rng over ChaCha8Rng provides little to no security benefits because the outputs of the RNG are not exposed anywhere. They are only used for computing MACs and HKDF outputs
  • Macro for handling encryption and decryption of requests, where the encryption algorithm can be chosen by the client
  • Functions for encrypting and decrypting locally stored data

Limitations

The current limitations that can be changed include:

  • dependency on ECDH and ECDSA
  • dependency on HKDF rather than any other KDF

The current limitations that cannot be changed is primarily the fact that some configurations of the VersioningConfig can break. This is the definition of the versioning config:

pub struct VersioningConfig<
    const EPOCH: u64,
    const VERSION_LIFETIME: u64,
    const VERSION_BITS: u8,
    const TIMESTAMP_BITS: u8,
    const TIMESTAMP_PRECISION_LOSS: u8,
    const MAX_EXPIRATION_TIME: u64,
>;

Every VERSION_LIFETIME seconds, the version increments. There is a lower bound on this set to 10 minutes, but if you set it to 10 minutes, and the VERSION_BITS is set to 3, the code will break after 80 minutes.

The versioning is primarily supposed to handle rekeying the HKDF and MAC periodically. It does not NEED to change every 10 minutes, but if you want it to in a personal project where you know that you will only be alive for less than 80 more years, then you could make the program expire in 80 years.

Security and Compatibility Notice

This library has not received an audit, and it is still a work in progress. If any changes are made in the core functionality of generating keys from IDs, or how the IDs or metadata are represented, it can and will break your code. I will add that I've tested all of this code and it works. There is only one change I might put out that might change things (aside from restructuring or adding functionality), which is supporting up to 57 bits for the length of the version in the private_key_generator. The only thing that this might break is encrypt_resource, where the version is encoded in the encrypted data as 4 bytes, and it would need to be able to encode it as either 4 or 8 bytes to avoid breaking changes.

Requirements

private_key_generator is compatible with no-std, but without std, this library's validate_id functions will not be able to validate the timestamps of IDs. However, if you are able to determine the current time in seconds since UNIX_EPOCH, you can call get_expiration_time() and manually validate the expiration time.

key_manager is dependent on std.

key_manager also depends on RustCrypto's elliptic_curves with the following features enabled:

  • jwk for generating unique private keys with the same ID for different curves
  • ecdh

You might need to ensure that those features are enabled when you use RustCrypto libraries' curves.

License

All crates licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

crypto-on-the-edge's People

Contributors

nstilt1 avatar

Watchers

 avatar

crypto-on-the-edge's Issues

IDs are not inherently safe, depending on the constant parameters

The issue

With the VersioningConfig, a version lifetime can be specified by a user that determines how frequently versions change. The lower bound for this is currently 600 seconds for flexibility, and IDs' constant parameters independently determine how many bits are allotted for the version number.

With the current configuration, someone could make a KeyGenerator that switches versions every 10 minutes, and they can use multiple ID types that have a different amount of bits reserved for the version. If there's one ID that can store a 32-bit version and another ID that can store a 5-bit version... the code will kind of break after about 5.3 hours, and it would be a little challenging to prevent users from doing this without making some changes.

Potential solutions

I think the solution for this is to change where the consts are defined. I want to move const VERSION_BITS: u8 to the KeyGenerator's VersioningConfig, and then add a const USE_VERSIONS: bool in BinaryId. This way, a user could opt-out of having a version encoding in an ID, but if they opt-in, it will always be the same size, and it will be able to be validated at compile time. I could add another const (like const BREAKING_POINT_YEARS: u64) that offers a compile-time validation that the user's chosen parameters will last for a minimum of BREAKING_POINT_YEARS.

I also want to move REQUIRE_EXPIRING_KEYS from the VersioningConfig to a BinaryId. This way, if a user wants to require a specific ID type to expire, but not necessarily every type of key ID, then they will be able to do so.

I kind of want to validate timestamp sizes as well because the timestamps will need to be able to represent at least VERSION_LIFETIME + MAX_KEY_EXPIRATION_TIME seconds.

Maximum version limit is too low

Currently, the maximum version of IDs supported by this library is 63 because it is restricted by the amount of bits in the metadata byte, where 2 bits are already being used by this library.

The VERSION number is primarily useful for being able to change any of the following aspects of the process:

  • hmac_key of the KeyGenerator
  • the hash function used in the KeyGenerator
  • EPOCH of the BinaryId
  • the signature algorithm associated with a Key ID (although this isn't being directly associated by this library)

With cryptographically-relevant quantum computers on the horizon and potential currently unknown vulnerabilities in our arbitrary algorithms, it might be necessary to eventually change at least the signature algorithm, and that could be assisted by having versioned IDs.

There are two problems with using more bits, which may be insignificant:

  1. For every bit that the version numbers use, there are half as many possible values for that ID version.
  2. Using multiple bytes for the metadata (based on the value of VERSION_BITS) might make the documentation slightly more complicated and would add an extra to check regarding the METADATA_OFFSET value.

(1) might not be significant because we sort of gain between 4-5 extra pseudorandom bytes from the timestamp compression, and the user could compensate for this by making the IdLen larger.

(2) is not exactly an issue with the code because it is fixable.

There are at least 2 ways to increase the maximum VERSION_BITS to either 13 or 14.

  1. One way to increase the version would be to check if VERSION_BITS is greater than 6, and if it is, use VERSION_BITS - 6 bits in the next byte for the upper bits. This could allow for up to 14 bits to be used, or up to 16384 versions.
  2. Another way would ensure that minimal bits are used for VERSIONs less than or equal to 2^5 is by using the largest bit in the first metadata byte to determine if it should use extra bits in the next byte for the version, restricting the total amount of bits to 13, or up to 8192 versions

Is it necessary to fix this?

Obfuscate encoded Versions and Timestamps; there are too many 0s in IDs

With the current way that version numbers and timestamps are encoded, there could be a ton of 0s in the ID compared to how many 1s are present since the ID and timestamps are encoded as they are represented in binary.

I have thought about this for a while and have come to the conclusion that it might be best to use an additional Mac instance. This implementation will depend on the utils provided in #7. The procedure will be like so:

MasterKeyGenerator has an additional "static" Mac member (its key never changes).
ID is generated as [Prefix][RandomBytes][empty mac slice]
The first 2 bits of metadata are encoded (the two bools)
Call insert_ints_into_slice(), preserving those two bools, overwriting all remaining space that will be taking up TIMESTAMP_BITS + VERSION_BITS with 0s
Compute the "static" mac
XOR the version with the first 4 bytes of the mac as a u32
XOR the timestamp with the next 8 bytes of the mac as a u64
Call insert_ints_into_slice() to insert these values into the ID

By keeping this MAC's key static, different versions and timestamps will be readable, while this section of the ID will always look different (except for the two bools). The MAC and its key do not need to be super secure, as it is primarily just to have some more random-looking IDs. The only requirement for the MAC is that it must be able to output 12 bytes.

Change initialization methods to only take data for initializing one HKDF

The initialization methods are a little too complex, requiring 1-2 byte slices and an initialized MAC and a mutable rng seed. This seems like it's a bit too much, given that we are working with an HKDF that is capable of generating the seeds/keys.

I believe it should be much simpler to just take 1 or 2 byte slices to initialize a master HKDF, which is then used to derive the internal HKDF, MAC, and RNG.

The only problem with this approach is that there would then be a single point of failure, but it should be okay since the binary and source code are still single points of failure regardless.

KeyGenerator: Use subtractive features for timestamp range and precision

Rust seems to be designed to last a very long time. With the default features, the ID timestamps will only last about 136 years into the future, and there is a feature that decreases the precision of timestamps, and a feature that extends the timestamp period to 34841 years.

I feel like there could be a significant number of users/developers (in about 136 years) who would be caught off guard if the default settings for this library were enabled, and if the EPOCH date wasn't being routinely updated in their backends.

It makes a little sense to make the features subtractive, but I feel like the optimal solution would be to implement a MasterKey as in #1. Or mayhaps there could be a timestamp compression-level parameter rather than a feature, or there could be a Config struct similar to base64 if it feels like there are too many parameters for IDs.

Add a separate MAC member for the KeyGenerator

The performance of ID generation and validation is significantly degraded due to the HKDF being used for the HMAC. When generating a fresh key and ID, it requires at least 2 HKDF extractions, and the HKDF's hash function is likely to be very slow... especially since this library truncates the MAC. So there is no need to have a MAC with 256 bits of security when we truncate it to a few bytes.

Impracticalities when automating versioning

It is impractical to automate versioning, updating EPOCH times, and rotating HMAC keys with the current state of this library. The following challenges are easy to address:

  • changing BinaryId::VERSION and EPOCH from constant parameters to an argument in BinaryId::generate() and every other method that uses it

Example Implementation

This little example would depend on the above change, but it might be a little impractical. It also might be better to use a seeded CSPRNG as the core and use the version number in the RNG's nonce.

pub struct MasterKey<K: CryptoKeyGenerator, const VERSION_LIFESPAN: u64, const MASTER_EPOCH: u64> {
    core: K,
    current_version: u16,
    current_epoch: u64,
    // the current version's key generator, primarily used for generating new IDs
    current_keygenerator: K
}

fn now() -> u64 {
    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
}

impl<...> MasterKey<...> {
    fn get_current_version() -> u16 {
        (now() - MASTER_EPOCH) / VERSION_LIFESPAN
    }
    fn get_version_epoch(version: u16) -> u64 {
        MASTER_EPOCH + VERSION_LIFESPAN * version as u64
    }
    
    pub fn new(hmac_key: &[u8], app_id: &[u8]) -> Self {
        let version = Self::get_current_version();
        let current_epoch = get_version_epoch(version);
        let core: K::new(hmac_key, app_id);
        Self {
            core,
            current_version: version,
            current_epoch,
            // this method does not exist as of now, and it probably won't exist
            current_generator: core.generate_key_generator(version, current_epoch)
        }
    }

    /// using the current generator to make new IDs
    pub fn generate_some_id(&self) -> Id {
        self.current_generator.generate_some_id(...);
    }
    ...
}

While it may be in the realm of being possible, it would add an extra argument when using KeyGenerator to generate IDs directly, and it would increase the amount of computations required to generate a single ID of an arbitrary version, whether it is the current version or a version less than the current version.

It might be a bit inefficient when a given MasterKey is required to generate and validate IDs from different versions automatically because it does not seem simple to store non-current-version KeyGenerators without using alloc, which might limit the number of times the program needs to regenerate a Key Generator in a single session.

Then... let's say 5 versions down, your main signature algorithm or your hash function is found to have a critical vulnerability, and you need to switch either the signature algorithm or the hash function, or both. You would need a way to tell MasterKey that for versions 4 and below, it needs to use X algorithm, and use Y algorithm if the version is greater than 4. Switching signature algorithms would be a little bit easier since those key IDs can expire, but switching hash algorithms in the HMAC/HKDF would be a bit more challenging.

I'm not saying that the current state of this crate is ideal, but I am saying that completely automating crypto-system changes might be infeasible. This really depends upon your own threat model, and maybe some things are just meant to be a little bit more manual.

My question is:
Is there a good way to automate HMAC key rotations using a regular version argument that would make it reasonable to change VERSION from a constant parameter to a non-const?

Securely erasing memory

Currently, there are a few aspects of this crate that can't be easily erased from memory:

  1. temporary values and other stack-based values containing key material when creating private keys may be copied at will by the compiler
  2. hkdf might use some temporary values as well

This library doesn't perform some of the more large operations to be able to sufficiently implement "stack bleaching", such as signing data with private keys. This library merely generates the key, so a library that uses this library might be more suited for stack bleaching.

My question is: is it even worth it to eliminate all temporary variables, or should people try to use stack bleaching until there is a new development with Rust and/or the LLVM that allows for the developer to indicate that the compiler should not make certain stack data persist longer than required?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.