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

Some serializers fail to serialize/deserialize VerifyingKeys & Signatures correctly (k256) #536

Closed
BGluth opened this issue Mar 16, 2022 · 10 comments · Fixed by RustCrypto/traits#967

Comments

@BGluth
Copy link

BGluth commented Mar 16, 2022

It seems that some Serde serializers have issues when serializing VerifyingKeys and Signatures. They all serialize fine, but Serde returns an error for certain serializers when deserializing back to the original type.

I've tested it with three serializers, and it seems to be a bit of a hit or miss if it works. It's a bit verbose, but I've included tests that reproduce the issue. I would normally just include a Rust Playground link, but they don't support k256.

#[cfg(test)]
mod test {
    use k256::ecdsa::{Signature, SigningKey, signature::Signer};
    use rand::rngs::OsRng;
    use serde::{Deserialize, Serialize};
    use std::fmt::Debug;

    fn ser_deser_test<'a, D, S, T, U>(ser: S, deser: D, val: T)
    where
        D: Fn(&U) -> Result<T, String>,
        S: Fn(&T) -> Result<U, String>,
        T: Debug + Deserialize<'a> + PartialEq + Serialize,
    {
        let ser = ser(&val).unwrap();
        let deser = deser(&ser).expect("Deserialization failure");

        assert_eq!(val, deser);
    }

    fn create_fake_sig() -> Signature {
        let k = SigningKey::random(&mut OsRng);
        let msg: Vec<_> = (0..100).collect();

        k.sign(&msg)
    }

    #[test]
    fn json_signing_key() {
        ser_deser_test(
            |val| serde_json::to_string(val).map_err(|err| err.to_string()),
            |str| serde_json::from_str(str).map_err(|err| err.to_string()),
            SigningKey::random(&mut OsRng).verifying_key(),
        );
    }

    #[test]
    fn json_signature() {
        ser_deser_test(
            |val| serde_json::to_string(val).map_err(|err| err.to_string()),
            |str| serde_json::from_str(str).map_err(|err| err.to_string()),
            create_fake_sig(),
        );
    }

    #[test]
    fn toml_signing_key() {
        ser_deser_test(
            |val| toml::to_string(val).map_err(|err| err.to_string()),
            |str| toml::from_str(str).map_err(|err| err.to_string()),
            SigningKey::random(&mut OsRng).verifying_key(),
        );
    }

    #[test]
    fn toml_signature() {
        ser_deser_test(
            |val| toml::to_string(val).map_err(|err| err.to_string()),
            |str| toml::from_str(str).map_err(|err| err.to_string()),
            create_fake_sig(),
        );
    }

    #[test]
    fn bincode_signing_key() {
        ser_deser_test(
            |val| bincode::serialize(val).map_err(|err| err.to_string()),
            |bytes| bincode::deserialize(bytes).map_err(|err| err.to_string()),
            SigningKey::random(&mut OsRng).verifying_key(),
        );
    }

    #[test]
    fn bincode_signature() {
        ser_deser_test(
            |val| bincode::serialize(val).map_err(|err| err.to_string()),
            |bytes| bincode::deserialize(bytes).map_err(|err| err.to_string()),
            create_fake_sig(),
        );
    }
}

Dependencies:

[dependencies]
k256 = {version = "0.10.4", features = ["serde", "pem"] }
bincode = "1.3.3"
serde_json = "1.0.59"
rand = {version = "0.8.5", features = ["getrandom"] }
toml = "0.5.8"
serde = "1.0"
serde_yaml = "0.8"

With results:

