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

openssl key wrap differs #21

Closed
soleinik-figment opened this issue Sep 5, 2022 · 13 comments
Closed

openssl key wrap differs #21

soleinik-figment opened this issue Sep 5, 2022 · 13 comments

Comments

@soleinik-figment
Copy link

soleinik-figment commented Sep 5, 2022

Hello,
Thank you for making aes_kw library available!

However, there might be issue, or lack of understanding on my side...
I'm trying to upload ed25519 key into HashiCorp Vault and using aes_kw to wrap ed25519 signing key with AES256... Unfortunately, Vault unable to unwrap ed25519...

Using same ed25519 and AES256 keys, openssl produces correct wrapping and Vault imports key with no error. I'm not sure what the issues is, very well could be "user error" :-).

Attached code snippet outlines my expectations..

`
const IN_ED25519: &str =
"RWaT0oB0VB8e3v0MBesnwguR5b+gTeS/gFqALEDcmE+cRiE2qOeld+yiO+zyamGGGSBp3AcHNCFuewZqqxdRUw==";
const IN_AES256: &str = "nB0HVnvTXp65QtpDsAM0vq2LI9G/wQGOhOQ04l1y2JM=";

//openssl enc -id-aes256-wrap-pad -iv A65959A6 -K $( hexdump -v -e '/1 "%02x"' < "./in-aes-key.bin" ) -in "in-ed25519.bin" -out "out-wrapped-ed25519.bin"
const WRAPPED_IN_ED_WITH_IN_AES: &str =
    "t1/OHaQBU7YjJrtNxYRGXkdURFRN2v2K5MzFzSOFK10Ek1KyGIW9GMoCy7jdpXJ88XMsyYgB0pk=";

pub fn main() {
    let in_aes_key = base64::decode(IN_AES256).unwrap();
    let in_ed25519_key = base64::decode(IN_ED25519).unwrap();

    let mut aes_key = [0u8; 32];
    aes_key.copy_from_slice(&in_aes_key[..32]);

    let kek = aes_kw::KekAes256::from(aes_key);
    let wrapped_input_key = kek
        .wrap_with_padding_vec(&in_ed25519_key)
        .expect("input key wrapping error!");

    println!("openssl:{}", WRAPPED_IN_ED_WITH_IN_AES);
    println!("aes_kw :{}", base64::encode(wrapped_input_key.clone()));
    assert_eq!(
        base64::decode(WRAPPED_IN_ED_WITH_IN_AES).unwrap(),
        wrapped_input_key
    );
}

`
for the reference, HashiCorp uses golang to unwrap, here is the link
https://github.com/google/tink/blob/master/go/kwp/subtle/kwp.go#L184

openssl produces 56 bytes and aes_kw - 40
aes_kw:ivR/2HMzPVKz6p8gPqQMHITCBmY80y8hjhdswGCOHSOlfaGezVGUZw==, size:40
openssl:t1/OHaQBU7YjJrtNxYRGXkdURFRN2v2K5MzFzSOFK10Ek1KyGIW9GMoCy7jdpXJ88XMsyYgB0pk=, size:56

@newpavlov
Copy link
Member

cc @cryptographix

@tarcieri
Copy link
Member

tarcieri commented Sep 6, 2022

I'm not exactly an AES-KW expert, but I'm unclear why the output is 56-bytes in the case of openssl. I would expect the output to be 40 bytes.

From RFC5649 § 4.1:

If m is not a multiple of 8, pad the plaintext octet string on the
right with octets {Q[m+1], ..., Q[r]} of zeros, where r is the
smallest multiple of 8 that is greater than m. If m is a multiple
of 8, then there is no padding, and r = m
.

(emphasis mine)

Since 32 is a multiple of 8, there shouldn't be additional padding. So I would expect the resulting message to equal the length of the original plaintext (32) plus the semiblock size (8): 32 + 8 = 40.

I'm confused why the output from OpenSSL would be 16-bytes longer.

@cryptographix
Copy link
Contributor

cryptographix commented Sep 6, 2022

Just tested the openssl command, and it also generates a wrapped key of 40 bytes.
-rw-r--r-- 1 sean staff 32 Sep 6 12:57 in-aes-key.bin
-rw-r--r-- 1 sean staff 32 Sep 6 12:57 in-ed25519.bin
-rw-r--r-- 1 sean staff 40 Sep 6 12:57 out-wrapped-ed25519.bin

