aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHasini Gunasinghe <hasinitg@google.com>2022-10-05 18:21:37 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-10-05 18:21:37 +0000
commit5b999ae2f47ff77f0237f47d167d88f899d9fad0 (patch)
tree18f4f488b4851461b1e12c09c67e4f1eef270f2c
parenta3db53d3e13a3c79d68c8dd8a757d6150c2b1ae1 (diff)
parentaf5a7adffc08d56cd83ba5ccd7f33836d0598a37 (diff)
downloadsec1-5b999ae2f47ff77f0237f47d167d88f899d9fad0.tar.gz
Import platform/external/rust/crates/sec1 am: af5a7adffc
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/sec1/+/2238280 Change-Id: Iab3ee5e4aa87e65b42acd1cff01422b3e4326240 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.cargo_vcs_info.json6
-rw-r--r--Android.bp29
-rw-r--r--CHANGELOG.md51
-rw-r--r--Cargo.toml113
-rw-r--r--Cargo.toml.orig41
l---------LICENSE1
-rw-r--r--LICENSE-APACHE201
-rw-r--r--METADATA20
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS1
-rw-r--r--README.md58
-rw-r--r--cargo2android.json9
-rw-r--r--patches/std.diff15
-rw-r--r--src/error.rs84
-rw-r--r--src/lib.rs78
-rw-r--r--src/parameters.rs76
-rw-r--r--src/point.rs769
-rw-r--r--src/private_key.rs164
-rw-r--r--src/traits.rs131
-rw-r--r--tests/examples/p256-priv.derbin0 -> 121 bytes
-rw-r--r--tests/examples/p256-priv.pem5
-rw-r--r--tests/private_key.rs32
-rw-r--r--tests/traits.rs100
23 files changed, 1984 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..eac35cf
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+ "git": {
+ "sha1": "2e3a6ab6d9b3d541604711aadb9bc1d5f53abc02"
+ },
+ "path_in_vcs": "sec1"
+} \ No newline at end of file
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..5a62adb
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,29 @@
+// This file is generated by cargo2android.py --config cargo2android.json.
+// Do not modify this file as changes will be overridden on upgrade.
+
+
+
+rust_library_host {
+ name: "libsec1",
+ crate_name: "sec1",
+ cargo_env_compat: true,
+ cargo_pkg_version: "0.3.0",
+ srcs: ["src/lib.rs"],
+ edition: "2021",
+ features: [
+ "alloc",
+ "der",
+ "pkcs8",
+ "zeroize",
+ ],
+ rustlibs: [
+ "libder",
+ "libpkcs8",
+ "libzeroize",
+ ],
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.virt",
+ ],
+ vendor_available: true,
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..eb96da0
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,51 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## 0.3.0 (2022-05-08)
+### Added
+- Make `der` feature optional but on-by-default ([#497])
+- Make `point` feature optional but on-by-default ([#516])
+
+### Changed
+- Use `base16ct` and `serdect` crates ([#648])
+- Bump `der` to v0.6 ([#653])
+- Bump `pkcs8` to v0.9 ([#656])
+
+[#497]: https://github.com/RustCrypto/formats/pull/497
+[#516]: https://github.com/RustCrypto/formats/pull/516
+[#648]: https://github.com/RustCrypto/formats/pull/648
+[#653]: https://github.com/RustCrypto/formats/pull/653
+[#656]: https://github.com/RustCrypto/formats/pull/656
+
+## 0.2.1 (2021-11-18)
+### Added
+- `serde` feature ([#248])
+- Hexadecimal serialization/deserialization support for `EncodedPoint` ([#248])
+
+[#248]: https://github.com/RustCrypto/formats/pull/248
+
+## 0.2.0 (2021-11-17) [YANKED]
+### Added
+- `pkcs8` feature ([#229])
+
+### Changed
+- Rename `From/ToEcPrivateKey` => `DecodeEcPrivateKey`/`EncodeEcPrivateKey` ([#122])
+- Use `der::Document` to impl `EcPrivateKeyDocument` ([#133])
+- Rust 2021 edition upgrade; MSRV 1.56 ([#136])
+- Bump `der` crate dependency to v0.5 ([#222])
+
+### Removed
+- I/O related errors ([#158])
+
+[#122]: https://github.com/RustCrypto/formats/pull/122
+[#133]: https://github.com/RustCrypto/formats/pull/133
+[#136]: https://github.com/RustCrypto/formats/pull/136
+[#158]: https://github.com/RustCrypto/formats/pull/158
+[#222]: https://github.com/RustCrypto/formats/pull/222
+[#229]: https://github.com/RustCrypto/formats/pull/229
+
+## 0.1.0 (2021-09-22)
+- Initial release
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..40cd053
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,113 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+rust-version = "1.57"
+name = "sec1"
+version = "0.3.0"
+authors = ["RustCrypto Developers"]
+description = """
+Pure Rust implementation of SEC1: Elliptic Curve Cryptography encoding formats
+including ASN.1 DER-serialized private keys as well as the
+Elliptic-Curve-Point-to-Octet-String encoding
+"""
+readme = "README.md"
+keywords = [
+ "crypto",
+ "key",
+ "elliptic-curve",
+ "secg",
+]
+categories = [
+ "cryptography",
+ "data-structures",
+ "encoding",
+ "no-std",
+ "parser-implementations",
+]
+license = "Apache-2.0 OR MIT"
+repository = "https://github.com/RustCrypto/formats/tree/master/sec1"
+resolver = "2"
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = [
+ "--cfg",
+ "docsrs",
+]
+
+[dependencies.base16ct]
+version = "0.1.1"
+optional = true
+default-features = false
+
+[dependencies.der]
+version = "0.6"
+features = ["oid"]
+optional = true
+
+[dependencies.generic-array]
+version = "0.14.4"
+optional = true
+default-features = false
+
+[dependencies.pkcs8]
+version = "0.9"
+optional = true
+default-features = false
+
+[dependencies.serdect]
+version = "0.1"
+features = ["alloc"]
+optional = true
+default-features = false
+
+[dependencies.subtle]
+version = "2"
+optional = true
+default-features = false
+
+[dependencies.zeroize]
+version = "1"
+optional = true
+default-features = false
+
+[dev-dependencies.hex-literal]
+version = "0.3"
+
+[dev-dependencies.tempfile]
+version = "3"
+
+[features]
+alloc = [
+ "der/alloc",
+ "pkcs8/alloc",
+ "zeroize/alloc",
+]
+default = [
+ "der",
+ "point",
+]
+pem = [
+ "alloc",
+ "der/pem",
+ "pkcs8/pem",
+]
+point = [
+ "base16ct",
+ "generic-array",
+]
+serde = ["serdect"]
+std = [
+ "der/std",
+ "alloc",
+]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..f1ee649
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,41 @@
+[package]
+name = "sec1"
+version = "0.3.0"
+description = """
+Pure Rust implementation of SEC1: Elliptic Curve Cryptography encoding formats
+including ASN.1 DER-serialized private keys as well as the
+Elliptic-Curve-Point-to-Octet-String encoding
+"""
+authors = ["RustCrypto Developers"]
+license = "Apache-2.0 OR MIT"
+repository = "https://github.com/RustCrypto/formats/tree/master/sec1"
+categories = ["cryptography", "data-structures", "encoding", "no-std", "parser-implementations"]
+keywords = ["crypto", "key", "elliptic-curve", "secg"]
+readme = "README.md"
+edition = "2021"
+rust-version = "1.57"
+
+[dependencies]
+base16ct = { version = "0.1.1", optional = true, default-features = false, path = "../base16ct" }
+der = { version = "0.6", optional = true, features = ["oid"], path = "../der" }
+generic-array = { version = "0.14.4", optional = true, default-features = false }
+pkcs8 = { version = "0.9", optional = true, default-features = false, path = "../pkcs8" }
+serdect = { version = "0.1", optional = true, default-features = false, features = ["alloc"], path = "../serdect" }
+subtle = { version = "2", optional = true, default-features = false }
+zeroize = { version = "1", optional = true, default-features = false }
+
+[dev-dependencies]
+hex-literal = "0.3"
+tempfile = "3"
+
+[features]
+default = ["der", "point"]
+alloc = ["der/alloc", "pkcs8/alloc", "zeroize/alloc"]
+pem = ["alloc", "der/pem", "pkcs8/pem"]
+point = ["base16ct", "generic-array"]
+serde = ["serdect"]
+std = ["der/std", "alloc"]
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
diff --git a/LICENSE b/LICENSE
new file mode 120000
index 0000000..6b579aa
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+LICENSE-APACHE \ No newline at end of file
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
new file mode 100644
index 0000000..78173fa
--- /dev/null
+++ b/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..4bc13a3
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,20 @@
+name: "sec1"
+description: "Pure Rust implementation of SEC1: Elliptic Curve Cryptography encoding formats including ASN.1 DER-serialized private keys (also described in RFC5915) as well as the Elliptic-Curve-Point-to-Octet-String and Octet-String-to-Elliptic Curve-Point encoding algorithms."
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://crates.io/crates/sec1"
+ }
+ url {
+ type: ARCHIVE
+ value: "https://static.crates.io/crates/sec1/sec1-0.3.0.crate"
+ }
+ version: "0.3.0"
+ # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same.
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2022
+ month: 9
+ day: 6
+ }
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..45dc4dd
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:master:/OWNERS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fe1d9b6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,58 @@
+# [RustCrypto]: SEC1 Elliptic Curve Cryptography Formats
+
+[![crate][crate-image]][crate-link]
+[![Docs][docs-image]][docs-link]
+[![Build Status][build-image]][build-link]
+![Apache2/MIT licensed][license-image]
+![Rust Version][rustc-image]
+[![Project Chat][chat-image]][chat-link]
+
+[Documentation][docs-link]
+
+## About
+
+Pure Rust implementation of [SEC1: Elliptic Curve Cryptography] encoding
+formats including ASN.1 DER-serialized private keys (also described in
+[RFC5915]) as well as the `Elliptic-Curve-Point-to-Octet-String` and
+`Octet-String-to-Elliptic-Curve-Point` encoding algorithms.
+
+## Minimum Supported Rust Version
+
+This crate requires **Rust 1.57** at a minimum.
+
+We may change the MSRV in the future, but it will be accompanied by a minor
+version bump.
+
+## License
+
+Licensed under either of:
+
+ * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
+ * [MIT license](http://opensource.org/licenses/MIT)
+
+at your option.
+
+### Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted
+for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
+dual licensed as above, without any additional terms or conditions.
+
+[//]: # (badges)
+
+[crate-image]: https://buildstats.info/crate/sec1
+[crate-link]: https://crates.io/crates/sec1
+[docs-image]: https://docs.rs/sec1/badge.svg
+[docs-link]: https://docs.rs/sec1/
+[license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg
+[rustc-image]: https://img.shields.io/badge/rustc-1.57+-blue.svg
+[chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg
+[chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/300570-formats
+[build-image]: https://github.com/RustCrypto/formats/workflows/sec1/badge.svg?branch=master&event=push
+[build-link]: https://github.com/RustCrypto/formats/actions
+
+[//]: # (links)
+
+[RustCrypto]: https://github.com/rustcrypto
+[SEC1: Elliptic Curve Cryptography]: https://www.secg.org/sec1-v2.pdf
+[RFC5915]: https://datatracker.ietf.org/doc/html/rfc5915
diff --git a/cargo2android.json b/cargo2android.json
new file mode 100644
index 0000000..ef068cf
--- /dev/null
+++ b/cargo2android.json
@@ -0,0 +1,9 @@
+{
+ "apex-available": [
+ "//apex_available:platform",
+ "com.android.virt"
+ ],
+ "run": true,
+ "vendor-available": true,
+ "features": "alloc,der,pkcs8"
+}
diff --git a/patches/std.diff b/patches/std.diff
new file mode 100644
index 0000000..93800a5
--- /dev/null
+++ b/patches/std.diff
@@ -0,0 +1,15 @@
+diff --git a/src/lib.rs b/src/lib.rs
+index 8e01b1f..6b658b6 100644
+--- a/src/lib.rs
++++ b/src/lib.rs
+@@ -17,6 +17,10 @@
+ //! serializers/deserializers will autodetect if a "human friendly" textual
+ //! encoding is being used, and if so encode the points as hexadecimal.
+
++/// Local Android change: Use std to allow building as a dylib.
++#[cfg(android_dylib)]
++extern crate std;
++
+ #[cfg(feature = "alloc")]
+ #[allow(unused_extern_crates)]
+ extern crate alloc;
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..3700ac5
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,84 @@
+//! Error types
+
+use core::fmt;
+
+#[cfg(feature = "pem")]
+use der::pem;
+
+/// Result type with `sec1` crate's [`Error`] type.
+pub type Result<T> = core::result::Result<T, Error>;
+
+/// Error type
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[non_exhaustive]
+pub enum Error {
+ /// ASN.1 DER-related errors.
+ #[cfg(feature = "der")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "der")))]
+ Asn1(der::Error),
+
+ /// Cryptographic errors.
+ ///
+ /// These can be used by EC implementations to signal that a key is
+ /// invalid for cryptographic reasons. This means the document parsed
+ /// correctly, but one of the values contained within was invalid, e.g.
+ /// a number expected to be a prime was not a prime.
+ Crypto,
+
+ /// PKCS#8 errors.
+ #[cfg(feature = "pkcs8")]
+ Pkcs8(pkcs8::Error),
+
+ /// Errors relating to the `Elliptic-Curve-Point-to-Octet-String` or
+ /// `Octet-String-to-Elliptic-Curve-Point` encodings.
+ PointEncoding,
+
+ /// Version errors
+ Version,
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ #[cfg(feature = "der")]
+ Error::Asn1(err) => write!(f, "SEC1 ASN.1 error: {}", err),
+ Error::Crypto => f.write_str("SEC1 cryptographic error"),
+ #[cfg(feature = "pkcs8")]
+ Error::Pkcs8(err) => write!(f, "{}", err),
+ Error::PointEncoding => f.write_str("elliptic curve point encoding error"),
+ Error::Version => f.write_str("SEC1 version error"),
+ }
+ }
+}
+
+#[cfg(feature = "der")]
+#[cfg_attr(docsrs, doc(cfg(feature = "der")))]
+impl From<der::Error> for Error {
+ fn from(err: der::Error) -> Error {
+ Error::Asn1(err)
+ }
+}
+
+#[cfg(feature = "pem")]
+impl From<pem::Error> for Error {
+ fn from(err: pem::Error) -> Error {
+ der::Error::from(err).into()
+ }
+}
+
+#[cfg(feature = "pkcs8")]
+impl From<pkcs8::Error> for Error {
+ fn from(err: pkcs8::Error) -> Error {
+ Error::Pkcs8(err)
+ }
+}
+
+#[cfg(feature = "pkcs8")]
+impl From<pkcs8::spki::Error> for Error {
+ fn from(err: pkcs8::spki::Error) -> Error {
+ Error::Pkcs8(pkcs8::Error::PublicKey(err))
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for Error {}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..6b658b6
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,78 @@
+#![no_std]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+#![doc = include_str!("../README.md")]
+#![doc(
+ html_logo_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg",
+ html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg"
+)]
+#![forbid(unsafe_code, clippy::unwrap_used)]
+#![warn(missing_docs, rust_2018_idioms, unused_qualifications)]
+
+//! ## `serde` support
+//!
+//! When the `serde` feature of this crate is enabled, the [`EncodedPoint`]
+//! type receives impls of [`serde::Serialize`] and [`serde::Deserialize`].
+//!
+//! Additionally, when both the `alloc` and `serde` features are enabled, the
+//! serializers/deserializers will autodetect if a "human friendly" textual
+//! encoding is being used, and if so encode the points as hexadecimal.
+
+/// Local Android change: Use std to allow building as a dylib.
+#[cfg(android_dylib)]
+extern crate std;
+
+#[cfg(feature = "alloc")]
+#[allow(unused_extern_crates)]
+extern crate alloc;
+#[cfg(feature = "std")]
+extern crate std;
+
+#[cfg(feature = "point")]
+pub mod point;
+
+mod error;
+#[cfg(feature = "der")]
+mod parameters;
+#[cfg(feature = "der")]
+mod private_key;
+#[cfg(feature = "der")]
+mod traits;
+
+#[cfg(feature = "der")]
+pub use der;
+
+pub use crate::error::{Error, Result};
+
+#[cfg(feature = "point")]
+pub use crate::point::EncodedPoint;
+
+#[cfg(feature = "point")]
+pub use generic_array::typenum::consts;
+
+#[cfg(feature = "der")]
+pub use crate::{parameters::EcParameters, private_key::EcPrivateKey, traits::DecodeEcPrivateKey};
+
+#[cfg(feature = "alloc")]
+pub use crate::traits::EncodeEcPrivateKey;
+
+#[cfg(feature = "pem")]
+#[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
+pub use der::pem::{self, LineEnding};
+
+#[cfg(feature = "pkcs8")]
+#[cfg_attr(docsrs, doc(cfg(feature = "pkcs8")))]
+pub use pkcs8;
+
+#[cfg(feature = "pkcs8")]
+use pkcs8::ObjectIdentifier;
+
+#[cfg(all(doc, feature = "serde"))]
+use serdect::serde;
+
+/// Algorithm [`ObjectIdentifier`] for elliptic curve public key cryptography
+/// (`id-ecPublicKey`).
+///
+/// <http://oid-info.com/get/1.2.840.10045.2.1>
+#[cfg(feature = "pkcs8")]
+#[cfg_attr(docsrs, doc(cfg(feature = "pkcs8")))]
+pub const ALGORITHM_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
diff --git a/src/parameters.rs b/src/parameters.rs
new file mode 100644
index 0000000..ed9d152
--- /dev/null
+++ b/src/parameters.rs
@@ -0,0 +1,76 @@
+use der::{
+ asn1::{AnyRef, ObjectIdentifier},
+ DecodeValue, EncodeValue, FixedTag, Header, Length, Reader, Tag, Writer,
+};
+
+/// Elliptic curve parameters as described in
+/// [RFC5480 Section 2.1.1](https://datatracker.ietf.org/doc/html/rfc5480#section-2.1.1):
+///
+/// ```text
+/// ECParameters ::= CHOICE {
+/// namedCurve OBJECT IDENTIFIER
+/// -- implicitCurve NULL
+/// -- specifiedCurve SpecifiedECDomain
+/// }
+/// -- implicitCurve and specifiedCurve MUST NOT be used in PKIX.
+/// -- Details for SpecifiedECDomain can be found in [X9.62].
+/// -- Any future additions to this CHOICE should be coordinated
+/// -- with ANSI X9.
+/// ```
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(docsrs, doc(cfg(feature = "der")))]
+pub enum EcParameters {
+ /// Elliptic curve named by a particular OID.
+ ///
+ /// > namedCurve identifies all the required values for a particular
+ /// > set of elliptic curve domain parameters to be represented by an
+ /// > object identifier.
+ NamedCurve(ObjectIdentifier),
+}
+
+impl<'a> DecodeValue<'a> for EcParameters {
+ fn decode_value<R: Reader<'a>>(decoder: &mut R, header: Header) -> der::Result<Self> {
+ ObjectIdentifier::decode_value(decoder, header).map(Self::NamedCurve)
+ }
+}
+
+impl EncodeValue for EcParameters {
+ fn value_len(&self) -> der::Result<Length> {
+ match self {
+ Self::NamedCurve(oid) => oid.value_len(),
+ }
+ }
+
+ fn encode_value(&self, writer: &mut dyn Writer) -> der::Result<()> {
+ match self {
+ Self::NamedCurve(oid) => oid.encode_value(writer),
+ }
+ }
+}
+
+impl EcParameters {
+ /// Obtain the `namedCurve` OID.
+ pub fn named_curve(self) -> Option<ObjectIdentifier> {
+ match self {
+ Self::NamedCurve(oid) => Some(oid),
+ }
+ }
+}
+
+impl<'a> From<&'a EcParameters> for AnyRef<'a> {
+ fn from(params: &'a EcParameters) -> AnyRef<'a> {
+ match params {
+ EcParameters::NamedCurve(oid) => oid.into(),
+ }
+ }
+}
+
+impl From<ObjectIdentifier> for EcParameters {
+ fn from(oid: ObjectIdentifier) -> EcParameters {
+ EcParameters::NamedCurve(oid)
+ }
+}
+
+impl FixedTag for EcParameters {
+ const TAG: Tag = Tag::ObjectIdentifier;
+}
diff --git a/src/point.rs b/src/point.rs
new file mode 100644
index 0000000..eb0d2ca
--- /dev/null
+++ b/src/point.rs
@@ -0,0 +1,769 @@
+//! Support for the SEC1 `Elliptic-Curve-Point-to-Octet-String` and
+//! `Octet-String-to-Elliptic-Curve-Point` encoding algorithms.
+//!
+//! Described in [SEC1: Elliptic Curve Cryptography] (Version 2.0) section 2.3.3 (p.10).
+//!
+//! [SEC1: Elliptic Curve Cryptography]: https://www.secg.org/sec1-v2.pdf
+
+use crate::{Error, Result};
+use base16ct::HexDisplay;
+use core::{
+ cmp::Ordering,
+ fmt::{self, Debug},
+ ops::Add,
+ str,
+};
+use generic_array::{
+ typenum::{U1, U28, U32, U48, U66},
+ ArrayLength, GenericArray,
+};
+
+#[cfg(feature = "alloc")]
+use alloc::boxed::Box;
+
+#[cfg(feature = "serde")]
+use serdect::serde::{de, ser, Deserialize, Serialize};
+
+#[cfg(feature = "subtle")]
+use subtle::{Choice, ConditionallySelectable};
+
+#[cfg(feature = "zeroize")]
+use zeroize::Zeroize;
+
+/// Trait for supported modulus sizes which precomputes the typenums for
+/// various point encodings so they don't need to be included as bounds.
+// TODO(tarcieri): replace this all with const generic expressions.
+pub trait ModulusSize: 'static + ArrayLength<u8> + Copy + Debug {
+ /// Size of a compressed point for the given elliptic curve when encoded
+ /// using the SEC1 `Elliptic-Curve-Point-to-Octet-String` algorithm
+ /// (including leading `0x02` or `0x03` tag byte).
+ type CompressedPointSize: 'static + ArrayLength<u8> + Copy + Debug;
+
+ /// Size of an uncompressed point for the given elliptic curve when encoded
+ /// using the SEC1 `Elliptic-Curve-Point-to-Octet-String` algorithm
+ /// (including leading `0x04` tag byte).
+ type UncompressedPointSize: 'static + ArrayLength<u8> + Copy + Debug;
+
+ /// Size of an untagged point for given elliptic curve, i.e. size of two
+ /// serialized base field elements.
+ type UntaggedPointSize: 'static + ArrayLength<u8> + Copy + Debug;
+}
+
+macro_rules! impl_modulus_size {
+ ($($size:ty),+) => {
+ $(impl ModulusSize for $size {
+ type CompressedPointSize = <$size as Add<U1>>::Output;
+ type UncompressedPointSize = <Self::UntaggedPointSize as Add<U1>>::Output;
+ type UntaggedPointSize = <$size as Add>::Output;
+ })+
+ }
+}
+
+impl_modulus_size!(U28, U32, U48, U66);
+
+/// SEC1 encoded curve point.
+///
+/// This type is an enum over the compressed and uncompressed encodings,
+/// useful for cases where either encoding can be supported, or conversions
+/// between the two forms.
+#[derive(Clone, Default)]
+pub struct EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ bytes: GenericArray<u8, Size::UncompressedPointSize>,
+}
+
+#[allow(clippy::len_without_is_empty)]
+impl<Size> EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ /// Decode elliptic curve point (compressed or uncompressed) from the
+ /// `Elliptic-Curve-Point-to-Octet-String` encoding described in
+ /// SEC 1: Elliptic Curve Cryptography (Version 2.0) section
+ /// 2.3.3 (page 10).
+ ///
+ /// <http://www.secg.org/sec1-v2.pdf>
+ pub fn from_bytes(input: impl AsRef<[u8]>) -> Result<Self> {
+ let input = input.as_ref();
+
+ // Validate tag
+ let tag = input
+ .first()
+ .cloned()
+ .ok_or(Error::PointEncoding)
+ .and_then(Tag::from_u8)?;
+
+ // Validate length
+ let expected_len = tag.message_len(Size::to_usize());
+
+ if input.len() != expected_len {
+ return Err(Error::PointEncoding);
+ }
+
+ let mut bytes = GenericArray::default();
+ bytes[..expected_len].copy_from_slice(input);
+ Ok(Self { bytes })
+ }
+
+ /// Decode elliptic curve point from raw uncompressed coordinates, i.e.
+ /// encoded as the concatenated `x || y` coordinates with no leading SEC1
+ /// tag byte (which would otherwise be `0x04` for an uncompressed point).
+ pub fn from_untagged_bytes(bytes: &GenericArray<u8, Size::UntaggedPointSize>) -> Self {
+ let (x, y) = bytes.split_at(Size::to_usize());
+ Self::from_affine_coordinates(x.into(), y.into(), false)
+ }
+
+ /// Encode an elliptic curve point from big endian serialized coordinates
+ /// (with optional point compression)
+ pub fn from_affine_coordinates(
+ x: &GenericArray<u8, Size>,
+ y: &GenericArray<u8, Size>,
+ compress: bool,
+ ) -> Self {
+ let tag = if compress {
+ Tag::compress_y(y.as_slice())
+ } else {
+ Tag::Uncompressed
+ };
+
+ let mut bytes = GenericArray::default();
+ bytes[0] = tag.into();
+ bytes[1..(Size::to_usize() + 1)].copy_from_slice(x);
+
+ if !compress {
+ bytes[(Size::to_usize() + 1)..].copy_from_slice(y);
+ }
+
+ Self { bytes }
+ }
+
+ /// Return [`EncodedPoint`] representing the additive identity
+ /// (a.k.a. point at infinity)
+ pub fn identity() -> Self {
+ Self::default()
+ }
+
+ /// Get the length of the encoded point in bytes
+ pub fn len(&self) -> usize {
+ self.tag().message_len(Size::to_usize())
+ }
+
+ /// Get byte slice containing the serialized [`EncodedPoint`].
+ pub fn as_bytes(&self) -> &[u8] {
+ &self.bytes[..self.len()]
+ }
+
+ /// Get boxed byte slice containing the serialized [`EncodedPoint`]
+ #[cfg(feature = "alloc")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
+ pub fn to_bytes(&self) -> Box<[u8]> {
+ self.as_bytes().to_vec().into_boxed_slice()
+ }
+
+ /// Is this [`EncodedPoint`] compact?
+ pub fn is_compact(&self) -> bool {
+ self.tag().is_compact()
+ }
+
+ /// Is this [`EncodedPoint`] compressed?
+ pub fn is_compressed(&self) -> bool {
+ self.tag().is_compressed()
+ }
+
+ /// Is this [`EncodedPoint`] the additive identity? (a.k.a. point at infinity)
+ pub fn is_identity(&self) -> bool {
+ self.tag().is_identity()
+ }
+
+ /// Compress this [`EncodedPoint`], returning a new [`EncodedPoint`].
+ pub fn compress(&self) -> Self {
+ match self.coordinates() {
+ Coordinates::Compressed { .. }
+ | Coordinates::Compact { .. }
+ | Coordinates::Identity => self.clone(),
+ Coordinates::Uncompressed { x, y } => Self::from_affine_coordinates(x, y, true),
+ }
+ }
+
+ /// Get the SEC1 tag for this [`EncodedPoint`]
+ pub fn tag(&self) -> Tag {
+ // Tag is ensured valid by the constructor
+ Tag::from_u8(self.bytes[0]).expect("invalid tag")
+ }
+
+ /// Get the [`Coordinates`] for this [`EncodedPoint`].
+ #[inline]
+ pub fn coordinates(&self) -> Coordinates<'_, Size> {
+ if self.is_identity() {
+ return Coordinates::Identity;
+ }
+
+ let (x, y) = self.bytes[1..].split_at(Size::to_usize());
+
+ if self.is_compressed() {
+ Coordinates::Compressed {
+ x: x.into(),
+ y_is_odd: self.tag() as u8 & 1 == 1,
+ }
+ } else if self.is_compact() {
+ Coordinates::Compact { x: x.into() }
+ } else {
+ Coordinates::Uncompressed {
+ x: x.into(),
+ y: y.into(),
+ }
+ }
+ }
+
+ /// Get the x-coordinate for this [`EncodedPoint`].
+ ///
+ /// Returns `None` if this point is the identity point.
+ pub fn x(&self) -> Option<&GenericArray<u8, Size>> {
+ match self.coordinates() {
+ Coordinates::Identity => None,
+ Coordinates::Compressed { x, .. } => Some(x),
+ Coordinates::Uncompressed { x, .. } => Some(x),
+ Coordinates::Compact { x } => Some(x),
+ }
+ }
+
+ /// Get the y-coordinate for this [`EncodedPoint`].
+ ///
+ /// Returns `None` if this point is compressed or the identity point.
+ pub fn y(&self) -> Option<&GenericArray<u8, Size>> {
+ match self.coordinates() {
+ Coordinates::Compressed { .. } | Coordinates::Identity => None,
+ Coordinates::Uncompressed { y, .. } => Some(y),
+ Coordinates::Compact { .. } => None,
+ }
+ }
+}
+
+impl<Size> AsRef<[u8]> for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ #[inline]
+ fn as_ref(&self) -> &[u8] {
+ self.as_bytes()
+ }
+}
+
+#[cfg(feature = "subtle")]
+impl<Size> ConditionallySelectable for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+ <Size::UncompressedPointSize as ArrayLength<u8>>::ArrayType: Copy,
+{
+ fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
+ let mut bytes = GenericArray::default();
+
+ for (i, byte) in bytes.iter_mut().enumerate() {
+ *byte = u8::conditional_select(&a.bytes[i], &b.bytes[i], choice);
+ }
+
+ Self { bytes }
+ }
+}
+
+impl<Size> Copy for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+ <Size::UncompressedPointSize as ArrayLength<u8>>::ArrayType: Copy,
+{
+}
+
+impl<Size> Debug for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "EncodedPoint({:?})", self.coordinates())
+ }
+}
+
+impl<Size: ModulusSize> Eq for EncodedPoint<Size> {}
+
+impl<Size> PartialEq for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn eq(&self, other: &Self) -> bool {
+ self.as_bytes() == other.as_bytes()
+ }
+}
+
+impl<Size: ModulusSize> PartialOrd for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl<Size: ModulusSize> Ord for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.as_bytes().cmp(other.as_bytes())
+ }
+}
+
+impl<Size: ModulusSize> TryFrom<&[u8]> for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ type Error = Error;
+
+ fn try_from(bytes: &[u8]) -> Result<Self> {
+ Self::from_bytes(bytes)
+ }
+}
+
+#[cfg(feature = "zeroize")]
+impl<Size> Zeroize for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn zeroize(&mut self) {
+ self.bytes.zeroize();
+ *self = Self::identity();
+ }
+}
+
+impl<Size> fmt::Display for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{:X}", self)
+ }
+}
+
+impl<Size> fmt::LowerHex for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{:x}", HexDisplay(self.as_bytes()))
+ }
+}
+
+impl<Size> fmt::UpperHex for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{:X}", HexDisplay(self.as_bytes()))
+ }
+}
+
+/// Decode a SEC1-encoded point from hexadecimal.
+///
+/// Upper and lower case hexadecimal are both accepted, however mixed case is
+/// rejected.
+impl<Size> str::FromStr for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ type Err = Error;
+
+ fn from_str(hex: &str) -> Result<Self> {
+ let mut buf = GenericArray::<u8, Size::UncompressedPointSize>::default();
+ base16ct::mixed::decode(hex, &mut buf)
+ .map_err(|_| Error::PointEncoding)
+ .and_then(Self::from_bytes)
+ }
+}
+
+#[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+impl<Size> Serialize for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
+ where
+ S: ser::Serializer,
+ {
+ serdect::slice::serialize_hex_upper_or_bin(&self.as_bytes(), serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+impl<'de, Size> Deserialize<'de> for EncodedPoint<Size>
+where
+ Size: ModulusSize,
+{
+ fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
+ where
+ D: de::Deserializer<'de>,
+ {
+ let bytes = serdect::slice::deserialize_hex_or_bin_vec(deserializer)?;
+ Self::from_bytes(&bytes).map_err(de::Error::custom)
+ }
+}
+
+/// Enum representing the coordinates of either compressed or uncompressed
+/// SEC1-encoded elliptic curve points.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum Coordinates<'a, Size: ModulusSize> {
+ /// Identity point (a.k.a. point at infinity)
+ Identity,
+
+ /// Compact curve point
+ Compact {
+ /// x-coordinate
+ x: &'a GenericArray<u8, Size>,
+ },
+
+ /// Compressed curve point
+ Compressed {
+ /// x-coordinate
+ x: &'a GenericArray<u8, Size>,
+
+ /// Is the y-coordinate odd?
+ y_is_odd: bool,
+ },
+
+ /// Uncompressed curve point
+ Uncompressed {
+ /// x-coordinate
+ x: &'a GenericArray<u8, Size>,
+
+ /// y-coordinate
+ y: &'a GenericArray<u8, Size>,
+ },
+}
+
+impl<'a, Size: ModulusSize> Coordinates<'a, Size> {
+ /// Get the tag octet needed to encode this set of [`Coordinates`]
+ pub fn tag(&self) -> Tag {
+ match self {
+ Coordinates::Compact { .. } => Tag::Compact,
+ Coordinates::Compressed { y_is_odd, .. } => {
+ if *y_is_odd {
+ Tag::CompressedOddY
+ } else {
+ Tag::CompressedEvenY
+ }
+ }
+ Coordinates::Identity => Tag::Identity,
+ Coordinates::Uncompressed { .. } => Tag::Uncompressed,
+ }
+ }
+}
+
+/// Tag byte used by the `Elliptic-Curve-Point-to-Octet-String` encoding.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[repr(u8)]
+pub enum Tag {
+ /// Identity point (`0x00`)
+ Identity = 0,
+
+ /// Compressed point with even y-coordinate (`0x02`)
+ CompressedEvenY = 2,
+
+ /// Compressed point with odd y-coordinate (`0x03`)
+ CompressedOddY = 3,
+
+ /// Uncompressed point (`0x04`)
+ Uncompressed = 4,
+
+ /// Compact point (`0x05`)
+ Compact = 5,
+}
+
+impl Tag {
+ /// Parse a tag value from a byte
+ pub fn from_u8(byte: u8) -> Result<Self> {
+ match byte {
+ 0 => Ok(Tag::Identity),
+ 2 => Ok(Tag::CompressedEvenY),
+ 3 => Ok(Tag::CompressedOddY),
+ 4 => Ok(Tag::Uncompressed),
+ 5 => Ok(Tag::Compact),
+ _ => Err(Error::PointEncoding),
+ }
+ }
+
+ /// Is this point compact?
+ pub fn is_compact(self) -> bool {
+ matches!(self, Tag::Compact)
+ }
+
+ /// Is this point compressed?
+ pub fn is_compressed(self) -> bool {
+ matches!(self, Tag::CompressedEvenY | Tag::CompressedOddY)
+ }
+
+ /// Is this point the identity point?
+ pub fn is_identity(self) -> bool {
+ self == Tag::Identity
+ }
+
+ /// Compute the expected total message length for a message prefixed
+ /// with this tag (including the tag byte), given the field element size
+ /// (in bytes) for a particular elliptic curve.
+ pub fn message_len(self, field_element_size: usize) -> usize {
+ 1 + match self {
+ Tag::Identity => 0,
+ Tag::CompressedEvenY | Tag::CompressedOddY => field_element_size,
+ Tag::Uncompressed => field_element_size * 2,
+ Tag::Compact => field_element_size,
+ }
+ }
+
+ /// Compress the given y-coordinate, returning a `Tag::Compressed*` value
+ fn compress_y(y: &[u8]) -> Self {
+ // Is the y-coordinate odd in the SEC1 sense: `self mod 2 == 1`?
+ if y.as_ref().last().expect("empty y-coordinate") & 1 == 1 {
+ Tag::CompressedOddY
+ } else {
+ Tag::CompressedEvenY
+ }
+ }
+}
+
+impl TryFrom<u8> for Tag {
+ type Error = Error;
+
+ fn try_from(byte: u8) -> Result<Self> {
+ Self::from_u8(byte)
+ }
+}
+
+impl From<Tag> for u8 {
+ fn from(tag: Tag) -> u8 {
+ tag as u8
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{Coordinates, Tag};
+ use core::str::FromStr;
+ use generic_array::{typenum::U32, GenericArray};
+ use hex_literal::hex;
+
+ #[cfg(feature = "alloc")]
+ use alloc::string::ToString;
+
+ #[cfg(feature = "subtle")]
+ use subtle::ConditionallySelectable;
+
+ type EncodedPoint = super::EncodedPoint<U32>;
+
+ /// Identity point
+ const IDENTITY_BYTES: [u8; 1] = [0];
+
+ /// Example uncompressed point
+ const UNCOMPRESSED_BYTES: [u8; 65] = hex!("0411111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222222");
+
+ /// Example compressed point: `UNCOMPRESSED_BYTES` after point compression
+ const COMPRESSED_BYTES: [u8; 33] =
+ hex!("021111111111111111111111111111111111111111111111111111111111111111");
+
+ #[test]
+ fn decode_compressed_point() {
+ // Even y-coordinate
+ let compressed_even_y_bytes =
+ hex!("020100000000000000000000000000000000000000000000000000000000000000");
+
+ let compressed_even_y = EncodedPoint::from_bytes(&compressed_even_y_bytes[..]).unwrap();
+
+ assert!(compressed_even_y.is_compressed());
+ assert_eq!(compressed_even_y.tag(), Tag::CompressedEvenY);
+ assert_eq!(compressed_even_y.len(), 33);
+ assert_eq!(compressed_even_y.as_bytes(), &compressed_even_y_bytes[..]);
+
+ assert_eq!(
+ compressed_even_y.coordinates(),
+ Coordinates::Compressed {
+ x: &hex!("0100000000000000000000000000000000000000000000000000000000000000").into(),
+ y_is_odd: false
+ }
+ );
+
+ assert_eq!(
+ compressed_even_y.x().unwrap(),
+ &hex!("0100000000000000000000000000000000000000000000000000000000000000").into()
+ );
+ assert_eq!(compressed_even_y.y(), None);
+
+ // Odd y-coordinate
+ let compressed_odd_y_bytes =
+ hex!("030200000000000000000000000000000000000000000000000000000000000000");
+
+ let compressed_odd_y = EncodedPoint::from_bytes(&compressed_odd_y_bytes[..]).unwrap();
+
+ assert!(compressed_odd_y.is_compressed());
+ assert_eq!(compressed_odd_y.tag(), Tag::CompressedOddY);
+ assert_eq!(compressed_odd_y.len(), 33);
+ assert_eq!(compressed_odd_y.as_bytes(), &compressed_odd_y_bytes[..]);
+
+ assert_eq!(
+ compressed_odd_y.coordinates(),
+ Coordinates::Compressed {
+ x: &hex!("0200000000000000000000000000000000000000000000000000000000000000").into(),
+ y_is_odd: true
+ }
+ );
+
+ assert_eq!(
+ compressed_odd_y.x().unwrap(),
+ &hex!("0200000000000000000000000000000000000000000000000000000000000000").into()
+ );
+ assert_eq!(compressed_odd_y.y(), None);
+ }
+
+ #[test]
+ fn decode_uncompressed_point() {
+ let uncompressed_point = EncodedPoint::from_bytes(&UNCOMPRESSED_BYTES[..]).unwrap();
+
+ assert!(!uncompressed_point.is_compressed());
+ assert_eq!(uncompressed_point.tag(), Tag::Uncompressed);
+ assert_eq!(uncompressed_point.len(), 65);
+ assert_eq!(uncompressed_point.as_bytes(), &UNCOMPRESSED_BYTES[..]);
+
+ assert_eq!(
+ uncompressed_point.coordinates(),
+ Coordinates::Uncompressed {
+ x: &hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
+ y: &hex!("2222222222222222222222222222222222222222222222222222222222222222").into()
+ }
+ );
+
+ assert_eq!(
+ uncompressed_point.x().unwrap(),
+ &hex!("1111111111111111111111111111111111111111111111111111111111111111").into()
+ );
+ assert_eq!(
+ uncompressed_point.y().unwrap(),
+ &hex!("2222222222222222222222222222222222222222222222222222222222222222").into()
+ );
+ }
+
+ #[test]
+ fn decode_identity() {
+ let identity_point = EncodedPoint::from_bytes(&IDENTITY_BYTES[..]).unwrap();
+ assert!(identity_point.is_identity());
+ assert_eq!(identity_point.tag(), Tag::Identity);
+ assert_eq!(identity_point.len(), 1);
+ assert_eq!(identity_point.as_bytes(), &IDENTITY_BYTES[..]);
+ assert_eq!(identity_point.coordinates(), Coordinates::Identity);
+ assert_eq!(identity_point.x(), None);
+ assert_eq!(identity_point.y(), None);
+ }
+
+ #[test]
+ fn decode_invalid_tag() {
+ let mut compressed_bytes = COMPRESSED_BYTES.clone();
+ let mut uncompressed_bytes = UNCOMPRESSED_BYTES.clone();
+
+ for bytes in &mut [&mut compressed_bytes[..], &mut uncompressed_bytes[..]] {
+ for tag in 0..=0xFF {
+ // valid tags
+ if tag == 2 || tag == 3 || tag == 4 || tag == 5 {
+ continue;
+ }
+
+ (*bytes)[0] = tag;
+ let decode_result = EncodedPoint::from_bytes(&*bytes);
+ assert!(decode_result.is_err());
+ }
+ }
+ }
+
+ #[test]
+ fn decode_truncated_point() {
+ for bytes in &[&COMPRESSED_BYTES[..], &UNCOMPRESSED_BYTES[..]] {
+ for len in 0..bytes.len() {
+ let decode_result = EncodedPoint::from_bytes(&bytes[..len]);
+ assert!(decode_result.is_err());
+ }
+ }
+ }
+
+ #[test]
+ fn from_untagged_point() {
+ let untagged_bytes = hex!("11111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222222");
+ let uncompressed_point =
+ EncodedPoint::from_untagged_bytes(GenericArray::from_slice(&untagged_bytes[..]));
+ assert_eq!(uncompressed_point.as_bytes(), &UNCOMPRESSED_BYTES[..]);
+ }
+
+ #[test]
+ fn from_affine_coordinates() {
+ let x = hex!("1111111111111111111111111111111111111111111111111111111111111111");
+ let y = hex!("2222222222222222222222222222222222222222222222222222222222222222");
+
+ let uncompressed_point = EncodedPoint::from_affine_coordinates(&x.into(), &y.into(), false);
+ assert_eq!(uncompressed_point.as_bytes(), &UNCOMPRESSED_BYTES[..]);
+
+ let compressed_point = EncodedPoint::from_affine_coordinates(&x.into(), &y.into(), true);
+ assert_eq!(compressed_point.as_bytes(), &COMPRESSED_BYTES[..]);
+ }
+
+ #[test]
+ fn compress() {
+ let uncompressed_point = EncodedPoint::from_bytes(&UNCOMPRESSED_BYTES[..]).unwrap();
+ let compressed_point = uncompressed_point.compress();
+ assert_eq!(compressed_point.as_bytes(), &COMPRESSED_BYTES[..]);
+ }
+
+ #[cfg(feature = "subtle")]
+ #[test]
+ fn conditional_select() {
+ let a = EncodedPoint::from_bytes(&COMPRESSED_BYTES[..]).unwrap();
+ let b = EncodedPoint::from_bytes(&UNCOMPRESSED_BYTES[..]).unwrap();
+
+ let a_selected = EncodedPoint::conditional_select(&a, &b, 0.into());
+ assert_eq!(a, a_selected);
+
+ let b_selected = EncodedPoint::conditional_select(&a, &b, 1.into());
+ assert_eq!(b, b_selected);
+ }
+
+ #[test]
+ fn identity() {
+ let identity_point = EncodedPoint::identity();
+ assert_eq!(identity_point.tag(), Tag::Identity);
+ assert_eq!(identity_point.len(), 1);
+ assert_eq!(identity_point.as_bytes(), &IDENTITY_BYTES[..]);
+
+ // identity is default
+ assert_eq!(identity_point, EncodedPoint::default());
+ }
+
+ #[test]
+ fn decode_hex() {
+ let point = EncodedPoint::from_str(
+ "021111111111111111111111111111111111111111111111111111111111111111",
+ )
+ .unwrap();
+ assert_eq!(point.as_bytes(), COMPRESSED_BYTES);
+ }
+
+ #[cfg(feature = "alloc")]
+ #[test]
+ fn to_bytes() {
+ let uncompressed_point = EncodedPoint::from_bytes(&UNCOMPRESSED_BYTES[..]).unwrap();
+ assert_eq!(&*uncompressed_point.to_bytes(), &UNCOMPRESSED_BYTES[..]);
+ }
+
+ #[cfg(feature = "alloc")]
+ #[test]
+ fn to_string() {
+ let point = EncodedPoint::from_bytes(&COMPRESSED_BYTES[..]).unwrap();
+ assert_eq!(
+ point.to_string(),
+ "021111111111111111111111111111111111111111111111111111111111111111"
+ );
+ }
+}
diff --git a/src/private_key.rs b/src/private_key.rs
new file mode 100644
index 0000000..2362432
--- /dev/null
+++ b/src/private_key.rs
@@ -0,0 +1,164 @@
+//! SEC1 elliptic curve private key support.
+//!
+//! Support for ASN.1 DER-encoded elliptic curve private keys as described in
+//! SEC1: Elliptic Curve Cryptography (Version 2.0) Appendix C.4 (p.108):
+//!
+//! <https://www.secg.org/sec1-v2.pdf>
+
+use crate::{EcParameters, Error, Result};
+use core::fmt;
+use der::{
+ asn1::{BitStringRef, ContextSpecific, OctetStringRef},
+ Decode, DecodeValue, Encode, Header, Reader, Sequence, Tag, TagMode, TagNumber,
+};
+
+#[cfg(feature = "alloc")]
+use der::SecretDocument;
+
+#[cfg(feature = "pem")]
+use der::pem::PemLabel;
+
+/// `ECPrivateKey` version.
+///
+/// From [RFC5913 Section 3]:
+/// > version specifies the syntax version number of the elliptic curve
+/// > private key structure. For this version of the document, it SHALL
+/// > be set to ecPrivkeyVer1, which is of type INTEGER and whose value
+/// > is one (1).
+///
+/// [RFC5915 Section 3]: https://datatracker.ietf.org/doc/html/rfc5915#section-3
+const VERSION: u8 = 1;
+
+/// Context-specific tag number for the elliptic curve parameters.
+const EC_PARAMETERS_TAG: TagNumber = TagNumber::new(0);
+
+/// Context-specific tag number for the public key.
+const PUBLIC_KEY_TAG: TagNumber = TagNumber::new(1);
+
+/// SEC1 elliptic curve private key.
+///
+/// Described in [SEC1: Elliptic Curve Cryptography (Version 2.0)]
+/// Appendix C.4 (p.108) and also [RFC5915 Section 3]:
+///
+/// ```text
+/// ECPrivateKey ::= SEQUENCE {
+/// version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
+/// privateKey OCTET STRING,
+/// parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
+/// publicKey [1] BIT STRING OPTIONAL
+/// }
+/// ```
+///
+/// When encoded as PEM (text), keys in this format begin with the following:
+///
+/// ```text
+/// -----BEGIN EC PRIVATE KEY-----
+/// ```
+///
+/// [SEC1: Elliptic Curve Cryptography (Version 2.0)]: https://www.secg.org/sec1-v2.pdf
+/// [RFC5915 Section 3]: https://datatracker.ietf.org/doc/html/rfc5915#section-3
+#[derive(Clone)]
+#[cfg_attr(docsrs, doc(cfg(feature = "der")))]
+pub struct EcPrivateKey<'a> {
+ /// Private key data.
+ pub private_key: &'a [u8],
+
+ /// Elliptic curve parameters.
+ pub parameters: Option<EcParameters>,
+
+ /// Public key data, optionally available if version is V2.
+ pub public_key: Option<&'a [u8]>,
+}
+
+impl<'a> DecodeValue<'a> for EcPrivateKey<'a> {
+ fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> der::Result<Self> {
+ reader.read_nested(header.length, |reader| {
+ if u8::decode(reader)? != VERSION {
+ return Err(der::Tag::Integer.value_error());
+ }
+
+ let private_key = OctetStringRef::decode(reader)?.as_bytes();
+ let parameters = reader.context_specific(EC_PARAMETERS_TAG, TagMode::Explicit)?;
+ let public_key = reader
+ .context_specific::<BitStringRef<'_>>(PUBLIC_KEY_TAG, TagMode::Explicit)?
+ .map(|bs| bs.as_bytes().ok_or_else(|| Tag::BitString.value_error()))
+ .transpose()?;
+
+ Ok(EcPrivateKey {
+ private_key,
+ parameters,
+ public_key,
+ })
+ })
+ }
+}
+
+impl<'a> Sequence<'a> for EcPrivateKey<'a> {
+ fn fields<F, T>(&self, f: F) -> der::Result<T>
+ where
+ F: FnOnce(&[&dyn Encode]) -> der::Result<T>,
+ {
+ f(&[
+ &VERSION,
+ &OctetStringRef::new(self.private_key)?,
+ &self.parameters.as_ref().map(|params| ContextSpecific {
+ tag_number: EC_PARAMETERS_TAG,
+ tag_mode: TagMode::Explicit,
+ value: *params,
+ }),
+ &self
+ .public_key
+ .map(|pk| {
+ BitStringRef::from_bytes(pk).map(|value| ContextSpecific {
+ tag_number: PUBLIC_KEY_TAG,
+ tag_mode: TagMode::Explicit,
+ value,
+ })
+ })
+ .transpose()?,
+ ])
+ }
+}
+
+impl<'a> TryFrom<&'a [u8]> for EcPrivateKey<'a> {
+ type Error = Error;
+
+ fn try_from(bytes: &'a [u8]) -> Result<EcPrivateKey<'a>> {
+ Ok(Self::from_der(bytes)?)
+ }
+}
+
+impl<'a> fmt::Debug for EcPrivateKey<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("EcPrivateKey")
+ .field("parameters", &self.parameters)
+ .field("public_key", &self.public_key)
+ .finish_non_exhaustive()
+ }
+}
+
+#[cfg(feature = "alloc")]
+#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
+impl TryFrom<EcPrivateKey<'_>> for SecretDocument {
+ type Error = Error;
+
+ fn try_from(private_key: EcPrivateKey<'_>) -> Result<Self> {
+ SecretDocument::try_from(&private_key)
+ }
+}
+
+#[cfg(feature = "alloc")]
+#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
+impl TryFrom<&EcPrivateKey<'_>> for SecretDocument {
+ type Error = Error;
+
+ fn try_from(private_key: &EcPrivateKey<'_>) -> Result<Self> {
+ Ok(Self::encode_msg(private_key)?)
+ }
+}
+
+#[cfg(feature = "pem")]
+#[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
+impl PemLabel for EcPrivateKey<'_> {
+ const PEM_LABEL: &'static str = "EC PRIVATE KEY";
+}
diff --git a/src/traits.rs b/src/traits.rs
new file mode 100644
index 0000000..cf0d971
--- /dev/null
+++ b/src/traits.rs
@@ -0,0 +1,131 @@
+//! Traits for parsing objects from SEC1 encoded documents
+
+use crate::Result;
+
+#[cfg(feature = "alloc")]
+use der::SecretDocument;
+
+#[cfg(feature = "pem")]
+use {crate::LineEnding, alloc::string::String, der::pem::PemLabel};
+
+#[cfg(feature = "pkcs8")]
+use {
+ crate::{EcPrivateKey, ALGORITHM_OID},
+ der::Decode,
+};
+
+#[cfg(feature = "std")]
+use std::path::Path;
+
+#[cfg(feature = "pem")]
+use zeroize::Zeroizing;
+
+/// Parse an [`EcPrivateKey`] from a SEC1-encoded document.
+#[cfg_attr(docsrs, doc(cfg(feature = "der")))]
+pub trait DecodeEcPrivateKey: Sized {
+ /// Deserialize SEC1 private key from ASN.1 DER-encoded data
+ /// (binary format).
+ fn from_sec1_der(bytes: &[u8]) -> Result<Self>;
+
+ /// Deserialize SEC1-encoded private key from PEM.
+ ///
+ /// Keys in this format begin with the following:
+ ///
+ /// ```text
+ /// -----BEGIN EC PRIVATE KEY-----
+ /// ```
+ #[cfg(feature = "pem")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
+ fn from_sec1_pem(s: &str) -> Result<Self> {
+ let (label, doc) = SecretDocument::from_pem(s)?;
+ EcPrivateKey::validate_pem_label(label)?;
+ Self::from_sec1_der(doc.as_bytes())
+ }
+
+ /// Load SEC1 private key from an ASN.1 DER-encoded file on the local
+ /// filesystem (binary format).
+ #[cfg(feature = "std")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+ fn read_sec1_der_file(path: impl AsRef<Path>) -> Result<Self> {
+ Self::from_sec1_der(SecretDocument::read_der_file(path)?.as_bytes())
+ }
+
+ /// Load SEC1 private key from a PEM-encoded file on the local filesystem.
+ #[cfg(all(feature = "pem", feature = "std"))]
+ #[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
+ #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+ fn read_sec1_pem_file(path: impl AsRef<Path>) -> Result<Self> {
+ let (label, doc) = SecretDocument::read_pem_file(path)?;
+ EcPrivateKey::validate_pem_label(&label)?;
+ Self::from_sec1_der(doc.as_bytes())
+ }
+}
+
+/// Serialize a [`EcPrivateKey`] to a SEC1 encoded document.
+#[cfg(feature = "alloc")]
+#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "der"))))]
+pub trait EncodeEcPrivateKey {
+ /// Serialize a [`SecretDocument`] containing a SEC1-encoded private key.
+ fn to_sec1_der(&self) -> Result<SecretDocument>;
+
+ /// Serialize this private key as PEM-encoded SEC1 with the given [`LineEnding`].
+ ///
+ /// To use the OS's native line endings, pass `Default::default()`.
+ #[cfg(feature = "pem")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
+ fn to_sec1_pem(&self, line_ending: LineEnding) -> Result<Zeroizing<String>> {
+ let doc = self.to_sec1_der()?;
+ Ok(doc.to_pem(EcPrivateKey::PEM_LABEL, line_ending)?)
+ }
+
+ /// Write ASN.1 DER-encoded SEC1 private key to the given path.
+ #[cfg(feature = "std")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+ fn write_sec1_der_file(&self, path: impl AsRef<Path>) -> Result<()> {
+ Ok(self.to_sec1_der()?.write_der_file(path)?)
+ }
+
+ /// Write ASN.1 DER-encoded SEC1 private key to the given path.
+ #[cfg(all(feature = "pem", feature = "std"))]
+ #[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
+ #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+ fn write_sec1_pem_file(&self, path: impl AsRef<Path>, line_ending: LineEnding) -> Result<()> {
+ let doc = self.to_sec1_der()?;
+ Ok(doc.write_pem_file(path, EcPrivateKey::PEM_LABEL, line_ending)?)
+ }
+}
+
+#[cfg(feature = "pkcs8")]
+#[cfg_attr(docsrs, doc(cfg(feature = "pkcs8")))]
+impl<T: pkcs8::DecodePrivateKey> DecodeEcPrivateKey for T {
+ fn from_sec1_der(private_key: &[u8]) -> Result<Self> {
+ let params_oid = EcPrivateKey::from_der(private_key)?
+ .parameters
+ .and_then(|params| params.named_curve());
+
+ let algorithm = pkcs8::AlgorithmIdentifier {
+ oid: ALGORITHM_OID,
+ parameters: params_oid.as_ref().map(Into::into),
+ };
+
+ Ok(Self::try_from(pkcs8::PrivateKeyInfo {
+ algorithm,
+ private_key,
+ public_key: None,
+ })?)
+ }
+}
+
+#[cfg(all(feature = "alloc", feature = "pkcs8"))]
+#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "pkcs8"))))]
+impl<T: pkcs8::EncodePrivateKey> EncodeEcPrivateKey for T {
+ fn to_sec1_der(&self) -> Result<SecretDocument> {
+ let doc = self.to_pkcs8_der()?;
+ let pkcs8_key = pkcs8::PrivateKeyInfo::from_der(doc.as_bytes())?;
+ pkcs8_key.algorithm.assert_algorithm_oid(ALGORITHM_OID)?;
+
+ let mut pkcs1_key = EcPrivateKey::from_der(pkcs8_key.private_key)?;
+ pkcs1_key.parameters = Some(pkcs8_key.algorithm.parameters_oid()?.into());
+ pkcs1_key.try_into()
+ }
+}
diff --git a/tests/examples/p256-priv.der b/tests/examples/p256-priv.der
new file mode 100644
index 0000000..c8528c3
--- /dev/null
+++ b/tests/examples/p256-priv.der
Binary files differ
diff --git a/tests/examples/p256-priv.pem b/tests/examples/p256-priv.pem
new file mode 100644
index 0000000..d5a1c1a
--- /dev/null
+++ b/tests/examples/p256-priv.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIGliQXFWGmM0DeDn2GnyoFSSVY4aBIaLap+FSoZniBiNoAoGCCqGSM49
+AwEHoUQDQgAEHKz/tV8vLO/YnYnrN0smgRUkUoAt7qCZFgaBN9g5z3/EgaREkjBN
+fvZqwRe+/oOo0I8VXytS+fYY3URwKQSODw==
+-----END EC PRIVATE KEY-----
diff --git a/tests/private_key.rs b/tests/private_key.rs
new file mode 100644
index 0000000..5b985da
--- /dev/null
+++ b/tests/private_key.rs
@@ -0,0 +1,32 @@
+//! SEC1 private key tests
+
+#![cfg(feature = "der")]
+
+use der::asn1::ObjectIdentifier;
+use hex_literal::hex;
+use sec1::{EcParameters, EcPrivateKey};
+
+/// NIST P-256 SEC1 private key encoded as ASN.1 DER.
+///
+/// Note: this key is extracted from the corresponding `p256-priv.der`
+/// example key in the `pkcs8` crate.
+const P256_DER_EXAMPLE: &[u8] = include_bytes!("examples/p256-priv.der");
+
+#[test]
+fn decode_p256_der() {
+ let key = EcPrivateKey::try_from(P256_DER_EXAMPLE).unwrap();
+
+ // Extracted using:
+ // $ openssl asn1parse -in tests/examples/p256-priv.pem
+ assert_eq!(
+ key.private_key,
+ hex!("69624171561A63340DE0E7D869F2A05492558E1A04868B6A9F854A866788188D")
+ );
+ assert_eq!(
+ key.parameters,
+ Some(EcParameters::NamedCurve(
+ ObjectIdentifier::new("1.2.840.10045.3.1.7").unwrap()
+ ))
+ );
+ assert_eq!(key.public_key, Some(hex!("041CACFFB55F2F2CEFD89D89EB374B2681152452802DEEA09916068137D839CF7FC481A44492304D7EF66AC117BEFE83A8D08F155F2B52F9F618DD447029048E0F").as_ref()));
+}
diff --git a/tests/traits.rs b/tests/traits.rs
new file mode 100644
index 0000000..4bcd679
--- /dev/null
+++ b/tests/traits.rs
@@ -0,0 +1,100 @@
+//! Tests for SEC1 encoding/decoding traits.
+
+#![cfg(any(feature = "pem", feature = "std"))]
+
+use der::SecretDocument;
+use sec1::{DecodeEcPrivateKey, EncodeEcPrivateKey, Result};
+
+#[cfg(feature = "pem")]
+use sec1::der::pem::LineEnding;
+
+#[cfg(feature = "std")]
+use tempfile::tempdir;
+
+#[cfg(all(feature = "pem", feature = "std"))]
+use std::fs;
+
+/// SEC1 `EcPrivateKey` encoded as ASN.1 DER
+const P256_DER_EXAMPLE: &[u8] = include_bytes!("examples/p256-priv.der");
+
+/// SEC1 `EcPrivateKey` encoded as PEM
+#[cfg(feature = "pem")]
+const P256_PEM_EXAMPLE: &str = include_str!("examples/p256-priv.pem");
+
+/// Mock private key type for testing trait impls against.
+pub struct MockPrivateKey(Vec<u8>);
+
+impl AsRef<[u8]> for MockPrivateKey {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+impl DecodeEcPrivateKey for MockPrivateKey {
+ fn from_sec1_der(bytes: &[u8]) -> Result<MockPrivateKey> {
+ Ok(MockPrivateKey(bytes.to_vec()))
+ }
+}
+
+impl EncodeEcPrivateKey for MockPrivateKey {
+ fn to_sec1_der(&self) -> Result<SecretDocument> {
+ Ok(SecretDocument::try_from(self.as_ref())?)
+ }
+}
+
+#[cfg(feature = "pem")]
+#[test]
+fn from_sec1_pem() {
+ let key = MockPrivateKey::from_sec1_pem(P256_PEM_EXAMPLE).unwrap();
+ assert_eq!(key.as_ref(), P256_DER_EXAMPLE);
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn read_sec1_der_file() {
+ let key = MockPrivateKey::read_sec1_der_file("tests/examples/p256-priv.der").unwrap();
+ assert_eq!(key.as_ref(), P256_DER_EXAMPLE);
+}
+
+#[cfg(all(feature = "pem", feature = "std"))]
+#[test]
+fn read_sec1_pem_file() {
+ let key = MockPrivateKey::read_sec1_pem_file("tests/examples/p256-priv.pem").unwrap();
+ assert_eq!(key.as_ref(), P256_DER_EXAMPLE);
+}
+
+#[cfg(feature = "pem")]
+#[test]
+fn to_sec1_pem() {
+ let pem = MockPrivateKey(P256_DER_EXAMPLE.to_vec())
+ .to_sec1_pem(LineEnding::LF)
+ .unwrap();
+
+ assert_eq!(&*pem, P256_PEM_EXAMPLE);
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn write_sec1_der_file() {
+ let dir = tempdir().unwrap();
+ let path = dir.path().join("example.der");
+ MockPrivateKey(P256_DER_EXAMPLE.to_vec())
+ .write_sec1_der_file(&path)
+ .unwrap();
+
+ let key = MockPrivateKey::read_sec1_der_file(&path).unwrap();
+ assert_eq!(key.as_ref(), P256_DER_EXAMPLE);
+}
+
+#[cfg(all(feature = "pem", feature = "std"))]
+#[test]
+fn write_sec1_pem_file() {
+ let dir = tempdir().unwrap();
+ let path = dir.path().join("example.pem");
+ MockPrivateKey(P256_DER_EXAMPLE.to_vec())
+ .write_sec1_pem_file(&path, LineEnding::LF)
+ .unwrap();
+
+ let pem = fs::read_to_string(path).unwrap();
+ assert_eq!(&pem, P256_PEM_EXAMPLE);
+}