babelouest / rhonabwy

Javascript Object Signing and Encryption (JOSE) library - JWK, JWKS, JWS, JWE and JWT
https://babelouest.github.io/rhonabwy/
GNU Lesser General Public License v2.1
45 stars 21 forks source link

Incorrect KDF for AxxxCBC with ECDH-ES #28

Closed ksivask closed 1 year ago

ksivask commented 1 year ago

Sample EC256 Key & Data:

root@9ac53b23f309:/tmp/jtests# cat ec256.keys
    {
      "crv": "P-256",
      "d": "oFsqPRb6-_4lsQxpo9SkpI_msq_77wEgIsCiwLaMWgA",
      "kid": "ExUMY91J9atex-1Ai5vbu6qJ4aAGq0RMQxo__QyZV4k",
      "kty": "EC",
      "x": "g2L0jXitI0uUgmsKCUJjMVr20NrU5uHVlTbbXEd5UvY",
      "y": "MCcXesyMUYS3l6PvyQeN6B6Ou1UiWQF-7OeIZBWmgK4"
    }

root@edb8bbbd1def:/j/tests# data='{"sub": "jdoe@example.com","name": "JohnDoe","iat": 1516239022}'

rhonabwy:

Using https://github.com/babelouest/rhonabwy

root@edb8bbbd1def:/j/tests# /j/jwtc/rnbyc -s "$data" -l ECDH-ES  -e A256CBC-HS512 -P ec256.jwks -K o256.jwks -a HS256

jwt: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IktPTWdUTFdhTDdzN0s5S0JBNldsc0EifQ.eyJzdWIiOiJqZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.3cr0Mhmthj0_-btt69sRLvbGb7ScD4lpcp-8MMArk90

r_jwe_encrypt_payload: jwe->key
BE CD C2 20 52 27 2E 77  9D 4E CA E3 D6 E1 0F 3B
36 2C 0F B7 F1 23 FF DF  36 C9 A3 13 81 F1 64 D0
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

jwe: eyJ0eXAiOiJKV1QiLCJjdHkiOiJKV1QiLCJhbGciOiJFQ0RILUVTIiwia2lkIjoiRXhVTVk5MUo5YXRleC0xQWk1dmJ1NnFKNGFBR3EwUk1ReG9fX1F5WlY0ayIsImVwayI6eyJrdHkiOiJFQyIsIngiOiJOWjZJSlk4WTNlZVhPLXFXZXJ4NUhNeUNsNkFMeWdQaEdFaDEydFpaTkcwIiwieSI6Inl3Vk5GamVpLURQWEpRZ2FxV0lOalZ1R2JEaW94VUp4MDlUZXprb0xVWmsiLCJjcnYiOiJQLTI1NiJ9LCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..z-JRY8K9BjcP3P86ZojJJw.ngURI13qrpyOIc5AvIwcdS9D151f8YVwmsvjWG0kQg9ASELPw82F7Sh5Azrt5to6slyQ4BDib9PdwI3BTg29Wd-jbUYlrsTYx53-Syy5pGSIOhsilTbWo-eIHUPyV8E0m1RC3une-M7uxgiQL79hYvhgC9eteuobsS8dai9FkPjCr2WiOqvevqYtwmAJ_sUkD83-2b98HeZ_ZcDt4E6OHej02DrMFSIs6bhHAspdn2AlxRyHEyO3Z1su7-xafP7zYnKuiG49UppFYoKI_OKMIw.eZxa077Rgt5ogrosk9GN0JJvl2cpljxZ4wRBOuvuWi4

# save jwt and jwe to files

root@9ac53b23f309:/tmp/jtests# cat jwt.txt
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IktPTWdUTFdhTDdzN0s5S0JBNldsc0EifQ.eyJzdWIiOiJqZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.3cr0Mhmthj0_-btt69sRLvbGb7ScD4lpcp-8MMArk90

root@9ac53b23f309:/tmp/jtests# cat jwe.txt
eyJ0eXAiOiJKV1QiLCJjdHkiOiJKV1QiLCJhbGciOiJFQ0RILUVTIiwia2lkIjoiRXhVTVk5MUo5YXRleC0xQWk1dmJ1NnFKNGFBR3EwUk1ReG9fX1F5WlY0ayIsImVwayI6eyJrdHkiOiJFQyIsIngiOiJOWjZJSlk4WTNlZVhPLXFXZXJ4NUhNeUNsNkFMeWdQaEdFaDEydFpaTkcwIiwieSI6Inl3Vk5GamVpLURQWEpRZ2FxV0lOalZ1R2JEaW94VUp4MDlUZXprb0xVWmsiLCJjcnYiOiJQLTI1NiJ9LCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..z-JRY8K9BjcP3P86ZojJJw.ngURI13qrpyOIc5AvIwcdS9D151f8YVwmsvjWG0kQg9ASELPw82F7Sh5Azrt5to6slyQ4BDib9PdwI3BTg29Wd-jbUYlrsTYx53-Syy5pGSIOhsilTbWo-eIHUPyV8E0m1RC3une-M7uxgiQL79hYvhgC9eteuobsS8dai9FkPjCr2WiOqvevqYtwmAJ_sUkD83-2b98HeZ_ZcDt4E6OHej02DrMFSIs6bhHAspdn2AlxRyHEyO3Z1su7-xafP7zYnKuiG49UppFYoKI_OKMIw.eZxa077Rgt5ogrosk9GN0JJvl2cpljxZ4wRBOuvuWi4

