ARM-software / psa-api

Documentation source and development of the PSA Certified API
https://arm-software.github.io/psa-api/
Other
58 stars 28 forks source link

Import and export of non-default key formats #207

Open athoelke opened 3 months ago

athoelke commented 3 months ago

Based on the discuss in #149, here is a stab at defining the tricky part of the API: the key data format specifiers and key data format options.

As per usual, it is flushing out some details that are worth discussing, so this is an early DRAFT, to enable that discussion. See the TODOs in the changes for specific details.

athoelke commented 3 months ago

Still flip-flopping on the best way to handle public key export:

  1. Define a key format option PSA_KEY_FORMAT_OPTION_PUBLIC_KEY which selects the public key when a key-pair is provided. Only compatible with public key formats (or multi-formats such as DEFAULT).

    If so - can we leave this option out if the format is already a public-key format? - i.e. this behaves like a default option for such formats.

    This is the approach in the first iteration of this PR.

  2. Define a parallel psa_export_formatted_public_key() - which acts as a select-public-key-from-key-pair on a provided key. Only compatible with public key formats (or multi-formats such as DEFAULT).

    If so - do we permit a public key format to be specified when calling psa_export_formatted_key() with a key pair - triggering implicit selection of the public key?

    I am starting to lean in this direction...

  3. Ensure that every public key format has an explicit key format specifier, including the default formats.

    There is then no way to generically export the public key from a key pair in the default format, unless we add something like PSA_KEY_FORMAT_DEFAULT_PUBLIC_KEY.

    If there are any other multi-formats that can embed a public key OR a private key, then this approach would need two format identifiers for this format...

athoelke commented 3 months ago

Second draft following discussion with @gilles-peskine-arm:

I still need to:

  1. Describe which options can be used with which key types for the DEFAULT format
  2. Decide if it is worth replacing ONE_ASYMMETRIC_KEY with PKCS8 as the format name (due to familiarity), or perhaps providing PKCS8 as a synonym?
  3. Describe the encrypted/authenticated variants of OneAsymmetricKey, and how they work in the new API functions.

And then it will be:

  1. Add formatted import and export functions.
  2. Add Key wrap/unwrap functions
  3. Add Key wrap algorithms
athoelke commented 3 months ago

Now with a first pass at fully defining psa_import_formatted_key().

This was a mostly cut and paste of the psa_import_key() documentation, but with some non-trivial differences:

athoelke commented 3 months ago

I've added a first attempt at fully defining psa_export_formatted_key() and psa_export_formatted_public_key().

This also updates the description of the non-formatted existing API, acknowledging the existence of non-default formats.

athoelke commented 2 months ago

Further thoughts since my return from vacation...

I think the best way to handle key policy information that may or may not be present in the formatted key data, is to provide an additional parameter to psa_import_formatted_key() that controls how the policies are combined. I think we need the following options:

  1. Combine the imported and key attribute policies (logical intersection of capabilities).
  2. Use the imported policy, if present, otherwise use the policy in key attributes.
  3. Use the imported policy - i.e. ignore policy in key attributes.

Would there be a good case for "4. Use the key attribute policy, ignoring any imported policy"? This seems risky as it is discounting any constraints that are present in the imported key?

(1) is the theoretically ideal option (as per psa_copy_key()), except that in many formats the usage constraints are optional, and the algorithm identifier may be inferred/deduced from key type rather than explicitly encoded. (2) helps deal with optional policy components - as it treats a missing imported policy as 'unknown' rather than 'nothing is allowed'. (3) is useful in scenarios where the application knows that the imported key has a fully specified policy, and so does not need to set up the key attributes.

This approach does not handle the usage flags that are related to permitted key management operations: PSA_KEY_USAGE_EXPORT, PSA_KEY_USAGE_COPY, and PSA_KEY_USAGE_CACHE. These capabilities are not encoded in export formats, and so I propose that these flags should always be taken from the key attributes.

