diff --git a/openssl-sys/src/lib.rs b/openssl-sys/src/lib.rs index eb7750f7..fb4d8d30 100644 --- a/openssl-sys/src/lib.rs +++ b/openssl-sys/src/lib.rs @@ -603,6 +603,7 @@ extern "C" { pub fn X509_STORE_CTX_get_ex_data(ctx: *mut X509_STORE_CTX, idx: c_int) -> *mut c_void; pub fn X509V3_EXT_conf_nid(conf: *mut c_void, ctx: *mut X509V3_CTX, ext_nid: c_int, value: *mut c_char) -> *mut X509_EXTENSION; + pub fn X509V3_EXT_conf(conf: *mut c_void, ctx: *mut X509V3_CTX, name: *mut c_char, value: *mut c_char) -> *mut X509_EXTENSION; pub fn X509V3_set_ctx(ctx: *mut X509V3_CTX, issuer: *mut X509, subject: *mut X509, req: *mut X509_REQ, crl: *mut X509_CRL, flags: c_int); pub fn i2d_RSA_PUBKEY(k: *mut RSA, buf: *const *mut u8) -> c_int; diff --git a/openssl/src/nid.rs b/openssl/src/nid.rs index c5b9e277..81cc4975 100644 --- a/openssl/src/nid.rs +++ b/openssl/src/nid.rs @@ -1,5 +1,5 @@ #[allow(non_camel_case_types)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Hash, PartialEq, Eq)] #[repr(usize)] pub enum Nid { Undefined, diff --git a/openssl/src/x509/extension.rs b/openssl/src/x509/extension.rs new file mode 100644 index 00000000..3faa0996 --- /dev/null +++ b/openssl/src/x509/extension.rs @@ -0,0 +1,212 @@ +use std::fmt; +use nid::Nid; + +/// Type-only version of the `Extension` enum. +/// +/// See the `Extension` documentation for more information on the different +/// variants. +#[derive(Clone,Hash,PartialEq,Eq)] +pub enum ExtensionType { + KeyUsage, + ExtKeyUsage, + SubjectAltName, + IssuerAltName, + OtherNid(Nid), + OtherStr(String), +} + +/// A X.509 v3 certificate extension. +/// +/// Only one extension of each type is allow in a certificate. +/// See RFC 3280 for more information about extensions. +#[derive(Clone)] +pub enum Extension { + /// The purposes of the key contained in the certificate + KeyUsage(Vec), + /// The extended purposes of the key contained in the certificate + ExtKeyUsage(Vec), + /// Subject Alternative Names + SubjectAltName(Vec<(AltNameOption,String)>), + /// Issuer Alternative Names + IssuerAltName(Vec<(AltNameOption,String)>), + /// Arbitrary extensions by NID. See `man x509v3_config` for value syntax. + /// + /// You must not use this to add extensions which this enum can express directly. + /// + /// ``` + /// use openssl::x509::extension::Extension::*; + /// use openssl::nid::Nid; + /// + /// # let generator = openssl::x509::X509Generator::new(); + /// generator.add_extension(OtherNid(Nid::BasicConstraints,"critical,CA:TRUE".to_owned())); + /// ``` + OtherNid(Nid,String), + /// Arbitrary extensions by OID string. See `man ASN1_generate_nconf` for value syntax. + /// + /// You must not use this to add extensions which this enum can express directly. + /// + /// ``` + /// use openssl::x509::extension::Extension::*; + /// + /// # let generator = openssl::x509::X509Generator::new(); + /// generator.add_extension(OtherStr("2.999.2".to_owned(),"ASN1:UTF8:example value".to_owned())); + /// ``` + OtherStr(String,String), +} + +impl Extension { + pub fn get_type(&self) -> ExtensionType { + match self { + &Extension::KeyUsage(_) => ExtensionType::KeyUsage, + &Extension::ExtKeyUsage(_) => ExtensionType::ExtKeyUsage, + &Extension::SubjectAltName(_) => ExtensionType::SubjectAltName, + &Extension::IssuerAltName(_) => ExtensionType::IssuerAltName, + &Extension::OtherNid(nid,_) => ExtensionType::OtherNid(nid), + &Extension::OtherStr(ref s,_) => ExtensionType::OtherStr(s.clone()), + } + } +} + +impl ExtensionType { + pub fn get_nid(&self) -> Option { + match self { + &ExtensionType::KeyUsage => Some(Nid::KeyUsage), + &ExtensionType::ExtKeyUsage => Some(Nid::ExtendedKeyUsage), + &ExtensionType::SubjectAltName => Some(Nid::SubjectAltName), + &ExtensionType::IssuerAltName => Some(Nid::IssuerAltName), + &ExtensionType::OtherNid(nid) => Some(nid), + &ExtensionType::OtherStr(_) => None, + } + } + + pub fn get_name<'a>(&'a self) -> Option<&'a str> { + match self { + &ExtensionType::OtherStr(ref s) => Some(s), + _ => None, + } + } +} + +// FIXME: This would be nicer as a method on Iterator. This can +// eventually be replaced by the successor to std::slice::SliceConcatExt.connect +fn join,T: ToString>(iter: I, sep: &str) -> String { + iter.enumerate().fold(String::new(), |mut acc, (idx, v)| { + if idx > 0 { acc.push_str(sep) }; + acc.push_str(&v.to_string()); + acc + }) +} + +impl ToString for Extension { + fn to_string(&self) -> String { + match self { + &Extension::KeyUsage(ref purposes) => join(purposes.iter(),","), + &Extension::ExtKeyUsage(ref purposes) => join(purposes.iter(),","), + &Extension::SubjectAltName(ref names) => join(names.iter().map(|&(ref opt,ref val)|opt.to_string()+":"+&val),","), + &Extension::IssuerAltName(ref names) => join(names.iter().map(|&(ref opt,ref val)|opt.to_string()+":"+&val),","), + &Extension::OtherNid(_,ref value) => value.clone(), + &Extension::OtherStr(_,ref value) => value.clone(), + } + } +} + +#[derive(Clone,Copy)] +pub enum KeyUsageOption { + DigitalSignature, + NonRepudiation, + KeyEncipherment, + DataEncipherment, + KeyAgreement, + KeyCertSign, + CRLSign, + EncipherOnly, + DecipherOnly, +} + +impl fmt::Display for KeyUsageOption { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + f.pad(match self { + &KeyUsageOption::DigitalSignature => "digitalSignature", + &KeyUsageOption::NonRepudiation => "nonRepudiation", + &KeyUsageOption::KeyEncipherment => "keyEncipherment", + &KeyUsageOption::DataEncipherment => "dataEncipherment", + &KeyUsageOption::KeyAgreement => "keyAgreement", + &KeyUsageOption::KeyCertSign => "keyCertSign", + &KeyUsageOption::CRLSign => "cRLSign", + &KeyUsageOption::EncipherOnly => "encipherOnly", + &KeyUsageOption::DecipherOnly => "decipherOnly", + }) + } +} + +#[derive(Clone)] +pub enum ExtKeyUsageOption { + ServerAuth, + ClientAuth, + CodeSigning, + EmailProtection, + TimeStamping, + MsCodeInd, + MsCodeCom, + MsCtlSign, + MsSgc, + MsEfs, + NsSgc, + /// An arbitrary key usage by OID. + Other(String), +} + +impl fmt::Display for ExtKeyUsageOption { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + f.pad(match self { + &ExtKeyUsageOption::ServerAuth => "serverAuth", + &ExtKeyUsageOption::ClientAuth => "clientAuth", + &ExtKeyUsageOption::CodeSigning => "codeSigning", + &ExtKeyUsageOption::EmailProtection => "emailProtection", + &ExtKeyUsageOption::TimeStamping => "timeStamping", + &ExtKeyUsageOption::MsCodeInd => "msCodeInd", + &ExtKeyUsageOption::MsCodeCom => "msCodeCom", + &ExtKeyUsageOption::MsCtlSign => "msCTLSign", + &ExtKeyUsageOption::MsSgc => "msSGC", + &ExtKeyUsageOption::MsEfs => "msEFS", + &ExtKeyUsageOption::NsSgc =>"nsSGC", + &ExtKeyUsageOption::Other(ref s) => &s[..], + }) + } +} + +#[derive(Clone, Copy)] +pub enum AltNameOption { + /// The value is specified as OID;content. See `man ASN1_generate_nconf` for more information on the content syntax. + /// + /// ``` + /// use openssl::x509::extension::Extension::*; + /// use openssl::x509::extension::AltNameOption::Other as OtherName; + /// + /// # let generator = openssl::x509::X509Generator::new(); + /// generator.add_extension(SubjectAltName(vec![(OtherName,"2.999.3;ASN1:UTF8:some other name".to_owned())])); + /// ``` + Other, + Email, + DNS, + //X400, // Not supported by OpenSSL + Directory, + //EDIParty, // Not supported by OpenSSL + URI, + IPAddress, + RegisteredID, +} + +impl fmt::Display for AltNameOption { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + f.pad(match self { + &AltNameOption::Other => "otherName", + &AltNameOption::Email => "email", + &AltNameOption::DNS => "DNS", + &AltNameOption::Directory => "dirName", + &AltNameOption::URI => "URI", + &AltNameOption::IPAddress => "IP", + &AltNameOption::RegisteredID => "RID", + }) + } +} diff --git a/openssl/src/x509/mod.rs b/openssl/src/x509/mod.rs index 5446f125..423a258f 100644 --- a/openssl/src/x509/mod.rs +++ b/openssl/src/x509/mod.rs @@ -9,6 +9,7 @@ use std::ptr; use std::ops::Deref; use std::fmt; use std::str; +use std::collections::HashMap; use asn1::{Asn1Time}; use bio::{MemBio}; @@ -20,6 +21,9 @@ use ffi; use ssl::error::{SslError, StreamError}; use nid; +pub mod extension; + +use self::extension::{ExtensionType,Extension}; #[cfg(test)] mod tests; @@ -98,92 +102,9 @@ impl X509StoreContext { } } -#[doc(hidden)] -trait AsStr<'a> { - fn as_str(&self) -> &'a str; -} - -#[derive(Clone, Copy)] -pub enum KeyUsage { - DigitalSignature, - NonRepudiation, - KeyEncipherment, - DataEncipherment, - KeyAgreement, - KeyCertSign, - CRLSign, - EncipherOnly, - DecipherOnly -} - -impl AsStr<'static> for KeyUsage { - fn as_str(&self) -> &'static str { - match self { - &KeyUsage::DigitalSignature => "digitalSignature", - &KeyUsage::NonRepudiation => "nonRepudiation", - &KeyUsage::KeyEncipherment => "keyEncipherment", - &KeyUsage::DataEncipherment => "dataEncipherment", - &KeyUsage::KeyAgreement => "keyAgreement", - &KeyUsage::KeyCertSign => "keyCertSign", - &KeyUsage::CRLSign => "cRLSign", - &KeyUsage::EncipherOnly => "encipherOnly", - &KeyUsage::DecipherOnly => "decipherOnly" - } - } -} - - -#[derive(Clone, Copy)] -pub enum ExtKeyUsage { - ServerAuth, - ClientAuth, - CodeSigning, - EmailProtection, - TimeStamping, - MsCodeInd, - MsCodeCom, - MsCtlSign, - MsSgc, - MsEfs, - NsSgc -} - -impl AsStr<'static> for ExtKeyUsage { - fn as_str(&self) -> &'static str { - match self { - &ExtKeyUsage::ServerAuth => "serverAuth", - &ExtKeyUsage::ClientAuth => "clientAuth", - &ExtKeyUsage::CodeSigning => "codeSigning", - &ExtKeyUsage::EmailProtection => "emailProtection", - &ExtKeyUsage::TimeStamping => "timeStamping", - &ExtKeyUsage::MsCodeInd => "msCodeInd", - &ExtKeyUsage::MsCodeCom => "msCodeCom", - &ExtKeyUsage::MsCtlSign => "msCTLSign", - &ExtKeyUsage::MsSgc => "msSGC", - &ExtKeyUsage::MsEfs => "msEFS", - &ExtKeyUsage::NsSgc =>"nsSGC" - } - } -} - - -// FIXME: a dirty hack as there is no way to -// implement ToString for Vec as both are defined -// in another crate -#[doc(hidden)] -trait ToStr { - fn to_str(&self) -> String; -} - -impl<'a, T: AsStr<'a>> ToStr for Vec { - fn to_str(&self) -> String { - self.iter().enumerate().fold(String::new(), |mut acc, (idx, v)| { - if idx > 0 { acc.push(',') }; - acc.push_str(v.as_str()); - acc - }) - } -} +// Backwards-compatibility +pub use self::extension::KeyUsageOption as KeyUsage; +pub use self::extension::ExtKeyUsageOption as ExtKeyUsage; #[allow(non_snake_case)] /// Generator of private key/certificate pairs @@ -225,8 +146,8 @@ pub struct X509Generator { bits: u32, days: u32, CN: String, - key_usage: Vec, - ext_key_usage: Vec, + // RFC 3280 ยง4.2: A certificate MUST NOT include more than one instance of a particular extension. + extensions: HashMap, hash_type: HashType, } @@ -245,8 +166,7 @@ impl X509Generator { bits: 1024, days: 365, CN: "rust-openssl".to_string(), - key_usage: Vec::new(), - ext_key_usage: Vec::new(), + extensions: HashMap::new(), hash_type: HashType::SHA1 } } @@ -270,15 +190,50 @@ impl X509Generator { self } - /// Sets what for certificate could be used - pub fn set_usage(mut self, purposes: &[KeyUsage]) -> X509Generator { - self.key_usage = purposes.to_vec(); + /// (deprecated) Sets what for certificate could be used + /// + /// This function is deprecated, use `X509Generator.add_extension` instead. + pub fn set_usage(self, purposes: &[KeyUsage]) -> X509Generator { + self.add_extension(Extension::KeyUsage(purposes.to_owned())) + } + + /// (deprecated) Sets allowed extended usage of certificate + /// + /// This function is deprecated, use `X509Generator.add_extension` instead. + pub fn set_ext_usage(self, purposes: &[ExtKeyUsage]) -> X509Generator { + self.add_extension(Extension::ExtKeyUsage(purposes.to_owned())) + } + + /// Add an extension to a certificate + /// + /// If the extension already exists, it will be replaced. + /// + /// ``` + /// use openssl::x509::extension::Extension::*; + /// use openssl::x509::extension::KeyUsageOption::*; + /// + /// # let generator = openssl::x509::X509Generator::new(); + /// generator.add_extension(KeyUsage(vec![DigitalSignature, KeyEncipherment])); + /// ``` + pub fn add_extension(mut self, ext: extension::Extension) -> X509Generator { + self.extensions.insert(ext.get_type(),ext); self } - /// Sets allowed extended usage of certificate - pub fn set_ext_usage(mut self, purposes: &[ExtKeyUsage]) -> X509Generator { - self.ext_key_usage = purposes.to_vec(); + /// Add multiple extensions to a certificate + /// + /// If any of the extensions already exist, they will be replaced. + /// + /// ``` + /// use openssl::x509::extension::Extension::*; + /// use openssl::x509::extension::KeyUsageOption::*; + /// + /// # let generator = openssl::x509::X509Generator::new(); + /// generator.add_extensions(vec![KeyUsage(vec![DigitalSignature, KeyEncipherment])]); + /// ``` + pub fn add_extensions(mut self, exts: I) -> X509Generator + where I: IntoIterator { + self.extensions.extend(exts.into_iter().map(|ext|(ext.get_type(),ext))); self } @@ -287,17 +242,22 @@ impl X509Generator { self } - fn add_extension(x509: *mut ffi::X509, extension: c_int, value: &str) -> Result<(), SslError> { + fn add_extension_internal(x509: *mut ffi::X509, exttype: &extension::ExtensionType, value: &str) -> Result<(), SslError> { unsafe { let mut ctx: ffi::X509V3_CTX = mem::zeroed(); ffi::X509V3_set_ctx(&mut ctx, x509, x509, ptr::null_mut(), ptr::null_mut(), 0); let value = CString::new(value.as_bytes()).unwrap(); - let ext = ffi::X509V3_EXT_conf_nid(ptr::null_mut(), + let ext=match exttype.get_nid() { + Some(nid) => ffi::X509V3_EXT_conf_nid(ptr::null_mut(), mem::transmute(&ctx), - extension, - value.as_ptr() as *mut c_char); - + nid as c_int, + value.as_ptr() as *mut c_char), + None => ffi::X509V3_EXT_conf(ptr::null_mut(), + mem::transmute(&ctx), + exttype.get_name().unwrap().as_ptr() as *mut c_char, + value.as_ptr() as *mut c_char), + }; let mut success = false; if ext != ptr::null_mut() { success = ffi::X509_add_ext(x509, ext, -1) != 0; @@ -376,14 +336,8 @@ impl X509Generator { try!(X509Generator::add_name(name, "CN", &self.CN)); ffi::X509_set_issuer_name(x509.handle, name); - if self.key_usage.len() > 0 { - try!(X509Generator::add_extension(x509.handle, ffi::NID_key_usage, - &self.key_usage.to_str())); - } - - if self.ext_key_usage.len() > 0 { - try!(X509Generator::add_extension(x509.handle, ffi::NID_ext_key_usage, - &self.ext_key_usage.to_str())); + for (exttype,ext) in self.extensions.iter() { + try!(X509Generator::add_extension_internal(x509.handle, exttype, &ext.to_string())); } let hash_fn = self.hash_type.evp_md(); diff --git a/openssl/src/x509/tests.rs b/openssl/src/x509/tests.rs index 4e1c4f15..8417ee5c 100644 --- a/openssl/src/x509/tests.rs +++ b/openssl/src/x509/tests.rs @@ -5,8 +5,10 @@ use std::fs::File; use crypto::hash::Type::{SHA256}; use x509::{X509, X509Generator}; -use x509::KeyUsage::{DigitalSignature, KeyEncipherment}; -use x509::ExtKeyUsage::{ClientAuth, ServerAuth}; +use x509::extension::Extension::{KeyUsage,ExtKeyUsage,SubjectAltName,OtherNid,OtherStr}; +use x509::extension::AltNameOption as SAN; +use x509::extension::KeyUsageOption::{DigitalSignature, KeyEncipherment}; +use x509::extension::ExtKeyUsageOption::{self, ClientAuth, ServerAuth}; use nid::Nid; #[test] @@ -16,16 +18,15 @@ fn test_cert_gen() { .set_valid_period(365*2) .set_CN("test_me") .set_sign_hash(SHA256) - .set_usage(&[DigitalSignature, KeyEncipherment]) - .set_ext_usage(&[ClientAuth, ServerAuth]); + .add_extension(KeyUsage(vec![DigitalSignature, KeyEncipherment])) + .add_extension(ExtKeyUsage(vec![ClientAuth, ServerAuth, ExtKeyUsageOption::Other("2.999.1".to_owned())])) + .add_extension(SubjectAltName(vec![(SAN::DNS,"example.com".to_owned())])) + .add_extension(OtherNid(Nid::BasicConstraints,"critical,CA:TRUE".to_owned())) + .add_extension(OtherStr("2.999.2".to_owned(),"ASN1:UTF8:example value".to_owned())); - let res = gen.generate(); - assert!(res.is_ok()); - - let (cert, pkey) = res.unwrap(); - - assert!(cert.write_pem(&mut io::sink()).is_ok()); - assert!(pkey.write_pem(&mut io::sink()).is_ok()); + let (cert, pkey) = gen.generate().unwrap(); + cert.write_pem(&mut io::sink()).unwrap(); + pkey.write_pem(&mut io::sink()).unwrap(); // FIXME: check data in result to be correct, needs implementation // of X509 getters