Skip to content

Commit d86746d

Browse files
committed
Adapt key blinding to match security proof (#94)
1 parent 7afa7de commit d86746d

File tree

1 file changed

+149
-45
lines changed

1 file changed

+149
-45
lines changed

draft-dijkhuis-cfrg-hdkeys.md

Lines changed: 149 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,19 @@ The parameters of an HDK instantiation are:
237237
- H(msg): Outputs `Ns` bytes.
238238
- `BL`: A key blinding scheme [Wilson2023] with opaque blinding factors and algebraic properties, consisting of the functions:
239239
- DeriveBlindKey(ikm): Outputs a blind key `bk` based on input keying material `ikm`.
240-
- DeriveBlindingFactor(bk, ctx): Outputs a blinding factor `bf` based on a blind key `bk` and an application context byte string `ctx`.
241240
- BlindPublicKey(pk, bk, ctx): Outputs the result public key `pk'` of blinding public key `pk` with blind key `bk` and application context byte string `ctx`.
242-
- BlindPrivateKey(sk, bf): Outputs the result private key `sk'` of blinding private key `sk` with blinding factor `bf`. This result `sk'` is such that if `bf = DeriveBlindingFactor(bk, ctx)` for some `bk` and `ctx`, `(sk', pk')` forms a key pair for `pk' = BlindPublicKey(pk, bk, ctx)`.
243-
- Combine(bf1, bf2): Outputs a blinding factor `bf` such that for all key pairs `(sk, pk)`:
241+
- BlindPrivateKey(sk, bk, ctx): Outputs the result private key `sk'` of blinding private key `sk` with blind key `bk` and application context byte string `ctx`. The result `sk'` is such that if `pk` is the public key for `sk`, then `(sk', pk')` forms a key pair for `pk' = BlindPublicKey(pk, bk, ctx)`.
242+
- Combine(k1, k2): Outputs a blinding factor `bf` given input keys `k1` and `k2` which are either private keys or blinding factors, with the following associative property. For all input keys `k1`, `k2`, `k3`:
244243

245244
~~~
246-
BlindPrivateKey(sk, bf) ==
247-
BlindPrivateKey(BlindPrivateKey(sk, bf1), bf2)
245+
Combine(Combine(k1, k2), k3) == Combine(k1, Combine(k2, k3))
248246
~~~
247+
- DeriveBlindingFactor(bk, ctx): Outputs a blinding factor `bf` based on a blind key `bk` and an application context byte string `ctx`, such that for all private keys `sk`:
249248

249+
~~~
250+
BlindPrivateKey(sk, bk, ctx) == Combine(sk, bf)
251+
~~~
252+
- SerializePublicKey(pk): Outputs a canonical byte string serialisation of public key `pk`.
250253
- `KEM`: A key encapsulation mechanism [RFC9180], consisting of the functions:
251254
- DeriveKeyPair(ikm): Outputs a key encapsulation key pair `(sk, pk)`.
252255
- Encap(pk): Outputs `(k, c)` consisting of a shared secret `k` and a ciphertext `c`, taking key encapsulation public key `pk`.
@@ -264,17 +267,18 @@ A local unit or remote party creates an HDK context from an index.
264267

265268
~~~
266269
Inputs:
270+
- pk, a public key to be blinded.
267271
- index, an integer between 0 and 2^32-1 (inclusive).
268272

269273
Outputs:
270274
- ctx, an application context byte string.
271275

272-
def CreateContext(index):
273-
ctx = ID || I2OSP(index, 4)
276+
def CreateContext(pk, index):
277+
ctx = SerializePublicKey(pk) || I2OSP(index, 4)
274278
return ctx
275279
~~~
276280

277-
This context byte string is used as input for DeriveBlindingFactor, BlindPublicKey, and [DeriveSalt](#the-hdk-salt).
281+
This context byte string is used as input for DeriveBlindingFactor, BlindPrivateKey, BlindPublicKey, and [DeriveSalt](#the-hdk-salt).
278282

279283
## The HDK salt
280284

@@ -306,25 +310,30 @@ Inputs:
306310
- index, an integer between 0 and 2^32-1 (inclusive).
307311
- pk, a public key to be blinded.
308312
- salt, a string of Ns bytes.
309-
- bf, a blinding factor to combine with, Nil otherwise.
313+
- bf, a blinding factor to combine with, if any, Nil otherwise.
314+
- skD, a private key to be blinded, if known, Nil otherwise.
310315

311316
Outputs:
312317
- pk', the blinded public key at the provided index.
313318
- salt', the salt for HDK derivation at the provided index.
314319
- bf', the blinding factor at the provided index.
320+
- bk, the current blind key.
321+
- ctx, the current key blinding application context byte string.
322+
- sk', the blinded private key.
315323

316-
def HDK(index, pk, salt, bf = Nil):
317-
ctx = CreateContext(index)
324+
def HDK(index, pk, salt, bf = Nil, skD = Nil):
325+
ctx = CreateContext(pk, index)
318326
salt' = DeriveSalt(salt, ctx)
319327

320328
bk = DeriveBlindKey(salt)
321-
pk' = BlindPublicKey(bk, ctx)
322-
bf' = if bf == Nil:
323-
DeriveBlindingFactor(bk, ctx)
324-
else:
325-
Combine(bf, DeriveBlindingFactor(bk, ctx))
329+
pk' = BlindPublicKey(pk, bk, ctx)
330+
sk' = if skD == Nil: Nil
331+
elif bf == Nil: BlindPrivateKey(skD, bk, ctx)
332+
else : BlindPrivateKey(Combine(skD, bf), bk, ctx)
333+
bf' = if bf == Nil: DeriveBlindingFactor(bk, ctx)
334+
else : Combine(bf, DeriveBlindingFactor(bk, ctx))
326335

327-
return (pk', salt', bf')
336+
return ((pk', salt', bf'), (bk, ctx), sk')
328337
~~~
329338

330339
A unit MUST NOT persist a blinded private key. Instead, if persistence is needed, a unit can persist either the blinding factor of each HDK, or a path consisting of the seed salt, indices and key handles. In both cases, the application of Combine in the HDK function enables reconstruction of the blinding factor with respect to the original private key, enabling application of for example BlindPrivateKey.
@@ -351,28 +360,30 @@ The unit MUST generate `skD` within a secure cryptographic device.
351360
Whenever the unit requires the HDK with some `index` at level 0, the unit computes:
352361

353362
~~~
354-
(pk, salt, bf) = HDK(index, pkD, seed)
355-
356-
sk = BlindPrivateKey(skD, bf) # optional
363+
((pk, salt, bf), (bk, ctx), sk) = HDK(index, pkD, seed, Nil, sk)
357364
~~~
358365

359366
Now the unit can use the blinded key pair `(sk, pk)` or derive child HDKeys.
360367

361-
Whenever the unit requires the HDK with some `index` at level `n > 0` based on a parent HDK `hdk = (pk, salt, bf)` with blinded key pair `(sk, pk)` at level `n`, the unit computes:
368+
Whenever the unit requires the HDK with some `index` at level `n > 0` based on a parent HDK `(pk, salt, bf)` with blinded key pair `(sk, pk)` at level `n`, the unit computes:
362369

363370
~~~
364-
(pk', salt', bf') = HDK(index, pk, salt)
365-
366-
sk' = BlindPrivateKey(sk, bf') # optional
371+
((pk', salt', bf'), (bk, ctx), sk') = HDK(index, pk, salt, bf, sk)
367372
~~~
368373

369374
Now the unit can use the blinded key pair `(sk', pk')` or derive child HDKeys.
370375

376+
Note that providing `sk` is optional. Alternatively, the unit can use the returned `bk` and `ctx` with the parent `bf` separately in a key blinding scheme, for example using:
377+
378+
~~~
379+
sk' = BlindPrivateKey(Combine(sk, bf), bk, ctx)
380+
~~~
381+
371382
## The remote HDK protocol
372383

373384
This is a protocol between a local unit and a remote issuer.
374385

375-
As a prerequisite, the unit possesses a `salt` of `Ns` bytes associated with a parent key pair `(sk, pk)` generated using the local HDK procedure.
386+
As a prerequisite, the unit possesses a `salt` of `Ns` bytes associated with a parent key pair `(sk, pk)` with blinding factor `bf` (potentially `Nil`) generated using the local HDK procedure.
376387

377388
~~~
378389
# 1. Unit computes:
@@ -388,17 +399,17 @@ As a prerequisite, the unit possesses a `salt` of `Ns` bytes associated with a p
388399
# Subsequently, for any index known to both parties:
389400

390401
# 5. Issuer computes:
391-
(pk', salt', bf') = HDK(index, pk, salt_kem)
402+
((pk', salt', bf'), _, _) = HDK(index, pk, salt_kem)
392403

393-
# 6. Issuer shares with unit: pk'
404+
# 6. Issuer shares with unit: pkA = pk'
394405

395406
# 7. Unit verifies integrity:
396407
salt_kem = Decap(kh, skR)
397-
(pk_expected', salt', bf') = HDK(index, pk, salt_kem)
398-
pk' == pk_expected'
408+
((pk', salt', bf'), (bk, ctx), _) = HDK(index, pk, salt_kem, bf)
409+
pk' == pkA
399410

400411
# 8. Unit computes:
401-
sk' = BlindPrivateKey(sk, bf) # optional
412+
sk' = BlindPrivateKey(Combine(sk, bf), bk, ctx) # optional
402413
~~~
403414

404415
After step 7, the unit can use the value of `salt'` to derive next-level HDKeys.
@@ -493,20 +504,26 @@ Verify(signature, pk, msg)
493504

494505
Instantiations of HDK using digital signatures provide:
495506

496-
- `BL`: A cryptographic construct that extends `DSA` as specified in [I-D.draft-irtf-cfrg-signature-key-blinding-07], implementing the interface from [Instantiation parameters](#instantiation-parameters).
507+
- `BL`: A cryptographic construct that extends `DSA` as specified in [I-D.draft-irtf-cfrg-signature-key-blinding-07], implementing the interface from [Instantiation parameters](#instantiation-parameters), as well as:
508+
- BlindKeySign(sk, bk, ctx, msg): Outputs the result of signing a message `msg` using the private key `sk` with the private blind key `bk` and application context byte string `ctx` such that for key pair `(sk, pk)`:
509+
510+
~~~
511+
Verify( BlindKeySign(sk, bk, ctx, msg),
512+
BlindPublicKey(pk, bk, ctx)) == 1
513+
~~~
497514

498-
While [I-D.draft-irtf-cfrg-signature-key-blinding-07] does not expose blinding factors, it provides public algorithms to compute these. In HDK, the computed blinding factors are applied in `BL` as follows:
515+
By design of `BL`, the same proof of possession protocol can be used with blinded key pairs and BlindKeySign, in such a way that the reader does not recognise that key blinding was used.
516+
517+
In the default implementation, BlindKeySign requires support from the secure cryptographic device protecting `sk`:
499518

500519
~~~
501-
def BlindSign(sk, bf, msg):
502-
sk' = BlindPrivateKey(sk, bf)
520+
def BlindKeySign(sk, bk, ctx, msg):
521+
sk' = BlindPrivateKey(sk, bk, ctx)
503522
signature = Sign(sk', msg)
504523
return signature
505524
~~~
506525

507-
By design of `BL`, the same proof of possession protocol can be used with blinded key pairs and BlindSign, in such a way that the reader does not recognise that key blinding was used.
508-
509-
In the default implementation, BlindSign requires support from the secure cryptographic device protecting `sk`. In some cases, BlindSign can be implemented in an alternative, distributed way. An example will be provided below.
526+
In some cases, BlindKeySign can be implemented in an alternative, distributed way. An example will be provided for [using EC-SDSA signatures](#using-ec-sdsa-signatures).
510527

511528
Applications MUST bind the message to be signed to the blinded public key. This mitigates attacks based on signature malleability. Several proof of possession protocols require including document data in the message, which includes the blinded public key indeed.
512529

@@ -520,7 +537,6 @@ Instantiations of HDK using prime-order groups require:
520537
- ScalarMult(A, k): Outputs the scalar multiplication between Element `A` and Scalar `k`.
521538
- ScalarBaseMult(k): Outputs the scalar multiplication between the base Element and Scalar `k`.
522539
- Order(): Outputs the order of the base Element.
523-
- SerializeElement(A): Outputs a byte string representing Element `A`.
524540
- SerializeScalar(k): Outputs a byte string representing Scalar `k`.`
525541
- HashToScalar(msg): Outputs the result of deterministically mapping a byte string `msg` to an element in the scalar field of the prime order subgroup of `G`, using the `hash_to_field` function from a hash-to-curve suite [RFC9380].
526542

@@ -543,7 +559,16 @@ def DeriveBlindingFactor(bk, ctx):
543559
return bf
544560
~~~
545561

546-
Note that DeriveBlindingFactor is compatible with the definitions in [I-D.draft-irtf-cfrg-signature-key-blinding-07]. The function is almost compatible with the definitions in [I-D.draft-bradleylundberg-cfrg-arkg-02]: only in AKRG, the context string needs to be prefixed with `0x00`.
562+
Note that DeriveBlindKey and DeriveBlindingFactor are compatible with the definitions in [I-D.draft-irtf-cfrg-signature-key-blinding-07]. We illustrate also what would be needed instead for full compatibility with [I-D.draft-bradleylundberg-cfrg-arkg-02] below and when [Using elliptic curves](#using-elliptic-curves):
563+
564+
~~~
565+
def DeriveBlindKey_ARKG(ikm):
566+
# There is no need for additional processing,
567+
# since bk is in ARKG as intermediate input
568+
# for a pseudo-random function only.
569+
bk = ikm
570+
return bk
571+
~~~
547572

548573
### Using additive blinding
549574

@@ -559,8 +584,10 @@ def BlindPublicKey(pk, bk, ctx):
559584
pk' = Add(pk, ScalarBaseMult(bf))
560585
return pk
561586

562-
def BlindPrivateKey(sk, bf):
587+
def BlindPrivateKey(sk, bk, ctx):
588+
bf = DeriveBlindingFactor(bk, ctx)
563589
sk' = sk + bf mod Order()
590+
if sk' == 0: abort with an error
564591
return sk
565592

566593
def Combine(bf1, bf2):
@@ -584,8 +611,10 @@ def BlindPublicKey(pk, bk, ctx):
584611
pk' = ScalarMult(pk, bf)
585612
return pk
586613

587-
def BlindPrivateKey(sk, bf):
614+
def BlindPrivateKey(sk, bk, ctx):
615+
bf = DeriveBlindingFactor(bk, ctx)
588616
sk' = sk * bf mod Order()
617+
if sk' == 1: abort with an error
589618
return sk
590619

591620
def Combine(bf1, bf2):
@@ -624,6 +653,25 @@ def HashToScalar(msg):
624653
return scalar
625654
~~~
626655

656+
We illustrate also what would be needed instead for full compatibility with [I-D.draft-bradleylundberg-cfrg-arkg-02] below:
657+
658+
~~~
659+
def DeriveBlindingFactor_ARKG(bk, ctx):
660+
bf = HashToScalar_ARKG(msg, ctx)
661+
return bf
662+
663+
def HashToScalar_ARKG(msg, info):
664+
scalar = hash_to_field(msg, 1) with the parameters:
665+
DST: DST || info
666+
F: GF(Order()), the scalar field
667+
of the prime order subgroup of EC
668+
p: Order()
669+
m: 1
670+
L: as defined in H2C
671+
expand_message: as defined in H2C
672+
return scalar
673+
~~~
674+
627675
### Using ECDH shared secrets
628676

629677
Instantiations of HDK using ECDH shared secrets use:
@@ -657,10 +705,11 @@ Now with the shared secret `Z_AB`, the unit and the reader can compute a secret
657705

658706
In this example, step 1 can be postponed in the interactions between the unit and the reader if a trustworthy earlier commitment to `pk` is available, for example in a sealed document.
659707

660-
Similarly, ECDH enables authentication of key pair `(sk', pk')` blinded from an original key pair `(sk, pk)` using a blinding factor `bf` such that:
708+
Similarly, ECDH enables authentication of key pair `(sk', pk')` blinded from an original key pair `(sk, pk)` using a blind key `ctx` and application context byte string `ctx` such that:
661709

662710
~~~
663-
sk' = BlindPrivateKey(sk, bf)
711+
bf = DeriveBlindingFactor(bk, ctx)
712+
sk' = BlindPrivateKey(sk, bk, ctx)
664713
= sk * bf mod Order()
665714
pk' = ScalarMult(pk, bf)
666715
~~~
@@ -688,14 +737,15 @@ Instantiations of HDK using EC-SDSA signatures provide:
688737

689738
- `DSA`: An EC-SDSA digital signature algorithm [TR03111], representing signatures as pairs `(c, s)`.
690739

691-
Note that in this case, the following definition is equivalent to the original definition of BlindSign:
740+
Note that in this case, the following definition is equivalent to the [original definition of BlindKeySign](#using-digital-signatures):
692741

693742
~~~
694-
def BlindSign(sk, bf, msg):
743+
def BlindKeySign(sk, bk, ctx, msg):
695744
# Compute signature within the secure cryptographic device.
696745
(c, s) = Sign(sk, msg)
697746

698747
# Post-process the signature outside of this device.
748+
bf = DeriveBlindingFactor(bk, ctx)
699749
s' = s + c * bf mod Order()
700750

701751
signature = (c, s')
@@ -872,6 +922,60 @@ HDK enables unit and issuers cooperatively to establish the cryptographic key ma
872922

873923
For the remote HDK protocol, HDK proposes an update to the OpenID4VCI endpoints. This proposal is under discussion in [openid/OpenID4VCI#359](https://github.com/openid/OpenID4VCI/issues/359). In the update, the unit shares a key encapsulation public key with the issuer, and the issuer returns a key handle. Then documents can be re-issued, potentially in batches, using synchronised indices. Alternatively, re-issued documents can have their own key handles.
874924

925+
## Applying HDK with ARKG
926+
927+
This section illustrates how an Asynchronous Remote Key Generation (ARKG) instance can be constructed using the interfaces from the current document. It is not fully compatible with [I-D.draft-bradleylundberg-cfrg-arkg-02] due to subtle differences, such as those in [Using prime-order groups](#using-prime-order-groups) and [Using elliptic curves](#using-elliptic-curves).
928+
929+
~~~
930+
def DeriveSeed(ikm, (skD, bf), pk):
931+
(skR, pkR) = DeriveKeyPair(ikm)
932+
skA = (skR, (skD, bf, pk))
933+
pkA = (pkR, pk)
934+
return (skA, pkA)
935+
936+
def DerivePublicKey((pkR, pk), index):
937+
(salt_kem, kh) = Encap(pkR)
938+
939+
bk = DeriveBlindKey(salt_kem)
940+
ctx = CreateContext(pk, index)
941+
pk' = BlindPublicKey(pk, bk, ctx)
942+
943+
return (pk', kh)
944+
945+
def DerivePrivateKey((skR, (skD, bf, pk)), (pk', kh), index):
946+
salt_kem = Decap(kh, skR)
947+
948+
bk = DeriveBlindKey(salt_kem)
949+
ctx = CreateContext(pk, index)
950+
pkE = BlindPublicKey(pk, bk, ctx)
951+
952+
if pk' != pkE: abort with an error
953+
954+
sk = Combine(skD, bf)
955+
sk' = BlindPrivateKey(sk, bk, ctx)
956+
957+
return sk'
958+
~~~
959+
960+
This enables the [remote HDK protocol](#the-remote-hdk-protocol) to be performed as such, given an `index` known to both parties:
961+
962+
~~~
963+
# 1. Unit computes:
964+
(skA, pkA) = DeriveSeed(salt, (skD, bf), pk)
965+
966+
# 2. Unit shares with issuer: pkA
967+
968+
# 3. Issuer computes:
969+
(pk', kh) = DerivePublicKey(pkA, index)
970+
971+
# 4. Issuer shares with unit: (pk', kh)
972+
973+
# 5. Unit verifies integrity and computes the private key:
974+
sk' = DerivePrivateKey(skA, (pk', kh), index)
975+
~~~
976+
977+
For using a single `kh` with multiple values of `index`, the DerivePublicKey needs to be refactored to be able to reuse the Encap output.
978+
875979
# Security considerations
876980

877981
## Confidentiality of key handles

0 commit comments

Comments
 (0)