I am also not sure if we would want to enable independent option 1/2/3 approaches for the permitted algorithm and the usage flags elements of the policy.

athoelke commented 2 months ago

I can envisage an API that parsed the key data to be able to report the attributes of the key that is encoded in the data, but without creating a key object. The application can then use or amend the reported key attributes before actually importing the key. If the application does not need this flexibility, it can just import the key.

I think this is my preferred approach for handling the use cases described by @gilles-peskine-arm:

I'm loading a key which may be of several types, with a format that doesn't contain (enough) policy information, the desired algorithm policy will depend on the type.

This API might look something like:

psa_status_t psa_inspect_formatted_key(psa_key_format_t format,
                                       const uint8_t * data,
                                       size_t data_length,
                                       psa_key_attributes_t * attributes);
psa_status_t psa_import_formatted_key(const psa_key_attributes_t * attributes,
                                      psa_key_format_t format,
                                      psa_key_import_options_t options,
                                      const uint8_t * data,
                                      size_t data_length,
                                      psa_key_id_t * key);

Alternative names for psa_inspect_formatted_key() might be psa_parse_formatted_key(), psa_query_formatted_key(), psa_formatted_key_info(), or psa_formatted_key_attributes()? Although, the latter seems too close in wording to psa_get_key_attributes(), but with very different behavior.

The application can call psa_inspect_formatted_key() to review the key type and available policy information, then update the policy as required before calling psa_import_formatted_key() to create the key object with specified policy.

I suspect we would want the reported attributes distinguish between an "unknown policy" (algorithm and/or usage flags not provided in import data - in effect this is probably 'anything permitted'), and a "nothing permitted policy"?

[An alternative could be to provide no standard API for such a use case, and have an implementation define their own custom key-import-policy option that carries out the specific behavior for every such use case. This is problematic if there are standard protocols that would depend on this use cases, or many different scenarios (with different specific key types and algorithms) in which this is needed.]

gilles-peskine-arm commented 2 months ago

1. Combine the imported and key attribute policies (logical intersection of capabilities). 2. Use the imported policy, if present, otherwise use the policy in key attributes.

(1) can be achieved by doing (2) followed by psa_copy_key, so providing it is redundant. But it is a common case, so providing it makes the API easier and more efficient.

The problem with “key attribute policies” is that they require a more complex policy language. It is common to want a “policy policy” like “set the algorithm to RSA-PSS if the key is RSA, to ECDSA if the key is ECC-Weierstrass, and to EdDSA if the key is ECC-Edwards”.

3. Use the imported policy - i.e. ignore policy in key attributes.

This fails in the common case of formats that lack policy information, or only have partial policy information.

Would there be a good case for "4. Use the key attribute policy, ignoring any imported policy"? This seems risky as it is discounting any constraints that are present in the imported key?

I don't know. Key formats don't always fully match what one needs to do with the key. Though if it's rare enough, there's always the possibility of doing an import with the EXPORT usage flag and then a manual copy.


psa_inspect_formatted_key

I'm not very keen on parsing the data twice, and applying credentials twice, but I don't have a better idea.

This is problematic if there are standard protocols that would depend on this use cases, or many different scenarios (with different specific key types and algorithms) in which this is needed.