root@edb8bbbd1def:/j/tests# /j/jwtc/rnbyc -t "$ntok" -K ec256.keys -H true -d
2023-06-29T03:33:20Z - rnbyc INFO: Starting rnbyc debug mode

r_jwe_decrypt_payload: jwe->key
BE CD C2 20 52 27 2E 77  9D 4E CA E3 D6 E1 0F 3B
36 2C 0F B7 F1 23 FF DF  36 C9 A3 13 81 F1 64 D0
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
Token payload decrypted
{
  "alg": "ECDH-ES",
  "cty": "JWT",
  "enc": "A256CBC-HS512",
  "epk": {
    "crv": "P-256",
    "kty": "EC",
    "x": "NZ6IJY8Y3eeXO-qWerx5HMyCl6ALygPhGEh12tZZNG0",
    "y": "ywVNFjei-DPXJQgaqWINjVuGbDioxUJx09TezkoLUZk"
  },
  "kid": "ExUMY91J9atex-1Ai5vbu6qJ4aAGq0RMQxo__QyZV4k",
  "typ": "JWT"
}
{
  "alg": "HS256",
  "kid": "KOMgTLWaL7s7K9KBA6WlsA",
  "typ": "JWT"
}
{
  "iat": 1516239022,
  "name": "JohnDoe",
  "sub": "jdoe@example.com"
}

NOTE: both encrypt and decrypt use {0} as key (last 32 bytes)

go-jose:

Cross verify with go-jose/jose-util: https://github.com/go-jose/go-jose

$git diff
diff --git a/cipher/ecdh_es.go b/cipher/ecdh_es.go
index 093c646..c54f174 100644
--- a/cipher/ecdh_es.go
+++ b/cipher/ecdh_es.go
@@ -22,6 +22,8 @@ import (
        "crypto/ecdsa"
        "crypto/elliptic"
        "encoding/binary"
+       "encoding/hex"
+       "fmt"
 )

 // DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA.
@@ -64,6 +66,8 @@ func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, p
        // Read on the KDF will never fail
        _, _ = reader.Read(key)

+       fmt.Printf("DeriveECDHES: key\n%s", hex.Dump(key))
+
        return key
 }

root@9ac53b23f309:/tmp/jtests# ./jose-util decrypt --key ec256.keys --in jwe.txt

DeriveECDHES: key
00000000  be cd c2 20 52 27 2e 77  9d 4e ca e3 d6 e1 0f 3b  |... R'.w.N.....;|
00000010  36 2c 0f b7 f1 23 ff df  36 c9 a3 13 81 f1 64 d0  |6,...#..6.....d.|
00000020  b7 34 92 35 ac 93 d5 87  cc b1 45 b4 e7 9b 43 9c  |.4.5......E...C.|
00000030  ed 8d b6 de 21 27 34 fa  e7 49 f2 46 dd 30 c9 c7  |....!'4..I.F.0..|
jose-util: error: unable to decrypt message: go-jose/go-jose: error in cryptographic primitive

Using another JWE tool

(gdb) x /64xb jwe.dcek_.data()
0x248ad60:      0xbe    0xcd    0xc2    0x20    0x52    0x27    0x2e    0x77
0x248ad68:      0x9d    0x4e    0xca    0xe3    0xd6    0xe1    0x0f    0x3b
0x248ad70:      0x36    0x2c    0x0f    0xb7    0xf1    0x23    0xff    0xdf
0x248ad78:      0x36    0xc9    0xa3    0x13    0x81    0xf1    0x64    0xd0
0x248ad80:      0xb7    0x34    0x92    0x35    0xac    0x93    0xd5    0x87
0x248ad88:      0xcc    0xb1    0x45    0xb4    0xe7    0x9b    0x43    0x9c
0x248ad90:      0xed    0x8d    0xb6    0xde    0x21    0x27    0x34    0xfa
0x248ad98:      0xe7    0x49    0xf2    0x46    0xdd    0x30    0xc9    0xc7

Patched rnbyc:

