From 1eb929b38adff6be7aa1f9f9701ff9dcad67639a Mon Sep 17 00:00:00 2001 From: DrHuangMHT Date: Fri, 5 May 2023 02:36:05 +0800 Subject: [PATCH] feat(identity): allow importing and exporting ECDSA keys Implement encoding to/decoding from DER-encoded secret key document for `ecdsa::SecretKey`. Implement encoding to/decoding from protobuf format for ECDSA keys. Bump dependency `p256` from 0.12 to 0.13. Bump dependency `sec1` from 0.3.0 to 0.7 Related: #3681. Pull-Request: #3863. --- Cargo.lock | 207 ++++++++++++++++++++++++++------ identity/CHANGELOG.md | 4 + identity/Cargo.toml | 5 +- identity/src/ecdsa.rs | 24 +++- identity/src/error.rs | 7 -- identity/src/keypair.rs | 70 ++++++++--- identity/src/peer_id.rs | 10 +- identity/src/test/secp256r1.pk8 | Bin 0 -> 138 bytes 8 files changed, 255 insertions(+), 72 deletions(-) create mode 100644 identity/src/test/secp256r1.pk8 diff --git a/Cargo.lock b/Cargo.lock index 5a37baaedfc..6357312d4e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -517,6 +517,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -861,9 +867,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cec318a675afcb6a1ea1d4340e2d377e56e47c266f28043ceccbf4412ddfdd3b" +checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" [[package]] name = "core-foundation" @@ -1026,6 +1032,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1247,7 +1265,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ "const-oid", - "pem-rfc7468", + "pem-rfc7468 0.6.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e58dffcdcc8ee7b22f0c1f71a69243d7c2d9ad87b5a14361f2424a1565c219" +dependencies = [ + "const-oid", + "pem-rfc7468 0.7.0", "zeroize", ] @@ -1326,6 +1355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer 0.10.3", + "const-oid", "crypto-common", "subtle", ] @@ -1365,21 +1395,22 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", "signature 1.6.4", ] [[package]] name = "ecdsa" -version = "0.15.1" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12844141594ad74185a926d030f3b605f6a903b4e3fec351f3ea338ac5b7637e" +checksum = "a48e5d537b8a30c0b023116d981b16334be1485af7ca68db3a2b7024cbc957fd" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", + "der 0.7.5", + "digest 0.10.6", + "elliptic-curve 0.13.4", + "rfc6979 0.4.0", "signature 2.0.0", ] @@ -1418,18 +1449,38 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", - "crypto-bigint", - "der", + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", "digest 0.10.6", - "ff", + "ff 0.12.1", "generic-array", - "group", + "group 0.12.1", "hkdf", - "pem-rfc7468", - "pkcs8", + "pem-rfc7468 0.6.0", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c71eaa367f2e5d556414a8eea812bc62985c879748d6403edabd9cb03f16e7" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.2", + "digest 0.10.6", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.1", "subtle", "zeroize", ] @@ -1515,6 +1566,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.1.17" @@ -1708,6 +1769,7 @@ checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1772,7 +1834,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -2556,17 +2629,18 @@ dependencies = [ "bs58", "criterion", "ed25519-dalek", + "hex-literal", "libsecp256k1", "log", "multiaddr", "multihash", - "p256 0.12.0", + "p256 0.13.2", "quick-protobuf", "quickcheck-ext", "rand 0.8.5", "ring", "rmp-serde", - "sec1", + "sec1 0.7.1", "serde", "serde_json", "sha2 0.10.6", @@ -3561,18 +3635,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ "ecdsa 0.14.8", - "elliptic-curve", + "elliptic-curve 0.12.3", "sha2 0.10.6", ] [[package]] name = "p256" -version = "0.12.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49c124b3cbce43bcbac68c58ec181d98ed6cc7e6d0aa7c3ba97b2563410b0e55" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa 0.15.1", - "elliptic-curve", + "ecdsa 0.16.6", + "elliptic-curve 0.13.4", "primeorder", "sha2 0.10.6", ] @@ -3584,7 +3658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" dependencies = [ "ecdsa 0.14.8", - "elliptic-curve", + "elliptic-curve 0.12.3", "sha2 0.10.6", ] @@ -3682,6 +3756,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -3743,8 +3826,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.5", + "spki 0.7.1", ] [[package]] @@ -3843,11 +3936,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "primeorder" -version = "0.12.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b54f7131b3dba65a2f414cf5bd25b66d4682e4608610668eae785750ba4c5b2" +checksum = "cf8d3875361e28f7753baefef104386e7aa47642c93023356d97fdef4003bfb5" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.13.4", ] [[package]] @@ -4212,11 +4305,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac 0.12.1", "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -4464,10 +4567,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.5", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -4708,7 +4825,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a5be806ab6f127c3da44b7378837ebf01dadca8510a0e572460216b228bd0e" +dependencies = [ + "base64ct", + "der 0.7.5", ] [[package]] @@ -5477,7 +5604,7 @@ dependencies = [ "ccm", "curve25519-dalek 3.2.0", "der-parser 8.1.0", - "elliptic-curve", + "elliptic-curve 0.12.3", "hkdf", "hmac 0.10.1", "log", @@ -5490,7 +5617,7 @@ dependencies = [ "rcgen 0.9.3", "ring", "rustls 0.19.1", - "sec1", + "sec1 0.3.0", "serde", "sha-1", "sha2 0.9.9", diff --git a/identity/CHANGELOG.md b/identity/CHANGELOG.md index c156928127a..9aacfac822d 100644 --- a/identity/CHANGELOG.md +++ b/identity/CHANGELOG.md @@ -2,8 +2,12 @@ - Raise MSRV to 1.65. See [PR 3715]. +- Add support for exporting and importing ECDSA keys via the libp2p [protobuf format]. + See [PR 3863]. [PR 3715]: https://github.com/libp2p/rust-libp2p/pull/3715 +[PR 3863]: https://github.com/libp2p/rust-libp2p/pull/3863 +[protobuf format]: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#keys ## 0.1.2 diff --git a/identity/Cargo.toml b/identity/Cargo.toml index a69c2c632dd..4a17e806554 100644 --- a/identity/Cargo.toml +++ b/identity/Cargo.toml @@ -19,10 +19,10 @@ libsecp256k1 = { version = "0.7.0", optional = true } log = "0.4" multiaddr = { version = "0.17.1", optional = true } multihash = { version = "0.17.0", default-features = false, features = ["std"], optional = true } -p256 = { version = "0.12", default-features = false, features = ["ecdsa", "std"], optional = true } +p256 = { version = "0.13", default-features = false, features = ["ecdsa", "std", "pem"], optional = true } quick-protobuf = { version = "0.8.1", optional = true } rand = { version = "0.8", optional = true } -sec1 = { version = "0.3.0", features = ["std"], optional = true } # Activate `std` feature until https://github.com/RustCrypto/traits/pull/1131 is released. +sec1 = { version = "0.7", default-features = false, optional = true } serde = { version = "1", optional = true, features = ["derive"] } sha2 = { version = "0.10.0", optional = true } thiserror = { version = "1.0", optional = true } @@ -45,6 +45,7 @@ base64 = "0.21.0" serde_json = "1.0" rmp-serde = "1.0" criterion = "0.4" +hex-literal = "0.4.1" [[bench]] name = "peer_id" diff --git a/identity/src/ecdsa.rs b/identity/src/ecdsa.rs index 2210e4d128b..90a8c3089c4 100644 --- a/identity/src/ecdsa.rs +++ b/identity/src/ecdsa.rs @@ -31,7 +31,9 @@ use p256::{ }, EncodedPoint, }; +use sec1::{DecodeEcPrivateKey, EncodeEcPrivateKey}; use void::Void; +use zeroize::Zeroize; /// An ECDSA keypair generated using `secp256r1` curve. #[derive(Clone)] @@ -118,10 +120,30 @@ impl SecretKey { /// Try to parse a secret key from a byte buffer containing raw scalar of the key. pub fn try_from_bytes(buf: impl AsRef<[u8]>) -> Result { - SigningKey::from_bytes(buf.as_ref()) + SigningKey::from_bytes(buf.as_ref().into()) .map_err(|err| DecodingError::failed_to_parse("ecdsa p256 secret key", err)) .map(SecretKey) } + + /// Encode the secret key into DER-encoded byte buffer. + pub(crate) fn encode_der(&self) -> Vec { + self.0 + .to_sec1_der() + .expect("Encoding to pkcs#8 format to succeed") + .to_bytes() + .to_vec() + } + + /// Try to decode a secret key from a DER-encoded byte buffer, zeroize the buffer on success. + pub(crate) fn try_decode_der(buf: &mut [u8]) -> Result { + match SigningKey::from_sec1_der(buf) { + Ok(key) => { + buf.zeroize(); + Ok(SecretKey(key)) + } + Err(e) => Err(DecodingError::failed_to_parse("ECDSA", e)), + } + } } impl fmt::Debug for SecretKey { diff --git a/identity/src/error.rs b/identity/src/error.rs index 1ac945e71e9..0c86b346032 100644 --- a/identity/src/error.rs +++ b/identity/src/error.rs @@ -33,13 +33,6 @@ pub struct DecodingError { } impl DecodingError { - #[cfg(not(all( - feature = "ecdsa", - feature = "rsa", - feature = "secp256k1", - feature = "ed25519", - not(target_arch = "wasm32") - )))] pub(crate) fn missing_feature(feature_name: &'static str) -> Self { Self { msg: format!("cargo feature `{feature_name}` is not enabled"), diff --git a/identity/src/keypair.rs b/identity/src/keypair.rs index 6f21125ff17..0b36ea31be9 100644 --- a/identity/src/keypair.rs +++ b/identity/src/keypair.rs @@ -223,29 +223,25 @@ impl Keypair { } /// Encode a private key as protobuf structure. - #[cfg_attr( - not(feature = "ed25519"), - allow(unreachable_code, unused_variables, unused_mut) - )] pub fn to_protobuf_encoding(&self) -> Result, DecodingError> { use quick_protobuf::MessageWrite; - #[cfg(not(feature = "ed25519"))] - return Err(DecodingError::missing_feature("ed25519")); - #[allow(deprecated)] let pk: proto::PrivateKey = match self { #[cfg(feature = "ed25519")] Self::Ed25519(data) => proto::PrivateKey { Type: proto::KeyType::Ed25519, - Data: data.encode().to_vec(), + Data: data.to_bytes().to_vec(), }, #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] Self::Rsa(_) => return Err(DecodingError::encoding_unsupported("RSA")), #[cfg(feature = "secp256k1")] Self::Secp256k1(_) => return Err(DecodingError::encoding_unsupported("secp256k1")), #[cfg(feature = "ecdsa")] - Self::Ecdsa(_) => return Err(DecodingError::encoding_unsupported("ECDSA")), + Self::Ecdsa(data) => proto::PrivateKey { + Type: proto::KeyType::ECDSA, + Data: data.secret().encode_der(), + }, }; let mut buf = Vec::with_capacity(pk.get_size()); @@ -256,7 +252,6 @@ impl Keypair { } /// Decode a private key from a protobuf structure and parse it as a [`Keypair`]. - #[cfg_attr(not(feature = "ed25519"), allow(unused_mut))] pub fn from_protobuf_encoding(bytes: &[u8]) -> Result { use quick_protobuf::MessageRead; @@ -265,18 +260,22 @@ impl Keypair { .map_err(|e| DecodingError::bad_protobuf("private key bytes", e)) .map(zeroize::Zeroizing::new)?; + #[allow(deprecated, unreachable_code)] match private_key.Type { - #[cfg(feature = "ed25519")] - proto::KeyType::Ed25519 => - { - #[allow(deprecated)] - ed25519::Keypair::decode(&mut private_key.Data).map(Keypair::Ed25519) + proto::KeyType::Ed25519 => { + #[cfg(feature = "ed25519")] + return ed25519::Keypair::try_from_bytes(&mut private_key.Data) + .map(Keypair::Ed25519); + Err(DecodingError::missing_feature("ed25519")) } - #[cfg(not(feature = "ed25519"))] - proto::KeyType::Ed25519 => Err(DecodingError::missing_feature("ed25519")), proto::KeyType::RSA => Err(DecodingError::decoding_unsupported("RSA")), proto::KeyType::Secp256k1 => Err(DecodingError::decoding_unsupported("secp256k1")), - proto::KeyType::ECDSA => Err(DecodingError::decoding_unsupported("ECDSA")), + proto::KeyType::ECDSA => { + #[cfg(feature = "ecdsa")] + return ecdsa::SecretKey::try_decode_der(&mut private_key.Data) + .map(|key| Keypair::Ecdsa(key.into())); + Err(DecodingError::missing_feature("ecdsa")) + } } } } @@ -715,6 +714,41 @@ mod tests { assert_eq!(expected_peer_id, peer_id); } + #[test] + #[cfg(all(feature = "ecdsa", feature = "peerid"))] + fn keypair_protobuf_roundtrip_ecdsa() { + let priv_key = Keypair::from_protobuf_encoding(&hex_literal::hex!( + "08031279307702010104203E5B1FE9712E6C314942A750BD67485DE3C1EFE85B1BFB520AE8F9AE3DFA4A4CA00A06082A8648CE3D030107A14403420004DE3D300FA36AE0E8F5D530899D83ABAB44ABF3161F162A4BC901D8E6ECDA020E8B6D5F8DA30525E71D6851510C098E5C47C646A597FB4DCEC034E9F77C409E62" + )) + .unwrap(); + let pub_key = PublicKey::try_decode_protobuf(&hex_literal::hex!("0803125b3059301306072a8648ce3d020106082a8648ce3d03010703420004de3d300fa36ae0e8f5d530899d83abab44abf3161f162a4bc901d8e6ecda020e8b6d5f8da30525e71d6851510c098e5c47c646a597fb4dcec034e9f77c409e62")).unwrap(); + + roundtrip_protobuf_encoding(&priv_key, &pub_key); + } + + #[cfg(feature = "peerid")] + fn roundtrip_protobuf_encoding(private_key: &Keypair, public_key: &PublicKey) { + assert_eq!(&private_key.public(), public_key); + + let encoded_priv = private_key.to_protobuf_encoding().unwrap(); + let decoded_priv = Keypair::from_protobuf_encoding(&encoded_priv).unwrap(); + + assert_eq!( + private_key.public().to_peer_id(), + decoded_priv.public().to_peer_id(), + "PeerId from roundtripped private key should be the same" + ); + + let encoded_public = private_key.public().encode_protobuf(); + let decoded_public = PublicKey::try_decode_protobuf(&encoded_public).unwrap(); + + assert_eq!( + private_key.public().to_peer_id(), + decoded_public.to_peer_id(), + "PeerId from roundtripped public key should be the same" + ); + } + #[test] #[cfg(feature = "peerid")] fn keypair_from_protobuf_encoding() { diff --git a/identity/src/peer_id.rs b/identity/src/peer_id.rs index 788e0b79666..51d9e4f1f6d 100644 --- a/identity/src/peer_id.rs +++ b/identity/src/peer_id.rs @@ -267,25 +267,27 @@ impl FromStr for PeerId { #[cfg(test)] mod tests { use super::*; - use crate::keypair::Keypair; #[test] + #[cfg(feature = "ed25519")] fn peer_id_is_public_key() { - let key = Keypair::generate_ed25519().public(); + let key = crate::Keypair::generate_ed25519().public(); let peer_id = key.to_peer_id(); assert_eq!(peer_id.is_public_key(&key), Some(true)); } #[test] + #[cfg(feature = "ed25519")] fn peer_id_into_bytes_then_from_bytes() { - let peer_id = Keypair::generate_ed25519().public().to_peer_id(); + let peer_id = crate::Keypair::generate_ed25519().public().to_peer_id(); let second = PeerId::from_bytes(&peer_id.to_bytes()).unwrap(); assert_eq!(peer_id, second); } #[test] + #[cfg(feature = "ed25519")] fn peer_id_to_base58_then_back() { - let peer_id = Keypair::generate_ed25519().public().to_peer_id(); + let peer_id = crate::Keypair::generate_ed25519().public().to_peer_id(); let second: PeerId = peer_id.to_base58().parse().unwrap(); assert_eq!(peer_id, second); } diff --git a/identity/src/test/secp256r1.pk8 b/identity/src/test/secp256r1.pk8 new file mode 100644 index 0000000000000000000000000000000000000000..81813a77b3e5703d99ca6318ee0862171d0a96f6 GIT binary patch literal 138 zcmXqLY-eI*Fc4;A*J|@PXUoLM#sOw9GqSVf8e}suGO{QXoHWsAT(-o<`RQAKo_}7) zl^mu=&f2zFU=!b|P1BX{FfVjrc4A=R(UTIooXHWw5tw&7E>8J($<)(NloV%O;$rF( qHm+q}SGDAghINqSB_3baZ~rQ{y`Lk(ctBRVxV~_yW&P%Jb?X7gpE3*p literal 0 HcmV?d00001