There are indeed many scenarios where a protocol can work with either RSA/FFDH or ECDSA/ECDH, and will soon be extended with PQC alternatives. A library for this protocol should provide a “load key” function that decides which algorithm to use for each key type (e.g. whether to use PKCS#1v1.5 or PSS for RSA). For agility and performance, this should not require too much complexity in the library. In particular I want to avoid the simplistic algorithm “try parsing as RSA, if it fails try parsing as ECC/ECDSA, if it fails try parsing as ECC/EdDSA, …”.

athoelke commented 2 months ago

There are indeed many scenarios where a protocol can work with either RSA/FFDH or ECDSA/ECDH, and will soon be extended with PQC alternatives. A library for this protocol should provide a “load key” function that decides which algorithm to use for each key type (e.g. whether to use PKCS#1v1.5 or PSS for RSA). For agility and performance, this should not require too much complexity in the library. In particular I want to avoid the simplistic algorithm “try parsing as RSA, if it fails try parsing as ECC/ECDSA, if it fails try parsing as ECC/EdDSA, …”.

The desire for agility - being able to migrate an application to supporting an alternative, or an additional, key/signature type without code changes - is an argument against a simple two-function inspect/import approach, as this requires conditional logic in the application code. And perhaps using a data-driven approach to these use cases might be better, without defining a complex policy-configuration-code scheme. Can we articulate the forms of policy logic that are required, in order to define a manageable data structure to express and encode what is wanted?

For example, we could define a key policy configuration as an array of (key-type, permitted-algorithm, usage-flags) tuples. When decoding the key data, the determined key type is matched to a specific policy, which is then used to construct the key object. Would this be sufficient? - or might there be a need to select on key size as well, e.g. in case this would affect the choice of hash function to parameterize a signature operation.

athoelke commented 2 months ago
  1. Combine the imported and key attribute policies (logical intersection of capabilities).
  2. Use the imported policy, if present, otherwise use the policy in key attributes.

(1) can be achieved by doing (2) followed by psa_copy_key, so providing it is redundant. But it is a common case, so providing it makes the API easier and more efficient.

The problem with “key attribute policies” is that they require a more complex policy language. It is common to want a “policy policy” like “set the algorithm to RSA-PSS if the key is RSA, to ECDSA if the key is ECC-Weierstrass, and to EdDSA if the key is ECC-Edwards”.

  1. Use the imported policy - i.e. ignore policy in key attributes.

This fails in the common case of formats that lack policy information, or only have partial policy information.

Would there be a good case for "4. Use the key attribute policy, ignoring any imported policy"? This seems risky as it is discounting any constraints that are present in the imported key?

I don't know. Key formats don't always fully match what one needs to do with the key. Though if it's rare enough, there's always the possibility of doing an import with the EXPORT usage flag and then a manual copy.

Let's make a start by encoding all four options and see how this looks. I suggest separating the options relating to permitted-algorithm from the cryptographic-usage flags (as some formats always have one, but may or may not include the other), but not providing an option related to the key-store management flags (copy/export/cache):

#define PSA_IMPORT_OPTION_ALG_COMBINE
#define PSA_IMPORT_OPTION_ALG_DELEGATE
#define PSA_IMPORT_OPTION_ALG_USE
#define PSA_IMPORT_OPTION_ALG_OVERRIDE

#define PSA_IMPORT_OPTION_USAGE_COMBINE
#define PSA_IMPORT_OPTION_USAGE_DELEGATE
#define PSA_IMPORT_OPTION_USAGE_USE
#define PSA_IMPORT_OPTION_USAGE_OVERRIDE

Questions

  1. I realise that USE is the same as DELEGATE without setting the policy in the key attributes (which is the default value). Perhaps we can drop the third one and pick the best name for the result?
  2. Would option names like PSA_IMPORT_POLICY_XXX_YYY etc be better than PSA_IMPORT_OPTION_XXX_YYY? - might we want non-policy import options in future?
athoelke commented 2 months ago

[Edited: initially submitted before complete]

The desire for agility - being able to migrate an application to supporting an alternative, or an additional, key/signature type without code changes - is an argument against a simple two-function inspect/import approach, as this requires conditional logic in the application code. And perhaps using a data-driven approach to these use cases might be better, without defining a complex policy-configuration-code scheme. Can we articulate the forms of policy logic that are required, in order to define a manageable data structure to express and encode what is wanted?

For example, we could define a key policy configuration as an array of (key-type, permitted-algorithm, usage-flags) tuples. When decoding the key data, the determined key type is matched to a specific policy, which is then used to construct the key object. Would this be sufficient? - or might there be a need to select on key size as well, e.g. in case this would affect the choice of hash function to parameterize a signature operation.

It seems that this could be a workable approach, though discussion on the design of the API is needed. For other data structures in the API, the implementation is free to design the detailed layout to match its requirements - for example, psa_key_attributes_t could be very simple for an implementation that has very limited key support. For the kind of policy-policy or policy config/selector we imagine here, we need a variable amount of tuples, depending on the application requirements. In addition, it might be valuable if the result could be a read-only data structure that is laid down by the compiler, rather than an object constructed at runtime.

Some possible approaches:

  1. Define an opaque structure for each type/policy tuple, a macro to initialize it, and perhaps some setter functions for good measure. The application declares an array of these, either with suitable initializers, or a sequence of setters:

    typedef /* imp-def */ psa_import_policy_config_t;
    #define PSA_IMPORT_POLICY_CONFIG(type, alg, usage) /* imp-def */
    
    const psa_import_policy_config_t policy[2] = {
        PSA_IMPORT_POLICY_CONFIG(
            PSA_KEY_TYPE_RSA_PUBLIC_KEY,
            PSA_ALG_RSA_PSS(PSA_ALG_ANY_HASH),
            PSA_KEY_USAGE_VERIFY),
        PSA_IMPORT_POLICY_CONFIG(
            PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1),
            PSA_ALG_ECDSA(PSA_ALG_ANY_HASH),
            PSA_KEY_USAGE_VERIFY)
        }

    When importing a key, the policy[] array is provided, along with the number of elements in the array, to psa_import_formatted_key().

  2. Define an opaque data structure for the entire policy configuration, with a limit on the number of config entries it can contain. Define a suite of macros to initialize this structure with different numbers of entries, and perhaps some setter functions too:

    typedef /* imp-def */ psa_import_policy_config_t;
    #define PSA_IMPORT_POLICY_CONFIG_1(type1, alg1, usage1) /* imp-def */
    #define PSA_IMPORT_POLICY_CONFIG_2(type1, alg1, usage1, \
                                       type2, alg2, usage2) /* imp-def */
    #define PSA_IMPORT_POLICY_CONFIG_3(type1, alg1, usage1, \
                                       type2, alg2, usage2, \
                                       type3, alg3, usage3) /* imp-def */
    #define PSA_IMPORT_POLICY_CONFIG_4(type1, alg1, usage1, \
                                       type2, alg2, usage2, \
                                       type3, alg3, usage3, \
                                       type4, alg4, usage4) /* imp-def */
    
    const psa_import_policy_config_t policy =
        PSA_IMPORT_POLICY_CONFIG_2(
            PSA_KEY_TYPE_RSA_PUBLIC_KEY,
            PSA_ALG_RSA_PSS(PSA_ALG_ANY_HASH),
            PSA_KEY_USAGE_VERIFY,
            PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1),
            PSA_ALG_ECDSA(PSA_ALG_ANY_HASH),
            PSA_KEY_USAGE_VERIFY);
    
    void psa_import_policy_set_config(psa_import_policy_config_t* config,
                                      size_t index,
                                      psa_key_type_t type,
                                      psa_algorithm_id_t alg,
                                      psa_key_usage_t usage);
    
    psa_import_policy_config_t policy2 = PSA_IMPORT_POLICY_CONFIG_INIT;
    psa_import_policy_set_config(&policy2, 0,
                                 PSA_KEY_TYPE_RSA_PUBLIC_KEY,
                                 PSA_ALG_RSA_PSS(PSA_ALG_ANY_HASH),
                                 PSA_KEY_USAGE_VERIFY);
    psa_import_policy_set_config(&policy2, 1,
                                 PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1),
                                 PSA_ALG_ECDSA(PSA_ALG_ANY_HASH),
                                 PSA_KEY_USAGE_VERIFY);

    When importing a key, the policy or policy2 object is provided, which embeds the number of entries.

  3. To enable a unlimited entry count, but without the application declaring an array, We could define a macro to fully declare the policy configuration (including the type), or build the full config as a linked list of entries. Both of these are quite different to any of the existing API but I can explore what it might look like if there is interest?