root@edb8bbbd1def:/j/tests# /j/jwtc/rnbyc.new -s "$data" -l ECDH-ES  -e A256CBC-HS512 -P ec256.jwks -K o256.jwks -a HS256

jwt: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImlpMXdBTHhXbWxBRHJEUnRuN0cwTlEifQ.eyJzdWIiOiJqZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.J96TAtD8CQo3tATeUGtKEdb1Pcz_p1Q1a0kFuoghQdg

r_jwe_encrypt_payload: jwe->key
14 FB 29 D7 C7 F7 7F 12  D9 9F D4 63 F6 98 F0 70
6E AD 6C B2 FE 7D EE F0  41 83 BB F9 87 E6 38 06
6A 83 2A 1A C9 6E DC 88  41 3D 20 BD 89 AA E7 2F
41 75 D9 E7 D7 19 BA 01  54 8F F7 0F A0 D5 A4 79

jwe:
eyJ0eXAiOiJKV1QiLCJjdHkiOiJKV1QiLCJhbGciOiJFQ0RILUVTIiwia2lkIjoiRXhVTVk5MUo5YXRleC0xQWk1dmJ1NnFKNGFBR3EwUk1ReG9fX1F5WlY0ayIsImVwayI6eyJrdHkiOiJFQyIsIngiOiJ4YTU5WnpveFlseG10eFRVeW5EaVNpUHpSYkYzdm5RS2Y2ZjcxZkNfSXFjIiwieSI6ImhzRk9lb0RvcmFJUHo3OXVDSERXTmhsbm5XZ3VIZk9sOEw3RnROUlM5cVUiLCJjcnYiOiJQLTI1NiJ9LCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..fbX7a7yaM3FprfHN5juiow.d6Ynb8fL2fcHMEOCcmj9DrJE1T02lU8FFLbLsOHLbKtJ6h26HxHBOnkJWPLWn9P8G9T4Sz49IboSiUpCRRbvYyb-NNLzEYIp4LzBdNQmFfgoUasE6ECtYZJk7NDBe1zwXGFAtmEVCdiGJlAsFsq9DiINKNvGpTQze7ks2d1DwToMjhQjVeNPGW5iBz0fpz_s8Q_CTs1RtnWjuUCjgriMV1sNrd-Ea6xVb5rW3FS99NvH6Xnu81Mu0wLMQFxkq4UFo_kc7XYQ8ToR0i902Ck6NQ.HkJMezu9E-_hbYbkGClfgLtdDobuqWLYFAxVAz1XDeQ

root@edb8bbbd1def:/j/tests# ntok="eyJ0eXAiOiJKV1QiLCJjdHkiOiJKV1QiLCJhbGciOiJFQ0RILUVTIiwia2lkIjoiRXhVTVk5MUo5YXRleC0xQWk1dmJ1NnFKNGFBR3EwUk1ReG9fX1F5WlY0ayIsImVwayI6eyJrdHkiOiJFQyIsIngiOiJ4YTU5WnpveFlseG10eFRVeW5EaVNpUHpSYkYzdm5RS2Y2ZjcxZkNfSXFjIiwieSI6ImhzRk9lb0RvcmFJUHo3OXVDSERXTmhsbm5XZ3VIZk9sOEw3RnROUlM5cVUiLCJjcnYiOiJQLTI1NiJ9LCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..fbX7a7yaM3FprfHN5juiow.d6Ynb8fL2fcHMEOCcmj9DrJE1T02lU8FFLbLsOHLbKtJ6h26HxHBOnkJWPLWn9P8G9T4Sz49IboSiUpCRRbvYyb-NNLzEYIp4LzBdNQmFfgoUasE6ECtYZJk7NDBe1zwXGFAtmEVCdiGJlAsFsq9DiINKNvGpTQze7ks2d1DwToMjhQjVeNPGW5iBz0fpz_s8Q_CTs1RtnWjuUCjgriMV1sNrd-Ea6xVb5rW3FS99NvH6Xnu81Mu0wLMQFxkq4UFo_kc7XYQ8ToR0i902Ck6NQ.HkJMezu9E-_hbYbkGClfgLtdDobuqWLYFAxVAz1XDeQ"

root@edb8bbbd1def:/j/tests# /j/jwtc/rnbyc.new -t "$ntok" -K ec256.keys -P o256.jwks -H true -d
2023-06-29T05:25:19Z - rnbyc INFO: Starting rnbyc debug mode

