Skip to content
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

Import and export of non-default key formats #207

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

athoelke
Copy link
Contributor

@athoelke athoelke commented Aug 5, 2024

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 athoelke added enhancement New feature or request Crypto API Issue or PR related to the Cryptography API DO NOT MERGE labels Aug 5, 2024
@athoelke athoelke added this to the Crypto API 1.3 milestone Aug 5, 2024
@athoelke
Copy link
Contributor Author

athoelke commented Aug 5, 2024

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 athoelke changed the title First attempt to define key formats and options Second attempt to define key formats and options Aug 6, 2024
@athoelke
Copy link
Contributor Author

athoelke commented Aug 6, 2024

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

  • Decided to do public key export via a dedicated function
  • Removed ECPoint as a format
  • Required public keys in key pair formats
  • Added options for domain parameters
  • Individual boolean options override defaults instead of providing both option elements
  • Added references for everything
  • Defined new terms for ASN.1, DER and PEM

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 athoelke changed the title Second attempt to define key formats and options Import and export of non-default key formats Aug 7, 2024
@athoelke
Copy link
Contributor Author

athoelke commented Aug 7, 2024

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:

  • Key type can be determined by the format and key data (true for all non-default formats that are defined at the moment), but not for DEFAULT format. So this complicates the wording around the required value(s) for the attribute key type
  • Permitted-algorithm policy is provided in a number of X.509 key formats. When this does not align with the Crypto API approach, what should be done?
  • Some key formats can optionally include a usage policy. How should we integrate/combine such a policy with an application supplied policy? - can an application fully delegate this to the imported key data?


The following attributes must be set for keys used in cryptographic operations:

* The key permitted-algorithm policy, see :secref:`permitted-algorithms`.
Copy link
Contributor

@gilles-peskine-arm gilles-peskine-arm Aug 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker in the API design: when 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. A typical example: my application can sign with either RSA-PKCS#1v1.5 or ECDSA. I want to set the algorithm policy to PSA_ALG_RSA_PKCS1V15_SIGN_ANY if the key turns out to be an RSA key, and to PSA_ALG_ECDSA_ANY if it turns out to be an ECC key.

In the Mbed TLS 3.x equivalent to this interface (pk API), where we were extending the legacy interface to bridge it with PSA, I resolved this by making it a three-step process:

  1. Parse the key. This creates a legacy object which has type information, but no binding policy information.
  2. Construct attributes, including a policy, based on the key type. There's a helper function that handles the common cases, but the application can tweak the result or just make up its own attributes.
  3. Import the key into PSA, with the policy and other attributes chosen at the previous step.

This only works because there's an intermediate stage with a key that is loaded and has type information but does not yet have a policy set. I'd rather avoid having to have this intermediate stage in the PSA API.

I'm currently working on the evolution of Mbed TLS pk API for the next major version, which will be a transition API (keeping old function names, but wrapping around PSA keys). I don't have a satisfactory solution yet in that context either.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The obvious way is to encode, either in the format argument or in a separate argument, the mapping for “if the loaded key has that type then use this policy”. But that gets pretty hairy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does get messy if we want to encode some form of conditional policy logic in a parameter to enable something like "if EC key then permit ECDSA, else if RSA then permit RSA-PSS, else if ....". How far should such flexibility extend?

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. The overhead of this approach is parsing the key data twice, which is perhaps particularly an issue for wrapped/encrypted formats.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a scenario where the key format provides enough information, and the application is prepared to delegate the policy setting to the key data, do we need a permitted algorithm and usage flag value that explicitly indicates this?

For example, if the application sets all the available usage flags - then a strict "intersection with the policy in the key data" would have this effect, but what should the final key policy be if the key data has no explicitly specified policy? (presumably not 'can do anything')

Similarly, we probably want to have different ways to differeniate "no algorithm is permitted" from "I am not specifying a permitted algorithm - use the use one encoded in the AlgorithmIdentifier in the key data".

@athoelke
Copy link
Contributor Author

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.

* Decided to do public key export via a dedicated function
* Removed ECPoint as a format
* Required public keys in key pair formats
* Added options for domain parameters
* Individual boolean options override defaults instead of providing both option elements
* Added references for everything
* Defined new terms for ASN.1, DER and PEM
* Open issue regarding handling of encoded policy in key data
@athoelke
Copy link
Contributor Author

athoelke commented Sep 6, 2024

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
Copy link
Contributor Author

athoelke commented Sep 6, 2024

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
Copy link
Contributor

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
Copy link
Contributor Author

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
Copy link
Contributor Author

  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
  • COMBINE - logical intersection of the policy in the attributes and the policy (if present) in the formatted key data
  • DELEGATE - use the policy in the formatted data, if present, otherwise use the policy in the attributes
  • USE - use the policy in the formatted data, if present, otherwise no policy (or should this be an error?)
  • OVERRIDE - use the policy in the attributes, ignoring any attributes in the formatted key data

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 athoelke added the API design Related the design of the API label Sep 18, 2024
@athoelke
Copy link
Contributor Author

athoelke commented Sep 18, 2024

[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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API design Related the design of the API Crypto API Issue or PR related to the Cryptography API DO NOT MERGE enhancement New feature or request
Projects
Development

Successfully merging this pull request may close these issues.

2 participants