shadowsocks/crypto2

How can I test AesSivCmac512 correctly?

Closed this issue ยท 13 comments

I use miscreant.aes.siv in my python script and I'm trying to migrate it to Rust using Crypto2 AES SIV. (miscreant.rs does not support Aes512Cmac now)

Mac and Key are 32bit. So I need the Aes512Cmac. But, It's not the same results.
I think my rust code is something wrong. I tried to figure it out and find it in the Crypto2 documents, but I couldn't.
How can I get the same results correctly like the miscreant-py?

Python test script

from miscreant.aes.siv import SIV

mac = [210, 74, 211, 191, 98, 164, 14, 238, 213, 192, 46, 64, 133, 107, 253, 41, 115, 180, 47, 142, 210, 48, 204, 114, 122, 55, 36, 204, 135, 225, 178, 130]
key = [232, 50, 179, 61, 118, 166, 202, 75, 10, 83, 158, 144, 212, 124, 251, 51, 71, 61, 233, 200, 200, 21, 19, 53, 41, 66, 242, 96, 230, 20, 194, 195]

print('MAC:', len(mac), [int(i) for i in mac])
print('KEY:', len(key), [int(i) for i in key])
for id in '', 'ff3bb8f2-dd51-4ac6-9c79-cb0ab79c23e5':
    siv = SIV(bytes(mac + key))
    out = siv.seal(id.encode())
    print('ID:', id)
    print('OUT:', len(out), [int(i) for i in out])
    print()
MAC: 32 [210, 74, 211, 191, 98, 164, 14, 238, 213, 192, 46, 64, 133, 107, 253, 41, 115, 180, 47, 142, 210, 48, 204, 114, 122, 55, 36, 204, 135, 225, 178, 130]
KEY: 32 [232, 50, 179, 61, 118, 166, 202, 75, 10, 83, 158, 144, 212, 124, 251, 51, 71, 61, 233, 200, 200, 21, 19, 53, 41, 66, 242, 96, 230, 20, 194, 195]

ID: 
OUT: 16 [248, 84, 15, 233, 58, 91, 208, 105, 201, 101, 161, 127, 136, 242, 21, 134]

ID: ff3bb8f2-dd51-4ac6-9c79-cb0ab79c23e5
OUT: 52 [86, 67, 223, 72, 65, 160, 109, 83, 86, 81, 231, 36, 195, 68, 40, 120, 43, 219, 25, 194, 233, 134, 143, 212, 219, 19, 187, 159, 110, 229, 109, 111, 150, 57, 230, 50, 24, 120, 210, 146, 178, 254, 204, 20, 31, 10, 22, 128, 97, 143, 172, 74]

Rust test code

use crypto2::aeadcipher::AesSivCmac512;

let id = "";
let mac: [u8; 32] = [210, 74, 211, 191, 98, 164, 14, 238, 213, 192, 46, 64, 133, 107, 253, 41, 115, 180, 47, 142, 210, 48, 204, 114, 122, 55, 36, 204, 135, 225, 178, 130];
let key: [u8; 32] = [232, 50, 179, 61, 118, 166, 202, 75, 10, 83, 158, 144, 212, 124, 251, 51, 71, 61, 233, 200, 200, 21, 19, 53, 41, 66, 242, 96, 230, 20, 194, 195];
let mut key = Vec::new();
key.write(mac).unwrap();
key.write(primary_master_key).unwrap();
let cipher = AesSivCmac512::new(&key);
let mut plaintext = Vec::new();
plaintext.write(id.as_bytes()).unwrap();
for _ in 0..AesSivCmac512::TAG_LEN {
    plaintext.insert(0, 0);
}
// for _ in 0..16 {
//     plaintext.push(0);
// }
cipher.encrypt_slice(&[], &mut plaintext);
println!("KEY {:?}", key);
println!("MAC {:?}", mac);
println!("ID {:?}", id);
println!("OUT {} {:?}", plaintext[AesSivCmac512::TAG_LEN..].len(), &plaintext[AesSivCmac512::TAG_LEN..]);
PLAINTEXT: 16 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
KEY [232, 50, 179, 61, 118, 166, 202, 75, 10, 83, 158, 144, 212, 124, 251, 51, 71, 61, 233, 200, 200, 21, 19, 53, 41, 66, 242, 96, 230, 20, 194, 195]
MAC [210, 74, 211, 191, 98, 164, 14, 238, 213, 192, 46, 64, 133, 107, 253, 41, 115, 180, 47, 142, 210, 48, 204, 114, 122, 55, 36, 204, 135, 225, 178, 130]
ID ""
OUT 0 []