@soleinik-figment can you post a list of sizes for your input files.
And maybe also check if the ed25519 key file is binary or incorrectly converted to base64.

Can you generate some test key files AES + ED25519 and post their hex contents?

@soleinik-figment
Copy link
Author

soleinik-figment commented Sep 6, 2022

Hello Tony (I feel like already met you before - tmkms :-) and cryptographix! thank you for looking into this!

=== this are the guides i follow for openssl =============
https://price2meet.com/gcp/docs/kms_docs_wrapping-a-key.pdf
https://cloud.google.com/kms/docs/wrapping-a-key

-rw-rw-r-- 1 1000 1000 32 Sep 6 16:13 in-aes256.der
-rw-rw-r-- 1 1000 1000 48 Sep 5 23:02 in-ed25519-prv.der
-rw-r--r-- 1 root root 56 Sep 6 16:13 out-ed25519-wrapped.bin

=========script ========
#!/bin/bash
OPENSSL_V110=openssl

TEMP_AES_KEY=./in-aes256.der
TRG_KEY=./in-ed25519-prv.der

"${OPENSSL_V110}" rand -out "${TEMP_AES_KEY}" 32

TRG_KEY_WRAPPED=out-ed25519-wrapped.bin

"${OPENSSL_V110}" enc -id-aes256-wrap-pad
-iv A65959A6
-nosalt
-K $( hexdump -v -e '/1 "%02x"' &lt; "${TEMP_AES_KEY}" )
-in "${TRG_KEY}"
-out "${TRG_KEY_WRAPPED}"

===========Dockerfile=========================
FROM debian:bookworm-slim

RUN apt-get update
&& apt-get install -y ca-certificates wget bash
&& apt-get -qy install perl patch vim less bsdmainutils mc

RUN apt-get -y remove openssl

RUN apt-get -qy install gcc

COPY ./patch.sh /

RUN apt-get -q update && apt-get -qy install wget make
&& wget https://www.openssl.org/source/openssl-1.1.0l.tar.gz
&& tar -xzvf openssl-1.1.0l.tar.gz
&& /patch.sh
&& cd openssl-1.1.0l
&& ./config
&& make install

WORKDIR /work

RUN ldconfig

ENV PATH "$PATH:/usr/local/ssl/bin"

===============================================
cat patch.sh
#!/bin/bash

cat <<-EOF | patch -d . -p0
--- orig/openssl-1.1.0l/apps/enc.c 2020-01-17 14:39:54.991708785 -0500
+++ openssl-1.1.0l/apps/enc.c 2020-01-17 14:41:33.215704269 -0500
@@ -482,6 +482,7 @@
*/

     BIO_get_cipher_ctx(benc, &ctx);
  • EVP_CIPHER_CTX_set_flags(ctx, EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);

     if (!EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, enc)) {
         BIO_printf(bio_err, "Error setting cipher %s\n",
    

EOF

@soleinik-figment
Copy link
Author

I see that ed25519 is not 32 bytes.
openssl genpkey -algorithm ed25519 -outform DER -out in-ed25519-prv.der

but, inside both keys are 32
openssl pkey -in ./in-ed25519-prv.der -pubout -text -inform DER

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA2hEZGvmVwqWZbquI2U1Fclwo0JgRPAVXA0HPK5PoXT8=
-----END PUBLIC KEY-----
ED25519 Private-Key:
priv:
c8:cf:55:a4:33:02:98:1c:2b:2f:41:42:dc:a1:57:
6c:b9:5f:c3:d2:0d:a2:e7:cb:c3:7f:33:bd:06:d3:
17:9e
pub:
da:11:19:1a:f9:95:c2:a5:99:6e:ab:88:d9:4d:45:
72:5c:28:d0:98:11:3c:05:57:03:41:cf:2b:93:e8:
5d:3f

@tarcieri
Copy link
Member

tarcieri commented Sep 6, 2022

Aah, sorry, it's a PKCS#8-encoded private key.

Well that's even more confusing, as it seems to be a 64-byte PKCS#8 document, so I would expect the ciphertext to be 64 + 8 = 72 bytes.

@cryptographix
Copy link
Contributor

cryptographix commented Sep 6, 2022

ok, Sergey, the problem is that the ed25519 key is in DER (PKCS8) format, as @tarcieri stated, and which includes additional bytes encoded in ASN.1.

od -t x1 <in-ed25519-prv.der
0000000 30 2e 02 01 00 30 05 06 03 2b 65 70 04 22 04 20
0000020 32 84 66 b5 8b 71 08 26 d1 83 83 86 5a 5c ea 17
0000040 b8 7e 4d 27 aa 38 37 d7 ac 8e 81 f4 2d d6 cc c5

The first 16 bytes are the ASN.1 that describes the contents as a ED25519 key. You can post the hex bytes into an ASN.1 decoder, such as (https://lapo.it/asn1js/#MC4CAQAwBQYDK2VwBCIEIDKEZrWLcQgm0YODhlpc6he4fk0nqjg316yOgfQt1szF)

You either need to add these initial bytes to the raw key, or use a suitable encoder.

@tarcieri
Copy link
Member

tarcieri commented Sep 6, 2022

Aha, so IN_ED25519 is a hex-encoded public + private key? (32-bytes || 32-bytes = 64-bytes)

That makes sense then. The 16-byte PKCS#8v1 header + 32-bytes = 48-bytes + 8-byte semiblock = 56-bytes (PKCS#8v1 omits the public key)

@soleinik-figment you can use ed25519::KeypairBytes to encode the private key as PKCS#8, although the PKCS#8 header is that fixed bytestring (30 2e 02 01 00 30 05 06 03 2b 65 70 04 22 04 20) so you can just prepend that as well if you'd like to avoid the dependency.

@soleinik-figment
Copy link
Author

@cryptographix - i'm kinda "fresh" in this field, I hope this is what you asked

od -t x1 < in-ed25519-prv.der

0000000 30 2e 02 01 00 30 05 06 03 2b 65 70 04 22 04 20
0000020 c8 cf 55 a4 33 02 98 1c 2b 2f 41 42 dc a1 57 6c
0000040 b9 5f c3 d2 0d a2 e7 cb c3 7f 33 bd 06 d3 17 9e
0000060

Tony - this is signing key (Tendermint), from that little that I understand - its pub+priv (and should be 64?)

@soleinik-figment
Copy link
Author

Tony, are you saying

    let mut aes_key = [0u8; KEY_SIZE_AES256 + 16]; <---- prepend to this?  
    aes_key.copy_from_slice(&v_aes_key[..KEY_SIZE_AES256]); < and shift this

    let kek = aes_kw::KekAes256::from(aes_key.clone());
    let wrapped_input_key = kek
        .wrap_with_padding_vec(&expanded_ed25519_key.to_bytes())
        .expect("input key wrapping error!");

@tarcieri
Copy link
Member

tarcieri commented Sep 6, 2022

@soleinik-figment the following should more or less work:

const PKCS8_HEADER: [u8; 16] = b"\x30\x2e\x02\x01\x00\x30\x05\x06\x03\x2b\x65\x70\x04\x22\x04\x20";

let secret_key = [200, 207, 85, 164, 51, 2, 152, 28, 43, 47, 65, 66, 220, 161, 87, 108, 185, 95, 195, 210, 13, 162, 231, 203, 195, 127, 51, 189, 6, 211, 23, 158];

debug_assert_eq!(secret_key.len(), 32);

let mut pkcs8_key = Vec::from(PKCS8_HEADER);
pkcs8_key.extend_from_slice(&secret_key);

...then you keywrap pkcs8_key

@soleinik-figment
Copy link
Author

@tarcieri - that worked!

    let key_pair = ed25519_dalek::Keypair::generate(&mut rand_v7::rngs::OsRng {});
    let secret_key = key_pair.secret.to_bytes();

    const PKCS8_HEADER: &[u8; 16] =
        b"\x30\x2e\x02\x01\x00\x30\x05\x06\x03\x2b\x65\x70\x04\x22\x04\x20";
        
    debug_assert_eq!(secret_key.len(), 32);

    let mut pkcs8_key = Vec::from(*PKCS8_HEADER);
    pkcs8_key.extend_from_slice(&secret_key);

and HashiCorp took it!

You guys awesome! thank you so much!

@soleinik-figment
Copy link
Author

Resolved! Issue turned out to be ed25519 private key PKCS8 encoding issue (to match openssl)

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

No branches or pull requests

4 participants