r_jwe_decrypt_payload: jwe->key
14 FB 29 D7 C7 F7 7F 12  D9 9F D4 63 F6 98 F0 70
6E AD 6C B2 FE 7D EE F0  41 83 BB F9 87 E6 38 06
6A 83 2A 1A C9 6E DC 88  41 3D 20 BD 89 AA E7 2F
41 75 D9 E7 D7 19 BA 01  54 8F F7 0F A0 D5 A4 79
Token payload decrypted
Token signature verified
{
  "alg": "ECDH-ES",
  "cty": "JWT",
  "enc": "A256CBC-HS512",
  "epk": {
    "crv": "P-256",
    "kty": "EC",
    "x": "xa59ZzoxYlxmtxTUynDiSiPzRbF3vnQKf6f71fC_Iqc",
    "y": "hsFOeoDoraIPz79uCHDWNhlnnWguHfOl8L7FtNRS9qU"
  },
  "kid": "ExUMY91J9atex-1Ai5vbu6qJ4aAGq0RMQxo__QyZV4k",
  "typ": "JWT"
}
{
  "alg": "HS256",
  "kid": "ii1wALxWmlADrDRtn7G0NQ",
  "typ": "JWT"
}
{
  "iat": 1516239022,
  "name": "JohnDoe",
  "sub": "jdoe@example.com"
}

NOTE: the key is now correct

Cross verify with go-jose/jose-util:

root@9ac53b23f309:/tmp/jtests# ./jose-util decrypt --key ec256.keys --in jwe.new
DeriveECDHES:
00000000  14 fb 29 d7 c7 f7 7f 12  d9 9f d4 63 f6 98 f0 70  |..)........c...p|
00000010  6e ad 6c b2 fe 7d ee f0  41 83 bb f9 87 e6 38 06  |n.l..}..A.....8.|
00000020  6a 83 2a 1a c9 6e dc 88  41 3d 20 bd 89 aa e7 2f  |j.*..n..A= ..../|
00000030  41 75 d9 e7 d7 19 ba 01  54 8f f7 0f a0 d5 a4 79  |Au......T......y|

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImlpMXdBTHhXbWxBRHJEUnRuN0cwTlEifQ.eyJzdWIiOiJqZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.J96TAtD8CQo3tATeUGtKEdb1Pcz_p1Q1a0kFuoghQdg
ksivask commented 1 year ago

The issue is in how src/jwe.c:_r_jwe_ecdh_decrypt() separates _r_concat_kdf() and gnutls_hash_fast() functionality. per the spec, 'n' rounds needs to performed. refer: https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.2

Since I am using this tool for cross functional testing, I have patched the source to follow the spec.

babelouest commented 1 year ago

Hello,

I'm not sure I'm following, can you quote the part in the specs where it specifies the number of rounds required to get the key?

ksivask commented 1 year ago

https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.2 section references the NIST spec for KDF.

https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf 5.8.1.1 The Single-Step KDF Specification

Also check https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2 for specs on various AES_XXX_CBC_HMAC_SHA_XXX.

The gnutls_hash_fast(GNUTLS_DIG_SHA256, kdf.data, kdf.size, derived_key) only generates 32 byte hash, thus only R_JWA_ENC_A128CBC works as expected. Both R_JWA_ENC_A192CBC and R_JWA_ENC_A256CBC dont have the full input key "K"

babelouest commented 1 year ago

So if I follow you, I should use gnutls_hash_fast with GNUTLS_DIG_SHA384 on R_JWA_ENC_A192CBC and GNUTLS_DIG_SHA512 on R_JWA_ENC_A256CBC? I may have misread the RFC where it states:

   It uses the HMAC message
   authentication code [[RFC2104](https://www.rfc-editor.org/rfc/rfc2104)]
   with the SHA-256 hash function [[SHS](https://www.rfc-editor.org/rfc/rfc7518.html#ref-SHS)] to
   provide message authentication, with the HMAC output truncated to 128
   bits, corresponding to the HMAC-SHA-256-128 algorithm defined in
   [[RFC4868](https://www.rfc-editor.org/rfc/rfc4868)].

But you mentioned the number of rounds to hash the key, I don't see anything in the specs that would suggest other number of round than 1, did I misread that too?

ksivask commented 1 year ago

no, thats not what I meant.

Per https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.2, the "Digest Method is SHA-256" for "Key Derivation for ECDH Key Agreement". Per the NIST, the reps = ceil(keydatalen / hashlen). please review the 5.8.1.1 section "Process".

for example: for A256CBC, you have to do 2 rounds of KDF. K is derived_key. 1st round: sha256(1, ZKey, OtherInfo) = 1st 32 bytes of K 2nd round: sha256(2, ZKey, OtherInfo) = 2nd 32 bytes of K.

For A192CBC, same as above, but K truncated to 48 bytes. thus MAC_KEY and ENC_KEY each get 24 bytes.

The above is roughly my patch in plaintext. I can send a PR after running the test cases.

babelouest commented 1 year ago

The above is roughly my patch in plaintext. I can send a PR after running the test cases.

Yes please, that would be appreciated

babelouest commented 1 year ago

Closed in https://github.com/babelouest/rhonabwy/pull/30