miscreant.rs does not support Aes512Cmac now

in miscreant.rs, the cipher AEAD_AES_SIV_CMAC_512 is called Aes256SivAead. but crypto2 is called AesSivCmac512.

Test Code:

extern crate hex;
extern crate crypto2;
extern crate miscreant;

use crypto2::aeadcipher::AesSivCmac512;


fn siv_encrypt(key: &[u8], plaintext: &[u8], py_ciphertext: &[u8]) {
    println!("================  SIV ENC ====================");
    println!("AES-SIV-CMAC-512 Key: {:?}", hex::encode(&key));
    println!("           Plaintext: {:?}", hex::encode(&plaintext));
    println!();

    // python miscreant AES-SIV-CMAC-512
    // https://github.com/miscreant/miscreant.py
    {
        println!("Python Miscreant result: {:?}", hex::encode(&py_ciphertext));
    }
    println!();

    // miscreant AES-SIV-CMAC-512
    {
        use miscreant::Aead;
        use miscreant::Aes256SivAead;

        let mut cipher = Aes256SivAead::new(&key);

        let nonce: [u8; 0] = [];
        let aad: [u8; 0]   = [];

        let mut ciphertext = vec![0u8; AesSivCmac512::TAG_LEN]; // V || P(C)
        ciphertext.extend_from_slice(&plaintext);

        cipher.encrypt_in_place(&nonce, &aad, &mut ciphertext);
        println!("       Miscreant result: {:?}", hex::encode(&ciphertext));
    }
    println!();


    // crypto2 AES-SIV-CMAC-512
    {
        let cipher = AesSivCmac512::new(&key);

        let nonce: [u8; 0] = [];
        let aad: [u8; 0]   = [];
        
        let mut ciphertext = vec![0u8; AesSivCmac512::TAG_LEN]; // V || P(C)
        ciphertext.extend_from_slice(&plaintext);

        let components: &[&[u8]] = &[];
        cipher.encrypt_slice(&components, &mut ciphertext);
        println!("         Crypto2 result: {:?}", hex::encode(&ciphertext));


        let mut ciphertext = vec![0u8; AesSivCmac512::TAG_LEN]; // V || P(C)
        ciphertext.extend_from_slice(&plaintext);

        let components: &[&[u8]] = &[&aad];
        cipher.encrypt_slice(&components, &mut ciphertext);
        println!("         Crypto2 result: {:?}", hex::encode(&ciphertext));


        let mut ciphertext = vec![0u8; AesSivCmac512::TAG_LEN]; // V || P(C)
        ciphertext.extend_from_slice(&plaintext);

        let components: &[&[u8]] = &[&aad, &nonce];
        cipher.encrypt_slice(&components, &mut ciphertext);
        println!("         Crypto2 result: {:?}", hex::encode(&ciphertext));
    }
    println!();
}

fn main() {
    let key: [u8; AesSivCmac512::KEY_LEN] = [
        210, 74, 211, 191, 98, 164, 14, 238, 213, 192, 46, 64, 133, 107, 253, 41, 
        115, 180, 47, 142, 210, 48, 204, 114, 122, 55, 36, 204, 135, 225, 178, 130,

        232, 50, 179, 61, 118, 166, 202, 75, 10, 83, 158, 144, 212, 124, 251, 51, 
        71, 61, 233, 200, 200, 21, 19, 53, 41, 66, 242, 96, 230, 20, 194, 195,
    ];

    let plaintext = "".as_bytes(); // empty plaintext
    let py_ciphertext = [248, 84, 15, 233, 58, 91, 208, 105, 201, 101, 161, 127, 136, 242, 21, 134];
    siv_encrypt(&key, &plaintext, &py_ciphertext);


    let plaintext = "ff3bb8f2-dd51-4ac6-9c79-cb0ab79c23e5".as_bytes();
    let py_ciphertext = [
        86, 67, 223, 72, 65, 160, 109, 83, 86, 81, 231, 36, 195, 68, 40, 120, 43, 219, 25, 
        194, 233, 134, 143, 212, 219, 19, 187, 159, 110, 229, 109, 111, 150, 57, 230, 50, 
        24, 120, 210, 146, 178, 254, 204, 20, 31, 10, 22, 128, 97, 143, 172, 74
    ];
    siv_encrypt(&key, &plaintext, &py_ciphertext);
}

