├── js ├── asn1js │ ├── LICENSE │ ├── hex.js │ ├── int10.js │ ├── base64.js │ └── asn1.js └── index.js ├── LICENSE ├── .gitattributes ├── .gitignore ├── README.md └── index.html /js/asn1js/LICENSE: -------------------------------------------------------------------------------- 1 | ASN.1 JavaScript decoder Copyright (c) 2008-2014 Lapo Luchini 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Roesler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /js/asn1js/hex.js: -------------------------------------------------------------------------------- 1 | // Hex JavaScript decoder 2 | // Copyright (c) 2008-2014 Lapo Luchini 3 | 4 | // Permission to use, copy, modify, and/or distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | /*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ 17 | (function (undefined) { 18 | "use strict"; 19 | 20 | var Hex = {}, 21 | decoder; 22 | 23 | Hex.decode = function(a) { 24 | var i; 25 | if (decoder === undefined) { 26 | var hex = "0123456789ABCDEF", 27 | ignore = " \f\n\r\t\u00A0\u2028\u2029"; 28 | decoder = []; 29 | for (i = 0; i < 16; ++i) 30 | decoder[hex.charAt(i)] = i; 31 | hex = hex.toLowerCase(); 32 | for (i = 10; i < 16; ++i) 33 | decoder[hex.charAt(i)] = i; 34 | for (i = 0; i < ignore.length; ++i) 35 | decoder[ignore.charAt(i)] = -1; 36 | } 37 | var out = [], 38 | bits = 0, 39 | char_count = 0; 40 | for (i = 0; i < a.length; ++i) { 41 | var c = a.charAt(i); 42 | if (c == '=') 43 | break; 44 | c = decoder[c]; 45 | if (c == -1) 46 | continue; 47 | if (c === undefined) 48 | throw 'Illegal character at offset ' + i; 49 | bits |= c; 50 | if (++char_count >= 2) { 51 | out[out.length] = bits; 52 | bits = 0; 53 | char_count = 0; 54 | } else { 55 | bits <<= 4; 56 | } 57 | } 58 | if (char_count) 59 | throw "Hex encoding incomplete: 4 bits missing"; 60 | return out; 61 | }; 62 | 63 | // export globals 64 | if (typeof module !== 'undefined') { module.exports = Hex; } else { window.Hex = Hex; } 65 | })(); 66 | -------------------------------------------------------------------------------- /js/asn1js/int10.js: -------------------------------------------------------------------------------- 1 | // Big integer base-10 printing library 2 | // Copyright (c) 2014 Lapo Luchini 3 | 4 | // Permission to use, copy, modify, and/or distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | /*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ 17 | (function () { 18 | "use strict"; 19 | 20 | var max = 10000000000000; // biggest integer that can still fit 2^53 when multiplied by 256 21 | 22 | function Int10(value) { 23 | this.buf = [+value || 0]; 24 | } 25 | 26 | Int10.prototype.mulAdd = function (m, c) { 27 | // assert(m <= 256) 28 | var b = this.buf, 29 | l = b.length, 30 | i, t; 31 | for (i = 0; i < l; ++i) { 32 | t = b[i] * m + c; 33 | if (t < max) 34 | c = 0; 35 | else { 36 | c = 0|(t / max); 37 | t -= c * max; 38 | } 39 | b[i] = t; 40 | } 41 | if (c > 0) 42 | b[i] = c; 43 | }; 44 | 45 | Int10.prototype.toString = function (base) { 46 | if ((base || 10) != 10) 47 | throw 'only base 10 is supported'; 48 | var b = this.buf, 49 | s = b[b.length - 1].toString(); 50 | for (var i = b.length - 2; i >= 0; --i) 51 | s += (max + b[i]).toString().substring(1); 52 | return s; 53 | }; 54 | 55 | Int10.prototype.valueOf = function () { 56 | var b = this.buf, 57 | v = 0; 58 | for (var i = b.length - 1; i >= 0; --i) 59 | v = v * max + b[i]; 60 | return v; 61 | }; 62 | 63 | Int10.prototype.simplify = function () { 64 | var b = this.buf; 65 | return (b.length == 1) ? b[0] : this; 66 | }; 67 | 68 | // export globals 69 | if (typeof module !== 'undefined') { module.exports = Int10; } else { window.Int10 = Int10; } 70 | })(); 71 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /js/asn1js/base64.js: -------------------------------------------------------------------------------- 1 | // Base64 JavaScript decoder 2 | // Copyright (c) 2008-2014 Lapo Luchini 3 | 4 | // Permission to use, copy, modify, and/or distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | /*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ 17 | (function (undefined) { 18 | "use strict"; 19 | 20 | var Base64 = {}, 21 | decoder; 22 | 23 | Base64.decode = function (a) { 24 | var i; 25 | if (decoder === undefined) { 26 | var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", 27 | ignore = "= \f\n\r\t\u00A0\u2028\u2029"; 28 | decoder = []; 29 | for (i = 0; i < 64; ++i) 30 | decoder[b64.charAt(i)] = i; 31 | for (i = 0; i < ignore.length; ++i) 32 | decoder[ignore.charAt(i)] = -1; 33 | } 34 | var out = []; 35 | var bits = 0, char_count = 0; 36 | for (i = 0; i < a.length; ++i) { 37 | var c = a.charAt(i); 38 | if (c == '=') 39 | break; 40 | c = decoder[c]; 41 | if (c == -1) 42 | continue; 43 | if (c === undefined) 44 | throw 'Illegal character at offset ' + i; 45 | bits |= c; 46 | if (++char_count >= 4) { 47 | out[out.length] = (bits >> 16); 48 | out[out.length] = (bits >> 8) & 0xFF; 49 | out[out.length] = bits & 0xFF; 50 | bits = 0; 51 | char_count = 0; 52 | } else { 53 | bits <<= 6; 54 | } 55 | } 56 | switch (char_count) { 57 | case 1: 58 | throw "Base64 encoding incomplete: at least 2 bits missing"; 59 | case 2: 60 | out[out.length] = (bits >> 10); 61 | break; 62 | case 3: 63 | out[out.length] = (bits >> 16); 64 | out[out.length] = (bits >> 8) & 0xFF; 65 | break; 66 | } 67 | return out; 68 | }; 69 | 70 | Base64.re = /-----BEGIN [^-]+-----([A-Za-z0-9+\/=\s]+)-----END [^-]+-----|begin-base64[^\n]+\n([A-Za-z0-9+\/=\s]+)====/; 71 | Base64.unarmor = function (a) { 72 | var m = Base64.re.exec(a); 73 | if (m) { 74 | if (m[1]) 75 | a = m[1]; 76 | else if (m[2]) 77 | a = m[2]; 78 | else 79 | throw "RegExp out of sync"; 80 | } 81 | return Base64.decode(a); 82 | }; 83 | 84 | // export globals 85 | if (typeof module !== 'undefined') { module.exports = Base64; } else { window.Base64 = Base64; } 86 | })(); 87 | 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Get HTTPS for free! 2 | 3 | Website: https://gethttpsforfree.com 4 | 5 | This is a project that allows you to get a free HTTPS certificate without 6 | having to install any software or having to share your private keys with anyone. 7 | It uses the non-profit [Let's Encrypt](https://letsencrypt.org/) certificate 8 | authority to issue the free certificates. Hooray for free certs! 9 | 10 | ## Donate 11 | 12 | If this script is useful to you, please donate to the EFF. I don't work there, 13 | but they do fantastic work. 14 | 15 | [https://eff.org/donate/](https://eff.org/donate/) 16 | 17 | ## How to use this website 18 | 19 | Go to: https://gethttpsforfree.com 20 | 21 | The website works by generating commands for you to run in your terminal, then 22 | making requests to the Let's Encrypt ACME API to issue your certificate. Simply 23 | visit the above website and follow the steps! If you don't know how to do 24 | something, try clicking the help links that explain how to complete the step. If 25 | you're still confused, please create an issue and I'll address your issue ASAP! 26 | 27 | Requirements for your local machine: 28 | * openssl 29 | * echo 30 | 31 | Requirements for your server: 32 | * python or any webserver that can serve a static file 33 | 34 | These should all be installed by default in Linux and Mac OSX. If you're 35 | running Windows, you might need to install [Cygwin](https://cygwin.com/install.html) 36 | to get openssl and echo working on Windows. 37 | 38 | ## How this website works 39 | 40 | This website works by making requests to the Let's Encrypt [API](https://acme-v02.api.letsencrypt.org/directory) 41 | (using the [ACME](https://github.com/ietf-wg-acme/acme) protocol). There's 5 steps to the process, 42 | which are explained below. Also, I encourage you to read the source code (it's not that long) and 43 | pop open your browser's debugger to see the ajax requests that are going on. Please, audit this! 44 | 45 | ### Step 1: Account Info 46 | 47 | First, the ACME protocol requires you register a public key and contact information 48 | so you can sign all the requests you make to the API. In this step, you need to 49 | put in an email and a public key. The javascript for this section then converts the 50 | public key to a JSON Web Key ([JWK](https://tools.ietf.org/html/rfc7517)). NOTE: 51 | currently only RSA 2048 and 4096 bit public keys are accepted by Let's Encrypt. 52 | 53 | So if you paste it in this public key: 54 | ``` 55 | -----BEGIN PUBLIC KEY----- 56 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5aok6d72rkrGpOPAICSS 57 | 3JPrA0tbVs3mYPWmG7c5tGEY+w1slyI+3V64NsLw8p9YqNLyX/YDsnmkOUMUx6Bu 58 | vx43daBrl//wz3hIOvidXyV4z65Nbrlto9qtLpfi+9lbEEYt2PLhr+KjguqjqOQj 59 | qi2PgqdITGG+BZkU8xIrPzZCR/UPBotV/dGBj9vO1whTGlzpkihvXLf4rEFoJoEE 60 | eOPMtqbxUp1KS41EgX2xFav9JHPVI1hm66K0eqlJrBl407j3xRNlekl4xorwfCkA 61 | xC7xclofg3JZ7RIhv3DdaNe07IZ0QYup9dDufIcCKruAgu0hwYMwDHmZNrrWxMia 62 | GQwagxs61mla6f7c1bvYY92PhfgpkQAN99MXdaTtvBbzDuY018QP+TVzzVH/hpjK 63 | aFx4JlYkcVGqbYamUiP7il4Hldqp6Mm65IH/8nxuZFrN4tJ5VyMeWeZ5sKBBrXZE 64 | 1Je8524COYnvljGnaFAVaDRhAcTSEykveY8jx/r6MB95LkWcue7FXIQyX0D3/2lU 65 | KTu/wrBCmhriqNa4FHcccLMyQkiMbs8mEoldNCwYDxvF5lYc19UDlleE855lME00 66 | E/ogStmazzFrNWCzEJ+Pa9JVlTQonKRgWqi+9cWwV+AMd+s2F3wO+H2tlexe8pLo 67 | Vw/42S44tHz4VuZuhpZvn3kCAwEAAQ== 68 | -----END PUBLIC KEY----- 69 | ``` 70 | 71 | This step converts it to this JWK: 72 | ``` 73 | { 74 | "alg": "RS256", 75 | "jwk": { 76 | "e": "AQAB", 77 | "kty": "RSA", 78 | "n": "5aok6d72rkrGpOPAICSS3JPrA0tbVs3mYPWmG7c5tGEY-w1slyI-3V64NsLw8p9YqNLyX_YDsnmkOUMUx6Buvx43daBrl__wz3hIOvidXyV4z65Nbrlto9qtLpfi-9lbEEYt2PLhr-KjguqjqOQjqi2PgqdITGG-BZkU8xIrPzZCR_UPBotV_dGBj9vO1whTGlzpkihvXLf4rEFoJoEEeOPMtqbxUp1KS41EgX2xFav9JHPVI1hm66K0eqlJrBl407j3xRNlekl4xorwfCkAxC7xclofg3JZ7RIhv3DdaNe07IZ0QYup9dDufIcCKruAgu0hwYMwDHmZNrrWxMiaGQwagxs61mla6f7c1bvYY92PhfgpkQAN99MXdaTtvBbzDuY018QP-TVzzVH_hpjKaFx4JlYkcVGqbYamUiP7il4Hldqp6Mm65IH_8nxuZFrN4tJ5VyMeWeZ5sKBBrXZE1Je8524COYnvljGnaFAVaDRhAcTSEykveY8jx_r6MB95LkWcue7FXIQyX0D3_2lUKTu_wrBCmhriqNa4FHcccLMyQkiMbs8mEoldNCwYDxvF5lYc19UDlleE855lME00E_ogStmazzFrNWCzEJ-Pa9JVlTQonKRgWqi-9cWwV-AMd-s2F3wO-H2tlexe8pLoVw_42S44tHz4VuZuhpZvn3k" 79 | } 80 | } 81 | ``` 82 | 83 | ### Step 2: Certificate Signing Request 84 | 85 | Second, you need to specify the domains you want certificates for. That's done 86 | through a certificate signing request ([CSR](https://en.wikipedia.org/wiki/Certificate_signing_request)). 87 | The javascript in this section uses the [ASN1.js](https://lapo.it/asn1js/) library 88 | to parse the CSR and read the domains. NOTE: the private key for the domain cert 89 | cannot be the same as your account private key, according to ACME. 90 | 91 | ### Step 3: Sign API Requests 92 | 93 | Third, you need tell the Let's Encrypt API that you want to register and create an order 94 | for a certificate (your CSR). These requests must be signed with your account private key, so 95 | this steps compiles the request payloads that need signatures to get the domain challenges 96 | you need to fulfill. 97 | 98 | Here's the list of requests that need to be made to the API: 99 | 100 | * `/acme/new-acct` - Register the account public key and accept the terms (discarded if already registered) 101 | * `/acme/acct/...` - Update the account with your email address 102 | * `/acme/new-order` - Creates a new order for a certificate for your domains in your CSR 103 | 104 | NOTE: Each request also requires an anti-replay nonce, so the javascript gets 105 | those by making ajax requests to the `/acme/new-nonce` endpoint. 106 | 107 | For each request the payload must be signed, and since this website doesn't ask 108 | for your private keys, you must copy-and-paste the signature commands into your 109 | terminal. 110 | 111 | These commands are structured like this: 112 | ``` 113 | PRIV_KEY=./account.key; \ #set the location of your account private key (change this location if different) 114 | echo -n "" | \ #pipe the payload into openssl 115 | openssl dgst -sha256 -hex -sign $PRIV_KEY #sign the payload using your private key and output hex 116 | ``` 117 | 118 | Once these signatures are pasted back into the inputs, the javascript makes the 119 | ajax requests to the above endpoints for `new-acct` and each `new-order`. If the 120 | account public key has already been registered the `new-acct` response is a 204 121 | No Content, which is ignored. 122 | 123 | ### Step 4: Verify Ownership 124 | 125 | The response for the `/new-order` has links to the authorization challenges needed 126 | prove you own the domain. The challenge that this website chooses is "http-01" or 127 | "dns-01", which requires that you host a specific file at a specific location 128 | or set a specific TXT value in your DNS for that domain. So, for 129 | each domain, this step shows you the file you need to host and the url you need 130 | to host it at. 131 | 132 | After the file is being hosted, you need to tell Let's Encrypt to check the 133 | verify the challenge for that domain. That request must also be signed so 134 | there's one more signature that must be performed. The reason why this wasn't 135 | included in step 3 is because the payload contains something in the response of 136 | `/new-order`. 137 | 138 | There's three options this website offers as copy-and-paste commands: python, file-based, 139 | and dns. The python command is a mini server you can copy-and-paste into your 140 | server's command line (NOTE: this needs sudo permissions!). The file-base option 141 | just lists the url where the challenge will check and the file contents that the 142 | file needs to contain. The DNS option lists the value you need to set as a TXT 143 | entry in your DNS. It's up to you to figure out how to make that happen. 144 | 145 | When you confirm that you're hosting the files, an ajax request is made to the 146 | challenge url to tell Let's Encrypt to verify the domain. Once this is done for 147 | all the domains in your CSR, the final signature is to finalize the order and 148 | sign your certificate. 149 | 150 | ### Step 5: Install Certificate 151 | 152 | The response from finalizing should be your new certificate! Congrats! This 153 | step prints the certificate and also prints the intermediate certificate you 154 | need to chain this certificate to the root certificate. 155 | 156 | ## Privacy 157 | 158 | This website is entirely static files and only makes ajax requests to the 159 | Let's Encrypt API. It does not track or remember anything when you leave. 160 | It is written with minimal extra libraries and styling to ensure that you 161 | can read through and audit the source code. 162 | 163 | Finally, since this website is completely static, it's un-hostable! Just 164 | right-click and "Save Page As...", save the complete website to your local 165 | computer, then open it in a browser. It still works when hosted locally! 166 | 167 | ## Feedback/Contributing 168 | 169 | I'd love to receive feedback, issues, and pull requests to make this script 170 | better. The main script itself, `js/index.js`, is less than 800 lines of code, so 171 | feel free to read through it! I tried to comment things well and make it crystal 172 | clear what it's doing. 173 | 174 | TODO (pull requests welcome): 175 | * `revoke.html` - A page with steps for revoking certificates 176 | * ~~Alternative file-based command instead of python server~~ 177 | * ~~Installation instructions for Apache~~ 178 | * Accept GPG public keys as account public keys 179 | 180 | ## What's NOT on the Roadmap 181 | 182 | * Third party libraries (asn1.js is the only one) 183 | * Fonts or images 184 | * CSS more than 5 kilobytes 185 | * Javascript that only changes UI 186 | * HTML that decreases source readability (added wrapping divs, etc.) 187 | 188 | This website is supposed to [work](http://motherfuckingwebsite.com/), nothing more. 189 | 190 | -------------------------------------------------------------------------------- /js/asn1js/asn1.js: -------------------------------------------------------------------------------- 1 | // ASN.1 JavaScript decoder 2 | // Copyright (c) 2008-2014 Lapo Luchini 3 | 4 | // Permission to use, copy, modify, and/or distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | /*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ 17 | /*global oids */ 18 | (function (undefined) { 19 | "use strict"; 20 | 21 | var Int10 = (typeof module !== 'undefined') ? require('./int10.js') : window.Int10, 22 | ellipsis = "\u2026", 23 | reTimeS = /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/, 24 | reTimeL = /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; 25 | 26 | function stringCut(str, len) { 27 | if (str.length > len) 28 | str = str.substring(0, len) + ellipsis; 29 | return str; 30 | } 31 | 32 | function Stream(enc, pos) { 33 | if (enc instanceof Stream) { 34 | this.enc = enc.enc; 35 | this.pos = enc.pos; 36 | } else { 37 | // enc should be an array or a binary string 38 | this.enc = enc; 39 | this.pos = pos; 40 | } 41 | } 42 | Stream.prototype.get = function (pos) { 43 | if (pos === undefined) 44 | pos = this.pos++; 45 | if (pos >= this.enc.length) 46 | throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length; 47 | return (typeof this.enc == "string") ? this.enc.charCodeAt(pos) : this.enc[pos]; 48 | }; 49 | Stream.prototype.hexDigits = "0123456789ABCDEF"; 50 | Stream.prototype.hexByte = function (b) { 51 | return this.hexDigits.charAt((b >> 4) & 0xF) + this.hexDigits.charAt(b & 0xF); 52 | }; 53 | Stream.prototype.hexDump = function (start, end, raw) { 54 | var s = ""; 55 | for (var i = start; i < end; ++i) { 56 | s += this.hexByte(this.get(i)); 57 | if (raw !== true) 58 | switch (i & 0xF) { 59 | case 0x7: s += " "; break; 60 | case 0xF: s += "\n"; break; 61 | default: s += " "; 62 | } 63 | } 64 | return s; 65 | }; 66 | Stream.prototype.isASCII = function (start, end) { 67 | for (var i = start; i < end; ++i) { 68 | var c = this.get(i); 69 | if (c < 32 || c > 176) 70 | return false; 71 | } 72 | return true; 73 | }; 74 | Stream.prototype.parseStringISO = function (start, end) { 75 | var s = ""; 76 | for (var i = start; i < end; ++i) 77 | s += String.fromCharCode(this.get(i)); 78 | return s; 79 | }; 80 | Stream.prototype.parseStringUTF = function (start, end) { 81 | var s = ""; 82 | for (var i = start; i < end; ) { 83 | var c = this.get(i++); 84 | if (c < 128) 85 | s += String.fromCharCode(c); 86 | else if ((c > 191) && (c < 224)) 87 | s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F)); 88 | else 89 | s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F)); 90 | } 91 | return s; 92 | }; 93 | Stream.prototype.parseStringBMP = function (start, end) { 94 | var str = "", hi, lo; 95 | for (var i = start; i < end; ) { 96 | hi = this.get(i++); 97 | lo = this.get(i++); 98 | str += String.fromCharCode((hi << 8) | lo); 99 | } 100 | return str; 101 | }; 102 | Stream.prototype.parseTime = function (start, end, shortYear) { 103 | var s = this.parseStringISO(start, end), 104 | m = (shortYear ? reTimeS : reTimeL).exec(s); 105 | if (!m) 106 | return "Unrecognized time: " + s; 107 | if (shortYear) { 108 | // to avoid querying the timer, use the fixed range [1970, 2069] 109 | // it will conform with ITU X.400 [-10, +40] sliding window until 2030 110 | m[1] = +m[1]; 111 | m[1] += (m[1] < 70) ? 2000 : 1900; 112 | } 113 | s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4]; 114 | if (m[5]) { 115 | s += ":" + m[5]; 116 | if (m[6]) { 117 | s += ":" + m[6]; 118 | if (m[7]) 119 | s += "." + m[7]; 120 | } 121 | } 122 | if (m[8]) { 123 | s += " UTC"; 124 | if (m[8] != 'Z') { 125 | s += m[8]; 126 | if (m[9]) 127 | s += ":" + m[9]; 128 | } 129 | } 130 | return s; 131 | }; 132 | Stream.prototype.parseInteger = function (start, end) { 133 | var v = this.get(start), 134 | neg = (v > 127), 135 | pad = neg ? 255 : 0, 136 | len, 137 | s = ''; 138 | // skip unuseful bits (not allowed in DER) 139 | while (v == pad && ++start < end) 140 | v = this.get(start); 141 | len = end - start; 142 | if (len === 0) 143 | return neg ? -1 : 0; 144 | // show bit length of huge integers 145 | if (len > 4) { 146 | s = v; 147 | len <<= 3; 148 | while (((s ^ pad) & 0x80) == 0) { 149 | s <<= 1; 150 | --len; 151 | } 152 | s = "(" + len + " bit)\n"; 153 | } 154 | // decode the integer 155 | if (neg) v = v - 256; 156 | var n = new Int10(v); 157 | for (var i = start + 1; i < end; ++i) 158 | n.mulAdd(256, this.get(i)); 159 | return s + n.toString(); 160 | }; 161 | Stream.prototype.parseBitString = function (start, end, maxLength) { 162 | var unusedBit = this.get(start), 163 | lenBit = ((end - start - 1) << 3) - unusedBit, 164 | intro = "(" + lenBit + " bit)\n", 165 | s = ""; 166 | for (var i = start + 1; i < end; ++i) { 167 | var b = this.get(i), 168 | skip = (i == end - 1) ? unusedBit : 0; 169 | for (var j = 7; j >= skip; --j) 170 | s += (b >> j) & 1 ? "1" : "0"; 171 | if (s.length > maxLength) 172 | return intro + stringCut(s, maxLength); 173 | } 174 | return intro + s; 175 | }; 176 | Stream.prototype.parseOctetString = function (start, end, maxLength) { 177 | if (this.isASCII(start, end)) 178 | return stringCut(this.parseStringISO(start, end), maxLength); 179 | var len = end - start, 180 | s = "(" + len + " byte)\n"; 181 | maxLength /= 2; // we work in bytes 182 | if (len > maxLength) 183 | end = start + maxLength; 184 | for (var i = start; i < end; ++i) 185 | s += this.hexByte(this.get(i)); 186 | if (len > maxLength) 187 | s += ellipsis; 188 | return s; 189 | }; 190 | Stream.prototype.parseOID = function (start, end, maxLength) { 191 | var s = '', 192 | n = new Int10(), 193 | bits = 0; 194 | for (var i = start; i < end; ++i) { 195 | var v = this.get(i); 196 | n.mulAdd(128, v & 0x7F); 197 | bits += 7; 198 | if (!(v & 0x80)) { // finished 199 | if (s === '') { 200 | n = n.simplify(); 201 | var m = n < 80 ? n < 40 ? 0 : 1 : 2; 202 | s = m + "." + (n - m * 40); 203 | } else 204 | s += "." + n.toString(); 205 | if (s.length > maxLength) 206 | return stringCut(s, maxLength); 207 | n = new Int10(); 208 | bits = 0; 209 | } 210 | } 211 | if (bits > 0) 212 | s += ".incomplete"; 213 | return s; 214 | }; 215 | 216 | function ASN1(stream, header, length, tag, sub) { 217 | if (!(tag instanceof ASN1Tag)) throw 'Invalid tag value.'; 218 | this.stream = stream; 219 | this.header = header; 220 | this.length = length; 221 | this.tag = tag; 222 | this.sub = sub; 223 | } 224 | ASN1.prototype.typeName = function () { 225 | switch (this.tag.tagClass) { 226 | case 0: // universal 227 | switch (this.tag.tagNumber) { 228 | case 0x00: return "EOC"; 229 | case 0x01: return "BOOLEAN"; 230 | case 0x02: return "INTEGER"; 231 | case 0x03: return "BIT_STRING"; 232 | case 0x04: return "OCTET_STRING"; 233 | case 0x05: return "NULL"; 234 | case 0x06: return "OBJECT_IDENTIFIER"; 235 | case 0x07: return "ObjectDescriptor"; 236 | case 0x08: return "EXTERNAL"; 237 | case 0x09: return "REAL"; 238 | case 0x0A: return "ENUMERATED"; 239 | case 0x0B: return "EMBEDDED_PDV"; 240 | case 0x0C: return "UTF8String"; 241 | case 0x10: return "SEQUENCE"; 242 | case 0x11: return "SET"; 243 | case 0x12: return "NumericString"; 244 | case 0x13: return "PrintableString"; // ASCII subset 245 | case 0x14: return "TeletexString"; // aka T61String 246 | case 0x15: return "VideotexString"; 247 | case 0x16: return "IA5String"; // ASCII 248 | case 0x17: return "UTCTime"; 249 | case 0x18: return "GeneralizedTime"; 250 | case 0x19: return "GraphicString"; 251 | case 0x1A: return "VisibleString"; // ASCII subset 252 | case 0x1B: return "GeneralString"; 253 | case 0x1C: return "UniversalString"; 254 | case 0x1E: return "BMPString"; 255 | } 256 | return "Universal_" + this.tag.tagNumber.toString(); 257 | case 1: return "Application_" + this.tag.tagNumber.toString(); 258 | case 2: return "[" + this.tag.tagNumber.toString() + "]"; // Context 259 | case 3: return "Private_" + this.tag.tagNumber.toString(); 260 | } 261 | }; 262 | ASN1.prototype.content = function (maxLength) { // a preview of the content (intended for humans) 263 | if (this.tag === undefined) 264 | return null; 265 | if (maxLength === undefined) 266 | maxLength = Infinity; 267 | var content = this.posContent(), 268 | len = Math.abs(this.length); 269 | if (!this.tag.isUniversal()) { 270 | if (this.sub !== null) 271 | return "(" + this.sub.length + " elem)"; 272 | return this.stream.parseOctetString(content, content + len, maxLength); 273 | } 274 | switch (this.tag.tagNumber) { 275 | case 0x01: // BOOLEAN 276 | return (this.stream.get(content) === 0) ? "false" : "true"; 277 | case 0x02: // INTEGER 278 | return this.stream.parseInteger(content, content + len); 279 | case 0x03: // BIT_STRING 280 | return this.sub ? "(" + this.sub.length + " elem)" : 281 | this.stream.parseBitString(content, content + len, maxLength); 282 | case 0x04: // OCTET_STRING 283 | return this.sub ? "(" + this.sub.length + " elem)" : 284 | this.stream.parseOctetString(content, content + len, maxLength); 285 | //case 0x05: // NULL 286 | case 0x06: // OBJECT_IDENTIFIER 287 | return this.stream.parseOID(content, content + len, maxLength); 288 | //case 0x07: // ObjectDescriptor 289 | //case 0x08: // EXTERNAL 290 | //case 0x09: // REAL 291 | //case 0x0A: // ENUMERATED 292 | //case 0x0B: // EMBEDDED_PDV 293 | case 0x10: // SEQUENCE 294 | case 0x11: // SET 295 | return "(" + this.sub.length + " elem)"; 296 | case 0x0C: // UTF8String 297 | return stringCut(this.stream.parseStringUTF(content, content + len), maxLength); 298 | case 0x12: // NumericString 299 | case 0x13: // PrintableString 300 | case 0x14: // TeletexString 301 | case 0x15: // VideotexString 302 | case 0x16: // IA5String 303 | //case 0x19: // GraphicString 304 | case 0x1A: // VisibleString 305 | //case 0x1B: // GeneralString 306 | //case 0x1C: // UniversalString 307 | return stringCut(this.stream.parseStringISO(content, content + len), maxLength); 308 | case 0x1E: // BMPString 309 | return stringCut(this.stream.parseStringBMP(content, content + len), maxLength); 310 | case 0x17: // UTCTime 311 | case 0x18: // GeneralizedTime 312 | return this.stream.parseTime(content, content + len, (this.tag.tagNumber == 0x17)); 313 | } 314 | return null; 315 | }; 316 | ASN1.prototype.toString = function () { 317 | return this.typeName() + "@" + this.stream.pos + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.sub === null) ? 'null' : this.sub.length) + "]"; 318 | }; 319 | ASN1.prototype.toPrettyString = function (indent) { 320 | if (indent === undefined) indent = ''; 321 | var s = indent + this.typeName() + " @" + this.stream.pos; 322 | if (this.length >= 0) 323 | s += "+"; 324 | s += this.length; 325 | if (this.tag.tagConstructed) 326 | s += " (constructed)"; 327 | else if ((this.tag.isUniversal() && ((this.tag.tagNumber == 0x03) || (this.tag.tagNumber == 0x04))) && (this.sub !== null)) 328 | s += " (encapsulates)"; 329 | s += "\n"; 330 | if (this.sub !== null) { 331 | indent += ' '; 332 | for (var i = 0, max = this.sub.length; i < max; ++i) 333 | s += this.sub[i].toPrettyString(indent); 334 | } 335 | return s; 336 | }; 337 | ASN1.prototype.posStart = function () { 338 | return this.stream.pos; 339 | }; 340 | ASN1.prototype.posContent = function () { 341 | return this.stream.pos + this.header; 342 | }; 343 | ASN1.prototype.posEnd = function () { 344 | return this.stream.pos + this.header + Math.abs(this.length); 345 | }; 346 | ASN1.prototype.toHexString = function (root) { 347 | return this.stream.hexDump(this.posStart(), this.posEnd(), true); 348 | }; 349 | ASN1.decodeLength = function (stream) { 350 | var buf = stream.get(), 351 | len = buf & 0x7F; 352 | if (len == buf) 353 | return len; 354 | if (len > 6) // no reason to use Int10, as it would be a huge buffer anyways 355 | throw "Length over 48 bits not supported at position " + (stream.pos - 1); 356 | if (len === 0) 357 | return null; // undefined 358 | buf = 0; 359 | for (var i = 0; i < len; ++i) 360 | buf = (buf * 256) + stream.get(); 361 | return buf; 362 | }; 363 | function ASN1Tag(stream) { 364 | var buf = stream.get(); 365 | this.tagClass = buf >> 6; 366 | this.tagConstructed = ((buf & 0x20) !== 0); 367 | this.tagNumber = buf & 0x1F; 368 | if (this.tagNumber == 0x1F) { // long tag 369 | var n = new Int10(); 370 | do { 371 | buf = stream.get(); 372 | n.mulAdd(128, buf & 0x7F); 373 | } while (buf & 0x80); 374 | this.tagNumber = n.simplify(); 375 | } 376 | } 377 | ASN1Tag.prototype.isUniversal = function () { 378 | return this.tagClass === 0x00; 379 | }; 380 | ASN1Tag.prototype.isEOC = function () { 381 | return this.tagClass === 0x00 && this.tagNumber === 0x00; 382 | }; 383 | ASN1.decode = function (stream) { 384 | if (!(stream instanceof Stream)) 385 | stream = new Stream(stream, 0); 386 | var streamStart = new Stream(stream), 387 | tag = new ASN1Tag(stream), 388 | len = ASN1.decodeLength(stream), 389 | start = stream.pos, 390 | header = start - streamStart.pos, 391 | sub = null, 392 | getSub = function () { 393 | sub = []; 394 | if (len !== null) { 395 | // definite length 396 | var end = start + len; 397 | while (stream.pos < end) 398 | sub[sub.length] = ASN1.decode(stream); 399 | if (stream.pos != end) 400 | throw "Content size is not correct for container starting at offset " + start; 401 | } else { 402 | // undefined length 403 | try { 404 | for (;;) { 405 | var s = ASN1.decode(stream); 406 | if (s.tag.isEOC()) 407 | break; 408 | sub[sub.length] = s; 409 | } 410 | len = start - stream.pos; // undefined lengths are represented as negative values 411 | } catch (e) { 412 | throw "Exception while decoding undefined length content: " + e; 413 | } 414 | } 415 | }; 416 | if (tag.tagConstructed) { 417 | // must have valid content 418 | getSub(); 419 | } else if (tag.isUniversal() && ((tag.tagNumber == 0x03) || (tag.tagNumber == 0x04))) { 420 | // sometimes BitString and OctetString are used to encapsulate ASN.1 421 | try { 422 | if (tag.tagNumber == 0x03) 423 | if (stream.get() != 0) 424 | throw "BIT STRINGs with unused bits cannot encapsulate."; 425 | getSub(); 426 | for (var i = 0; i < sub.length; ++i) 427 | if (sub[i].tag.isEOC()) 428 | throw 'EOC is not supposed to be actual content.'; 429 | } catch (e) { 430 | // but silently ignore when they don't 431 | sub = null; 432 | } 433 | } 434 | if (sub === null) { 435 | if (len === null) 436 | throw "We can't skip over an invalid tag with undefined length at offset " + start; 437 | stream.pos = start + Math.abs(len); 438 | } 439 | return new ASN1(streamStart, header, len, tag, sub); 440 | }; 441 | 442 | // export globals 443 | if (typeof module !== 'undefined') { module.exports = ASN1; } else { window.ASN1 = ASN1; } 444 | })(); 445 | 446 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | Get HTTPS for free! 24 | 25 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |

67 | Get HTTPS for free! 68 | Update: Added wildcard certificate support! 69 |

70 | 75 |
76 | You can now get free https certificates (incuding wildcard certificates) 77 | from the non-profit certificate authority 78 | Let's Encrypt! 79 | This is a website that will take you through the manual steps to get your free 80 | https certificate so you can make your own website use https! This website is 81 | open source 82 | and NEVER asks for your private keys. Never trust a website 83 | that asks for your private keys! 84 | 85 |
86 | 87 |
88 | NOTE: This website is for people who know how to generate certificate signing 89 | requests (CSRs)! If you're not familiar with how to do this, please use the 90 | official Let's Encrypt official client 91 | that can automatically issue and install https certificates for you. This 92 | website is designed for people who know what they are doing and just want to get 93 | their free https certificate. 94 |
95 | 96 |
97 | If you need to renew a certificate, simply complete these steps below again. 98 |
99 | 100 |
101 | 102 | 103 | 104 | 105 |

Step 1: Account Info

106 |
107 | Let's Encrypt requires that you register an account email and public key before 108 | issuing a certificate. The email is so that they can contact you if needed, and 109 | the public key is so you can securely sign your requests to issue/revoke/renew 110 | your certificates. Keep your account private key secret! Anyone who has it 111 | can impersonate you when making requests to Let's Encrypt! 112 |
113 | 114 |
115 |
116 |
117 | 118 |
119 | 120 |
121 | 122 | 123 |
124 | How to generate a new account keypair using openssl:
125 |
    126 |
  1. 127 | Generate an account private key if you don't have one:
    128 | (KEEP ACCOUNT.KEY SECRET!)
    129 | openssl genrsa 4096 > account.key 130 |
  2. 131 |
  3. 132 | Print your public key:
    133 | openssl rsa -in account.key -pubout 134 |
  4. 135 |
  5. 136 | Copy and paste the public key into the box below.
    137 |
  6. 138 |