test test::bincode_signing_key ... ok
test test::json_signature ... ok
test test::bincode_signature ... ok
test test::json_signing_key ... FAILED ("panicked at 'Deserialization failure: invalid type: sequence, expected a borrowed byte array at line 1 column 1")
test test::toml_signing_key ... FAILED ("panicked at 'Deserialization failure: "expected a right bracket, found a comma at line 1 column 4")
test test::toml_signature ... FAILED ("panicked at 'Deserialization failure: "expected an equals, found eof at line 1 column 131")

I'm not very experienced with cryptography, so let me know if I'm doing something that I shouldn't be. 🙂

@tarcieri
Copy link
Member

I'm guessing that the issue is JSON and TOML need toplevel objects/tables for a document to be valid. So you'll need to put the key/signature to be serialized in some kind of wrapper, e.g.

let signature = ...;
json! { signature }

@BGluth
Copy link
Author

BGluth commented Mar 16, 2022

Hmm yeah good thought, but adding the json! macro doesn't seem to change the output in this case:

let val = serde_json::to_string(&SigningKey::random(&mut OsRng).verifying_key()).unwrap();

println!("No macro: {}", val);
println!("macro:    {}", json! { val });
No macro: [48,86,48,16,6,7,42,134,72,206,61,2,1,6,5,43,129,4,0,10,3,66,0,4,151,186,209,214,82,234,77,92,223,222,77,182,58,126,76,14,210,40,246,84,246,31,230,123,226,176,86,8,212,12,34,89,205,44,227,187,169,45,191,199,156,171,190,66,116,19,133,33,107,206,96,64,211,234,77,10,64,148,237,193,199,7,198,178]
macro:    "[48,86,48,16,6,7,42,134,72,206,61,2,1,6,5,43,129,4,0,10,3,66,0,4,151,186,209,214,82,234,77,92,223,222,77,182,58,126,76,14,210,40,246,84,246,31,230,123,226,176,86,8,212,12,34,89,205,44,227,187,169,45,191,199,156,171,190,66,116,19,133,33,107,206,96,64,211,234,77,10,64,148,237,193,199,7,198,178]"

As a sanity check, all three serializers seem ok if I give them some arbitrary type:

#[derive(Clone, Copy, Deserialize, Serialize)]
struct SomeStruct {
    x: f32,
    y: String
}    

#[test]
fn general_ser_deser_test() {
    let some_struct = SomeStruct {
        x: 4.2,
        y: "cool string",
    };

    ser_deser_test(
        |val| serde_json::to_string(val).map_err(|err| err.to_string()),
        |str| serde_json::from_str(str).map_err(|err| err.to_string()),
        some_struct,
    );

    ser_deser_test(
        |val| toml::to_string(val).map_err(|err| err.to_string()),
        |str| toml::from_str(str).map_err(|err| err.to_string()),
        some_struct,
    );

    ser_deser_test(
        |val| bincode::serialize(val).map_err(|err| err.to_string()),
        |bytes| bincode::deserialize(bytes).map_err(|err| err.to_string()),
        some_struct,
    );
}

@tarcieri
Copy link
Member

You'll need to construct the outer JSON/TOML object before serializing as a string.

@BGluth
Copy link
Author

BGluth commented Mar 16, 2022

I think it's possible that I may be misunderstanding, but if the failure was caused by the outer JSON/TOML missing, then shouldn't the serialization tests of SomeStruct be failing?

@tarcieri
Copy link
Member

A struct maps directly to a JSON object or TOML table. So there isn't an issue there.

Can you try adding a json! wrapper which places the value inside a JSON object and confirm that fixes the issue?

@BGluth
Copy link
Author

BGluth commented Mar 16, 2022

Yeah I'm still getting a failure on deserialization:

#[test]
fn json_signing_key() {
    let k = SigningKey::random(&mut OsRng).verifying_key();

    let ser = &json!{ k }.to_string();
    let deser = serde_json::from_str(ser).expect("Deserialization failure");

    assert_eq!(k, deser);
}
Deserialization failure: Error("invalid type: sequence, expected a borrowed byte array", line: 1, column: 1)

@tarcieri
Copy link
Member

tarcieri commented Mar 16, 2022

Now you have the problem that you are trying to deserialize a SigningKey, but the JSON document contains a toplevel object with a single key k.

Try defining a struct to put the SigningKey in and serializing/deserializing that.

@BGluth
Copy link
Author

BGluth commented Mar 17, 2022

Thanks for the quick responses tarcieri.

It's unfortunately still failing, however it's failing when it tries to parse the value for the field of the struct, so I don't think it's related to something missing at the top of the JSON payload:

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
struct SomeStruct {
    k: VerifyingKey,
}

#[test]
fn json_signing_key() {
    let some_struct = SomeStruct {
        k: SigningKey::random(&mut OsRng).verifying_key(),
    };

    let ser = serde_json::to_string(&some_struct).unwrap();
    let deser = serde_json::from_str(&ser).expect("Deserialization failure");

    assert_eq!(some_struct, deser);
}
'Deserialization failure: Error("invalid type: sequence, expected a borrowed byte array", line: 1, column: 6)'

With the actual serialized payload, column 6 is the start of the value for the field x:

{"k":[48,86,48, ...

@tarcieri
Copy link
Member

Aah, that does appear to be a legitimate issue with the deserializer when used with a text-based format. It's currently decoding to a byte slice, which works with binary formats but not with text-based ones.

That's easy enough to fix by changing it to a Vec<u8>.

When encoding in JSON, there's also JWK as an interoperable format: https://docs.rs/elliptic-curve/latest/elliptic_curve/struct.JwkEcKey.html

tarcieri added a commit to RustCrypto/traits that referenced this issue Mar 17, 2022
This commit changes the `Serializer` and `Deserializer` impls on the
`PublicKey` type to work more like the ones on other types in this crate
and its downstream dependencies:

- Use binary DER serialization with binary formats (what it does today)
- Use an upper-case hex serialization of the DER in conjunction with
  "human readable" formats

Though richer formats exist for this use case (such as `JwkEcKey` which
is an existing implementation of JWK), hex-serialized DER is relatively
easy to work with, and can be decoded easily with online tools such as
https://lapo.it/asn1js/

Note that the `Deserialize` impl for `PublicKey` never worked with
human-readable formats as it was attempting to deserialize to `&[u8]`,
which doesn't work with formats like JSON or TOML because they need an
intermediate decoding step and therefore require deserialization to
`Vec<u8>`. So this isn't a breaking change from the `Deserialize` side.

Fixes RustCrypto/elliptic-curves#536
@tarcieri
Copy link
Member

So I elected to do what most of the other types in elliptic-curve and its downstream dependencies do: conditionally serialize the data as a hex string when the format is_human_readable:

RustCrypto/traits#967

Unfortunately in the current form it's a bit hard to test.

I'd really like to extract this serialization logic into a separate crate which can have the sort of tests you're doing in this issue against a number of different encoding formats so we can solve these serialization problems once and for all in a single place.

tarcieri added a commit to RustCrypto/traits that referenced this issue Mar 17, 2022
This commit changes the `Serializer` and `Deserializer` impls on the
`PublicKey` type to work more like the ones on other types in this crate
and its downstream dependencies:

- Use binary DER serialization with binary formats (what it does today)
- Use an upper-case hex serialization of the DER in conjunction with
  "human readable" formats

Though richer formats exist for this use case (such as `JwkEcKey` which
is an existing implementation of JWK), hex-serialized DER is relatively
easy to work with, and can be decoded easily with online tools such as
https://lapo.it/asn1js/

Note that the `Deserialize` impl for `PublicKey` never worked with
human-readable formats as it was attempting to deserialize to `&[u8]`,
which doesn't work with formats like JSON or TOML because they need an
intermediate decoding step and therefore require deserialization to
`Vec<u8>`. So this isn't a breaking change from the `Deserialize` side.

Fixes RustCrypto/elliptic-curves#536
ephemeralriggs pushed a commit to google/omaha-client that referenced this issue Feb 19, 2024
This CL replaces the default deserialization derivation with a custom
one which uses FromStr
(https://docs.rs/ecdsa/0.13.4/ecdsa/struct.VerifyingKey.html#impl-FromStr)
to deserialize from a PEM string into a p256::ecdsa::VerifyingKey. The
default deserialization derivation does not behave as expected:
RustCrypto/elliptic-curves#536.

This CL also adds a test case for deserializing that key from PEM, and
deserializing a PublicKeyAndId struct from JSON.

Change-Id: I2ea02fa56b70c430935e40c74f30d2a8e1173516
Bug: 95799
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/667108
Reviewed-by: Sen Jiang <senj@google.com>
Commit-Queue: James Buckland <jbuckland@google.com>
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 a pull request may close this issue.

2 participants