Result:

================  SIV ENC ====================
AES-SIV-CMAC-512 Key: "d24ad3bf62a40eeed5c02e40856bfd2973b42f8ed230cc727a3724cc87e1b282e832b33d76a6ca4b0a539e90d47cfb33473de9c8c81513352942f260e614c2c3"
           Plaintext: ""

Python Miscreant result: "f8540fe93a5bd069c965a17f88f21586"

       Miscreant result: "f1423fb8d115bdc05a056db221ecd4c7"

         Crypto2 result: "8290092e8208c6a7fc317ae0c1d4eb03"
         Crypto2 result: "307386bf3b8659c1b346738a29ce071e"
         Crypto2 result: "f1423fb8d115bdc05a056db221ecd4c7"
================  SIV ENC ====================
AES-SIV-CMAC-512 Key: "d24ad3bf62a40eeed5c02e40856bfd2973b42f8ed230cc727a3724cc87e1b282e832b33d76a6ca4b0a539e90d47cfb33473de9c8c81513352942f260e614c2c3"
           Plaintext: "66663362623866322d646435312d346163362d396337392d636230616237396332336535"

Python Miscreant result: "5643df4841a06d535651e724c34428782bdb19c2e9868fd4db13bb9f6ee56d6f9639e6321878d292b2fecc141f0a1680618fac4a"

       Miscreant result: "aaaacb6cb8ccc0ebb053d78ea5d8bb907dbf6c65e9e842c46f26249b08da5c547675d27b52ce1f6a99c2b212688753c964d37e77"

         Crypto2 result: "5643df4841a06d535651e724c34428782bdb19c2e9868fd4db13bb9f6ee56d6f9639e6321878d292b2fecc141f0a1680618fac4a"
         Crypto2 result: "4fcc56329c6a6eea569dbc65dcc880e3efdec99ee781449f655adff1ff2f53d6b579ccd75b9f583a57a5dc265139a8412c02ebcf"
         Crypto2 result: "aaaacb6cb8ccc0ebb053d78ea5d8bb907dbf6c65e9e842c46f26249b08da5c547675d27b52ce1f6a99c2b212688753c964d37e77"

If you set aad and nonce correctly, u can ses the crypto2 result is same rust-miscreant and python-miscreant.

There is only one exception, that is, when you encrypt a empty plaintext, the result will be different. That is because the implementation of python-miscreant is wrong. See the note here. The implementation of rust-miscreant and crypto2 is correct.

There is only one exception, that is, when you encrypt a empty plaintext, the result will be different.

Wow, I didn't know that. Thanks for your answer and the pieces of code.

Actually, my python script was written in Java at first and I used the Cryptomator siv-mode library. Both siv-mode library and miscreant-py module get same encrypted results when I put an empty plaintext. That's why I thought miscreant-py was right. And I guess the developers of the Cryptomator deal with an empty plaintext as a valid input because of Cryptomator Architecture.

The directory ID for the root directory is the empty string. For all other directories, it is a random sequence of at most 36 ASCII chars. We recommend using random UUID.

I found the same comment in the siv-mode library and the related issue #14. n>0 of the note seems to explain the plaintext always be a last vector, not zero-length vector. So, S2V should process a last zero-length vector(empty plaintext). If I am wrong, please correct me.

And I'm confusing a little. You mean, miscreant-py did implement wrong encryption when got an empty plaintext?
In Crypto2, an empty plaintext makes different results and Is the result right?

Test Java code:

import org.cryptomator.siv.SivMode;

public class CryptomatorSivMode {
    private static final SivMode AES_SIV = new SivMode();
    public static void main(String[] args) {
        byte[] ctrKey = {(byte)232, (byte)50, (byte)179, (byte)61, (byte)118, (byte)166, (byte)202, (byte)75, (byte)10, (byte)83, (byte)158, (byte)144, (byte)212, (byte)124, (byte)251, (byte)51, (byte)71, (byte)61, (byte)233, (byte)200, (byte)200, (byte)21, (byte)19, (byte)53, (byte)41, (byte)66, (byte)242, (byte)96, (byte)230, (byte)20, (byte)194, (byte)195};
        byte[] macKey = {(byte)210, (byte)74, (byte)211, (byte)191, (byte)98, (byte)164, (byte)14, (byte)238, (byte)213, (byte)192, (byte)46, (byte)64, (byte)133, (byte)107, (byte)253, (byte)41, (byte)115, (byte)180, (byte)47, (byte)142, (byte)210, (byte)48, (byte)204, (byte)114, (byte)122, (byte)55, (byte)36, (byte)204, (byte)135, (byte)225, (byte)178, (byte)130};
        byte[] encrypted = null;
        encrypted = AES_SIV.encrypt(ctrKey, macKey, "".getBytes());
        for(byte b : encrypted) System.out.print(String.format("%d, ", (int) b & 0xFF));
        System.out.println();
        for(byte b : encrypted) System.out.print(String.format("%02X", (int) b & 0xFF));
    }
}

