Skip to content

feat(crypto): CRP-2603 Add VetKD utils in TypeScript#2

Merged
randombit merged 2 commits intomainfrom
jack/js-vetkd-utils
Feb 26, 2025
Merged

feat(crypto): CRP-2603 Add VetKD utils in TypeScript#2
randombit merged 2 commits intomainfrom
jack/js-vetkd-utils

Conversation

@randombit
Copy link
Contributor

Avoiding the difficulties of wasm deployment.

@randombit randombit requested a review from altkdf February 18, 2025 23:49
@randombit
Copy link
Contributor Author

randombit commented Feb 18, 2025

Notes:

  • Tests are currently happy path only
  • IBE is implemented, not that we necessarily need it right away but I wanted to double check there was nothing missing from noble-curves that would prevent us from implementing it when we do need it.
  • This uses XMD for everything including the IBE message-mask hash, so it can't handle messages larger than 8K. I'll discuss this with Andrea

pk: G2Point;

constructor(bytes: Uint8Array) {
const pk = bls12_381.G2.ProjectivePoint.fromHex(bytes);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Check that the identity element is rejected


this.c1 = bls12_381.G1.ProjectivePoint.fromHex(bytes.subarray(0, G1_BYTES));
this.c2 = bls12_381.G2.ProjectivePoint.fromHex(bytes.subarray(G1_BYTES, G1_BYTES + G2_BYTES));
this.c3 = bls12_381.G1.ProjectivePoint.fromHex(bytes.subarray(G1_BYTES + G2_BYTES));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Check that identity elements are rejected


const calculated = augmentedHashToG1(pk, msg);
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add a randomized test with emulated replica side

},
"scripts": {
"build": "tsc",
"test": "jest",
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add a workflow for the tests, but not necessarily in this PR. I run the tests locally and they seem to work and finish successfully.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I imagine getting CI to work will (as typical) involve a lot of iterations, I was inclined to merge this without CI then add CI in a followup.

Copy link
Contributor

@altkdf altkdf Feb 26, 2025

Choose a reason for hiding this comment

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

I agree, created CRP-2710.

* "my-app" is deriving two keys, one for usage "foo" and the other for
* "bar". You might use as domain separators "my-app-foo" and "my-app-bar".
*/
export function deriveSymmetricKey(input: Uint8Array, domainSep: string, outputLength: number): Uint8Array {
Copy link
Contributor

Choose a reason for hiding this comment

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

do we want this (and similar) function(s) to be exported? if yes, maybe we could move this into a namespace with all the stuff that is not supposed to be used my a normal developer

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 seems not unreasonable to me that a developer would use this, eg for creating a tree of symmetric keys derived from a vetkd root. But we can for the time being annotate it with @internal which apparently hides it from documentation generators and IDE tooling.

}

export class EncryptedKey {
readonly #c1: G1Point;
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a reference in some paper we could point to to describe what those c{i}'s are? Also a sentence about what the Key is and what it is Encrypted with could make sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is from 6.2 of https://eprint.iacr.org/2023/616.pdf but the same idea goes back to the FullIdent Boneh-Franklin IBE scheme from https://crypto.stanford.edu/~dabo/papers/bfibe.pdf section 4.2

In IBE the public key is some point in the pairing group (in the original BF-IBE proposal derived by hashing the user identifier to a point), and the secret key is that same point, multiplied by the secret scalar controlled by the trusted issuer.

In VetKD, the public keys are instead a set of points specified by the tuple of (canister id, derivation path, derivation id) [which can be accessed via vetkd_public_key] and the private keys are the signatures, from vetkd_encrypted_key.

After retrieving a users public key with vetkd_public_key with some specified context and derivation id, anyone can IBE encrypt a message. Anyone who can retrieve the VetKey associated with that context and derivation id (and thus the BLS signature) also holds the key necessary to decrypt the IBE ciphertext.

I'll add some comments with references.

@altkdf
Copy link
Contributor

altkdf commented Feb 20, 2025

Two more things:

  • Should domainSep really be a string and not Uint8Array?
  • How do I explicitly store a VetKey as bytes? I want to 1) extract bytes, 2) eventually encrypt those bytes. Then do vice-versa when I need to fetch the VetKey from memory.

@randombit
Copy link
Contributor Author

randombit commented Feb 20, 2025

Should domainSep really be a string and not Uint8Array?

Domain separators are typically strings in my experience eg in TLS 1.3 key scheduling, TLS key exporter, hash to curve, CPace PAKE, KMAC/ParallellHash/TupleHash, or the domain separators used in the IC

Using non-ASCII/non-UTF8 bytestrings as domain separators does work just fine since all that matters for security is that they are unique. Just usually these are fairly static since the goal is to tie the output to a specific context (application, version, intended key usage, user identifiers, ...) most of which are more easily and comprehensibly expressed as strings.

The higher level functions of noble-curves take the domain separators as strings as well. The low level XMD function doesn't and instead uses a bytestring for whatever reason. I'm not sure if it's even possible to call eg hash to curve with a domain separator that's not a string.

How do I explicitly store a VetKey as bytes? I want to 1) extract bytes, 2) eventually encrypt those bytes. Then do vice-versa when I need to fetch the VetKey from memory.

TBH I had no expected that use case. I would have expected instead to derive a random symmetric key (eg 256 bits) then stash that, and if necessary use it to derive further keys. I guess in the immediate term you could use signatureBytes, but you would need a way to deserialize the VetKey, and probably something to verify it as well. I guess those also would come up for anyone doing IBE since there you must have the raw VetKey to decrypt, and users would not want to have to invoke VetKD each time they decrypted an IBE message. I'll add explicit serialize, deserialize, and verify to VetKey.

@altkdf
Copy link
Contributor

altkdf commented Feb 21, 2025

Domain separators are typically strings in my experience

But what we are doing here is essentially some sort of HKDF, no? I was thinking about it as salt in HKDF, which is defined in the RFC as an octet string. But I guess instead using the hex representation or some human-readable format like for principals is also an option, although this may add some communication, but not a huge amount of it.

@altkdf
Copy link
Contributor

altkdf commented Feb 21, 2025

TBH I had no expected that use case. I would have expected instead to derive a random symmetric key (eg 256 bits) then stash that, and if necessary use it to derive further keys. I guess in the immediate term you could use signatureBytes, but you would need a way to deserialize the VetKey, and probably something to verify it as well. I guess those also would come up for anyone doing IBE since there you must have the raw VetKey to decrypt, and users would not want to have to invoke VetKD each time they decrypted an IBE message. I'll add explicit serialize, deserialize, and verify to VetKey.

I think we wanted to remove decrypt_and_hash to exactly remove that intermediate step of applying another HKDF. So as an option we could also just hash the signature and eventually provide an interface like KeyDerivationMaterial or something like that?

@randombit
Copy link
Contributor Author

randombit commented Feb 21, 2025

I was thinking about it as salt in HKDF, which is defined in the RFC as an octet string.

It is much closer to the info field of HKDF. XMD does not have any kind of salt parameter, just the domain separator. And following RFC 9380 that's usually a string, for example when you hash to curve (in augmentedHashToG1), you use the string "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_AUG_", which is the standard identifier for this operation.

@randombit randombit requested a review from a team February 25, 2025 18:56
@randombit randombit force-pushed the jack/js-vetkd-utils branch 2 times, most recently from 355e6cd to d5a20c8 Compare February 26, 2025 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants