diafygi / acme-nosudo Goto Github PK
View Code? Open in Web Editor NEWFree HTTPS certificates without having to trust the letsencrypt cli with sudo/root
License: GNU Affero General Public License v3.0
Free HTTPS certificates without having to trust the letsencrypt cli with sudo/root
License: GNU Affero General Public License v3.0
This is extremely annoying. 😠 I had a validation for 4 domains running and was running the wrong python server for one of them. Now I need to start everything from scratch, which includes temporarily taking down the proper webserver.
It would be really great if your script offered to retry in case the validation failed.
...
Requesting signature...
Error: csr_data:
POST https://acme-v01.api.letsencrypt.org/acme/new-cert
{
"header": {
"alg": "RS256",
"jwk": {
"e": "AQAB",
"kty": "RSA",
"n": "SNIP"
}
},
"payload": "SNIP",
"protected": "SNIP",
"signature": "SNIP"
}
{"type":"urn:acme:error:malformed","detail":"Error unmarshaling certificate request","status":400}
Traceback (most recent call last):
File "./sign_csr.py", line 441, in <module>
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based)
File "./sign_csr.py", line 386, in sign_csr
resp = urllib2.urlopen(csr_url, csr_data)
File "/usr/lib/python2.7/urllib2.py", line 127, in urlopen
return _opener.open(url, data, timeout)
File "/usr/lib/python2.7/urllib2.py", line 410, in open
response = meth(req, response)
File "/usr/lib/python2.7/urllib2.py", line 523, in http_response
'http', request, response, code, msg, hdrs)
File "/usr/lib/python2.7/urllib2.py", line 448, in error
return self._call_chain(*args)
File "/usr/lib/python2.7/urllib2.py", line 382, in _call_chain
result = func(*args)
File "/usr/lib/python2.7/urllib2.py", line 531, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 400: Bad Request
Press Enter when you've run the above commands in a new terminal window...
STEP 4: You need to run this command on domain.com (don't stop the python command until the next step).
sudo python -c "import BaseHTTPServer; \
h = BaseHTTPServer.BaseHTTPRequestHandler; \
h.do_GET = lambda r: r.send_response(200) or r.end_headers() or r.wfile.write('cu9iNGw0OyYzNJen0qLKhS8nXfYx9GRgt_YJZVfthZ8.mOKrxpIJxDEEW190JjtXbUZ385PCg0U_xpRJbQZCHq4'); \
s = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), h); \
s.serve_forever()"
Press Enter when you've got the python command running on your server...
Requesting verification for domain.com...
Waiting for domain.com challenge to pass...
Traceback (most recent call last):
File "sign_csr.py", line 441, in <module>
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based)
File "sign_csr.py", line 372, in sign_csr
raise KeyError("'{0}' challenge did not pass: {1}".format(i['domain'], challenge_status))
KeyError: "'domain.com' challenge did not pass: {u'status': u'invalid', u'validationRecord': [{u'url': u'domain.com/.well-known/acme-challenge/cu9iNGw0OyYzNJen0qLKhS8nXfYx9GRgt_YJZVfthZ8', u'hostname': u'domain.com', u'addressUsed': u'52.30.135.131', u'port': u'80', u'addressesResolved': [u'52.30.135.131', u'52.19.83.189']}], u'keyAuthorization': u'cu9iNGw0OyYzNJen0qLKhS8nXfYx9GRgt_YJZVfthZ8.mOKrxpIJxDEEW190JjtXbUZ385PCg0U_xpRJbQZCHq4', u'uri': u'https://acme-v01.api.letsencrypt.org/acme/challenge/LFDuAzyge5nTj6Gn79vjIM4mrL0oGXxbAUCzJhiUjuc/1814396', u'token': u'cu9iNGw0OyYzNJen0qLKhS8nXfYx9GRgt_YJZVfthZ8', u'error': {u'type': u'urn:acme:error:unauthorized', u'detail': u'Invalid response from http://domain.com/.well-known/acme-challenge/cu9iNGw0OyYzNJen0qLKhS8nXfYx9GRgt_YJZVfthZ8 [52.30.135.131]: 503'}, u'type': u'http-01'}"
This is the error I get, you notice the IP addresses vary presumably because it's connecting to the load balancer which is sitting in front of the Python script listening on port 80.
Is there a way around that or do I have to temporarily mess with my DNS records in order to return a valid IP?
(PS. Not sure if this is the correct place to file this issue, perhaps the main letsencrypt repo would be better)
Thanks for writing this script, I like that one can read through it quickly and it doesn't contain code to change my configs. (That does not mean, that the official client is bad - it is just not as suited for power users as your script is.)
Now I have Let's Encrypt with Hiawatha working 👍
Here's a quick tutorial I wrote for other Hiawatha users. The only real difference is in creating the final certificate file, which also contains domain.key
:
cat domain.key signed.crt lets-encryt-x1-cross-signed.pem > letsencrypt_hiawatha.crt
Thanks for your work @diafygi !
I followed all the example steps to test and the outcome is:
$ python sign_csr.py --public-key user.pub domain.csr > domain.crt
Traceback (most recent call last):
File "sign_csr.py", line 441, in
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based)
File "sign_csr.py", line 26, in sign_csr
nonce_req = urllib2.Request("{}/directory".format(CA))
ValueError: zero length field name in format
Following the instructions with generating user key and pub as well as domain key and csr I got this error when invoking your script:
Reading pubkey file...
Traceback (most recent call last):
File "sign_csr.py", line 429, in <module>
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email)
File "sign_csr.py", line 39, in sign_csr
pub_hex, pub_exp = re.search("Modulus\:\s+00:([a-f0-9\:\s]+?)Exponent\: ([0-9]+)", out, re.MULTILINE|re.DOTALL).groups()
AttributeError: 'NoneType' object has no attribute 'groups'
I am on Mac OS X 10.10.5 with Python 2.7.10. I tried re-generating user / domain keys several times... Any ideas?
letsencrypt-nosudo
was added to Alpine Linux today.
If you can create a release
in the future it will help me track updates with Sibbel so I can keep the letsencrypt-nosudo
package updated in Alpine.
To install letsencrypt-nosudo
in an Alpine LXC container you just enable the testing
repo & :
apk add letsencrypt-nosudo
.
I get the following errors when using sign_csr.py, that I think I can isolate to bad gateways on servers that this tool uses:
just was able to do my 1st test after LE went public.
When teh script is generating the CSR it fails
Requesting signature...
Error: csr_data:
POST https://acme-v01.api.letsencrypt.org/acme/new-cert
{
"header": {
"alg": "RS256",
"jwk": {
"e": "AQAB",
"kty": "RSA",
encoded data
{"type":"urn:acme:error:malformed","detail":"Error unmarshaling certificate request","status":400}
Traceback (most recent call last):
File "letsencrypt-nosudo-master/sign_csr.py", line 441, in
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based)
File "letsencrypt-nosudo-master/sign_csr.py", line 386, in sign_csr
resp = urllib2.urlopen(csr_url, csr_data)
File "/usr/lib/python2.7/urllib2.py", line 127, in urlopen
return _opener.open(url, data, timeout)
File "/usr/lib/python2.7/urllib2.py", line 407, in open
response = meth(req, response)
File "/usr/lib/python2.7/urllib2.py", line 520, in http_response
'http', request, response, code, msg, hdrs)
File "/usr/lib/python2.7/urllib2.py", line 445, in error
return self._call_chain(_args)
File "/usr/lib/python2.7/urllib2.py", line 379, in _call_chain
result = func(_args)
File "/usr/lib/python2.7/urllib2.py", line 528, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 400: Bad Request
Thanks for this - I was able to get running in about ten minutes on FreeBSD shared hosting. :)
I made a few tweaks, and I'm hoping to get approval to publish them from my employer.
It'd be nice to have alternatives to HTTPServer - for example, offering commands like:
echo -n "{\"header\": {\"alg\": \"RS256\"},...}" > /var/www/.well-known/acme-challenge/qlWnoQpvBvNvOyZlAGLvsD
would work for most people (and doesn't require stopping your HTTP server.)
At first , i encountered some...
Value Error: Zero length field name in format
...errors (for example in line 21 / 44).
After fixing all that, i got this one:
Reading pubkey file...
Traceback (most recent call last):
File "sign_csr.py", line 429, in
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email)
File "sign_csr.py", line 45, in sign_csr
pub_exp = binascii.unhexlify(pub_exp)
TypeError: Odd-length string
Where is the problem, i don't get it! I also tried pub_exp.strip() to fix it, but it didn't work out that fine!
(I followed your tutorial for creating all certificates!)
What follows are the frustrations I had while setting up letsencrypt using your script, which I realize are my own fault. I suggest clarifying in your README the confusions I had in order to help the next poor soul with more ambition than brains in getting set up with letsencrypt :)
While using your tool I didn't realize I needed to append the cross-signed certificate to my certificate for the nginx config, and by the time I realized I needed that step, I had a hard time finding the cross-signed certificate. I wasn't even sure if the cross-signed certificate was something I needed to download or generate unique to my certificate (because I'm a TLS newbie).
I suggest a quick mention that the signed crt file will work in the future when all browsers trust the letsencrypt root, but currently one needs to grab the pem file labeled "Let’s Encrypt Authority X1 (IdenTrust cross-signed)" from https://letsencrypt.org/certificates/ and cat it to the back of the signed crt file. The reason I figured this out is because of the HN post from lolware has a link to the certificates page (sad aside: I had looked at that letsencrypt certificates page so many times and somehow missed that battery of useful pem files at the top).
Thanks for the useful tool. I think you did a great job and soem added notes will hopefully make some people's use of it go more smoothly.
The only problem was tempfile.NamedTemporaryFile() opens file with non-shared access, so openssl can neither read .json nor write to .sig files.
Quick fix was to close file handle and reopen it before seek(0) so openssl can do its job
openssl errors
.sig
openssl dgst Error opening output file system library:fopen:Broken pipe:
.json
register_bpl9wi.json: Permission denied
Since letsencrypt-nosudo already requires network access it seems like step 4-N (when HTTPServer is run) could trivially validate that the correct challenge is being served and give better feedback / give the user a chance to fix things.
Common-ish errors off the top of my head:
I'm following the readme and when I start up the python webserver this happens (There's a bunch of cryptology stuff here that I don't understand so I replaced it all with varying numbers of n's):
Press Enter when you've got the python command running on your server...
Requesting verification for my.totallyawesomesite.com...
Error: test_data:
POST https://acme-v01.api.letsencrypt.org/acme/challenge/nnnnnnnnnnnnnnnnnnnnnnnnn…
{
"header": {
"alg": "RS256",
"jwk": {
"e": "nnnn",
"kty": "RSA",
"n": "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn…"
}
},
"payload": "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn…",
"protected": "nnnnnnnnnnnnnnnnnnnnnnnnnnnnn…",
"signature": ""
}
{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: JWS verification error","status":400}
Traceback (most recent call last):
File "sign_csr.py", line 441, in <module>
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based)
File "sign_csr.py", line 341, in sign_csr
resp = urllib2.urlopen(test_url, test_data)
File "/usr/lib64/python2.6/urllib2.py", line 126, in urlopen
return _opener.open(url, data, timeout)
File "/usr/lib64/python2.6/urllib2.py", line 397, in open
response = meth(req, response)
File "/usr/lib64/python2.6/urllib2.py", line 510, in http_response
'http', request, response, code, msg, hdrs)
File "/usr/lib64/python2.6/urllib2.py", line 435, in error
return self._call_chain(*args)
File "/usr/lib64/python2.6/urllib2.py", line 369, in _call_chain
result = func(*args)
File "/usr/lib64/python2.6/urllib2.py", line 518, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 400: Bad Request
Is the signature supposed to be blank?
Is there any other way executing the challenge passing?
Provided that I have no sudo access, and that's the reason I'm here in first place, it's highly unlikely that the STEP 4 command which is the domain challenge step will work.
Please suggest.
Thanks in advance for the help.
"This Connection is Untrusted
You have asked ******** to connect securely to letsencrypt.daylightpirates.org, but we can't confirm that your connection is secure.
Normally, when you try to connect securely, sites will present trusted identification to prove that you are going to the right place. However, this site's identity can't be verified.
What Should I Do?
If you usually connect to this site without problems, this error could mean that someone is trying to impersonate the site, and you shouldn't continue."
DISCLAIMER:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
</IfModule>
This feature is currently turned OFF, but it takes a few hours to reload the settings, hence the redirect is still in place :(
nines9@magnesium:~/letsencrypt-nosudo$ ls -lha
total 92K
drwxr-xr-x 3 nines9 users 4.0K Nov 19 19:57 .
drwx---r-x 9 nines9 users 4.0K Nov 19 19:57 ..
drwxr-xr-x 8 nines9 users 4.0K Nov 19 19:57 .git
-rw-r--r-- 1 nines9 users 675 Nov 19 19:57 .gitignore
-rw-r--r-- 1 nines9 users 34K Nov 19 19:57 LICENSE
-rw-r--r-- 1 nines9 users 17K Nov 19 19:57 README.md
-rw-r--r-- 1 nines9 users 17K Nov 19 19:57 sign_csr.py
First command (terminal 1):
nines9@magnesium:~/letsencrypt-nosudo$ openssl genrsa 4096 > user.key
Generating RSA private key, 4096 bit long modulus
.................................++
.................................................................................................................................................................++
e is 65537 (0x10001)
Second command (terminal 1):
nines9@magnesium:~/letsencrypt-nosudo$ openssl rsa -in user.key -pubout > user.pub
writing RSA key
Third command (terminal 1):
nines9@magnesium:~/letsencrypt-nosudo$ #Alternatively, I want both 9nines.net and www.9nines.net
nines9@magnesium:~/letsencrypt-nosudo$ openssl genrsa 4096 > domain.key
Generating RSA private key, 4096 bit long modulus
.......................................................................................................................................................................................++
.............................................................................++
e is 65537 (0x10001)
nines9@magnesium:~/letsencrypt-nosudo$ openssl req -new -sha256 -key domain.key -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:9nines.net,DNS:www.9nines.net")) > domain.csr
Fourth command: Terminal 1, switching to Terminal 2
nines9@magnesium:~/letsencrypt-nosudo$ python sign_csr.py --public-key user.pub domain.csr > signed.crt
Reading pubkey file...
Found public key!
Reading csr file...
Found domains www.9nines.net, 9nines.net
STEP 1: What is your contact email? ([email protected])
Building request payloads...
Building request for www.9nines.net...
Building request for 9nines.net...
Building request for CSR...
STEP 2: You need to sign some files (replace 'user.key' with your user private key).
openssl dgst -sha256 -sign user.key -out register_gxKP4_.sig register_fFw75_.json
openssl dgst -sha256 -sign user.key -out domain_l3TNrL.sig domain_b0O1LU.json
openssl dgst -sha256 -sign user.key -out domain_VCB1zQ.sig domain_mjx5UI.json
openssl dgst -sha256 -sign user.key -out cert_J4eZU3.sig cert_WrHIam.json
Press Enter when you've run the above commands in a new terminal window...
First command: Terminal 2
nines9@magnesium:~/letsencrypt-nosudo$ openssl dgst -sha256 -sign user.key -out register_gxKP4_.sig register_fFw75_.json
Second command: Terminal 2
nines9@magnesium:~/letsencrypt-nosudo$ openssl dgst -sha256 -sign user.key -out domain_l3TNrL.sig domain_b0O1LU.json
Third command: Terminal 2
nines9@magnesium:~/letsencrypt-nosudo$ openssl dgst -sha256 -sign user.key -out domain_VCB1zQ.sig domain_mjx5UI.json
Fourth command: Terminal 2
nines9@magnesium:~/letsencrypt-nosudo$ openssl dgst -sha256 -sign user.key -out cert_J4eZU3.sig cert_WrHIam.json
Sixt command: Terminal 1
Registering [email protected]...
Requesting challenges for www.9nines.net...
Building challenge responses for www.9nines.net...
Requesting challenges for 9nines.net...
Building challenge responses for 9nines.net...
STEP 3: You need to sign some more files (replace 'user.key' with your user private key).
openssl dgst -sha256 -sign user.key -out challenge_fzPMHb.sig challenge_9hvDtS.json
openssl dgst -sha256 -sign user.key -out challenge_ahkiEM.sig challenge_tN2nj0.json
Press Enter when you've run the above commands in a new terminal window...
Fifth command: Terminal 2
nines9@magnesium:~/letsencrypt-nosudo$ openssl dgst -sha256 -sign user.key -out challenge_fzPMHb.sig challenge_9hvDtS.json
Sixt command: Terminal 2
nines9@magnesium:~/letsencrypt-nosudo$ openssl dgst -sha256 -sign user.key -out challenge_ahkiEM.sig challenge_tN2nj0.json
Seventh command: Terminal 1
STEP 4: You need to run this command on www.9nines.net (don't stop the python command until the next step).
sudo python -c "import BaseHTTPServer; \
h = BaseHTTPServer.BaseHTTPRequestHandler; \
h.do_GET = lambda r: r.send_response(200) or r.end_headers() or r.wfile.write('vcDQcSgQoxUI8ZBMkpZzAwnaNefIEQp5grxEtd-O-no.OFd93iap7pzVH0u9CGS6BL7qGaCscvDcA3uyxt-WO7M'); \
s = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), h); \
s.serve_forever()"
Press Enter when you've got the python command running on your server...
Eight command: Terminal 1
Requesting verification for www.9nines.net...
Waiting for www.9nines.net challenge to pass...
Traceback (most recent call last):
File "sign_csr.py", line 415, in <module>
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email)
File "sign_csr.py", line 347, in sign_csr
raise KeyError("'{}' challenge did not pass: {}".format(i['domain'], challenge_status))
KeyError: "'www.9nines.net' challenge did not pass: {u'status': u'invalid', u'validationRecord': [{u'url': u'http://www.9nines.net/.well-known/acme-challenge/vcDQcSgQoxUI8ZBMkpZzAwnaNefIEQp5grxEtd-O-no', u'hostname': u'www.9nines.net', u'addressUsed': u'64.62.186.8', u'port': u'80', u'addressesResolved': [u'64.62.186.8']}, {u'url': u'https://www.9nines.net/.well-known/acme-challenge/vcDQcSgQoxUI8ZBMkpZzAwnaNefIEQp5grxEtd-O-no', u'hostname': u'www.9nines.net', u'addressUsed': u'64.62.186.8', u'port': u'443', u'addressesResolved': [u'64.62.186.8']}], u'keyAuthorization': u'vcDQcSgQoxUI8ZBMkpZzAwnaNefIEQp5grxEtd-O-no.OFd93iap7pzVH0u9CGS6BL7qGaCscvDcA3uyxt-WO7M', u'uri': u'https://acme-v01.api.letsencrypt.org/acme/challenge/ex_fOS2M1_l-3RY5-kZHgnKd6ZxBaXaHW5HcLkJX7XE/435535', u'token': u'vcDQcSgQoxUI8ZBMkpZzAwnaNefIEQp5grxEtd-O-no', u'error': {u'type': u'urn:acme:error:unauthorized', u'detail': u'Invalid response from http://www.9nines.net/.well-known/acme-challenge/vcDQcSgQoxUI8ZBMkpZzAwnaNefIEQp5grxEtd-O-no [64.62.186.8]: 404'}, u'type': u'http-01'}"
That's it.
OUTPUT of the directory, Terminal 1:
nines9@magnesium:~/letsencrypt-nosudo$ ls -lha
total 108K
drwxr-xr-x 3 nines9 users 4.0K Nov 19 20:31 .
drwx---r-x 9 nines9 users 4.0K Nov 19 19:57 ..
-rw-r--r-- 1 nines9 users 1.6K Nov 19 20:10 domain.csr
-rw-r--r-- 1 nines9 users 3.2K Nov 19 20:09 domain.key
drwxr-xr-x 8 nines9 users 4.0K Nov 19 19:57 .git
-rw-r--r-- 1 nines9 users 675 Nov 19 19:57 .gitignore
-rw-r--r-- 1 nines9 users 34K Nov 19 19:57 LICENSE
-rw-r--r-- 1 nines9 users 17K Nov 19 19:57 README.md
-rw-r--r-- 1 nines9 users 17K Nov 19 19:57 sign_csr.py
-rw-r--r-- 1 nines9 users 0 Nov 19 20:31 signed.crt
-rw-r--r-- 1 nines9 users 3.2K Nov 19 20:02 user.key
-rw-r--r-- 1 nines9 users 800 Nov 19 20:02 user.pub
I tryed this 2 times:
a) with www.9nines.net and 9nines.net (in the e-mail they confirmed both, but I tryed again)
b) with 9nines.net only. Same result.
I'm not completely sure as I'm not whitelisted I couldn't test it. I believe when using the file-based challenge line 308
.format(n + 4, i['domain'], challenge['token'], responses[n]['data']))
will fail because challenge
is not available in that context.
Thanks for the very useful script.
Redirected from https://gist.github.com/kennwhite/aa74c164bcdf092a7a10. Tried on Debian Wheezy host.
Everything went alright except at STEP 4 running in the webserver.
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/lib/python2.7/SocketServer.py", line 419, in __init__
self.server_bind()
File "/usr/lib/python2.7/BaseHTTPServer.py", line 108, in server_bind
SocketServer.TCPServer.server_bind(self)
File "/usr/lib/python2.7/SocketServer.py", line 430, in server_bind
self.socket.bind(self.server_address)
File "/usr/lib/python2.7/socket.py", line 224, in meth
return getattr(self._sock,name)(*args)
socket.error: [Errno 98] Address already in use
Tried to revoke without success.
$ python revoke_crt.py --public-key user.pub domain.crt'
Reading pubkey file...
Found public key!
...
Traceback (most recent call last):
File "revoke_crt.py", line 136, in <module>
revoke_crt(args.public_key, args.crt_path)
File "revoke_crt.py", line 97, in revoke_crt
resp = urllib2.urlopen("{0}/acme/revoke-cert".format(CA), crt_data)
File "/usr/lib/python2.7/urllib2.py", line 127, in urlopen
return _opener.open(url, data, timeout)
File "/usr/lib/python2.7/urllib2.py", line 410, in open
response = meth(req, response)
File "/usr/lib/python2.7/urllib2.py", line 523, in http_response
'http', request, response, code, msg, hdrs)
File "/usr/lib/python2.7/urllib2.py", line 448, in error
return self._call_chain(*args)
File "/usr/lib/python2.7/urllib2.py", line 382, in _call_chain
result = func(*args)
File "/usr/lib/python2.7/urllib2.py", line 531, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 400: Bad Request
Guys,
Anyway to use “user registration” files created by “LE official client”?
I’d do it myself, but my python knowledge is not good enough to dig through official client script...
I’ll appreciate, if you can show an easy way to do it :)
Hello, thank you for your work.
Here is some code taht i use to automate user keys and domain ones.
Hope it will help.
"""
import argparse, subprocess, json, os, urllib2, sys, base64, binascii, time,
hashlib, tempfile, re, copy, textwrap, errno, shlex
def sign_csr(domain_to_crt, email, ssl_directory, server_path, gen_keys):
"""Use the ACME protocol to get an ssl certificate signed by a
certificate authority.
:param string domain_to_crt: Domain to certificate.
:param string email: user account contact email
:param string ssl_directory: path to write keys and certificates
:param string server_path: Public path to write challenge files
:param bool gen_keys: Boolean True to generate User and domain keys
if not generating names must be user.key, user.pub, domain.key, domain.csr
in ssl_directory
:returns: Signed Certificate (PEM format)
:rtype: string
"""
#CA = "https://acme-staging.api.letsencrypt.org"
CA = "https://acme-v01.api.letsencrypt.org"
TERMS = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
nonce_req = urllib2.Request("{0}/directory".format(CA))
nonce_req.get_method = lambda : 'HEAD'
#cert info modify to match your domain specs
# for '/CN=domain.com/O=orga LTD/C=ES/'
my_domain_CN = domain_to_crt
my_domain_C = "XX" # country XX
my_domain_O = "My Company"
my_domain_ST = "province"
my_domain_L = "Locality"
my_subj = "/CN={0}/O={1}/C={2}/ST={3}/L={4}".format(my_domain_CN,my_domain_O,my_domain_C,my_domain_ST,my_domain_L)
#user keys names
user_priv = "user.key"
user_pub = "user.pub"
user_priv = "{0}/{1}".format(ssl_directory, user_priv)
user_pub = "{0}/{1}".format(ssl_directory, user_pub)
#domain keys
domain_priv = "domain.key"
domain_csr = "domain.csr"
domain_priv = "{0}/{1}".format(ssl_directory, domain_priv)
domain_csr = "{0}/{1}".format(ssl_directory, domain_csr)
#report
report_file="report.log"
def _execute_command(command_line):
sys.stderr.write("Executing {0}\n".format(command_line))
args = shlex.split(command_line)
proc = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE);
out, err = proc.communicate()
if proc.returncode != 0:
_to_report("Error in {0} command".format(command_line))
raise IOError("Error executing #{0}# \n".format(command_line))
else:
_to_report("Command #{0}# OK!".format(command_line))
return 0
def _make_sure_path_exists(path):
try:
os.makedirs(path)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise IOError("Error during path generation ERROR:{0} \n".format(exception.errno))
def _init_directorys_and_files():
#ssl dir
_make_sure_path_exists(ssl_directory)
#report file
target = open("{0}/{1}".format(ssl_directory,report_file), 'a+')
target.write("Init Dirs and files: ok \n")
target.close()
return 0
def _to_report(text):
target = open("{0}/{1}".format(ssl_directory,report_file), 'a+')
target.write(text)
target.write("\n")
target.close()
return 0
def _user_keys(user_priv, user_pub):
"Initial User Keys with open ssl"
sys.stderr.write("User keys generation \n")
command_line = "openssl genrsa -out {0} 4096".format(user_priv)
args = shlex.split(command_line)
proc = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE);
out, err = proc.communicate()
if proc.returncode != 0:
_to_report("Error in private key generation")
raise IOError("Error generating private key {0} \n".format(user_priv))
else:
_to_report("Generation of user private key -{0}- OK!".format(user_priv))
command_line = "openssl rsa -out {0} -in {1} -pubout".format(user_pub, user_priv)
args = shlex.split(command_line)
proc = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE);
out, err = proc.communicate()
if proc.returncode != 0:
_to_report("Error in public key generation")
raise IOError("Error generating public key {0} \n".format(user_pub))
else:
_to_report("Generation of user public key -{0}- OK!".format(user_pub))
return 0
def _domain_keys(domain_priv, domain_csr):
"Initial Domain Private Key with open ssl"
sys.stderr.write("Domain keys generation \n")
command_line = "openssl genrsa -out {0} 4096".format(domain_priv)
args = shlex.split(command_line)
proc = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE);
out, err = proc.communicate()
if proc.returncode != 0:
_to_report("Error in private key generation")
raise IOError("Error generating private key {0} \n".format(domain_priv))
else:
_to_report("Generation of user private key -{0}- OK!".format(domain_priv))
command_line = ("openssl req -new -sha256 -key {0} -out {1} "
"-subj '{2}'").format(domain_priv, domain_csr,my_subj)
args = shlex.split(command_line)
proc = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE);
out, err = proc.communicate()
if proc.returncode != 0:
_to_report("Error in domain CSR generation")
raise IOError("Error generating domain CSR {0} \n".format(domain_csr))
else:
_to_report("Generation of domain CSR -{0}- OK!".format(domain_csr))
return 0
def _b64(b):
"Shortcut function to go from bytes to jwt base64 string"
return base64.urlsafe_b64encode(b).replace("=", "")
if gen_keys:
# Step 0: gen keys
_init_directorys_and_files()
_user_keys(user_priv, user_pub)
_domain_keys(domain_priv, domain_csr)
sys.stderr.write("CHECK ? \n")
else:
sys.stderr.write("NOT YET \n")
raise
# Step 1: Get account public key
sys.stderr.write("Reading User public Key file...\n")
proc = subprocess.Popen(["openssl", "rsa", "-pubin", "-in", user_pub, "-noout", "-text"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if proc.returncode != 0:
raise IOError("Error loading {0}".format(user_pub))
pub_hex, pub_exp = re.search(
"Modulus(?: \((?:2048|4096) bit\)|)\:\s+00:([a-f0-9\:\s]+?)Exponent\: ([0-9]+)",
out, re.MULTILINE|re.DOTALL).groups()
pub_mod = binascii.unhexlify(re.sub("(\s|:)", "", pub_hex))
pub_mod64 = _b64(pub_mod)
pub_exp = int(pub_exp)
pub_exp = "{0:x}".format(pub_exp)
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
pub_exp = binascii.unhexlify(pub_exp)
pub_exp64 = _b64(pub_exp)
header = {
"alg": "RS256",
"jwk": {
"e": pub_exp64,
"kty": "RSA",
"n": pub_mod64,
},
}
accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':'))
thumbprint = _b64(hashlib.sha256(accountkey_json).digest())
sys.stderr.write("Found public key!\n")
# Step 2: Get the domain names to be certified
sys.stderr.write("Reading csr file...\n")
proc = subprocess.Popen(["openssl", "req", "-in", domain_csr, "-noout", "-text"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if proc.returncode != 0:
raise IOError("Error loading {0}".format(domain_csr))
domains = set([])
common_name = re.search("Subject:.*? CN=([^\s,;/]+)", out)
if common_name is not None:
domains.add(common_name.group(1))
subject_alt_names = re.search("X509v3 Subject Alternative Name: \n +([^\n]+)\n", out, re.MULTILINE|re.DOTALL)
if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "):
if san.startswith("DNS:"):
domains.add(san[4:])
sys.stderr.write("Found domains {0}\n".format(", ".join(domains)))
# Step 3: contact email
# nothing just from cli
# Step 4: Generate the payloads that need to be signed
# registration
sys.stderr.write("Building request payloads...\n")
reg_nonce = urllib2.urlopen(nonce_req).headers['Replay-Nonce']
reg_raw = json.dumps({
"resource": "new-reg",
"contact": ["mailto:{0}".format(email)],
"agreement": TERMS,
}, sort_keys=True, indent=4)
reg_b64 = _b64(reg_raw)
reg_protected = copy.deepcopy(header)
reg_protected.update({"nonce": reg_nonce})
reg_protected64 = _b64(json.dumps(reg_protected, sort_keys=True, indent=4))
reg_file = tempfile.NamedTemporaryFile(dir=".", prefix="register_", suffix=".json")
reg_file.write("{0}.{1}".format(reg_protected64, reg_b64))
reg_file.flush()
reg_file_name = os.path.basename(reg_file.name)
reg_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="register_", suffix=".sig")
reg_file_sig_name = os.path.basename(reg_file_sig.name)
# need signature for each domain identifiers
ids = []
for domain in domains:
sys.stderr.write("Building request for {0}...\n".format(domain))
id_nonce = urllib2.urlopen(nonce_req).headers['Replay-Nonce']
id_raw = json.dumps({
"resource": "new-authz",
"identifier": {
"type": "dns",
"value": domain,
},
}, sort_keys=True)
id_b64 = _b64(id_raw)
id_protected = copy.deepcopy(header)
id_protected.update({"nonce": id_nonce})
id_protected64 = _b64(json.dumps(id_protected, sort_keys=True, indent=4))
id_file = tempfile.NamedTemporaryFile(dir=".", prefix="domain_", suffix=".json")
id_file.write("{0}.{1}".format(id_protected64, id_b64))
id_file.flush()
id_file_name = os.path.basename(id_file.name)
id_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="domain_", suffix=".sig")
id_file_sig_name = os.path.basename(id_file_sig.name)
ids.append({
"domain": domain,
"protected64": id_protected64,
"data64": id_b64,
"file": id_file,
"file_name": id_file_name,
"sig": id_file_sig,
"sig_name": id_file_sig_name,
})
# need signature for the final certificate issuance
sys.stderr.write("Building request for CSR...\n")
proc = subprocess.Popen(["openssl", "req", "-in", domain_csr, "-outform", "DER"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
csr_der, err = proc.communicate()
csr_der64 = _b64(csr_der)
csr_nonce = urllib2.urlopen(nonce_req).headers['Replay-Nonce']
csr_raw = json.dumps({
"resource": "new-cert",
"csr": csr_der64,
}, sort_keys=True, indent=4)
csr_b64 = _b64(csr_raw)
csr_protected = copy.deepcopy(header)
csr_protected.update({"nonce": csr_nonce})
csr_protected64 = _b64(json.dumps(csr_protected, sort_keys=True, indent=4))
csr_file = tempfile.NamedTemporaryFile(dir=".", prefix="cert_", suffix=".json")
csr_file.write("{0}.{1}".format(csr_protected64, csr_b64))
csr_file.flush()
csr_file_name = os.path.basename(csr_file.name)
csr_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="cert_", suffix=".sig")
csr_file_sig_name = os.path.basename(csr_file_sig.name)
# Step 5: Ask the user to sign the registration and requests
## MOD Esteban automatic generattion
_execute_command("openssl dgst -sha256 -sign {0} -out {1} {2}".format(user_priv, reg_file_sig_name, reg_file_name))
for i in ids:
_execute_command( "openssl dgst -sha256 -sign {0} -out {1} {2}".format( user_priv, i['sig_name'], i['file_name']))
_execute_command("openssl dgst -sha256 -sign {0} -out {1} {2}".format(user_priv, csr_file_sig_name, csr_file_name))
## MOD Esteban
# Step 6: Load the signatures
reg_file_sig.seek(0)
reg_sig64 = _b64(reg_file_sig.read())
for n, i in enumerate(ids):
i['sig'].seek(0)
i['sig64'] = _b64(i['sig'].read())
# Step 7: Register the user
sys.stderr.write("Registering {0}...\n".format(email))
reg_data = json.dumps({
"header": header,
"protected": reg_protected64,
"payload": reg_b64,
"signature": reg_sig64,
}, sort_keys=True, indent=4)
reg_url = "{0}/acme/new-reg".format(CA)
try:
resp = urllib2.urlopen(reg_url, reg_data)
result = json.loads(resp.read())
except urllib2.HTTPError as e:
err = e.read()
# skip already registered accounts
if "Registration key is already in use" in err:
sys.stderr.write("Already registered. Skipping...\n")
else:
sys.stderr.write("Error: reg_data:\n")
sys.stderr.write("POST {0}\n".format(reg_url))
sys.stderr.write(reg_data)
sys.stderr.write("\n")
sys.stderr.write(err)
sys.stderr.write("\n")
raise
# Step 8: Request challenges for each domain
responses = []
tests = []
for n, i in enumerate(ids):
sys.stderr.write("Requesting challenges for {0}...\n".format(i['domain']))
id_data = json.dumps({
"header": header,
"protected": i['protected64'],
"payload": i['data64'],
"signature": i['sig64'],
}, sort_keys=True, indent=4)
id_url = "{0}/acme/new-authz".format(CA)
try:
resp = urllib2.urlopen(id_url, id_data)
result = json.loads(resp.read())
except urllib2.HTTPError as e:
sys.stderr.write("Error: id_data:\n")
sys.stderr.write("POST {0}\n".format(id_url))
sys.stderr.write(id_data)
sys.stderr.write("\n")
sys.stderr.write(e.read())
sys.stderr.write("\n")
raise
challenge = [c for c in result['challenges'] if c['type'] == "http-01"][0]
keyauthorization = "{0}.{1}".format(challenge['token'], thumbprint)
# challenge request
sys.stderr.write("Building challenge responses for {0}...\n".format(i['domain']))
test_nonce = urllib2.urlopen(nonce_req).headers['Replay-Nonce']
test_raw = json.dumps({
"resource": "challenge",
"keyAuthorization": keyauthorization,
}, sort_keys=True, indent=4)
test_b64 = _b64(test_raw)
test_protected = copy.deepcopy(header)
test_protected.update({"nonce": test_nonce})
test_protected64 = _b64(json.dumps(test_protected, sort_keys=True, indent=4))
test_file = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".json")
test_file.write("{0}.{1}".format(test_protected64, test_b64))
test_file.flush()
test_file_name = os.path.basename(test_file.name)
test_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".sig")
test_file_sig_name = os.path.basename(test_file_sig.name)
tests.append({
"uri": challenge['uri'],
"protected64": test_protected64,
"data64": test_b64,
"file": test_file,
"file_name": test_file_name,
"sig": test_file_sig,
"sig_name": test_file_sig_name,
})
# challenge response for server
responses.append({
"uri": ".well-known/acme-challenge/{0}".format(challenge['token']),
"data": keyauthorization,
})
# Step 9: Ask the user to sign the challenge responses
## MOD Esteban automate this
for i in tests:
_execute_command("openssl dgst -sha256 -sign {0} -out {1} {2}".format(user_priv, i['sig_name'], i['file_name']))
## MOD Esteban
# Step 10: Load the response signatures
for n, i in enumerate(ids):
tests[n]['sig'].seek(0)
tests[n]['sig64'] = _b64(tests[n]['sig'].read())
# Step 11: Ask the user to host the token on their server
for n, i in enumerate(ids):
## MOD Esteban only file based write files
requested_URL = "http://{0}/{1}".format( i['domain'], responses[n]['uri'])
sys.stderr.write(requested_URL)
subdomain = i['domain'].split('.')[0]
if subdomain not in ["www" , domain_to_crt]:
requested_dir = "{0}/{1}".format( server_path, subdomain)
_to_report("Checking directory: {0}".format(requested_dir))
if not os.path.isdir(requested_dir):
_to_report("FAIL \n")
raise IOError("subdomain not in server path: {0}".format(requested_dir))
else:
requested_dir = server_path
if not os.path.isdir(requested_dir):
_to_report("FAIL \n")
raise IOError("domain not in server path: {0}".format(requested_dir))
_make_sure_path_exists("{0}/.well-known".format(requested_dir))
_make_sure_path_exists("{0}/.well-known/acme-challenge".format(requested_dir))
requested_file = "{0}/{1}".format(requested_dir, responses[n]['uri'])
content_data = "{0}".format(responses[n]['data'])
#write file to server
target = open(requested_file, 'w+')
target.write(content_data)
target.close()
## MOD Esteban
# Step 12: Let the CA know you're ready for the challenge
sys.stderr.write("Requesting verification for {0}...\n".format(i['domain']))
test_data = json.dumps({
"header": header,
"protected": tests[n]['protected64'],
"payload": tests[n]['data64'],
"signature": tests[n]['sig64'],
}, sort_keys=True, indent=4)
test_url = tests[n]['uri']
try:
resp = urllib2.urlopen(test_url, test_data)
test_result = json.loads(resp.read())
except urllib2.HTTPError as e:
sys.stderr.write("Error: test_data:\n")
sys.stderr.write("POST {0}\n".format(test_url))
sys.stderr.write(test_data)
sys.stderr.write("\n")
sys.stderr.write(e.read())
sys.stderr.write("\n")
raise
# Step 13: Wait for CA to mark test as valid
sys.stderr.write("Waiting for {0} challenge to pass...\n".format(i['domain']))
while True:
try:
resp = urllib2.urlopen(test_url)
challenge_status = json.loads(resp.read())
except urllib2.HTTPError as e:
sys.stderr.write("Error: test_data:\n")
sys.stderr.write("GET {0}\n".format(test_url))
sys.stderr.write(test_data)
sys.stderr.write("\n")
sys.stderr.write(e.read())
sys.stderr.write("\n")
raise
if challenge_status['status'] == "pending":
time.sleep(2)
elif challenge_status['status'] == "valid":
sys.stderr.write("Passed {0} challenge!\n".format(i['domain']))
break
else:
raise KeyError("'{0}' challenge did not pass: {1}".format(i['domain'], challenge_status))
# Step 14: Get the certificate signed
sys.stderr.write("Requesting signature...\n")
csr_file_sig.seek(0)
csr_sig64 = _b64(csr_file_sig.read())
csr_data = json.dumps({
"header": header,
"protected": csr_protected64,
"payload": csr_b64,
"signature": csr_sig64,
}, sort_keys=True, indent=4)
csr_url = "{0}/acme/new-cert".format(CA)
try:
resp = urllib2.urlopen(csr_url, csr_data)
signed_der = resp.read()
except urllib2.HTTPError as e:
sys.stderr.write("Error: csr_data:\n")
sys.stderr.write("POST {0}\n".format(csr_url))
sys.stderr.write(csr_data)
sys.stderr.write("\n")
sys.stderr.write(e.read())
sys.stderr.write("\n")
raise
# Step 15: Convert the signed cert from DER to PEM
sys.stderr.write("Certificate signed!\n")
signed_der64 = base64.b64encode(signed_der)
signed_pem = """\
-----BEGIN CERTIFICATE-----
{0}
-----END CERTIFICATE-----
""".format("\n".join(textwrap.wrap(signed_der64, 64)))
return signed_pem
if name == "main":
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""
Get a SSL certificate signed by a Let's Encrypt (ACME) certificate authority and
output that signed certificate. You do NOT need to run this script on your
server and this script does not ask for your private keys. It will print out
commands that you need to run with your private key or on your server as root,
which gives you a chance to review the commands instead of trusting this script.
NOTE: YOUR ACCOUNT KEY NEEDS TO BE DIFFERENT FROM YOUR DOMAIN KEY.
Prerequisites:
$ openssl genrsa 4096 > user.key
$ openssl rsa -in user.key -pubout > user.pub
$ openssl genrsa 4096 > domain.key
$ openssl req -new -sha256 -key domain.key -subj "/CN=example.com" > domain.csr
""")
parser.add_argument("-d", "--domain-to-crt", required=True, help="Domain name to cert")
parser.add_argument("-e", "--email", required=True, help="contact email for cert request")
parser.add_argument("-g", "--gen-keys", action='store_true', help="if set, a file-based response is used")
parser.add_argument("-sd", "--ssl-directory", default="./ssl_files", help="Directory to hold SSL keys")
parser.add_argument("-sp", "--server-path", default="~/public_html", help="Server path")
args = parser.parse_args()
signed_crt = sign_csr(args.domain_to_crt, args.email, "{0}_{1}".format(args.ssl_directory,args.domain_to_crt), args.server_path, args.gen_keys)
#signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based, ssl_directory=args.ssl_directory)
sys.stdout.write(signed_crt)
signed_crt_file = open("{0}/{1}".format("{0}_{1}".format(args.ssl_directory,args.domain_to_crt),"signed.crt"), 'w+')
signed_crt_file.write(signed_crt)
signed_crt_file.close()
"""
Need to figure out what format they want the signatures to be now
Or DSA keys for that matter.
In fact it seems to have been designed to only support RSA keys.
What is the renewal process like? Do you just run again against the same CSR?
Hello!
I stumbled across a problem occuring at Step 2. I successfully did:
python sign_csr.py --public-key user.pub domain.csr > signed.crt
After that I entered my email. But if I run the manual commands in a seperate command window this error occurs:
Error opening output file register_0h6rzr.sig
1384:error:02001020:system library:fopen:Broken pipe:.\crypto\bio\bss_file.c:168:fopen('register_0h6rzr.sig','wb')
1384:error:2006D002:BIO routines:BIO_new_file:system lib:.\crypto\bio\bss_file.c:173:
I'm all new to this so if I forgot to mention some important sidenotes of my project, please tell me. And thanks in advance! :)
{"type":"urn:acme:error:malformed","detail":"Error creating new cert :: Certificate public key must be different than account key"}
I'm using the following process to create public and private keys and CSR:
http://www.foxhop.net/ssl-certificates#how-i-manage-requesting-certs
Would it be possible to add a mode to LNS to run the commands to generate and sign the keys itself? It's not really that big a deal, but it would be faster if it I could just say "okay, you run and output a private key and a signed certificate at the end of the process". I would still need to run the web server, sure, but I usually just proxy the .well-known file to a specific port beforehand.
So, the process would be:
letsencrypt-nosudo --port=27318 <csr details>
Port 27318 would already be an HTTP proxy for the well-known file to the machine where this command would run, and the process would result to just two files, the private key and signed cert, which is much more plug-and-play.
great client but It should be mentioned, that python 2.7 is required.
$ python --version
Python 2.6.6
$ python letsencrypt-nosudo-master/sign_csr.py -p user.pub -e [email protected] www.domain.tld.csr
Traceback (most recent call last):
File "letsencrypt-nosudo-master/sign_csr.py", line 441, in
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based)
File "letsencrypt-nosudo-master/sign_csr.py", line 26, in sign_csr
nonce_req = urllib2.Request("{}/directory".format(CA))
ValueError: zero length field name in format
When I get to the point (Step 4 of: python sign_csr.py --public-key user.pub domain.csr > signed.crt) where I run on a separate terminal:
sudo python -c "import .....
It ends with:
socket.error: [Errno 98] Address already in use
I love EFF dearly, and obviously they're supporting Let's Encrypt, but I think that the Internet Security Research Group, as the 501(c)(3) that operates Let's Encrypt and is responsible for its financials, software development, and strategic direction, is the best target for the donations this project drives on its README.
Their Become a Sponsor page has a PayPal button at the bottom. I'm not sure the best way to adapt it into a README-friendly link.
Guys,
I was wondering if it’s possible to add code to use existing HTTP server?
I.e. we have a set of server behind load balancer (LB).
It’ll be nice, if instead of running HTTP server, your script can pass content and path to another user defined script (UDS).
UDS distributes content to all servers behind LB.
Then your script runs domain ownership verification part and get certificate.
UDS can be written in any OS supported language.
I wish I can do it myself, but I don’t know python well enough...
Hi, when i try to execute :
python sign_csr.py --public-key user.pub domain.csr > signed.crt
it run, and ask me for email ( all seem okey ) .. but after that i got this :
"signature": ""
}
{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}
Traceback (most recent call last):
File "sign_csr.py", line 446, in
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based)
File "sign_csr.py", line 198, in sign_csr
resp = urllib2.urlopen(reg_url, reg_data)
File "/usr/local/lib/python2.7/urllib2.py", line 154, in urlopen
return opener.open(url, data, timeout)
File "/usr/local/lib/python2.7/urllib2.py", line 437, in open
response = meth(req, response)
File "/usr/local/lib/python2.7/urllib2.py", line 550, in http_response
'http', request, response, code, msg, hdrs)
File "/usr/local/lib/python2.7/urllib2.py", line 475, in error
return self._call_chain(_args)
File "/usr/local/lib/python2.7/urllib2.py", line 409, in _call_chain
result = func(_args)
File "/usr/local/lib/python2.7/urllib2.py", line 558, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 400: Bad Request
Now, that certbot/certbot#504 (CSRs) and certbot/certbot#502 (manual authenticator) are merged in, official Let's Encrypt client provides the same features as letsencrypt-nosudo
! You can try it out by running letsencrypt --authenticator manual auth --csr csr.der
. To use simpleHttp
challenge without TLS use --no-simple-http-tls
.
Please consider adding appropriate notice to your project. You are all more than welcome to contribute "upstream"! :)
Hello,
what would be the syntax to create a working certificate sign request for multiple domains?
For example: github.com, *.github.com
It would be nice if you could add this to the example usage.
Thanks 👍
I believe I did everything correctly, but I retrieve the following error when issuing the new cert:
Press Enter when you've got the python command running on your server...
Requesting verification for secret4u.ddns.net...
Waiting for secret4u.ddns.net challenge to pass...
Passed secret4u.ddns.net challenge!
Requesting signature...
Error: csr_data:
POST https://acme-v01.api.letsencrypt.org/acme/new-cert
{
"header": {
"alg": "RS256",
"jwk": {
"e": "AQAB",
"kty": "RSA",
"n": "uihD9jAbqFuhRM2AcFfFpuMTouyTgC0bZw7HmA9d1_by_923QDYQvSb9z5t5u467bm4pLla5RxBg-xgPoKRZjhVD1AzfaXeQAMxREaO_BoxekWwLzt2AW8mu1cCX9a7UsvUCxt3NwCQ6PUkI2PuUpF0PSgKisUwWlMrf-qiOEdvyED85erSIvbN9uoZ2xQcAfbuShy_hvIQ4BUKfKw9UEWhxxn7t6t1t_A1Q_yWqND2uMtVpK9g3ujt5V-I9QIk_iLGrwlgPHuhp031j4JeeB9K-YE0PEjiAhzniz8GkMP2_-_yIsDZwkXlpb6g9aj5XesRrCUrLp8mx521V_7egLeFpv8hWyJe9Kx-5geIskIaD5ilfxsk_xbR8EGuvxommjR8AuvfaMUKac1MF3PUVgNiYSuSD1VSGd1lt3B3Rorv0BsdifyaUIaQpe0x_2XYNvZRMgnX889ZmCYpXE04BPUuyw2srGnVI8z0mOaGjKlVo47dkMH00NuYyu0VWUsV4PdURb8Oe7uuTKpcJEouzKUxjL2taXCHGDKemb9W7SYyFOZGnyajgg53uCT7pgamzBWcrsCWAPhl_1x8JmrJeJUP9HeudN19hEYutk9tshTZXdPQyeWckmenj6ZaUkS8QKCh3wUu2fzKFsc4FZbsvWW9bB1ebIKjW3J5pi9QHUYE"
}
},
"payload": "ewogICAgImNzciI6ICJNSUlFWVRDQ0Fra0NBUUF3SERFYU1CZ0dBMVVFQXd3UmMyVmpjbVYwTkhVdVpHUnVjeTV1WlhRd2dnSWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUNEd0F3Z2dJS0FvSUNBUUNzVU5LOFJDNXd4WlV5VHpFclRsV0lrS0hMejJUYmE1eW85SFBLMUNvUWo4THV4VnIxc0ZqaDFpX1cwazk0NmZGMEVkMXB0RVpDbmVfaEdxMnNYc1BNNU5sV3UwQ0VQRllqam9RZ0hWWGNOV09qa1pwMGl4Umk2Q0J6N1NyQm5EcnR4SDY2dTE3X3JMTFRTMnVTbldKVm1XMXk1VVpxQkc2bE90RmN6c1BSQXlZdEUwS25jS0s0cEJtMTVZVWRZRGFhTWFFYVEwVk1nNENsd1FpampFV25YUV85RnhVd0I2TExYd01NTTdGS0NXY2YtcWZqSEVxU0RDS3V3QWRPamZ0SEh4dXJ4ZGlOaEZheVZvZGQyOFpsNWdGZ2psMnM5aF9hQkU0Ny1xcGIya3IzN0hQSE4tZlhDSXhjMlp3eHBzV3V6OHl4N05CY21JSUdYUmZNR3pFQXhpd3JVTmdRenhGUGY5UlZaLS16b0NENTd4aXd1SzhzVTFDWUVtbUk4dlVia2VocXZyVElNcy1zOTZJeGotWDA3WmFYbFpYSk1wYmhobXlaeURaajhaQUdKYk14UWxTYkxUcUJ5cEd4dlhpa2Nza2VrVEZ4Ym5VcmRnVlNSekxrQmtjeXF1VXJRbmptWkxJQUs0MXVnWk5LZ2I4eUpXenZoTEtTQ2tnMkU3MXF6cEZFcEQ1QnFOTmx3eUMwbUwxYUNrMG1Na1lvaUN4NTNWNnVCSTV5clZxOWR6cTJNbFMyeUFlZ1pIaTMyQlZVQmtMTm1Bd2JhMW9TSXBhYW9OMWY2MS10MzVwV2xaNXdLVm1xbzJDVm8yOC1LQ0d3NjZDODdGZjJqVjB2NExBSkpfaDg2aWRuclhZRkY3QTNwdkR3ZnhlZkdGWmM1QTB5Zk9rMUpuQUtNd0lEQVFBQm9BQXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnSUJBRzByV2hhUTBVZXZWYTBuV0JpNHZvMWNsamNMUmNrTFNUQmZlR2xValBjTmVYbkRnVE92bXRlc2t4eFdRNmt4Vlc2UmVfdHBTaEQ3cDNWcUZyNW9ZQlJCNmpna24zUXV5aGJkVk1PcGVZTmpLemxHRml1dDJSd29GREhWX3NkMmdqbDZjdlZGZHJRQUFGdVpYbWg5SU5DSUVEWWZ4SUFwZkxNZkFSQzB5V3hpazhjVjROREJxUTdCY292ckFOM19BTEpVYlRxVmhlYVVOZ2dzTF9IZzNrbjQ2cDJpRTJEdWUzZTJCcUk3eVItRzVLVkhFaFYzRUcyTWhBY0o1SWpfRE4wLTE2dnVleUNPTU1XMUhOQTRocG9Ocy1KV291UDUxMWxKamFWVnpOdW9sUmhpZ0VoVVpsQzh3LUVaa2xuVFd1SEhtcy1CVU53QW1uZHdBMG9ONG91dTZpaC1pY2p1amxFTU9Hdk9OZmhFbmx0aTJFTWp1R2gyNHN4Q0wzdDdhRlgtcE9OZFlVTFgxZTNOa0U5c0ppdXd0eUF1OXgyT293WjFldTRvQm91anFxdXRreWI1dFVDWmNlbmFGaVNkY1RobFVZTThJbGViWU5yOTU0T2hkVjhWQ1hKdzZtR3lVVm1zUzNVdmVoajl6X3pSVWVNMDR3bUUyR21QdzU4aXdpTmR4NFkwN3QtSE03aGJwWUQ3a0VaUi1weUJvRUFTamsycEQtT2EtMERLU3BCVURSVXhLU0U4eDNQSGtkaWRaNF92aEU3a1JoT0xwazF3SGNUdDFVQWYydUEySlpoUDhBeTNUbnR5TEt3UWhUS2JQdUlVX2xoUFBTd2dELXVkeHp2U2trR0NsNmtkbjNSaWZyWjF3Rkg2WVZ3R1pQUmtzYjhod0p6RWJidVUiLCAKICAgICJyZXNvdXJjZSI6ICJuZXctY2VydCIKfQ",
"protected": "ewogICAgImFsZyI6ICJSUzI1NiIsIAogICAgImp3ayI6IHsKICAgICAgICAiZSI6ICJBUUFCIiwgCiAgICAgICAgImt0eSI6ICJSU0EiLCAKICAgICAgICAibiI6ICJ1aWhEOWpBYnFGdWhSTTJBY0ZmRnB1TVRvdXlUZ0MwYlp3N0htQTlkMV9ieV85MjNRRFlRdlNiOXo1dDV1NDY3Ym00cExsYTVSeEJnLXhnUG9LUlpqaFZEMUF6ZmFYZVFBTXhSRWFPX0JveGVrV3dMenQyQVc4bXUxY0NYOWE3VXN2VUN4dDNOd0NRNlBVa0kyUHVVcEYwUFNnS2lzVXdXbE1yZi1xaU9FZHZ5RUQ4NWVyU0l2Yk45dW9aMnhRY0FmYnVTaHlfaHZJUTRCVUtmS3c5VUVXaHh4bjd0NnQxdF9BMVFfeVdxTkQydU10VnBLOWczdWp0NVYtSTlRSWtfaUxHcndsZ1BIdWhwMDMxajRKZWVCOUstWUUwUEVqaUFoem5pejhHa01QMl8tX3lJc0Rad2tYbHBiNmc5YWo1WGVzUnJDVXJMcDhteDUyMVZfN2VnTGVGcHY4aFd5SmU5S3gtNWdlSXNrSWFENWlsZnhza194YlI4RUd1dnhvbW1qUjhBdXZmYU1VS2FjMU1GM1BVVmdOaVlTdVNEMVZTR2QxbHQzQjNSb3J2MEJzZGlmeWFVSWFRcGUweF8yWFlOdlpSTWduWDg4OVptQ1lwWEUwNEJQVXV5dzJzckduVkk4ejBtT2FHaktsVm80N2RrTUgwME51WXl1MFZXVXNWNFBkVVJiOE9lN3V1VEtwY0pFb3V6S1V4akwydGFYQ0hHREtlbWI5VzdTWXlGT1pHbnlhamdnNTN1Q1Q3cGdhbXpCV2Nyc0NXQVBobF8xeDhKbXJKZUpVUDlIZXVkTjE5aEVZdXRrOXRzaFRaWGRQUXllV2NrbWVuajZaYVVrUzhRS0NoM3dVdTJmektGc2M0Rlpic3ZXVzliQjFlYklLalczSjVwaTlRSFVZRSIKICAgIH0sIAogICAgIm5vbmNlIjogInpldTc2Z2IxZkdDT3FRSFJUMzlwUzFTZ29WQzJKYjRRcXBON2Uzd0JiMVEiCn0",
"signature": "kh_MaUJUw8X-4p5B0RPgDpJPUkT9hKAA8aEb0DwUKQMufNs7ZNIFq5wliKSaI3HMfaxJeZC5WeKXneWlSTOhBvgv4LiYhGFyXsy8V591ydbe-UwGApUR8fpW9H2ZD9-o6aKTGNc4uy7pE6ghPcCao2ftFSyejfqhfeZC0iJsh-1I9yOGCT1mOhymGZ-bMBGwCc1Cz6ch7gmu4b9zwV-GiWgAbE5YGAkZeX9jgrpfGEjstI7FlpMglhScnSBHyYust87MocxOhJQnWl6wOOWpM51d1rgqVfTQYSx2rj8-6hhMuAh_peie2_TFxnNo-Fi8Vk3UmUAQqmf6ed4EwOKO5D9Gszgqd2d_yBZ2t41t4dfAue1C_u9AjNle7aR2QxWEg0wvAhbxCbZjHYMy15crbM2o3Fj7vLdYHdzCODlur1L9CDhs0b74BXT6Io8C5gC5OBv0NwQG8Qj9DThogfOkbllXWqgYielXWxyhU1jSdFs-OhxlUmBM-WdznbvmZvtFwLqQHmYJ03EjOXR-BdYlzMvGBTpaaAEKPpDzmLIgVWx8qfMFxzHZaVML16Go7OxkRhRlA1F9wk9eabEHfH3Uskym3QMdVymJl4Bi99-gV4uXDV1kCeFx-3-hUv0LgfBKnJNLFs96TN9mugx4zgppLnfddWr8uQib3xDKB8kPDrM"
}
{"type":"urn:acme:error:rateLimited","detail":"Error creating new cert :: Too many certificates already issued for: ddns.net","status":429}
Traceback (most recent call last):
File "sign_csr.py", line 441, in
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based)
File "sign_csr.py", line 386, in sign_csr
resp = urllib2.urlopen(csr_url, csr_data)
File "/usr/lib/python2.7/urllib2.py", line 127, in urlopen
return _opener.open(url, data, timeout)
File "/usr/lib/python2.7/urllib2.py", line 410, in open
response = meth(req, response)
File "/usr/lib/python2.7/urllib2.py", line 523, in http_response
'http', request, response, code, msg, hdrs)
File "/usr/lib/python2.7/urllib2.py", line 448, in error
return self._call_chain(_args)
File "/usr/lib/python2.7/urllib2.py", line 382, in _call_chain
result = func(_args)
File "/usr/lib/python2.7/urllib2.py", line 531, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 429: Unknown
Seems like there's a limit on domains at least those broad dyndns ones. Is it at all possible to issue certs for free domains? I'll switch to any dyndns provider as long as it is supported by letsencrypt.
Thanks for any hint
On Mac, the path is /System/Library/OpenSSL/openssl.cnf
not /etc/ssl/openssl.cnf
.
Hello, will be interesting adding a parameter -p like -f that calls a simple php script in the server that creates the validation file in .well-known/acme-challenge/
Instead of using sudo python to complete the challenge, can you provide the file and contents to save?
I'm trying to create a certificate on a server that I don't have root/sudo access.
If instead of providing:
sudo python -c "import BaseHTTPServer; \
h = BaseHTTPServer.BaseHTTPRequestHandler; \ h.do_GET = lambda r: r.send_response(200) or r.end_headers() or r.wfile.write('{\"header\": {\"alg\": \"RS256\"}, \"protected\": \"eyJhbGciOiAiUlMyNTYifQ\", \"payload\": \"ewogICAgInRscyI6IGZhbHNlLCAKICAgICJ0b2tlbiI6ICJkbzVaWkMwMHVwZmNFN0tjeEhzOGNyS2FNaE02UFdBdTMtMnVwZ00zRG00IiwgCiAgICAidHlwZSI6ICJzaW1wbGVIdHRwIgp9\", \"signature\": \"Gp5V68da_XdC96piXs1YOhrv4USOQBNnhIL-CMmxvKSigmxAJ8z00xsgWS6nsYD8LPpMVa3GkXhb10qfbymPiWhtMpMYZ31kMLFwgpHrY9xkiNP-WK9Zljz6L-WAzxCOmF1Ov71z_75iEJij86E2f9EmTjDlmDmGAjP9lziII42uyyjjIZg9claU1GtFZUrfXd-uNHHEGHFUpoyLHQcyWCP1T04Xx4q4dY51VeOJNOmIv9csIjkbOma7EqFMAHwYAplAUE45FQ5N9lJvpymD49BoEgQj_kjH-UPnxO3q0QB0i-MJJCiwQYAhMKV618jV9rNE181zJ1FRkX48knMzqoE4oG3yEFUg2D_vAdFG3VCuotnuxrZ7BEzDPWyEm0z8XakxWQW-xHSADtKWRr1qsQCy7qVsoAKnVFQ_1b4rAzET1YfrmhSH4MVhMB5n9tOnjtPQ0OsJVbf0oVLh5AC1rbXe68weOQExDVJgsk56x3FvvwrmdaLe2TnbPJmzpkYUf1OK88e8KmhVYb34veuY1luDOBJQyQ9fOAGZC0F-g7SpWg1lp3hQzf5enkycHMK-fNAfFH7t1m1Ej_CvUuxfBVhI0W8ANpFWL4r8PxTZaZzE6NO38MYgB9nrICiKJuuTQQbsXdjOm22QuxrG1XpWA-vQCtbk-L891Ko6MdAUMzQ\"}'); \ s = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), h); \ s.serve_forever()"
If you could provide an option to save a file on the webserver that would be good.
How about using one tempfolder which contains all the temporary files?
This would have the advantage that the requests and the signatures would have nearly identical names(just different suffixes) so you could just go to that directory and
for f in *.json; do openssl dgst -sha256 -sign user.key -out ${f%.json}.sig ${f} done
instead of calling openssl for every file on its own.
It would be really useful to be able to run the Python HTTP server on a port above 1024, so that non-root users could still verify their request.
Hey,
I found #26 Multiple Domains in which you explained that wildcards do not work with Let's Encrypt but I would like to see this stated in the readme.
I have not looked at the Let's Encrypt specification but it makes sense. Would be nice if you could add it. I would do it myself and create a pull request but I thought it might be better if you do it in your writing style.
Thanks for the work on the script. Works incredibly well. I used it on 64-bit Ubuntu and OSX 10.10
Right now people are forced to use the Python script, which means taking down the main webserver. That's not very pretty. Please show the URL in case someone wants to actually serve the file instead of using the Python daemon.
Compatibility to python 2.6 changing {} to {0} .. {n} in .format lines
here is code since you do not accep branching or pull request
Hi, everything is in the title.
With the letsencrypt client there is the renew subcommand, so is there something similar with the nosudo command ?
I shall post rev #3 of the code in a minute...ok i lie...soon. SLEEEP!
Anyways, are you a complete noob here?Youre telling by the code that you know how to mangle up http(S) requests and process them (advanced python) but you cant even produce a simple syscall to the underlying os to exec() something? Its os.system() btw in python. exec, I believe is pascal. I forget what they call it in C, because i WRITE PASCAL.
Take a line from Novell a few years back. Let the server maintain/watch the server. Linux is great at one thing very well, running. Running code, it doesnt matter. And by running, we AUTOMATE. Its like a McDonalds in here its so automated.
Seriously? The only part that cant be automated and requires user input is the random seed crap that needs to sit on the server. And I'll get to that part.
Yes, I can do better. AS I said, changes forthcoming. Revision #2 is already posted online.
It took me a minute after fighting with firefox and my DNS settings to make sure things were in order and what was working.I would up on the waitlist with one working cert.
Jokes on the script because thats all I need. DNS shortcut took care of the rest.
Dont get me started on firefox.
This is BASIC PYTHON. Any BOOK can teach you the basics.Im even going on a limb here and saying that some places online teach python(OH WAIT, they DO!) for FREE. I applaud the effort but LEARN to CODE.
Loving the client for getting certs, but after about 85 or so days...
Will there be a set of instructions on how to renew, or do we just get new certs again?
This script should be able to find and validate multiple subject alternative names in one csr.
user@hostname:~$ export SAN="subjectAltName=DNS:example.com,DNS:www.example.com"
user@hostname:~$ openssl req \
> -new \
> -key domain.key \
> -sha256 \
> -subj "/" \
> -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\n$SAN")) \
> | openssl req -text -noout
Certificate Request:
Data:
Version: 0 (0x0)
Subject:
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:a8:05:50:86:49:98:c8:05:01:e9:50:18:7f:2f:
b4:89:09:29:d1:c1:58:d8:14:bb:58:1d:25:50:11:
bb:43:d8:28:03:a5:de:59:49:bb:d2:f7:d3:79:5c:
c6:99:2c:98:ff:99:23:8c:df:96:7c:ea:4b:62:2a:
a4:c2:84:f5:5d:62:7f:7d:c4:7c:e2:c3:db:e6:58:
03:c2:26:9d:02:da:bb:84:d9:11:82:fe:38:12:9b:
c7:b6:ff:b2:40:30:38:b1:44:d8:47:1d:43:4a:29:
58:6b:49:ec:33:d7:dc:a7:1b:90:05:3a:f5:e6:16:
98:08:5d:2d:7e:b4:ea:a2:a4:b1:84:89:f7:f1:c4:
67:a6:a1:06:70:dd:4e:6b:0c:f8:b5:9b:bc:3f:06:
ee:90:d6:86:29:52:d3:af:f6:d4:2f:c6:cf:4b:5a:
b8:cd:01:74:6d:5c:25:a8:02:1c:7c:e8:66:3d:46:
07:b1:9d:ef:cc:eb:90:b6:bf:7b:33:e0:5f:b2:9b:
e8:b4:12:67:2f:8d:0d:9b:54:9d:95:6e:09:83:cb:
f3:5b:1f:31:8e:3b:ca:4e:08:e0:40:c0:60:40:72:
dd:0d:3e:99:ec:7c:ac:c4:3c:ba:85:9d:d9:d9:6b:
02:2e:bf:a8:a3:02:1d:eb:c8:58:e3:04:b3:a5:f1:
67:37
Exponent: 65537 (0x10001)
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com
Signature Algorithm: sha256WithRSAEncryption
99:a5:f0:c3:6d:5c:fc:90:5a:de:3e:3e:15:f1:5c:90:18:48:
04:26:bb:3e:3a:13:7b:14:93:fe:ff:d4:87:ee:06:8b:f3:82:
83:54:d8:99:82:48:e3:27:d5:5e:d7:88:2d:c0:70:b6:34:93:
57:16:c5:7a:8a:3d:38:43:b2:06:21:ee:82:e2:6b:6a:5a:22:
e0:68:a0:ed:88:d1:a6:42:b7:d4:a0:f6:5d:18:7f:41:f1:3e:
cf:5c:f8:36:8c:8f:53:db:eb:e6:ac:f9:0d:cd:11:f0:80:84:
7d:68:14:a9:58:21:40:fb:3e:9c:c8:1d:b0:89:14:f2:a0:55:
2a:ab:3a:0e:be:12:6b:23:d4:35:84:0c:40:13:c9:bd:cf:5d:
bf:04:e1:20:01:99:28:fc:58:bf:08:64:ee:8d:b4:35:fc:e9:
00:70:64:c8:5c:98:0b:6a:56:0e:44:9a:4a:2e:9f:0e:87:1a:
43:06:0c:40:79:b2:92:34:85:72:d4:db:6b:2a:77:7c:40:02:
9e:b0:88:2c:41:fc:0e:ed:1d:75:d7:de:4f:09:3c:84:82:ea:
fe:24:69:5d:af:a4:a9:0f:c8:8f:56:69:13:fa:e4:94:3c:64:
82:4f:60:e2:46:85:b3:80:a6:74:de:38:e5:75:ba:22:fc:2c:
a6:9f:af:1b
If you mis-type the csr file path when executing the script, you get an unhelpful and nasty error. It should tell you that it can't find the file rather than giving this error:
paul@ubuntu:~/letsencrypt$ python sign_csr.py user.pub filethatdoesntexist.csr > signed.crt
Reading pubkey file...
Found public key!
Reading csr file...
Traceback (most recent call last):
File "sign_csr.py", line 331, in <module>
signed_crt = sign_csr(args.pubkey_path, args.csr_path)
File "sign_csr.py", line 60, in sign_csr
domain = re.search("Subject:.*? CN=([^\s,;/]+).*?", out, re.MULTILINE|re.DOTALL).groups()[0]
AttributeError: 'NoneType' object has no attribute 'groups'
If user.key is supplied
Either generate (and save) "ready to run" scripts:
openssl dgst -sha256 -sign my_secret_path/user.key -out register_jxcwae.sig register_gtnj7b.json
openssl dgst -sha256 -sign my_secret_path/user.key -out domain_5a64yt.sig domain_ieex7l.json
openssl dgst -sha256 -sign my_secret_path/user.key -out cert_ccna96.sig cert_pf6pwm.json
Or execute “openssl –sign {}” right away.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.