139 |
140 |
141 | 142 |
143 | 144 |
145 | 146 | 147 |
148 |
149 | 150 |
151 | 152 | 153 | 154 | 155 |

Step 2: Certificate Signing Request

156 |
157 | This is the certificate signing request (CSR) that you send to Let's Encrypt 158 | in order to issue you a signed certificate. It contains the website domains you 159 | want to issue certs for and the public key of your TLS private key. Keep your 160 | TLS private key secret! Anyone who has it can man-in-the-middle your website! 161 |
162 | 163 |
164 |
165 | 166 | 167 |
168 | How to generate a new Certificate Signing Request (CSR):
169 |
    170 |
  1. 171 | Generate a TLS private key if you don't have one:
    172 | (KEEP DOMAIN.KEY SECRET!)
    173 | openssl genrsa 4096 > domain.key 174 |
  2. 175 |
  3. 176 | Generate a CSR for your the domains you want certs for:
    177 | (replace "foo.com" with your domain)
    178 | Linux: 179 |
    180 |     #change "/etc/ssl/openssl.cnf" as needed:
    181 |     #  Debian: /etc/ssl/openssl.cnf
    182 |     #  RHEL and CentOS: /etc/pki/tls/openssl.cnf
    183 |     #  Mac OSX: /System/Library/OpenSSL/openssl.cnf
    184 | 
    185 |     openssl req -new -sha256 -key domain.key -subj "/" \
    186 |       -reqexts SAN -config <(cat /etc/ssl/openssl.cnf \
    187 |       <(printf "\n[SAN]\nsubjectAltName=DNS:foo.com,DNS:www.foo.com"))
    188 |     
    189 |
  4. 190 |
  5. 191 | Copy and paste the CSR into the box below.
    192 |
  6. 193 |
194 |
195 |
196 | 197 |
198 | 199 |
200 | 201 | 202 |
203 |
204 | 205 |
206 | 207 | 208 | 209 | 210 |

Step 3: Sign API Requests (waiting...)

211 | 324 | 325 |
326 | 327 | 328 | 329 | 330 |

Step 4: Verify Ownership (waiting...)

331 | 643 | 644 |
645 | 646 | 647 | 648 | 649 |

Step 5: Install Certificate (waiting...)

