-
Notifications
You must be signed in to change notification settings - Fork 56
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
Refactor Key Management in @web5/crypto
#271
Comments
ProposalGeneral Design Choices
Base InterfaceDefine a export interface CryptoApi {
// Key Generation Operations
generateKey(options: GenerateKeyOptions): Promise<KeyIdentifier>;
// Key Derivation Operations
deriveBits(options: DeriveBitsOptions): Promise<Uint8Array>;
deriveKey(options: DeriveKeyOptions): Promise<KeyIdentifier>;
// Key Wrapping Operations
wrapKey(options: WrapKeyOptions): Promise<Uint8Array>;
unwrapKey(options: UnwrapKeyOptions): Promise<JsonWebKey>;
// Signature Operations
sign(options: SignOptions): Promise<Uint8Array>;
verify(options: VerifyOptions): Promise<boolean>;
// Hashing Operations
digest(options: DigestOptions): Promise<Uint8Array>;
// Cipher Operations
encrypt(options: EncryptOptions): Promise<Uint8Array>;
decrypt(options: DecryptOptions): Promise<Uint8Array>;
// Import/Export Operations
importKey(options: ImportKeyOptions): Promise<KeyIdentifier>;
exportKey(options: ExportKeyOptions): Promise<JsonWebKey>;
} Concrete ImplementationsAt a minimum, develop and publish two implementations of the
Consumers of our SDKs can develop their own Usage ExamplesInitializationIn-memory: import { InMemoryCrypto } from `@web5/crypto`;
const crypto = new InMemoryCrypto(); AWS KMS: import { AwsKmsCrypto } from `@web5/crypto`;
import { KMSClient } from '@aws-sdk/client-kms';
const kmsClient = new KMSClient({ region: 'us-east-1' })
const crypto = new AwsKmsCrypto({ kmsClient }); Key Generationconst dataEncKeyId = await crypto.generateKey({ algorithm: { name: 'A256CTR' }});
console.log(dataEncKeyId) // vDNSXebfrWOE2fzXhJXbRH28tZhuv2FEd9XYow-SpRA const credSignKeyId = await crypto.generateKey({ algorithm: { name: 'ES256K' }});
console.log(credSignKeyId) // riJYyUykKbIU9hgO27p-smjDncj082aRcmknKS7XQ-w Signingconst data = new Uint8Array(32);
const signature = await crypto.sign({ algorithm: { name: 'ES256K' }, data, keyId: credSignKeyId }); Verificationconst isValid = await crypto.verify({ algorithm: { name: 'ES256K' }, data, keyId: credSignKeyId, signature }); Encryptionconst plaintext = new Uint8Array(1024);
const ciphertext = await crypto.encrypt({ algorithm: { name: 'A256CTR', data: plaintext, keyId: dataEncKeyId }); Decryptionconst plaintextOutput = await crypto.decrypt({ algorithm: { name: 'A256CTR', data: ciphertext, keyId: dataEncKeyId }); DID CreationBy passing a concrete import { DidDhtMethod, DidIonMethod, DidJwkMethod, DidKeyMethod } from '@web5/dids';
const didA = await DidDhtMethod.create({ crypto });
const didB = await DidIonMethod.create({ crypto });
const didB = await DidJwkMethod.create({ crypto });
const didC = await DidKeyMethod.create({ crypto }); AppendixGeneral Type Definitionsexport interface AlgorithmIdentifier {
name: string;
}
export type KeyIdentifier = string;
|
Overall awesome stuff! Major pieces of feedback/questions: First, related to
What's the motivation for supporting raw keys vs only using JWKs? Related to encryption. I think it's important to make key rotation first class. Cloud managers typically provide some form of automatically doing this, but it really depends on the use case. How do you envision key rotation being facilitated by the Related to naming, I'm curious why Lastly, should there be special provisions in order to handle large streams of data (or large files) that need to be signed or encrypted? |
Does |
|
Having one |
Great questions and feedback @andresuribe87 and @tomdaffurn.
The proposal is to only use the JWK key format in the
Good question and a topic that deserves more consideration. How do we balance the simplicity and consistency of an API that provides a set of common crypto operations with more fine-grained configuration and advanced functionality that is only available in some scenarios (e.g., HSM backed)? What should be considered a "common" operation? Will consumers of our SDKs use them to manage key rotation or will they implement these policies directly in the underlying KMS? I used the Web Crypto API as inspiration, but it doesn't account for features like key rotation. Additionally, if something like JWK thumbprint is used as a key identifier, how would that work post-key rotation? We should consider this and other questions to arrive at a consensus.
This design choice was inspired by the Web Crypto API since it is supported by 98%+ of desktop and mobile browser installations (billions of devices). Yes, other options were considered and I think both @mistermoe and I have prototyped half a dozen versions over the past few months. In general, the approach was to provide a single API that exposed a common set of cryptographic algorithms including digital signature, cipher, hashing, and key derivation. A few mentions of other options considered:
That's an insightful question and one I've been thinking about a lot lately. The current Web Crypto API supports only one-shot encryption and hashing. Good read on the subject here and here. A draft community report still under consideration by W3C and browser vendors here.
The HMAC is primarily considered a signing algorithm, and is therefore, accessed through the
Yes, that seems like a good approach and is the direction
The core methods of the prototype I created for In contrast, an AWS or GCP KMS implementation would involve constructing and executing commands to the KMS client and handling any format/encoding conversion.
Apologies for the confusion. I should have explained that better. That's only present in the base type definitions. Concrete implementations would override these with the algorithms that they support. For example, an AWS implementation might initially only support signatures with An example would be the base export namespace CryptoApi {
export interface GenerateKeyOptions {
algorithm: AlgorithmIdentifier;
}
export interface SignOptions {
algorithm: AlgorithmIdentifier;
keyId: KeyIdentifier;
data: Uint8Array;
}
} And the specific implementation choices made for an namespace InMemoryCrypto {
export interface GenerateKeyOptions extends CryptoApi.GenerateKeyOptions {
algorithm:
| CryptoApi.AesOptions
| CryptoApi.ChaChaOptions
| CryptoApi.EcOptions
| CryptoApi.HmacOptions
| CryptoApi.Pbkdf2Options;
}
export interface SignOptions extends CryptoApi.SignOptions {
algorithm:
| CryptoApi.EcdsaOptions
| CryptoApi.EdDsaOptions
| CryptoApi.HmacOptions;
}
} Taking this approach enables IDE/IntelliSense hints to automatically list only those algorithms that are available: |
Is this something that needs to be exposed as part of the public API? It seems like it's a lower-level implementation detail. In general, I'm a big fan of making public APIs as slim as possible. Smaller public APIs means a smaller surface that needs to be maintained, implemented, and less choice for developers (which translates into less time learning how to use the API). Going from private -> public is much easier than going from public -> private, in case it's something that we really want to expose to the public.
Just noting that my strong preference is to have smaller interfaces like the ones mentioned above. It's possible in most languages that I'm familiar with, including Kotlin, golang and C++. I would also slims down the dependencies that are needed, as devs can pull in only what they need, instead of the whole bundled API. Another benefit is that this conversation can be separated into smaller pieces. For instance, I think we should include encryption until we have a rock solid story of how we're going to support key-rotation (I believe it's crucial that we make sure our design promotes the correct way of doing encryption, which include key-rotation). |
@frankhinek a couple of additional use cases that I think are important to address. There is often times where applications need to know whether the private key associated with a public key is available in the instance that implements
(2) is what we've implemented in kotlin as shown in https://github.com/TBD54566975/web5-kt/blob/bbef724240451e143e5ba72d829d25c371e440dd/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt#L64 But I actually think we should implement (1). Let me elaborate why below. A second, more advanced, use case that's specific to DidIon (and possibly DidWeb). When a user boots up an application for the n-th time, they will likely want to know whether their instance of An alternative is to expose transaction functionality with commits and rollbacks, but that sounds very nasty. |
@frankhinek many props for always taking the time to thoroughly explain your proposals and the rationale behind decsions. Re: ah ok so, if |
Yes, that's certainly an approach that could be taken. Ideally implementers have enough of the basic structure and reference implementations in place that they can extend or modify to meet specific use case requirements. |
A few thoughts:
|
@web5/crypto
, @web5/dids
, and @web5/credentials
@web5/crypto
Design Guidelines
Feature Proposal Tasks:
Implementation Tasks:
The text was updated successfully, but these errors were encountered: