Skip to content

Commit

Permalink
Add Prime256v1Identity (#508)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamspofford-dfinity authored Jan 24, 2024
1 parent 73da09c commit ef55bd6
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

* Added a prime256v1-based `Identity` impl to complement the ed25519 and secp256k1 `Identity` impls.

## [0.32.0] - 2024-01-18

* Added the chunked wasm API to ic-utils. Existing code that uses `install_code` should probably update to `install`, which works the same but silently handles large wasm modules.
Expand Down
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ic-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ ic-certification = { workspace = true }
ic-transport-types = { workspace = true }
ic-verify-bls-signature = "0.1"
k256 = { version = "0.13.1", features = ["pem"] }
p256 = { version = "0.13.2", features = ["pem"] }
leb128 = { workspace = true }
pkcs8 = { version = "0.10.2", features = ["std"] }
sec1 = { version = "0.7.2", features = ["pem"] }
Expand Down
4 changes: 2 additions & 2 deletions ic-agent/src/identity/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ pub enum PemError {
Io(#[from] std::io::Error),

/// An unsupported curve was detected
#[error("Only secp256k1 curve is supported: {0:?}")]
UnsupportedKeyCurve(Vec<u8>),
#[error("Only {0} curve is supported: {1:?}")]
UnsupportedKeyCurve(String, Vec<u8>),

/// An error occurred while reading the file in PEM format.
#[cfg(feature = "pem")]
Expand Down
3 changes: 3 additions & 0 deletions ic-agent/src/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{agent::EnvelopeContent, export::Principal};
pub(crate) mod anonymous;
pub(crate) mod basic;
pub(crate) mod delegated;
pub(crate) mod prime256v1;
pub(crate) mod secp256k1;

#[cfg(feature = "pem")]
Expand All @@ -20,6 +21,8 @@ pub use delegated::DelegatedIdentity;
#[doc(inline)]
pub use ic_transport_types::{Delegation, SignedDelegation};
#[doc(inline)]
pub use prime256v1::Prime256v1Identity;
#[doc(inline)]
pub use secp256k1::Secp256k1Identity;

#[cfg(feature = "pem")]
Expand Down
228 changes: 228 additions & 0 deletions ic-agent/src/identity/prime256v1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
use crate::{agent::EnvelopeContent, export::Principal, Identity, Signature};

#[cfg(feature = "pem")]
use crate::identity::error::PemError;

use p256::{
ecdsa::{self, signature::Signer, SigningKey, VerifyingKey},
pkcs8::{Document, EncodePublicKey},
SecretKey,
};
#[cfg(feature = "pem")]
use std::{fs::File, io, path::Path};

use super::Delegation;

/// A cryptographic identity based on the Prime256v1 elliptic curve.
///
/// The caller will be represented via [`Principal::self_authenticating`], which contains the SHA-224 hash of the public key.
#[derive(Clone, Debug)]
pub struct Prime256v1Identity {
private_key: SigningKey,
_public_key: VerifyingKey,
der_encoded_public_key: Document,
}

impl Prime256v1Identity {
/// Creates an identity from a PEM file. Shorthand for calling `from_pem` with `std::fs::read`.
#[cfg(feature = "pem")]
pub fn from_pem_file<P: AsRef<Path>>(file_path: P) -> Result<Self, PemError> {
Self::from_pem(File::open(file_path)?)
}

/// Creates an identity from a PEM certificate.
#[cfg(feature = "pem")]
pub fn from_pem<R: io::Read>(pem_reader: R) -> Result<Self, PemError> {
use sec1::{pem::PemLabel, EcPrivateKey};

const EC_PARAMETERS: &str = "EC PARAMETERS";
const PRIME256V1: &[u8] = b"\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07";

let contents = pem_reader.bytes().collect::<Result<Vec<u8>, io::Error>>()?;

for pem in pem::parse_many(contents)? {
if pem.tag() == EC_PARAMETERS && pem.contents() != PRIME256V1 {
return Err(PemError::UnsupportedKeyCurve(
"prime256v1".to_string(),
pem.contents().to_vec(),
));
}

if pem.tag() != EcPrivateKey::PEM_LABEL {
continue;
}
let private_key =
SecretKey::from_sec1_der(pem.contents()).map_err(|_| pkcs8::Error::KeyMalformed)?;
return Ok(Self::from_private_key(private_key));
}
Err(pem::PemError::MissingData.into())
}

/// Creates an identity from a private key.
pub fn from_private_key(private_key: SecretKey) -> Self {
let public_key = private_key.public_key();
let der_encoded_public_key = public_key
.to_public_key_der()
.expect("Cannot DER encode prime256v1 public key.");
Self {
private_key: private_key.into(),
_public_key: public_key.into(),
der_encoded_public_key,
}
}
}

impl Identity for Prime256v1Identity {
fn sender(&self) -> Result<Principal, String> {
Ok(Principal::self_authenticating(
self.der_encoded_public_key.as_ref(),
))
}

fn public_key(&self) -> Option<Vec<u8>> {
Some(self.der_encoded_public_key.as_ref().to_vec())
}

fn sign(&self, content: &EnvelopeContent) -> Result<Signature, String> {
self.sign_arbitrary(&content.to_request_id().signable())
}

fn sign_delegation(&self, content: &Delegation) -> Result<Signature, String> {
self.sign_arbitrary(&content.signable())
}

fn sign_arbitrary(&self, content: &[u8]) -> Result<Signature, String> {
let ecdsa_sig: ecdsa::Signature = self
.private_key
.try_sign(content)
.map_err(|err| format!("Cannot create prime256v1 signature: {}", err))?;
let r = ecdsa_sig.r().as_ref().to_bytes();
let s = ecdsa_sig.s().as_ref().to_bytes();
let mut bytes = [0u8; 64];
if r.len() > 32 || s.len() > 32 {
return Err("Cannot create prime256v1 signature: malformed signature.".to_string());
}
bytes[(32 - r.len())..32].clone_from_slice(&r);
bytes[32 + (32 - s.len())..].clone_from_slice(&s);
let signature = Some(bytes.to_vec());
let public_key = self.public_key();
Ok(Signature {
public_key,
signature,
delegations: None,
})
}
}

#[cfg(feature = "pem")]
#[cfg(test)]
mod test {
use super::*;
use candid::Encode;
use p256::{
ecdsa::{signature::Verifier, Signature},
elliptic_curve::PrimeField,
FieldBytes, Scalar,
};

// WRONG_CURVE_IDENTITY_FILE is generated from the following command:
// > openssl ecparam -name secp160r2 -genkey
// it uses the secp160r2 curve instead of prime256v1 and should
// therefore be rejected by Prime256v1Identity when loading an identity
const WRONG_CURVE_IDENTITY_FILE: &str = "\
-----BEGIN EC PARAMETERS-----
BgUrgQQAHg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR
oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ==
-----END EC PRIVATE KEY-----
";

// WRONG_CURVE_IDENTITY_FILE_NO_PARAMS is generated from the following command:
// > openssl ecparam -name secp160r2 -genkey -noout
// it uses the secp160r2 curve instead of prime256v1 and should
// therefore be rejected by Prime256v1Identity when loading an identity
const WRONG_CURVE_IDENTITY_FILE_NO_PARAMS: &str = "\
-----BEGIN EC PRIVATE KEY-----
MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR
oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ==
-----END EC PRIVATE KEY-----
";

// IDENTITY_FILE was generated from the the following commands:
// > openssl ecparam -name prime256v1 -genkey -noout -out identity.pem
// > cat identity.pem
const IDENTITY_FILE: &str = "\
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIL1ybmbwx+uKYsscOZcv71MmKhrNqfPP0ke1unET5AY4oAoGCCqGSM49
AwEHoUQDQgAEUbbZV4NerZTPWfbQ749/GNLu8TaH8BUS/I7/+ipsu+MPywfnBFIZ
Sks4xGbA/ZbazsrMl4v446U5UIVxCGGaKw==
-----END EC PRIVATE KEY-----
";

// DER_ENCODED_PUBLIC_KEY was generated from the the following commands:
// > openssl ec -in identity.pem -pubout -outform DER -out public.der
// > hexdump -ve '1/1 "%.2x"' public.der
const DER_ENCODED_PUBLIC_KEY: &str = "3059301306072a8648ce3d020106082a8648ce3d0301070342000451b6d957835ead94cf59f6d0ef8f7f18d2eef13687f01512fc8efffa2a6cbbe30fcb07e70452194a4b38c466c0fd96dacecacc978bf8e3a53950857108619a2b";

#[test]
#[should_panic(expected = "UnsupportedKeyCurve")]
fn test_prime256v1_reject_wrong_curve() {
Prime256v1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE.as_bytes()).unwrap();
}

#[test]
#[should_panic(expected = "KeyMalformed")]
fn test_prime256v1_reject_wrong_curve_no_id() {
Prime256v1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE_NO_PARAMS.as_bytes()).unwrap();
}

#[test]
fn test_prime256v1_public_key() {
// Create a prime256v1 identity from a PEM file.
let identity = Prime256v1Identity::from_pem(IDENTITY_FILE.as_bytes())
.expect("Cannot create prime256v1 identity from PEM file.");

// Assert the DER-encoded prime256v1 public key matches what we would expect.
assert!(DER_ENCODED_PUBLIC_KEY == hex::encode(identity.der_encoded_public_key));
}

#[test]
fn test_prime256v1_signature() {
// Create a prime256v1 identity from a PEM file.
let identity = Prime256v1Identity::from_pem(IDENTITY_FILE.as_bytes())
.expect("Cannot create prime256v1 identity from PEM file.");

// Create a prime256v1 signature for a hello-world canister.
let message = EnvelopeContent::Call {
nonce: None,
ingress_expiry: 0,
sender: identity.sender().unwrap(),
canister_id: "bkyz2-fmaaa-aaaaa-qaaaq-cai".parse().unwrap(),
method_name: "greet".to_string(),
arg: Encode!(&"world").unwrap(),
};
let signature = identity
.sign(&message)
.expect("Cannot create prime256v1 signature.")
.signature
.expect("Cannot find prime256v1 signature bytes.");

// Import the prime256v1 signature.
let r: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice(
&signature[0..32],
)))
.expect("Cannot extract r component from prime256v1 signature bytes.");
let s: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice(&signature[32..])))
.expect("Cannot extract s component from prime256v1 signature bytes.");
let ecdsa_sig = Signature::from_scalars(r, s)
.expect("Cannot create prime256v1 signature from r and s components.");

// Assert the prime256v1 signature is valid.
identity
._public_key
.verify(&message.to_request_id().signable(), &ecdsa_sig)
.expect("Cannot verify prime256v1 signature.");
}
}
5 changes: 4 additions & 1 deletion ic-agent/src/identity/secp256k1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ impl Secp256k1Identity {

for pem in pem::parse_many(contents)? {
if pem.tag() == EC_PARAMETERS && pem.contents() != SECP256K1 {
return Err(PemError::UnsupportedKeyCurve(pem.contents().to_vec()));
return Err(PemError::UnsupportedKeyCurve(
"secp256k1".to_string(),
pem.contents().to_vec(),
));
}

if pem.tag() != EcPrivateKey::PEM_LABEL {
Expand Down
18 changes: 17 additions & 1 deletion ref-tests/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ic_agent::agent::http_transport::ReqwestTransport;
use ic_agent::identity::Secp256k1Identity;
use ic_agent::identity::{Prime256v1Identity, Secp256k1Identity};
use ic_agent::{export::Principal, identity::BasicIdentity, Agent, Identity};
use ic_identity_hsm::HardwareIdentity;
use ic_utils::interfaces::{management_canister::builders::MemoryAllocation, ManagementCanister};
Expand Down Expand Up @@ -77,6 +77,22 @@ yeMC60IsMNxDjLqElV7+T7dkb5Ki7Q==
Ok(identity)
}

pub fn create_prime256v1_identity() -> Result<Prime256v1Identity, String> {
// generated from the following command:
// $ openssl ecparam -name prime256v1 -genkey -noout -out identity.pem
// $ cat identity.pem
let identity_file = "\
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIL1ybmbwx+uKYsscOZcv71MmKhrNqfPP0ke1unET5AY4oAoGCCqGSM49
AwEHoUQDQgAEUbbZV4NerZTPWfbQ749/GNLu8TaH8BUS/I7/+ipsu+MPywfnBFIZ
Sks4xGbA/ZbazsrMl4v446U5UIVxCGGaKw==
-----END EC PRIVATE KEY-----";

let identity = Prime256v1Identity::from_pem(identity_file.as_bytes())
.expect("Cannot create prime256v1 identity from PEM file.");
Ok(identity)
}

pub async fn create_agent(identity: impl Identity + 'static) -> Result<Agent, String> {
let port_env = std::env::var("IC_REF_PORT").unwrap_or_else(|_| "8001".into());
let port = port_env
Expand Down
Loading

0 comments on commit ef55bd6

Please sign in to comment.