Results:

248, 84, 15, 233, 58, 91, 208, 105, 201, 101, 161, 127, 136, 242, 21, 134, 
F8540FE93A5BD069C965A17F88F21586

Hmm, miscreant's rust version also produces the same result as crypto2.

Follow the RFC-5297

the plaintext is `P`, the associated data is `AD1` through `ADn`, 
`V` is the synthetic `IV`, the ciphertext is `C`, and `Z` is the output.

Algorithmically, SIV Encrypt can be described as:

    SIV-ENCRYPT(K, P, AD1, ..., ADn) {
        K1 = leftmost(K, len(K)/2)
        K2 = rightmost(K, len(K)/2)

        V = S2V(K1, AD1, ..., ADn, P)

        ...
    }

S2V with key K on a vector of n inputs S1, S2, ..., Sn-1, Sn, and len(Sn) < 128:

Algorithmically S2V can be described as:

    S2V(K, S1, ..., Sn) {
        if n = 0 then
            # The code here leads to different calculation results
            return V = AES-CMAC(K, <one>)
        fi

        ...
    }

When you try to encrypt a empty plaintext without AD and NONCE,

the miscreant's python implementation (also the Java implementation you mentioned) thinks N is not equal to 0, while my implementation thinks N should be equal to 0.

The RFC-5297 does not clearly define this issue,
and i personally do not take a position on this issue.

I saw google/wycheproof collected this case.

and the test vectors shows that my implementation is correct :)

Test Code:

#[test]
fn test_aes_siv_cmac_256_wycheproof_t2() {
    // https://github.com/google/wycheproof/blob/master/testvectors/aes_siv_cmac_test.json#L29-L36
    let key = hex::decode("2b27e429fb6c02678e589ccc4437c5adfb44b331ab6d21ea321727e6ec03d354").unwrap();
    let aad: [u8; 0]       = [];
    let plaintext: [u8; 0] = [];

    let cipher = AesSivCmac256::new(&key);

    let mut ciphertext = vec![0u8; AesSivCmac256::TAG_LEN];
    ciphertext.extend_from_slice(&plaintext);

    cipher.encrypt_slice(&[&aad], &mut ciphertext);

    assert_eq!(&ciphertext[..],
        &hex::decode("b2b2354e3724dcdaa85ecf029b49a90c").unwrap()[..]);
}

#[test]
fn test_aes_siv_cmac_256_wycheproof_t3() {
    // https://github.com/google/wycheproof/blob/master/testvectors/aes_siv_cmac_test.json#L39-L46
    let key = hex::decode("e40992eb4f649e5d49134652aecc24bafa6b45ce8dd9e9d371ede7d5de84fa72").unwrap();
    let aad = hex::decode("8268c5194a71aed0fc1dafe3").unwrap();
    let plaintext: [u8; 0] = [];
    
    let cipher = AesSivCmac256::new(&key);
    
    let mut ciphertext = vec![0u8; AesSivCmac256::TAG_LEN];
    ciphertext.extend_from_slice(&plaintext);
    
    cipher.encrypt_slice(&[&aad], &mut ciphertext);
    
    assert_eq!(&ciphertext[..],
        &hex::decode("92bc07ee200fbd488b7f70a10da26a21").unwrap()[..]);
}

BTW:

the google/wycheproof test vectors is widely used in different cryptographic libraries, For example:

Thanks for response.

the miscreant's python implementation (also the Java implementation you mentioned) thinks N is not equal to 0, while my implementation thinks N should be equal to 0.

The RFC-5297 does not clearly define this issue,
and i personally do not take a position on this issue.

I agree your opinion :)

I already tested miscreant.go and it's the same as Crypto2. I got to know miscreant.py makes the different result when I put a zero-length vector.

In addition, miscreant.rs just uses aes-siv to encrypt and decrypt messages. I saw its test-vector and they already has been testing it.