650 | 765 | 766 |
767 | 768 | 773 | 774 | 775 | 776 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains the functions needed to run index.html 3 | */ 4 | 5 | // global variables 6 | var DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory"; 7 | //var DIRECTORY_URL = "https://acme-staging-v02.api.letsencrypt.org/directory"; 8 | var DIRECTORY = { 9 | // "keyChange": "https://... 10 | // "meta": { 11 | // "termsOfService": "https://..." 12 | // }, 13 | // "newAccount": "https://...", 14 | // "newNonce": "https://...", 15 | // "newOrder": "https://...", 16 | // "revokeCert": "https://...", 17 | }; 18 | var ACCOUNT = { 19 | // "pubkey": "-----BEGIN PUBLIC KEY...", 20 | // "alg": "RS256", 21 | // "jwk": {"e": "deadbeef...", "kty": "RSA", "n": "deadbeef..."}, 22 | // "thumbprint": "deadbeef...", 23 | // "account_uri": "https://...", 24 | // 25 | // // newAccount - account registration (or to get the account_uri) 26 | // "registration_payload_json": {"termsOfServiceAgreed": true}, 27 | // "registration_payload_b64": "deadbeef...", 28 | // "registration_protected_json": {"url": "...", "alg": "...", "nonce": "...", "jwk": {...}}, 29 | // "registration_protected_b64": "deadbeef...", 30 | // "registration_sig": "deadbeef...", 31 | // "registration_response": {"status": "valid", "contact": [..], "termsOfServiceAgreed": true, "orders": "..."}, 32 | // 33 | // // account contact update 34 | // "update_payload_json": {"contact": ["mailto:..."]}, 35 | // "update_payload_b64": "deadbeef...", 36 | // "update_protected_json": {"url": "...", "alg": "...", "nonce": "...", "kid": "..."}, 37 | // "update_protected_b64": "deadbeef...", 38 | // "update_sig": "deadbeef...", 39 | // "update_response": {"status": "valid", "contact": [..], "termsOfServiceAgreed": true, "orders": "..."}, 40 | }; 41 | var ORDER = { 42 | // "csr_pem": "-----BEGIN CERTIFICATE REQUEST...", 43 | // "csr_der": "deadbeef...", (DER encoded) 44 | // 45 | // // create order for identifiers 46 | // "order_payload_json": {"identifiers": [{"type": "dns", "value": "aaa.com"}, ...]}, 47 | // "order_payload_b64": "deadbeef...", 48 | // "order_protected_json": {"url": "...", "alg": "...", "nonce": "...", "kid": "..."}, 49 | // "order_protected_b64": "deadbeef...", 50 | // "order_sig": "deadbeef...", 51 | // "order_response": {"status": "valid", "identifiers": [...], "authorizations": [...], "finalize": "...", ...}, 52 | // "order_uri": "https://...", 53 | // 54 | // // get csr signed 55 | // "finalize_uri": "https://...", 56 | // "finalize_payload_json": {"csr": "..."}, 57 | // "finalize_payload_b64": "deadbeef...", 58 | // "finalize_protected_json": {"url": "...", "alg": "...", "nonce": "...", "kid": "..."}, 59 | // "finalize_protected_b64": "deadbeef...", 60 | // "finalize_sig": "deadbeef...", 61 | // "finalize_response": {"status": "valid", "certificate": "...", ...}, 62 | }; 63 | var AUTHORIZATIONS = { 64 | // // one authorization for each domain 65 | // "https://...": { 66 | // "authorization": {"status": "valid", "identifier": {...}, "challenges": [...], "wildcard": false, ...}, 67 | // 68 | // // python server HTTP challenge 69 | // "python_challenge_uri": "https://...", 70 | // "python_challenge_object": {"type": "http-01", ...}, 71 | // "python_challenge_protected_json": {"url": "...", "alg": "...", "nonce": "...", "kid": "..."}, 72 | // "python_challenge_protected_b64": "deadbeef...", 73 | // "python_challenge_sig": "deadbeef...", 74 | // "python_challenge_response": {"type": "http-01", "url": "...", "token": "..."}, 75 | // 76 | // // file-based HTTP challenge 77 | // "file_challenge_uri": "https://...", 78 | // "file_challenge_object": {"type": "http-01", ...}, 79 | // "file_challenge_protected_json": {"url": "...", "alg": "...", "nonce": "...", "kid": "..."}, 80 | // "file_challenge_protected_b64": "deadbeef...", 81 | // "file_challenge_sig": "deadbeef...", 82 | // "file_challenge_response": {"type": "http-01", "url": "...", "token": "..."}, 83 | // 84 | // // DNS challenge 85 | // "dns_challenge_uri": "https://...", 86 | // "dns_challenge_object": {"type": "dns-01", ...}, 87 | // "dns_challenge_protected_json": {"url": "...", "alg": "...", "nonce": "...", "kid": "..."}, 88 | // "dns_challenge_protected_b64": "deadbeef...", 89 | // "dns_challenge_sig": "deadbeef...", 90 | // "dns_challenge_response": {"type": "dns-01", "url": "...", "token": "..."}, 91 | // }, 92 | // ... 93 | }; 94 | var RESULT_PLACEHOLDER = "Paste the hex output here (e.g. \"(stdin)= f2cf67e4...\")"; 95 | 96 | /* 97 | * Helper Functions 98 | */ 99 | 100 | // display errors 101 | function fail(status_element, error_message){ 102 | // debug 103 | if(window.location.search.indexOf("debug") !== -1 && console){ 104 | console.log("DIRECTORY_URL", DIRECTORY_URL); 105 | console.log("DIRECTORY", DIRECTORY); 106 | console.log("ACCOUNT", ACCOUNT); 107 | console.log("ORDER", ORDER); 108 | console.log("AUTHORIZATIONS", AUTHORIZATIONS); 109 | } 110 | status_element.style.display = "inline"; 111 | status_element.className = status_element.className + " error"; 112 | status_element.innerHTML = ""; 113 | status_element.appendChild(document.createTextNode("Error: " + error_message)); 114 | } 115 | 116 | // show warning if no webcrypto digest 117 | window.crypto = window.crypto || window.msCrypto; //for IE11 118 | if(window.crypto && window.crypto.webkitSubtle){ 119 | window.crypto.subtle = window.crypto.webkitSubtle; //for Safari 120 | } 121 | var DIGEST = window.crypto ? (window.crypto.subtle ? window.crypto.subtle.digest : undefined) : undefined; 122 | document.getElementById("digest_error").style.display = DIGEST ? "none" : "block"; 123 | 124 | // SHA-256 shim for standard promise-based and IE11 event-based 125 | function sha256(bytes, callback){ 126 | var hash = window.crypto.subtle.digest({name: "SHA-256"}, bytes); 127 | // IE11 128 | if(!hash.then){ 129 | hash.oncomplete = function(e){ 130 | callback(new Uint8Array(e.target.result), undefined); 131 | }; 132 | hash.onerror = function(e){ 133 | callback(undefined, e); 134 | }; 135 | } 136 | // standard promise-based 137 | else{ 138 | hash.then(function(result){ 139 | callback(new Uint8Array(result), undefined); 140 | }) 141 | .catch(function(error){ 142 | callback(undefined, error); 143 | }); 144 | } 145 | } 146 | 147 | // url-safe base64 encoding 148 | function b64(bytes){ 149 | var str64 = typeof(bytes) === "string" ? window.btoa(bytes) : window.btoa(String.fromCharCode.apply(null, bytes)); 150 | return str64.replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, ""); 151 | } 152 | 153 | // parse openssl hex output 154 | var OPENSSL_HEX = /(?:\(stdin\)= |)([a-f0-9]{512,1024})/ 155 | function hex2b64(hex){ 156 | if(!OPENSSL_HEX.test(hex)){ 157 | return null; 158 | } 159 | hex = OPENSSL_HEX.exec(hex)[1]; 160 | var bytes = []; 161 | while(hex.length >= 2){ 162 | bytes.push(parseInt(hex.substring(0, 2), 16)); 163 | hex = hex.substring(2, hex.length); 164 | } 165 | return b64(new Uint8Array(bytes)); 166 | } 167 | 168 | // url-safe base64 encoding 169 | function cachebuster(){ 170 | return "cachebuster=" + b64(window.crypto.getRandomValues(new Uint8Array(8))); 171 | } 172 | 173 | // helper function to get a nonce via an ajax request to the ACME directory 174 | function getNonce(callback){ 175 | var xhr = new XMLHttpRequest(); 176 | xhr.open("GET", DIRECTORY['newNonce'] + "?" + cachebuster()); 177 | xhr.onload = function(){ 178 | callback(xhr.getResponseHeader("Replay-Nonce"), undefined); 179 | }; 180 | xhr.onerror = function(){ 181 | callback(undefined, xhr); 182 | }; 183 | xhr.send(); 184 | } 185 | 186 | // helper function to get an authorization 187 | function getAuthorization(auth_url, callback){ 188 | var xhr = new XMLHttpRequest(); 189 | xhr.open("GET", auth_url + "?" + cachebuster()); 190 | xhr.onload = function(){ 191 | 192 | // update authorization 193 | AUTHORIZATIONS[auth_url]['authorization'] = JSON.parse(xhr.responseText); 194 | 195 | // clear stale challenge objects 196 | AUTHORIZATIONS[auth_url]['python_challenge_uri'] = undefined; 197 | AUTHORIZATIONS[auth_url]['python_challenge_object'] = undefined; 198 | AUTHORIZATIONS[auth_url]['file_challenge_uri'] = undefined; 199 | AUTHORIZATIONS[auth_url]['file_challenge_object'] = undefined; 200 | AUTHORIZATIONS[auth_url]['dns_challenge_uri'] = undefined; 201 | AUTHORIZATIONS[auth_url]['dns_challenge_object'] = undefined; 202 | 203 | // update challenges 204 | var challenges = AUTHORIZATIONS[auth_url]['authorization']['challenges']; 205 | for(var i = 0; i < challenges.length; i++){ 206 | var challenge = challenges[i]; 207 | 208 | // HTTP challenge 209 | if(challenge['type'] === "http-01"){ 210 | AUTHORIZATIONS[auth_url]['python_challenge_uri'] = challenge['url']; 211 | AUTHORIZATIONS[auth_url]['python_challenge_object'] = challenge; 212 | AUTHORIZATIONS[auth_url]['file_challenge_uri'] = challenge['url']; 213 | AUTHORIZATIONS[auth_url]['file_challenge_object'] = challenge; 214 | } 215 | 216 | // DNS challenge 217 | if(challenge['type'] === "dns-01"){ 218 | AUTHORIZATIONS[auth_url]['dns_challenge_uri'] = challenge['url']; 219 | AUTHORIZATIONS[auth_url]['dns_challenge_object'] = challenge; 220 | } 221 | } 222 | 223 | // make the callback with the updated authorization 224 | callback(AUTHORIZATIONS[auth_url]['authorization'], undefined); 225 | }; 226 | xhr.onerror = function(){ 227 | callback(undefined, xhr); 228 | }; 229 | xhr.send(); 230 | } 231 | 232 | /* 233 | * Step 0: Let's Encrypt Directory 234 | */ 235 | 236 | // get the directory with links to all the other endpoints 237 | function populateDirectory(){ 238 | var xhr = new XMLHttpRequest(); 239 | xhr.open("GET", DIRECTORY_URL + "?" + cachebuster()); 240 | xhr.onload = function(){ 241 | // set the directory urls 242 | DIRECTORY = JSON.parse(xhr.responseText); 243 | // set the terms of service links 244 | document.getElementById("tos").setAttribute("href", DIRECTORY['meta']['termsOfService']); 245 | document.getElementById("howto_tos").setAttribute("href", DIRECTORY['meta']['termsOfService']); 246 | // enable buttons so user can continue 247 | document.getElementById("validate_account").addEventListener("submit", validateAccount); 248 | document.getElementById("validate_account_submit").removeAttribute("disabled"); 249 | document.getElementById("validate_csr").addEventListener("submit", validateCSR); 250 | document.getElementById("validate_csr_submit").removeAttribute("disabled"); 251 | document.getElementById("validate_registration").addEventListener("submit", validateRegistration); 252 | document.getElementById("validate_update").addEventListener("submit", validateUpdate); 253 | document.getElementById("validate_order").addEventListener("submit", validateOrder); 254 | document.getElementById("validate_finalize").addEventListener("submit", validateFinalize); 255 | }; 256 | xhr.onerror = function(){ 257 | fail(document.getElementById("validate_account_status"), "Let's Encrypt appears to be down. Please try again later."); 258 | }; 259 | xhr.send(); 260 | } 261 | populateDirectory(); 262 | 263 | /* 264 | * Step 1: Account Info 265 | */ 266 | 267 | // validate account info 268 | function validateAccount(e){ 269 | e.preventDefault(); 270 | 271 | // clear previous status 272 | var status = document.getElementById("validate_account_status"); 273 | status.style.display = "inline"; 274 | status.className = ""; 275 | status.innerHTML = "validating..."; 276 | 277 | // validate email 278 | var email_re = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; 279 | var email = document.getElementById("email").value; 280 | if(!email_re.test(email)){ 281 | return fail(status, "Account email doesn't look valid."); 282 | } 283 | 284 | // update email in interface 285 | document.getElementById("account_email").innerHTML = ""; 286 | document.getElementById("account_email").appendChild(document.createTextNode(email)); 287 | 288 | // parse account public key 289 | var pubkey = document.getElementById("pubkey").value; 290 | if(pubkey === ""){ 291 | return fail(status, "You need to include an account public key."); 292 | } 293 | var unarmor = /-----BEGIN PUBLIC KEY-----([A-Za-z0-9+\/=\s]+)-----END PUBLIC KEY-----/; 294 | if(!unarmor.test(pubkey)){ 295 | return fail(status, "Your public key isn't formatted correctly."); 296 | } 297 | 298 | // find RSA modulus and exponent 299 | try{ 300 | var pubkeyAsn1 = ASN1.decode(Base64.decode(unarmor.exec(pubkey)[1])); 301 | var modulusRaw = pubkeyAsn1.sub[1].sub[0].sub[0]; 302 | var modulusStart = modulusRaw.header + modulusRaw.stream.pos + 1; 303 | var modulusEnd = modulusRaw.length + modulusRaw.stream.pos + modulusRaw.header; 304 | var modulusHex = modulusRaw.stream.hexDump(modulusStart, modulusEnd); 305 | var modulus = Hex.decode(modulusHex); 306 | var exponentRaw = pubkeyAsn1.sub[1].sub[0].sub[1]; 307 | var exponentStart = exponentRaw.header + exponentRaw.stream.pos; 308 | var exponentEnd = exponentRaw.length + exponentRaw.stream.pos + exponentRaw.header; 309 | var exponentHex = exponentRaw.stream.hexDump(exponentStart, exponentEnd); 310 | var exponent = Hex.decode(exponentHex); 311 | } 312 | catch(err){ 313 | return fail(status, "Failed validating RSA public key."); 314 | } 315 | 316 | // generate the jwk header and bytes 317 | var jwk = { 318 | "e": b64(new Uint8Array(exponent)), 319 | "kty": "RSA", 320 | "n": b64(new Uint8Array(modulus)), 321 | } 322 | var jwk_json = JSON.stringify(jwk); 323 | var jwk_bytes = []; 324 | for(var i = 0; i < jwk_json.length; i++){ 325 | jwk_bytes.push(jwk_json.charCodeAt(i)); 326 | } 327 | 328 | // calculate thumbprint 329 | sha256(new Uint8Array(jwk_bytes), function(hash, err){ 330 | if(err){ 331 | return fail(status, "Thumbprint failed: " + err.message); 332 | } 333 | 334 | // update the global account object 335 | var registration_payload = {"termsOfServiceAgreed": true}; 336 | var account_payload = {"contact": ["mailto:" + email]}; 337 | ACCOUNT = { 338 | "pubkey": pubkey, 339 | "alg": "RS256", 340 | "jwk": jwk, 341 | "thumbprint": b64(hash), 342 | "account_uri": undefined, 343 | 344 | // newAccount - account registration (or to get the account_url) 345 | "registration_payload_json": registration_payload, 346 | "registration_payload_b64": b64(JSON.stringify(registration_payload)), 347 | "registration_protected_json": undefined, 348 | "registration_protected_b64": undefined, 349 | "registration_sig": undefined, 350 | "registration_response": undefined, 351 | 352 | // account contact update 353 | "update_payload_json": account_payload, 354 | "update_payload_b64": b64(JSON.stringify(account_payload)), 355 | "update_protected_json": undefined, 356 | "update_protected_b64": undefined, 357 | "update_sig": undefined, 358 | "update_response": undefined, 359 | }; 360 | 361 | // show the success text (simulate a delay so it looks like we thought hard) 362 | window.setTimeout(function(){ 363 | status.style.display = "inline"; 364 | status.className = ""; 365 | status.innerHTML = ""; 366 | status.appendChild(document.createTextNode("Looks good! Proceed to Step 2!")); 367 | }, 300); 368 | }); 369 | } 370 | 371 | /* 372 | * Step 2: CSR 373 | */ 374 | 375 | // validate CSR 376 | function validateCSR(e){ 377 | e.preventDefault(); 378 | 379 | // clear previous status 380 | var status = document.getElementById("validate_csr_status"); 381 | status.style.display = "inline"; 382 | status.className = ""; 383 | status.innerHTML = "validating..."; 384 | 385 | // hide following steps 386 | document.getElementById("step3").style.display = "none"; 387 | document.getElementById("step3_pending").style.display = "inline"; 388 | document.getElementById("step4").style.display = "none"; 389 | document.getElementById("step4_pending").style.display = "inline"; 390 | document.getElementById("step5").style.display = "none"; 391 | document.getElementById("step5_pending").style.display = "inline"; 392 | 393 | // reset registration status 394 | document.getElementById("validate_registration_sig_status").style.display = "none"; 395 | document.getElementById("validate_registration_sig_status").className = ""; 396 | document.getElementById("validate_registration_sig_status").innerHTML = ""; 397 | 398 | // reset account update signature 399 | document.getElementById("update_sig_cmd").value = "waiting until terms are accepted..."; 400 | document.getElementById("update_sig_cmd").removeAttribute("readonly"); 401 | document.getElementById("update_sig_cmd").setAttribute("disabled", ""); 402 | document.getElementById("update_sig").value = ""; 403 | document.getElementById("update_sig").setAttribute("placeholder", "waiting until terms are accepted..."); 404 | document.getElementById("update_sig").setAttribute("disabled", ""); 405 | document.getElementById("validate_update_sig").setAttribute("disabled", ""); 406 | document.getElementById("validate_update_sig_status").style.display = "none"; 407 | document.getElementById("validate_update_sig_status").className = ""; 408 | document.getElementById("validate_update_sig_status").innerHTML = ""; 409 | 410 | // reset new order signature 411 | document.getElementById("order_sig_cmd").value = "waiting until account contact is updated..."; 412 | document.getElementById("order_sig_cmd").removeAttribute("readonly"); 413 | document.getElementById("order_sig_cmd").setAttribute("disabled", ""); 414 | document.getElementById("order_sig").value = ""; 415 | document.getElementById("order_sig").setAttribute("placeholder", "waiting until account contact is updated..."); 416 | document.getElementById("order_sig").setAttribute("disabled", ""); 417 | document.getElementById("validate_order_sig").setAttribute("disabled", ""); 418 | document.getElementById("validate_order_sig_status").style.display = "none"; 419 | document.getElementById("validate_order_sig_status").className = ""; 420 | document.getElementById("validate_order_sig_status").innerHTML = ""; 421 | 422 | // make sure there's an account public key and email 423 | if(ACCOUNT['pubkey'] === undefined){ 424 | return fail(status, "Need to complete Step 1 first."); 425 | } 426 | 427 | // parse csr 428 | var csr = document.getElementById("csr").value; 429 | if(csr === ""){ 430 | return fail(status, "You need to include a CSR."); 431 | } 432 | var unarmor = /-----BEGIN CERTIFICATE REQUEST-----([A-Za-z0-9+\/=\s]+)-----END CERTIFICATE REQUEST-----/; 433 | if(!unarmor.test(csr)){ 434 | return fail(status, "Your CSR isn't formatted correctly."); 435 | } 436 | var csr_der = b64(new Uint8Array(Base64.decode(unarmor.exec(csr)[1]))); 437 | 438 | // find domains in the csr 439 | var domains = []; 440 | try{ 441 | var csrAsn1 = ASN1.decode(Base64.decode(unarmor.exec(csr)[1])); 442 | 443 | // look for commonName in attributes 444 | if(csrAsn1.sub[0].sub[1].sub){ 445 | var csrIds = csrAsn1.sub[0].sub[1].sub; 446 | for(var i = 0; i < csrIds.length; i++){ 447 | var oidRaw = csrIds[i].sub[0].sub[0]; 448 | var oidStart = oidRaw.header + oidRaw.stream.pos; 449 | var oidEnd = oidRaw.length + oidRaw.stream.pos + oidRaw.header; 450 | var oid = oidRaw.stream.parseOID(oidStart, oidEnd, Infinity); 451 | if(oid === "2.5.4.3"){ 452 | var cnRaw = csrIds[i].sub[0].sub[1]; 453 | var cnStart = cnRaw.header + cnRaw.stream.pos; 454 | var cnEnd = cnRaw.length + cnRaw.stream.pos + cnRaw.header; 455 | domains.push(cnRaw.stream.parseStringUTF(cnStart, cnEnd)); 456 | } 457 | } 458 | } 459 | 460 | // look for subjectAltNames 461 | if(csrAsn1.sub[0].sub[3].sub){ 462 | 463 | // find the PKCS#9 ExtensionRequest 464 | var xtns = csrAsn1.sub[0].sub[3].sub; 465 | for(var i = 0; i < xtns.length; i++){ 466 | var oidRaw = xtns[i].sub[0]; 467 | var oidStart = oidRaw.header + oidRaw.stream.pos; 468 | var oidEnd = oidRaw.length + oidRaw.stream.pos + oidRaw.header; 469 | var oid = oidRaw.stream.parseOID(oidStart, oidEnd, Infinity); 470 | if(oid === "1.2.840.113549.1.9.14"){ 471 | 472 | // find any subjectAltNames 473 | for(var j = 0; j < xtns[i].sub[1].sub.length ? xtns[i].sub[1].sub : 0; j++){ 474 | for(var k = 0; k < xtns[i].sub[1].sub[j].sub.length ? xtns[i].sub[1].sub[j].sub : 0; k++){ 475 | var oidRaw = xtns[i].sub[1].sub[j].sub[k].sub[0]; 476 | var oidStart = oidRaw.header + oidRaw.stream.pos; 477 | var oidEnd = oidRaw.length + oidRaw.stream.pos + oidRaw.header; 478 | var oid = oidRaw.stream.parseOID(oidStart, oidEnd, Infinity); 479 | if(oid === "2.5.29.17"){ 480 | 481 | // add each subjectAltName 482 | var sans = xtns[i].sub[1].sub[j].sub[k].sub[1].sub[0].sub; 483 | for(var s = 0; s < sans.length; s++){ 484 | var sanRaw = sans[s]; 485 | var tag = sanRaw.tag.tagNumber; 486 | if(tag !== 2) 487 | continue; // ignore any other subjectAltName type than dNSName (2) 488 | var sanStart = sanRaw.header + sanRaw.stream.pos; 489 | var sanEnd = sanRaw.length + sanRaw.stream.pos + sanRaw.header; 490 | domains.push(sanRaw.stream.parseStringUTF(sanStart, sanEnd)); 491 | } 492 | } 493 | } 494 | } 495 | } 496 | } 497 | } 498 | } 499 | catch(err){ 500 | return fail(status, "Failed validating CSR."); 501 | } 502 | 503 | // reject CSRs with no domains 504 | if(domains.length === 0){ 505 | return fail(status, "Couldn't find any domains in the CSR."); 506 | } 507 | 508 | // build order payload 509 | var finalize_payload = {"csr": csr_der}; 510 | var order_payload = {"identifiers": []}; 511 | for(var i = 0; i < domains.length; i++){ 512 | order_payload['identifiers'].push({"type": "dns", "value": domains[i]}); 513 | } 514 | 515 | // update the globals 516 | ORDER = { 517 | "csr_pem": csr, 518 | "csr_der": csr_der, 519 | 520 | // order for identifiers 521 | "order_payload_json": order_payload, 522 | "order_payload_b64": b64(JSON.stringify(order_payload)), 523 | "order_protected_json": undefined, 524 | "order_protected_b64": undefined, 525 | "order_sig": undefined, 526 | "order_response": undefined, 527 | "order_uri": undefined, 528 | 529 | // order finalizing 530 | "finalize_uri": undefined, 531 | "finalize_payload_json": finalize_payload, 532 | "finalize_payload_b64": b64(JSON.stringify(finalize_payload)), 533 | "finalize_protected_json": undefined, 534 | "finalize_protected_b64": undefined, 535 | "finalize_sig": undefined, 536 | "finalize_response": undefined, 537 | }; 538 | 539 | // set the shortest domain for the ssl test at the end 540 | var shortest_domain = domains[0]; 541 | for(var d = 0; d < domains.length; d++){ 542 | if(shortest_domain.length > domains[d].length){ 543 | shortest_domain = domains[d]; 544 | } 545 | } 546 | document.getElementById("ssltest_domain").value = shortest_domain; 547 | 548 | // get nonce for registration 549 | getNonce(function(nonce, err){ 550 | if(err){ 551 | return fail(status, "Failed terms nonce request (code: " + err.status + "). " + err.responseText); 552 | } 553 | 554 | // populate registration signature (payload populated in validateAccount()) 555 | ACCOUNT['registration_protected_json'] = { 556 | "url": DIRECTORY['newAccount'], 557 | "alg": ACCOUNT['alg'], 558 | "nonce": nonce, 559 | "jwk": ACCOUNT['jwk'], 560 | } 561 | ACCOUNT['registration_protected_b64'] = b64(JSON.stringify(ACCOUNT['registration_protected_json'])); 562 | document.getElementById("registration_sig_cmd").value = "" + 563 | "echo | set /p=\"" + ACCOUNT['registration_protected_b64'] + "." + ACCOUNT['registration_payload_b64'] + "\" | " + 564 | "openssl dgst -sha256 -hex -sign account.key"; 565 | document.getElementById("registration_sig").value = ""; 566 | document.getElementById("registration_sig").setAttribute("placeholder", RESULT_PLACEHOLDER); 567 | 568 | // show step 3 569 | status.style.display = "inline"; 570 | status.className = ""; 571 | status.innerHTML = ""; 572 | status.appendChild(document.createTextNode("Found domains! Proceed to Step 3! (" + domains.join(", ") + ")")); 573 | document.getElementById("step3").style.display = "block"; 574 | document.getElementById("step3_pending").style.display = "none"; 575 | }); 576 | } 577 | 578 | /* 579 | * Step 3a: Register Account (POST /newAccount) 580 | */ 581 | function validateRegistration(e){ 582 | e.preventDefault(); 583 | 584 | // clear previous status 585 | var status = document.getElementById("validate_registration_sig_status"); 586 | status.style.display = "inline"; 587 | status.className = ""; 588 | status.innerHTML = "accepting..."; 589 | 590 | // hide following steps 591 | document.getElementById("step4").style.display = "none"; 592 | document.getElementById("step4_pending").style.display = "inline"; 593 | document.getElementById("step5").style.display = "none"; 594 | document.getElementById("step5_pending").style.display = "inline"; 595 | 596 | // reset account update signature 597 | document.getElementById("update_sig_cmd").value = "waiting until terms are accepted..."; 598 | document.getElementById("update_sig_cmd").removeAttribute("readonly"); 599 | document.getElementById("update_sig_cmd").setAttribute("disabled", ""); 600 | document.getElementById("update_sig").value = ""; 601 | document.getElementById("update_sig").setAttribute("placeholder", "waiting until terms are accepted..."); 602 | document.getElementById("update_sig").setAttribute("disabled", ""); 603 | document.getElementById("validate_update_sig").setAttribute("disabled", ""); 604 | document.getElementById("validate_update_sig_status").style.display = "none"; 605 | document.getElementById("validate_update_sig_status").className = ""; 606 | document.getElementById("validate_update_sig_status").innerHTML = ""; 607 | 608 | // reset new order signature 609 | document.getElementById("order_sig_cmd").value = "waiting until account contact is updated..."; 610 | document.getElementById("order_sig_cmd").removeAttribute("readonly"); 611 | document.getElementById("order_sig_cmd").setAttribute("disabled", ""); 612 | document.getElementById("order_sig").value = ""; 613 | document.getElementById("order_sig").setAttribute("placeholder", "waiting until account contact is updated..."); 614 | document.getElementById("order_sig").setAttribute("disabled", ""); 615 | document.getElementById("validate_order_sig").setAttribute("disabled", ""); 616 | document.getElementById("validate_order_sig_status").style.display = "none"; 617 | document.getElementById("validate_order_sig_status").className = ""; 618 | document.getElementById("validate_order_sig_status").innerHTML = ""; 619 | 620 | // validate registration payload exists 621 | if(ACCOUNT['registration_payload_b64'] === undefined){ 622 | return fail(status, "Terms payload not found. Please go back to Step 1."); 623 | } 624 | 625 | // validate the signature 626 | var registration_sig = hex2b64(document.getElementById("registration_sig").value); 627 | if(registration_sig === null){ 628 | return fail(status, "You need to run the above commands and paste the output in the text boxes below each command."); 629 | } 630 | ACCOUNT['registration_sig'] = registration_sig; 631 | 632 | // send newAccount request to CA 633 | var registration_xhr = new XMLHttpRequest(); 634 | registration_xhr.open("POST", DIRECTORY['newAccount']); 635 | registration_xhr.setRequestHeader("Content-Type", "application/jose+json"); 636 | registration_xhr.onreadystatechange = function(){ 637 | if(registration_xhr.readyState === 4){ 638 | 639 | // successful registration 640 | if(registration_xhr.status === 200 || registration_xhr.status === 201 || registration_xhr.status === 204){ 641 | 642 | // set account_uri 643 | ACCOUNT['account_uri'] = registration_xhr.getResponseHeader("Location"); 644 | 645 | // get nonce for account update 646 | getNonce(function(nonce, err){ 647 | if(err){ 648 | return fail(status, "Failed update nonce request (code: " + err.status + "). " + err.responseText); 649 | } 650 | 651 | // populate update signature (payload populated in validateAccount()) 652 | ACCOUNT['update_protected_json'] = { 653 | "url": ACCOUNT['account_uri'], 654 | "alg": ACCOUNT['alg'], 655 | "nonce": nonce, 656 | "kid": ACCOUNT['account_uri'], 657 | } 658 | ACCOUNT['update_protected_b64'] = b64(JSON.stringify(ACCOUNT['update_protected_json'])); 659 | document.getElementById("update_sig_cmd").value = "" + 660 | "echo | set /p=\"" + ACCOUNT['update_protected_b64'] + "." + ACCOUNT['update_payload_b64'] + "\" | " + 661 | "openssl dgst -sha256 -hex -sign account.key"; 662 | document.getElementById("update_sig_cmd").setAttribute("readonly", ""); 663 | document.getElementById("update_sig_cmd").removeAttribute("disabled"); 664 | document.getElementById("update_sig").value = ""; 665 | document.getElementById("update_sig").setAttribute("placeholder", RESULT_PLACEHOLDER); 666 | document.getElementById("update_sig").removeAttribute("disabled"); 667 | document.getElementById("validate_update_sig").removeAttribute("disabled"); 668 | 669 | // complete step 3a 670 | status.innerHTML = "Accepted! Proceed to next command below."; 671 | }); 672 | } 673 | 674 | // error registering 675 | else{ 676 | return fail(status, "Account registration failed. Please start back at Step 1. " + registration_xhr.responseText); 677 | } 678 | } 679 | }; 680 | registration_xhr.send(JSON.stringify({ 681 | "protected": ACCOUNT['registration_protected_b64'], 682 | "payload": ACCOUNT['registration_payload_b64'], 683 | "signature": ACCOUNT['registration_sig'], 684 | })); 685 | } 686 | 687 | /* 688 | * Step 3b: Update Account Contact (POST /ACCOUNT['account_uri']) 689 | */ 690 | function validateUpdate(e){ 691 | e.preventDefault(); 692 | 693 | // clear previous status 694 | var status = document.getElementById("validate_update_sig_status"); 695 | status.style.display = "inline"; 696 | status.className = ""; 697 | status.innerHTML = "updating..."; 698 | 699 | // hide following steps 700 | document.getElementById("step4").style.display = "none"; 701 | document.getElementById("step4_pending").style.display = "inline"; 702 | document.getElementById("step5").style.display = "none"; 703 | document.getElementById("step5_pending").style.display = "inline"; 704 | 705 | // reset new order signature 706 | document.getElementById("order_sig_cmd").value = "waiting until account contact is updated..."; 707 | document.getElementById("order_sig_cmd").removeAttribute("readonly"); 708 | document.getElementById("order_sig_cmd").setAttribute("disabled", ""); 709 | document.getElementById("order_sig").value = ""; 710 | document.getElementById("order_sig").setAttribute("placeholder", "waiting until account contact is updated..."); 711 | document.getElementById("order_sig").setAttribute("disabled", ""); 712 | document.getElementById("validate_order_sig").setAttribute("disabled", ""); 713 | document.getElementById("validate_order_sig_status").style.display = "none"; 714 | document.getElementById("validate_order_sig_status").className = ""; 715 | document.getElementById("validate_order_sig_status").innerHTML = ""; 716 | 717 | // validate update payload exists 718 | if(ACCOUNT['update_payload_b64'] === undefined){ 719 | return fail(status, "Update payload not found. Please go back to Step 1."); 720 | } 721 | 722 | // validate the signature 723 | var update_sig = hex2b64(document.getElementById("update_sig").value); 724 | if(update_sig === null){ 725 | return fail(status, "You need to run the above commands and paste the output in the text boxes below each command."); 726 | } 727 | ACCOUNT['update_sig'] = update_sig; 728 | 729 | // send update request to CA account_uri 730 | var update_xhr = new XMLHttpRequest(); 731 | update_xhr.open("POST", ACCOUNT['account_uri']); 732 | update_xhr.setRequestHeader("Content-Type", "application/jose+json"); 733 | update_xhr.onreadystatechange = function(){ 734 | if(update_xhr.readyState === 4){ 735 | 736 | // successful update 737 | if(update_xhr.status === 200){ 738 | 739 | // get nonce for new order 740 | getNonce(function(nonce, err){ 741 | if(err){ 742 | return fail(status, "Failed order nonce request (code: " + err.status + "). " + err.responseText); 743 | } 744 | 745 | // populate order signature (payload populated in validateCSR()) 746 | ORDER['order_protected_json'] = { 747 | "url": DIRECTORY['newOrder'], 748 | "alg": ACCOUNT['alg'], 749 | "nonce": nonce, 750 | "kid": ACCOUNT['account_uri'], 751 | } 752 | ORDER['order_protected_b64'] = b64(JSON.stringify(ORDER['order_protected_json'])); 753 | document.getElementById("order_sig_cmd").value = "" + 754 | "echo | set /p=\"" + ORDER['order_protected_b64'] + "." + ORDER['order_payload_b64'] + "\" | " + 755 | "openssl dgst -sha256 -hex -sign account.key"; 756 | document.getElementById("order_sig_cmd").setAttribute("readonly", ""); 757 | document.getElementById("order_sig_cmd").removeAttribute("disabled"); 758 | document.getElementById("order_sig").value = ""; 759 | document.getElementById("order_sig").setAttribute("placeholder", RESULT_PLACEHOLDER); 760 | document.getElementById("order_sig").removeAttribute("disabled"); 761 | document.getElementById("validate_order_sig").removeAttribute("disabled"); 762 | 763 | // complete step 3b 764 | status.innerHTML = "Updated! Proceed to next command below."; 765 | }); 766 | } 767 | 768 | // error registering 769 | else{ 770 | return fail(status, "Account contact update failed. Please start back at Step 1. " + update_xhr.responseText); 771 | } 772 | } 773 | }; 774 | update_xhr.send(JSON.stringify({ 775 | "protected": ACCOUNT['update_protected_b64'], 776 | "payload": ACCOUNT['update_payload_b64'], 777 | "signature": ACCOUNT['update_sig'], 778 | })); 779 | } 780 | 781 | /* 782 | * Step 3c: Create New Order (POST /newOrder) 783 | */ 784 | function validateOrder(e){ 785 | e.preventDefault(); 786 | 787 | // clear previous status 788 | var status = document.getElementById("validate_order_sig_status"); 789 | status.style.display = "inline"; 790 | status.className = ""; 791 | status.innerHTML = "ordering..."; 792 | 793 | // hide following steps 794 | document.getElementById("step4").style.display = "none"; 795 | document.getElementById("step4_pending").style.display = "inline"; 796 | document.getElementById("step5").style.display = "none"; 797 | document.getElementById("step5_pending").style.display = "inline"; 798 | 799 | // validate order payload exists 800 | if(ORDER['order_payload_b64'] === undefined){ 801 | return fail(status, "Order payload not found. Please go back to Step 1."); 802 | } 803 | 804 | // validate the signature 805 | var order_sig = hex2b64(document.getElementById("order_sig").value); 806 | if(order_sig === null){ 807 | return fail(status, "You need to run the above commands and paste the output in the text boxes below each command."); 808 | } 809 | ORDER['order_sig'] = order_sig; 810 | 811 | // send newOrder request to CA 812 | var order_xhr = new XMLHttpRequest(); 813 | order_xhr.open("POST", DIRECTORY['newOrder']); 814 | order_xhr.setRequestHeader("Content-Type", "application/jose+json"); 815 | order_xhr.onreadystatechange = function(){ 816 | if(order_xhr.readyState === 4){ 817 | 818 | // successful order 819 | if(order_xhr.status === 200 || order_xhr.status === 201){ 820 | 821 | // set order response and uri 822 | ORDER['order_response'] = JSON.parse(order_xhr.responseText); 823 | ORDER['order_uri'] = order_xhr.getResponseHeader("Location"); 824 | ORDER['finalize_uri'] = ORDER['order_response']['finalize']; 825 | 826 | // clear out any previous authorizations and challenge forms 827 | AUTHORIZATIONS = {}; 828 | document.getElementById("challenge_domains").innerHTML = ""; 829 | 830 | // recursively render authorizations since asynchronous 831 | function buildAuthorization(n, callback){ 832 | var auth_url = ORDER['order_response']['authorizations'][n]; 833 | AUTHORIZATIONS[auth_url] = {}; 834 | getAuthorization(auth_url, function(auth_obj, err){ 835 | if(err){ 836 | return fail(status, "Failed auth #" + n + " lookup (code: " + err.status + "). " + err.responseText); 837 | } 838 | 839 | // figure out which domain this authorization is checking 840 | var d = auth_obj['identifier']['value']; // domain name (e.g. foo.com) 841 | var d_ = d.replace(/[\.]/g, "_"); // id-friendly domain name (e.g. foo_com) 842 | 843 | // distinguish wildcard cert authorizations 844 | if(auth_obj['wildcard']){ 845 | d_ = "__" + d_; 846 | } 847 | 848 | // make challenge section for this authorization 849 | var template = document.getElementById("challenge_examplecom_template").cloneNode(true); 850 | 851 | // section 852 | var section_id = "challenge_" + d_; 853 | template.setAttribute("id", section_id); 854 | template.style.display = "block"; 855 | template.querySelector(".domain").innerHTML = ""; 856 | template.querySelector(".domain").appendChild(document.createTextNode(auth_obj['wildcard'] ? "*." + d : d)); 857 | 858 | // tabs 859 | template.querySelector("input.challenge_python").setAttribute("name", "radio_" + d_); 860 | template.querySelector("input.challenge_python").setAttribute("id", "radio_" + d_ + "_python"); 861 | template.querySelector("label.challenge_python").setAttribute("for", "radio_" + d_ + "_python"); 862 | template.querySelector("label.challenge_python").style.display = "none"; 863 | template.querySelector("input.challenge_file").setAttribute("name", "radio_" + d_); 864 | template.querySelector("input.challenge_file").setAttribute("id", "radio_" + d_ + "_file"); 865 | template.querySelector("label.challenge_file").setAttribute("for", "radio_" + d_ + "_file"); 866 | template.querySelector("label.challenge_file").style.display = "none"; 867 | template.querySelector("input.challenge_dns").setAttribute("name", "radio_" + d_); 868 | template.querySelector("input.challenge_dns").setAttribute("id", "radio_" + d_ + "_dns"); 869 | template.querySelector("label.challenge_dns").setAttribute("for", "radio_" + d_ + "_dns"); 870 | template.querySelector("label.challenge_dns").style.display = "none"; 871 | 872 | // help texts 873 | template.querySelector(".howto_python").setAttribute("id", "howto_" + d_ + "_python"); 874 | template.querySelector(".howto_python_label").setAttribute("for", "howto_" + d_ + "_python"); 875 | template.querySelector(".howto_python_sig").setAttribute("id", "howto_" + d_ + "_python_sig"); 876 | template.querySelector(".howto_python_sig_label").setAttribute("for", "howto_" + d_ + "_python_sig"); 877 | template.querySelector(".howto_file").setAttribute("id", "howto_" + d_ + "_file"); 878 | template.querySelector(".howto_file_label").setAttribute("for", "howto_" + d_ + "_file"); 879 | template.querySelector(".howto_file_sig").setAttribute("id", "howto_" + d_ + "_file_sig"); 880 | template.querySelector(".howto_file_sig_label").setAttribute("for", "howto_" + d_ + "_file_sig"); 881 | template.querySelector(".howto_dns").setAttribute("id", "howto_" + d_ + "_dns"); 882 | template.querySelector(".howto_dns_label").setAttribute("for", "howto_" + d_ + "_dns"); 883 | template.querySelector(".howto_dns_sig").setAttribute("id", "howto_" + d_ + "_dns_sig"); 884 | template.querySelector(".howto_dns_sig_label").setAttribute("for", "howto_" + d_ + "_dns_sig"); 885 | 886 | // event listeners 887 | template.querySelector(".confirm_python").addEventListener("submit", confirmChallenge); 888 | template.querySelector(".confirm_file").addEventListener("submit", confirmChallenge); 889 | template.querySelector(".confirm_dns").addEventListener("submit", confirmChallenge); 890 | template.querySelector(".validate_python_sig").addEventListener("submit", validateChallenge); 891 | template.querySelector(".validate_file_sig").addEventListener("submit", validateChallenge); 892 | template.querySelector(".validate_dns_sig").addEventListener("submit", validateChallenge); 893 | 894 | // python option data 895 | if(AUTHORIZATIONS[auth_url]['python_challenge_object'] !== undefined){ 896 | 897 | // populate values 898 | var token = AUTHORIZATIONS[auth_url]['python_challenge_object']['token']; 899 | var keyauth = token + "." + ACCOUNT['thumbprint']; 900 | var link = "http://" + d + "/.well-known/acme-challenge/" + token; 901 | template.querySelector(".python_link").innerHTML = ""; 902 | template.querySelector(".python_link").appendChild(document.createTextNode(link)); 903 | template.querySelector(".python_link").setAttribute("href", link); 904 | template.querySelector(".python_domain").innerHTML = ""; 905 | template.querySelector(".python_domain").appendChild(document.createTextNode(d)); 906 | template.querySelector(".python_server").value = "" + 907 | "sudo python2 -c \"import BaseHTTPServer; \\\n" + 908 | " h = BaseHTTPServer.BaseHTTPRequestHandler; \\\n" + 909 | " h.do_GET = lambda r: r.send_response(200) or r.end_headers() " + 910 | "or r.wfile.write('" + keyauth + "'); \\\n" + 911 | " s = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), h); \\\n" + 912 | " s.serve_forever()\""; 913 | template.querySelector(".confirm_python_submit").value = "I'm now running this command on " + d; 914 | template.querySelector(".validate_python_sig_submit").value = "Submit challenge for " + d; 915 | template.querySelector("label.challenge_python").style.display = "inline"; 916 | 917 | // set data attributes 918 | var challenge_url = AUTHORIZATIONS[auth_url]['python_challenge_object']['url']; 919 | template.querySelector(".confirm_python").dataset.option = "python"; 920 | template.querySelector(".confirm_python").dataset.section = section_id; 921 | template.querySelector(".confirm_python").dataset.auth = auth_url; 922 | template.querySelector(".confirm_python").dataset.challenge = challenge_url; 923 | } 924 | 925 | // file-based option data 926 | if(AUTHORIZATIONS[auth_url]['file_challenge_object'] !== undefined){ 927 | 928 | // populate values 929 | var token = AUTHORIZATIONS[auth_url]['file_challenge_object']['token']; 930 | 931 | var keyauth = token + "." + ACCOUNT['thumbprint']; 932 | var link = "http://" + d + "/.well-known/acme-challenge/" + token; 933 | var server_config = "" + 934 | "#nginx example\n" + 935 | "location /.well-known/acme-challenge/ {\n" + 936 | " alias /path/to/www/;\n" + 937 | " try_files $uri =404;\n" + 938 | "}\n\n" + 939 | "#apache example\n" + 940 | "Alias /.well-known/acme-challenge /path/to/www/.well-known/acme-challenge"; 941 | var echo = "echo | set /p=\"" + keyauth + "\" > /path/to/www/.well-known/acme-challenge/" + token; 942 | template.querySelector(".file_config").innerHTML = ""; 943 | template.querySelector(".file_config").appendChild(document.createTextNode(server_config)); 944 | template.querySelector(".file_echo").innerHTML = ""; 945 | template.querySelector(".file_echo").appendChild(document.createTextNode(echo)); 946 | template.querySelector(".file_link").innerHTML = ""; 947 | template.querySelector(".file_link").appendChild(document.createTextNode(link)); 948 | template.querySelector(".file_link").setAttribute("href", link); 949 | template.querySelector(".file_url").value = link; 950 | template.querySelector(".file_data").value = keyauth; 951 | template.querySelector(".confirm_file_submit").value = "I'm now serving this file on " + d; 952 | template.querySelector(".validate_file_sig_submit").value = "Submit challenge for " + d; 953 | template.querySelector("label.challenge_file").style.display = "inline"; 954 | 955 | // set data attributes 956 | var challenge_url = AUTHORIZATIONS[auth_url]['file_challenge_object']['url']; 957 | template.querySelector(".confirm_file").dataset.option = "file"; 958 | template.querySelector(".confirm_file").dataset.section = section_id; 959 | template.querySelector(".confirm_file").dataset.auth = auth_url; 960 | template.querySelector(".confirm_file").dataset.challenge = challenge_url; 961 | } 962 | 963 | // DNS option data 964 | if(AUTHORIZATIONS[auth_url]['dns_challenge_object'] !== undefined){ 965 | 966 | // SHA-256 digest of keyauth 967 | var token = AUTHORIZATIONS[auth_url]['dns_challenge_object']['token']; 968 | var keyauth = token + "." + ACCOUNT['thumbprint']; 969 | var keyauth_bytes = []; 970 | for(var i = 0; i < keyauth.length; i++){ 971 | keyauth_bytes.push(keyauth.charCodeAt(i)); 972 | } 973 | sha256(new Uint8Array(keyauth_bytes), function(hash, err){ 974 | if(err){ 975 | return fail(status, "Generating DNS data failed: " + err.message); 976 | } 977 | var dns_data = b64(hash); 978 | 979 | // populate dns option 980 | var dig = "dig +short @ns.yournameserver.com _acme-challenge." + d + " TXT"; 981 | template.querySelector(".dns_dig").innerHTML = ""; 982 | template.querySelector(".dns_dig").appendChild(document.createTextNode(dig)); 983 | template.querySelector(".dns_domain").innerHTML = ""; 984 | template.querySelector(".dns_domain").appendChild(document.createTextNode(d)); 985 | template.querySelector(".dns_value").innerHTML = ""; 986 | template.querySelector(".dns_value").appendChild(document.createTextNode(dns_data)); 987 | template.querySelector(".dns_subdomain").value = "_acme-challenge." + d; 988 | template.querySelector(".dns_data").value = dns_data; 989 | template.querySelector(".confirm_dns_submit").value = "I can see the TXT record for " + d; 990 | template.querySelector(".validate_dns_sig_submit").value = "Submit challenge for " + d; 991 | template.querySelector("label.challenge_dns").style.display = "inline"; 992 | 993 | // data attributes 994 | var challenge_url = AUTHORIZATIONS[auth_url]['dns_challenge_object']['url']; 995 | template.querySelector(".confirm_dns").dataset.option = "dns"; 996 | template.querySelector(".confirm_dns").dataset.section = section_id; 997 | template.querySelector(".confirm_dns").dataset.auth = auth_url; 998 | template.querySelector(".confirm_dns").dataset.challenge = challenge_url; 999 | 1000 | // auto-select Option 3 if no other options 1001 | if(AUTHORIZATIONS[auth_url]['python_challenge_object'] === undefined 1002 | && AUTHORIZATIONS[auth_url]['file_challenge_object'] === undefined){ 1003 | template.querySelector("input.challenge_python").removeAttribute("checked"); 1004 | template.querySelector("input.challenge_dns").setAttribute("checked", ""); 1005 | template.querySelector("label.challenge_dns").innerHTML = "Option 1 - DNS record (wildcard)"; 1006 | } 1007 | 1008 | // recurse if needed 1009 | document.getElementById("challenge_domains").appendChild(template); 1010 | if((n + 1) < ORDER['order_response']['authorizations'].length){ 1011 | return buildAuthorization(n + 1, callback); 1012 | } 1013 | else{ 1014 | return callback(); 1015 | } 1016 | }); 1017 | } 1018 | 1019 | // no DNS option, so recurse without hashing anything 1020 | else{ 1021 | document.getElementById("challenge_domains").appendChild(template); 1022 | if((n + 1) < ORDER['order_response']['authorizations'].length){ 1023 | return buildAuthorization(n + 1, callback); 1024 | } 1025 | else{ 1026 | return callback(); 1027 | } 1028 | } 1029 | }); 1030 | } 1031 | 1032 | // kickoff rendering authorization html 1033 | buildAuthorization(0, function(){ 1034 | 1035 | // show step 4 1036 | document.getElementById("step4").style.display = "block"; 1037 | document.getElementById("step4_pending").style.display = "none"; 1038 | 1039 | // complete step 3c 1040 | status.innerHTML = "Ordered! Proceed to Step 4!"; 1041 | }); 1042 | } 1043 | 1044 | // error registering 1045 | else{ 1046 | return fail(status, "Order failed. Please start back at Step 1. " + order_xhr.responseText); 1047 | } 1048 | } 1049 | }; 1050 | order_xhr.send(JSON.stringify({ 1051 | "protected": ORDER['order_protected_b64'], 1052 | "payload": ORDER['order_payload_b64'], 1053 | "signature": ORDER['order_sig'], 1054 | })); 1055 | } 1056 | 1057 | /* 1058 | * Step 4a: Confirm Challenge 1059 | */ 1060 | function confirmChallenge(e){ 1061 | e.preventDefault(); 1062 | 1063 | // find the relevant resources 1064 | var section_id = e.target.dataset.section // challenge_examplecom 1065 | var option = e.target.dataset.option; // "python", "file", or "dns" 1066 | var auth_url = e.target.dataset.auth; 1067 | var d = AUTHORIZATIONS[auth_url]['authorization']['identifier']['value']; 1068 | var challenge_url = e.target.dataset.challenge; 1069 | var section = document.getElementById(section_id); 1070 | var status = section.querySelector(".confirm_" + option + "_status"); 1071 | var validate_form = section.querySelector(".validate_" + option + "_sig"); 1072 | var validate_submit = section.querySelector(".validate_" + option + "_sig_submit"); 1073 | var validate_cmd = section.querySelector("." + option + "_sig_cmd"); 1074 | var validate_input = section.querySelector("." + option + "_sig"); 1075 | var validate_status_class = option + "_sig_status" 1076 | var validate_status = section.querySelector("." + validate_status_class); 1077 | 1078 | // clear previous status 1079 | status.style.display = "inline"; 1080 | status.className = ""; 1081 | status.innerHTML = "confirming..."; 1082 | 1083 | // hide following steps 1084 | document.getElementById("step5").style.display = "none"; 1085 | document.getElementById("step5_pending").style.display = "inline"; 1086 | 1087 | // reset validate challenge signature 1088 | validate_cmd.value = "waiting until confirmation is done..."; 1089 | validate_cmd.removeAttribute("readonly"); 1090 | validate_cmd.setAttribute("disabled", ""); 1091 | validate_input.value = ""; 1092 | validate_input.setAttribute("placeholder", "waiting until confirmation is done..."); 1093 | validate_input.setAttribute("disabled", ""); 1094 | validate_submit.setAttribute("disabled", ""); 1095 | validate_status.style.display = "none"; 1096 | validate_status.className = validate_status_class; 1097 | validate_status.innerHTML = ""; 1098 | 1099 | // reset finalize signature 1100 | document.getElementById("finalize_sig_cmd").value = "waiting until challenges are done..."; 1101 | document.getElementById("finalize_sig_cmd").removeAttribute("readonly"); 1102 | document.getElementById("finalize_sig_cmd").setAttribute("disabled", ""); 1103 | document.getElementById("finalize_sig").value = ""; 1104 | document.getElementById("finalize_sig").setAttribute("placeholder", "waiting until challenges are done..."); 1105 | document.getElementById("finalize_sig").setAttribute("disabled", ""); 1106 | document.getElementById("validate_finalize_sig").setAttribute("disabled", ""); 1107 | document.getElementById("validate_finalize_sig_status").style.display = "none"; 1108 | document.getElementById("validate_finalize_sig_status").className = ""; 1109 | document.getElementById("validate_finalize_sig_status").innerHTML = ""; 1110 | 1111 | // get nonce for challenge 1112 | getNonce(function(nonce, err){ 1113 | if(err){ 1114 | return fail(status, "Failed challenge nonce request (domain: " + d + ") (code: " + err.status + "). " + err.responseText); 1115 | } 1116 | 1117 | // populate challenge signature (payload is empty {}) 1118 | var protected_json = { 1119 | "url": challenge_url, 1120 | "alg": ACCOUNT['alg'], 1121 | "nonce": nonce, 1122 | "kid": ACCOUNT['account_uri'], 1123 | }; 1124 | var protected_b64 = b64(JSON.stringify(protected_json)); 1125 | AUTHORIZATIONS[auth_url][option + '_protected_json'] = protected_json 1126 | AUTHORIZATIONS[auth_url][option + '_protected_b64'] = protected_b64; 1127 | validate_cmd.value = "" + 1128 | "echo | set /p=\"" + protected_b64 + "." + b64(JSON.stringify({})) + "\" | " + 1129 | "openssl dgst -sha256 -hex -sign account.key"; 1130 | validate_cmd.setAttribute("readonly", ""); 1131 | validate_cmd.removeAttribute("disabled"); 1132 | validate_input.value = ""; 1133 | validate_input.setAttribute("placeholder", RESULT_PLACEHOLDER); 1134 | validate_input.removeAttribute("disabled"); 1135 | validate_submit.removeAttribute("disabled"); 1136 | 1137 | // set data properties so validateChallenge() knows which challenge this is 1138 | validate_form.dataset.option = option; 1139 | validate_form.dataset.section = section_id; 1140 | validate_form.dataset.auth = auth_url; 1141 | validate_form.dataset.challenge = challenge_url; 1142 | 1143 | // complete step 4a 1144 | status.innerHTML = "Ready for the next command!"; 1145 | }); 1146 | } 1147 | 1148 | /* 1149 | * Step 4b: Verify Ownership (POST /challenge['url'], ...) 1150 | */ 1151 | function validateChallenge(e){ 1152 | e.preventDefault(); 1153 | 1154 | // find the relevant resources 1155 | var section_id = e.target.dataset.section; // challenge_examplecom 1156 | var option = e.target.dataset.option; // "python", "file", or "dns" 1157 | var auth_url = e.target.dataset.auth; 1158 | var d = AUTHORIZATIONS[auth_url]['authorization']['identifier']['value']; 1159 | var challenge_url = e.target.dataset.challenge; 1160 | var section = document.getElementById(section_id); 1161 | var status_class = option + "_sig_status"; 1162 | var status = section.querySelector("." + status_class); 1163 | var sig_input = section.querySelector("." + option + "_sig"); 1164 | 1165 | // clear previous status 1166 | status.style.display = "inline"; 1167 | status.className = status_class; 1168 | status.innerHTML = "submitting..."; 1169 | 1170 | // hide following steps 1171 | document.getElementById("step5").style.display = "none"; 1172 | document.getElementById("step5_pending").style.display = "inline"; 1173 | 1174 | // reset finalize signature 1175 | document.getElementById("finalize_sig_cmd").value = "waiting until challenges are done..."; 1176 | document.getElementById("finalize_sig_cmd").removeAttribute("readonly"); 1177 | document.getElementById("finalize_sig_cmd").setAttribute("disabled", ""); 1178 | document.getElementById("finalize_sig").value = ""; 1179 | document.getElementById("finalize_sig").setAttribute("placeholder", "waiting until challenges are done..."); 1180 | document.getElementById("finalize_sig").setAttribute("disabled", ""); 1181 | document.getElementById("validate_finalize_sig").setAttribute("disabled", ""); 1182 | document.getElementById("validate_finalize_sig_status").style.display = "none"; 1183 | document.getElementById("validate_finalize_sig_status").className = ""; 1184 | document.getElementById("validate_finalize_sig_status").innerHTML = ""; 1185 | 1186 | // validate challenge protected exists 1187 | if(AUTHORIZATIONS[auth_url][option + '_protected_b64'] === undefined){ 1188 | return fail(status, "Update payload not found. Please go back to Step 1."); 1189 | } 1190 | 1191 | // validate the signature 1192 | var challenge_sig = hex2b64(sig_input.value); 1193 | if(challenge_sig === null){ 1194 | return fail(status, "You need to run the above commands and paste the output in the text boxes below each command."); 1195 | } 1196 | AUTHORIZATIONS[auth_url][option + '_challenge_sig'] = challenge_sig; 1197 | 1198 | // submit challenge to CA 1199 | var challenge_xhr = new XMLHttpRequest(); 1200 | challenge_xhr.open("POST", challenge_url); 1201 | challenge_xhr.setRequestHeader("Content-Type", "application/jose+json"); 1202 | challenge_xhr.onreadystatechange = function(){ 1203 | if(challenge_xhr.readyState === 4){ 1204 | 1205 | // successful challenge submission 1206 | if(challenge_xhr.status === 200){ 1207 | 1208 | // set challenge response 1209 | AUTHORIZATIONS[auth_url][option + '_challenge_response'] = JSON.parse(challenge_xhr.responseText); 1210 | 1211 | // poll to watch the authorization for status === "valid" 1212 | function checkAuthorization(){ 1213 | status.innerHTML = "checking..."; 1214 | 1215 | // poll authorization 1216 | getAuthorization(auth_url, function(auth_obj, err){ 1217 | 1218 | // authorization failed 1219 | if(err){ 1220 | return fail(status, "Authorization failed. Please start back at Step 1. " + err.responseText); 1221 | } 1222 | 1223 | // authorization still pending, so wait a second and check again 1224 | if(auth_obj['status'] === "pending"){ 1225 | status.innerHTML = "waiting..."; 1226 | window.setTimeout(checkAuthorization, 1000); 1227 | } 1228 | 1229 | // authorization valid 1230 | else if(auth_obj['status'] === "valid"){ 1231 | 1232 | // see if all the authorizations are valid 1233 | var all_valid = true; 1234 | for(var a_url in AUTHORIZATIONS){ 1235 | if(AUTHORIZATIONS[a_url]['authorization']['status'] !== "valid"){ 1236 | all_valid = false; 1237 | } 1238 | } 1239 | if(all_valid){ 1240 | 1241 | // get nonce for finalizing 1242 | getNonce(function(nonce, err){ 1243 | if(err){ 1244 | return fail(status, "Failed finalize nonce request (code: " + err.status + "). " + err.responseText); 1245 | } 1246 | 1247 | // populate order finalize signature (payload populated in validateCSR()) 1248 | ORDER['finalize_protected_json'] = { 1249 | "url": ORDER['finalize_uri'], 1250 | "alg": ACCOUNT['alg'], 1251 | "nonce": nonce, 1252 | "kid": ACCOUNT['account_uri'], 1253 | } 1254 | ORDER['finalize_protected_b64'] = b64(JSON.stringify(ORDER['finalize_protected_json'])); 1255 | document.getElementById("finalize_sig_cmd").value = "" + 1256 | "echo | set /p=\"" + ORDER['finalize_protected_b64'] + "." + ORDER['finalize_payload_b64'] + "\" | " + 1257 | "openssl dgst -sha256 -hex -sign account.key"; 1258 | document.getElementById("finalize_sig_cmd").setAttribute("readonly", ""); 1259 | document.getElementById("finalize_sig_cmd").removeAttribute("disabled"); 1260 | document.getElementById("finalize_sig").value = ""; 1261 | document.getElementById("finalize_sig").setAttribute("placeholder", RESULT_PLACEHOLDER); 1262 | document.getElementById("finalize_sig").removeAttribute("disabled"); 1263 | document.getElementById("validate_finalize_sig").removeAttribute("disabled"); 1264 | 1265 | // proceed finalize order 1266 | status.innerHTML = "Domain verified! Go to next command."; 1267 | }); 1268 | } 1269 | 1270 | // proceed to next authorization 1271 | else{ 1272 | status.innerHTML = "Domain verified! Go to next command."; 1273 | } 1274 | } 1275 | 1276 | // authorization failed 1277 | else{ 1278 | return fail(status, "Domain challenge failed. Please start back at Step 1. " + JSON.stringify(auth_obj)); 1279 | } 1280 | }); 1281 | } 1282 | // start polling authorization 1283 | checkAuthorization(); 1284 | } 1285 | 1286 | // error submitting challenge 1287 | else{ 1288 | return fail(status, "Challenge submission failed. Please start back at Step 1. " + challenge_xhr.responseText); 1289 | } 1290 | } 1291 | }; 1292 | challenge_xhr.send(JSON.stringify({ 1293 | "protected": AUTHORIZATIONS[auth_url][option + '_protected_b64'], 1294 | "payload": b64(JSON.stringify({})), // always empty payload 1295 | "signature": AUTHORIZATIONS[auth_url][option + '_challenge_sig'], 1296 | })); 1297 | } 1298 | 1299 | /* 1300 | * Step 4c: Issue Certificate (POST /order['finalize']) 1301 | */ 1302 | function validateFinalize(e){ 1303 | e.preventDefault(); 1304 | 1305 | // clear previous status 1306 | var status = document.getElementById("validate_finalize_sig_status"); 1307 | status.style.display = "inline"; 1308 | status.className = ""; 1309 | status.innerHTML = "finalizing..."; 1310 | 1311 | // hide following steps 1312 | document.getElementById("step5").style.display = "none"; 1313 | document.getElementById("step5_pending").style.display = "inline"; 1314 | 1315 | // validate update payload exists 1316 | if(ORDER['finalize_protected_b64'] === undefined){ 1317 | return fail(status, "Finalize payload not found. Please go back to Step 1."); 1318 | } 1319 | 1320 | // validate the signature 1321 | var finalize_sig = hex2b64(document.getElementById("finalize_sig").value); 1322 | if(finalize_sig === null){ 1323 | return fail(status, "You need to run the above commands and paste the output in the text boxes below each command."); 1324 | } 1325 | ORDER['finalize_sig'] = finalize_sig; 1326 | 1327 | // send update request to CA account_uri 1328 | var finalize_xhr = new XMLHttpRequest(); 1329 | finalize_xhr.open("POST", ORDER['finalize_uri']); 1330 | finalize_xhr.setRequestHeader("Content-Type", "application/jose+json"); 1331 | finalize_xhr.onreadystatechange = function(){ 1332 | if(finalize_xhr.readyState === 4){ 1333 | 1334 | // successful update 1335 | if(finalize_xhr.status === 200){ 1336 | 1337 | // set finalize response 1338 | ORDER['finalize_response'] = JSON.parse(finalize_xhr.responseText); 1339 | 1340 | // poll to watch the order for status === "valid" 1341 | function checkForCert(){ 1342 | status.innerHTML = "checking..."; 1343 | 1344 | // poll order for certificate 1345 | var poll_cert_xhr = new XMLHttpRequest(); 1346 | poll_cert_xhr.open("GET", ORDER['order_uri'] + "?" + cachebuster()); 1347 | poll_cert_xhr.onload = function(){ 1348 | var order = JSON.parse(poll_cert_xhr.responseText) 1349 | ORDER['order_response'] = order; 1350 | 1351 | // order still processing 1352 | if(order['status'] === "pending" || order['status'] === "processing" || order['status'] === "ready"){ 1353 | status.innerHTML = "processing..."; 1354 | window.setTimeout(checkForCert, 1000); 1355 | } 1356 | 1357 | // order is ready for finalizing 1358 | else if(order['status'] === "valid"){ 1359 | 1360 | // no certificate url 1361 | if(order['certificate'] === undefined){ 1362 | return fail(status, "Certificate not provided. Please start back at Step 1. " + poll_cert_xhr.responseText); 1363 | } 1364 | 1365 | // get certificate 1366 | var cert_xhr = new XMLHttpRequest(); 1367 | cert_xhr.open("GET", order['certificate']); 1368 | cert_xhr.onload = function(){ 1369 | 1370 | // format cert into PEM format 1371 | document.getElementById("crt").value = cert_xhr.responseText; 1372 | 1373 | // proceed step 5 1374 | document.getElementById("step5").style.display = "block"; 1375 | document.getElementById("step5_pending").style.display = "none"; 1376 | status.innerHTML = "Certificate signed! Proceed to next step."; 1377 | 1378 | // alert when navigating away 1379 | window.onbeforeunload = function(){ 1380 | return "Be sure to save your signed certificate! " + 1381 | "It will be lost if you navigate away from this " + 1382 | "page before saving it, and you might not be able " + 1383 | "to get another one issued!"; 1384 | }; 1385 | }; 1386 | 1387 | // certificate download error 1388 | cert_xhr.onerror = function(){ 1389 | return fail(status, "Order request failed. Please start back at Step 1. " + cert_xhr.responseText); 1390 | }; 1391 | cert_xhr.send(); 1392 | } 1393 | 1394 | // order invalid 1395 | else{ 1396 | return fail(status, "Order processing failed. Please start back at Step 1. " + poll_cert_xhr.responseText); 1397 | } 1398 | }; 1399 | 1400 | // order poll error 1401 | poll_cert_xhr.onerror = function(){ 1402 | return fail(status, "Order request failed. Please start back at Step 1. " + poll_cert_xhr.responseText); 1403 | }; 1404 | poll_cert_xhr.send(); 1405 | } 1406 | // start polling order 1407 | checkForCert(); 1408 | } 1409 | 1410 | // error finalizing 1411 | else{ 1412 | return fail(status, "Finalizing failed. Please start back at Step 1. " + finalize_xhr.responseText); 1413 | } 1414 | } 1415 | }; 1416 | finalize_xhr.send(JSON.stringify({ 1417 | "protected": ORDER['finalize_protected_b64'], 1418 | "payload": ORDER['finalize_payload_b64'], 1419 | "signature": ORDER['finalize_sig'], 1420 | })); 1421 | } 1422 | 1423 | --------------------------------------------------------------------------------