1 2 /** Small helpers for libCURL 3 * 4 * This module contains all the OpenSSL related helpers to wrap 5 * functionality of the D language binding provided by the dub module 6 * 'openssl'. 7 * 8 * See: https://github.com/D-Programming-Deimos/openssl 9 */ 10 module acme.openssl_helpers; 11 12 import deimos.openssl.evp; 13 import deimos.openssl.pem; 14 import deimos.openssl.rsa; 15 import deimos.openssl.x509v3; 16 17 import std.conv; 18 import std.string; 19 import std.typecons; 20 21 import acme.exception; 22 23 /* ----------------------------------------------------------------------- */ 24 25 /** Get the contents of a big number as string 26 * 27 * Param: 28 * bn - pointer to a big number structure 29 * Returns: 30 * a string representing the BIGNUM 31 */ 32 string getBigNumber(BIGNUM* bn) 33 { 34 BIO * bio = BIO_new(BIO_s_mem()); 35 scope(exit) BIO_free(bio); 36 BN_print(bio, bn); 37 char[2048] buffer; 38 auto rc = BIO_gets(bio, buffer.ptr, buffer.length); 39 auto num = buffer[0..rc].to!string; 40 return num; 41 } 42 43 /** Get the content bytes of a big number as string 44 * 45 * Param: 46 * bn - pointer to a big number structure 47 * Returns: 48 * a string representing the BIGNUM 49 */ 50 ubyte[] getBigNumberBytes(const BIGNUM* bn) 51 { 52 /* Get number of bytes to store a BIGNUM */ 53 int numBytes = BN_num_bytes(bn); 54 ubyte[] buffer; 55 buffer.length = numBytes; 56 57 /* Copy bytes of BIGNUM to our buffer */ 58 BN_bn2bin(bn, buffer.ptr); 59 60 return buffer; 61 } 62 63 64 /* ----------------------------------------------------------------------- */ 65 66 /** Export BIO contents as an array of chars 67 * 68 * Param: 69 * bio - pointer to a BIO structure 70 * Returns: 71 * An array of chars representing the BIO structure 72 */ 73 char[] toVector(BIO * bio) 74 { 75 enum buffSize = 1024; 76 char[buffSize] buffer; 77 char[] rc; 78 79 int count = 0; 80 do 81 { 82 count = BIO_read(bio, buffer.ptr, buffer.length); 83 if (count > 0) 84 { 85 rc ~= buffer[0..count]; 86 } 87 } 88 while (count > 0); 89 90 return rc; 91 } 92 93 /** Export BIO contents as an array of immutable chars (string) 94 * 95 * Param: 96 * bio - pointer to a BIO structure 97 * Returns: 98 * An array of immutable chars representing the BIO structure 99 */ 100 string toString(BIO *bio) 101 { 102 char[] v = toVector(bio); 103 return to!string(v); 104 } 105 106 /* ----------------------------------------------------------------------- */ 107 108 /** Encode data as Base64 109 * 110 * We use openssl to do this since we're already linking to it. As an 111 * alternative we could also use the phobos routines. 112 * 113 * Params: 114 * t - data to encode as base64 115 * Returns: 116 * An array of chars with the base64 encoded data. 117 */ 118 char[] base64Encode(T)(T t) 119 if ( is(T : string) || is(T : char[]) || is(T : ubyte[])) 120 { 121 BIO * bio = BIO_new(BIO_s_mem()); 122 BIO * b64 = BIO_new(BIO_f_base64()); 123 124 // OpenSSL inserts new lines by default to make it look like PEM format. 125 // Turn that off. 126 BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 127 128 BIO_push(b64, bio); 129 if (BIO_write(b64, cast(void*)(t.ptr), t.length.to!int) <= 0 || 130 BIO_flush(b64) < 0) 131 { 132 throw new AcmeException("Can't encode data as base64."); 133 } 134 return toVector(bio); 135 } 136 137 /** Encode data as URl-safe Base64 138 * 139 * We need url safe base64 encoding and openssl only gives us regular 140 * base64, so we convert it here. Also trim trailing '=' from data 141 * (see RFC). 142 * 143 * The following replacements are done: 144 * * '+' is converted to '-' 145 * * '/' is converted to '_' 146 * * '=' terminates the output at this point, stripping all '=' chars 147 * 148 * Params: 149 * t - data to encode as base64 150 * Returns: 151 * An array of chars with the base64 encoded data. 152 */ 153 char[] base64EncodeUrlSafe(T)(T t) 154 if ( is(T : string) || is(T : char[]) || is(T : ubyte[])) 155 { 156 /* Do a Standard Base64 Encode */ 157 char[] s = base64Encode(t); 158 159 /* Do the replacements */ 160 foreach (i, ref v; s) 161 { 162 if (s[i] == '+') { s[i] = '-'; } 163 else if (s[i] == '/') { s[i] = '_'; } 164 else if (s[i] == '=') { s.length = i; break; } 165 } 166 return s; 167 } 168 169 /** Encode BIGNUM data as URl-safe Base64 170 * 171 * We need url safe base64 encoding and openssl only gives us regular 172 * base64, so we convert it here. Also trim trailing '=' from data 173 * (see RFC). 174 * 175 * The following replacements are done: 176 * * '+' is converted to '-' 177 * * '/' is converted to '_' 178 * * '=' terminates the output at this point, stripping all '=' chars 179 * 180 * Params: 181 * bn - pointer to BIGNUM to encode as base64 182 * Returns: 183 * An array of chars with the base64 encoded data. 184 */ 185 char[] base64EncodeUrlSafe(const BIGNUM* bn) 186 { 187 /* Get contents bytes of a BIGNUM */ 188 ubyte[] buffer = getBigNumberBytes(bn); 189 190 /* Encode the buffer as URL-safe base64 string */ 191 return base64EncodeUrlSafe(buffer); 192 } 193 194 /** Calculate the SHA256 of a string 195 * 196 * We use openssl to do this since we're already linking to it. We could 197 * also use functions from the phobos library. 198 * 199 * Param: 200 * s - string to calculate hash from 201 * Returns: 202 * ubyte[SHA256_DIGEST_LENGTH] for the hash 203 */ 204 ubyte[SHA256_DIGEST_LENGTH] sha256Encode(const char[] s) 205 { 206 ubyte[SHA256_DIGEST_LENGTH] hash; 207 SHA256_CTX sha256; 208 if (!SHA256_Init(&sha256) || 209 !SHA256_Update(&sha256, s.ptr, s.length) || 210 !SHA256_Final(hash.ptr, &sha256)) 211 { 212 throw new AcmeException("Error hashing string data"); 213 } 214 return hash; 215 } 216 217 /** Convert certificate from DER format to PEM format 218 * 219 * Params: 220 * der - DER encoded certificate 221 * Returns: 222 * a PEM-encoded certificate 223 */ 224 string convertDERtoPEM(const char[] der) 225 { 226 /* Write DER to BIO buffer */ 227 BIO* derBio = BIO_new(BIO_s_mem()); 228 BIO_write(derBio, cast(const(void)*)der.ptr, der.length.to!int); 229 230 /* Add conversion filter */ 231 X509* x509 = d2i_X509_bio(derBio, null); 232 233 /* Write DER through filter to as PEM to other BIO buffer */ 234 BIO* pemBio = BIO_new(BIO_s_mem()); 235 PEM_write_bio_X509(pemBio, x509); 236 237 /* Output data as data string */ 238 return toString(pemBio); 239 } 240 241 /** Extract expiry date from a PEM encoded Zertificate 242 * 243 * Params: 244 * cert - PEM encoded certificate to query 245 * extractor - function or delegate process an ASN1_TIME* argument. 246 */ 247 T extractExpiryData(T, alias extractor)(const(char[]) cert) 248 { 249 BIO* bio = BIO_new(BIO_s_mem()); 250 if (BIO_write(bio, cast(const(void)*) cert.ptr, cert.length.to!int) <= 0) 251 { 252 throw new AcmeException("Can't write PEM data to BIO struct."); 253 } 254 X509* x509 = PEM_read_bio_X509(bio, null, null, null); 255 256 ASN1_TIME * t = X509_get_notAfter(x509); 257 258 T rc = extractor(t); 259 return rc; 260 } 261 262 /* ----------------------------------------------------------------------- */ 263 264 /// Return tuple of makeCertificateSigningRequest 265 alias tupleCsrPkey = Tuple!(string, "csr", string, "pkey"); 266 267 /** Create a CSR with our domains 268 * 269 * Params: 270 * domainNames - Names of domains, first element is subject of cert 271 * Returns: 272 * tupleCsrPkey containing CSr and PKey 273 */ 274 tupleCsrPkey makeCertificateSigningRequest(string[] domainNames) 275 { 276 if (domainNames.length < 1) 277 { 278 throw new AcmeException("We need at least one domain name."); 279 } 280 281 BIGNUM* bn = BN_new(); 282 if (!BN_set_word(bn, RSA_F4)) 283 { 284 throw new AcmeException("Can't set word."); 285 } 286 287 RSA* rsa = RSA_new(); 288 enum bits = 2048; 289 if (!RSA_generate_key_ex(rsa, bits, bn, null)) 290 { 291 throw new AcmeException("Can't generate key."); 292 } 293 294 /* Set first element of domainNames as cert CN subject */ 295 X509_REQ* req = X509_REQ_new(); 296 auto name = domainNames[0]; 297 298 X509_NAME* cn = X509_REQ_get_subject_name(req); 299 if (!X509_NAME_add_entry_by_txt(cn, 300 "CN", 301 MBSTRING_ASC, 302 cast(const ubyte*)(name.toStringz), 303 -1, -1, 0)) 304 { 305 throw new AcmeException("Can't add CN entry."); 306 } 307 308 /* Add other domainName as extension */ 309 if (domainNames.length > 1) 310 { 311 // We have multiple Subject Alternative Names 312 auto extensions = sk_X509_EXTENSION_new_null(); 313 if (!extensions) 314 { 315 throw new AcmeException("Unable to allocate Subject Alternative Name extensions"); 316 } 317 318 foreach (i, ref v ; domainNames) 319 { 320 auto cstr = ("DNS:" ~ v).toStringz; 321 auto nid = X509V3_EXT_conf_nid(null, null, NID_subject_alt_name, cast(char*)cstr); 322 if (!sk_X509_EXTENSION_push(extensions, nid)) 323 { 324 throw new AcmeException("Unable to add Subject Alternative Name to extensions"); 325 } 326 } 327 328 if (X509_REQ_add_extensions(req, extensions) != 1) 329 { 330 throw new AcmeException("Unable to add Subject Alternative Names to CSR"); 331 } 332 333 sk_X509_EXTENSION_pop_free(extensions, &X509_EXTENSION_free); 334 } 335 336 EVP_PKEY* key = EVP_PKEY_new(); 337 if (!EVP_PKEY_assign_RSA(key, rsa)) 338 { 339 throw new AcmeException("Can't set RSA key."); 340 } 341 rsa = null; // rsa will be freed when key is freed. 342 343 BIO* keyBio = BIO_new(BIO_s_mem()); 344 if (PEM_write_bio_PrivateKey(keyBio, key, null, null, 0, null, null) != 1) 345 { 346 throw new AcmeException("Can't set private key."); 347 } 348 349 string privateKey = toString(keyBio); 350 351 if (!X509_REQ_set_pubkey(req, key)) 352 { 353 throw new AcmeException("Can't set subkey."); 354 } 355 356 if (!X509_REQ_sign(req, key, EVP_sha256())) 357 { 358 throw new AcmeException("Can't sign."); 359 } 360 361 BIO* reqBio = BIO_new(BIO_s_mem()); 362 if (i2d_X509_REQ_bio(reqBio, req) < 0) 363 { 364 throw new AcmeException("Can't setup sign request"); 365 } 366 367 tupleCsrPkey rc = tuple(base64EncodeUrlSafe(toVector(reqBio)).to!string, privateKey); 368 return rc; 369 } 370 371 /* ----------------------------------------------------------------------- */ 372