I think I have to implement own encryption and decryption code for an empty input using Crypto2.
Could you give me some hints instead of coping-and-paste whole AesSiv of Crypto2?

@hwiorn

I think I have to implement own encryption and decryption code for an empty input using Crypto2.
Could you give me some hints instead of coping-and-paste whole AesSiv of Crypto2?

I can add a special function to deal with this problem, but I haven't thought of a name yet.

I can add a special function to deal with this problem, but I haven't thought of a name yet.

Wow! Thankfully, I can migrate rest my code to Rust until you add the function.
Please let me know. I'll wait for the update :)

@hwiorn See commit 9de9753

// Default encrypt
cipher.encrypt_slice(&[], &mut ciphertext); // not ignore empty

// With option
let ignore_empty = true;
cipher.encrypt_slice_opt(&[], &mut ciphertext, ignore_empty);

I would recommend:

struct EncryptOpts {
    ignore_empty: bool,
}

impl Default for EncryptOpts {
    fn default() -> EncryptOpts {
        EncryptOpts {
            ignore_empty: false
        }
    }
}

let opt = EncryptOpts {
    ignore_empty: true
};
cipher.encrypt_slice_opt(&[], &mut ciphertext, &opt);

@zonyitoo

See commit 773d32d

// Default encrypt
cipher.encrypt_slice(&[], &mut ciphertext); // not ignore empty

// With option
let opts = AesSivCmacOpts { ignore_empty: true };
cipher.encrypt_slice_opt(&[], &mut ciphertext, &opts);

@LuoZijun

I tested a SIV decryption when plaintext is greater than AesSivCmac512::BLOCK_LEN and got an assertion failure.

Can it be the same problem? Could you give me advice?

{
    let key = hex::decode("d24ad3bf62a40eeed5c02e40856bfd2973b42f8ed230cc727a3724cc87e1b282\
e832b33d76a6ca4b0a539e90d47cfb33473de9c8c81513352942f260e614c2c3").unwrap();
    let plaintext = hex::decode("65c2c7afe890092fe7515260637d9fe01b74699022564f79").unwrap();
    let cipher = AesSivCmac512::new(&key);
    let mut ciphertext = vec![0u8; AesSivCmac512::TAG_LEN];
    ciphertext.extend_from_slice(&plaintext);
    // let components: &[&[u8]] = &[];
    cipher.decrypt_slice(&[], &mut ciphertext);
}
thread 'main' panicked at 'assertion failed: m.len() >= Self::BLOCK_LEN', /home/gglee/.cargo/registry/src/github.com-1ecc6299db9ec823/crypto2-0.1.1/src/blockmode/siv.rs:394:1

Same python code using miscreant.py:

from miscreant.aes.siv import SIV
from binascii import unhexlify, hexlify
plaintext = unhexlify('65c2c7afe890092fe7515260637d9fe01b74699022564f79')
siv = SIV(unhexlify('d24ad3bf62a40eeed5c02e40856bfd2973b42f8ed230cc727a3724cc87e1b282e832b33d76a6ca4b0a539e90d47cfb33473de9c8c81513352942f260e614c2c3'))
print(hexlify(siv.open(plaintext, associated_data=[b''])))

result:

b'746573742e747874'

@hwiorn

The debugging assertion is indeed incorrect, but it is not related to the problem you are encountering.

let key = hex::decode("d24ad3bf62a40eeed5c02e40856bfd2973b42f8ed230cc727a3724cc87e1b282\
e832b33d76a6ca4b0a539e90d47cfb33473de9c8c81513352942f260e614c2c3").unwrap();
let mut ciphertext = hex::decode("65c2c7afe890092fe7515260637d9fe01b74699022564f79").unwrap();

let components: &[&[u8]] = &[&[]]; // NOTE: same as your python args.

let cipher = AesSivCmac512::new(&key);
let ret = cipher.decrypt_slice(&components, &mut ciphertext);
assert_eq!(ret, true);

let cleartext = &ciphertext[AesSivCmac512::TAG_LEN..]; // V | P
assert_eq!(cleartext, &hex::decode("746573742e747874").unwrap()[..]);

Ah.. I don't have to extend TAG size in ciphertext.... I noticed that just now....

let components: &[&[u8]] = &[&[]]; // NOTE: same as your python args.

Actually before I wrote the comment, yesterday I already tried every components arg you mentioned before, but no luck. Because of invalid decryption code.

I see my code was the problem. Thank you!