Skip to content
This repository was archived by the owner on Jan 29, 2018. It is now read-only.

Commit

Permalink
Implemented basic EC keys support.
Browse files Browse the repository at this point in the history
Currently, only NIST P-256 curve is supported, but not P-384.

The key type is automatically determined by reading the file
until a line with "--BEGIN <type> PRIVATE KEY--" is encountered.
  • Loading branch information
drdaeman committed Feb 27, 2016
1 parent f61f72c commit f995b09
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 16 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ acme-tiny, to register a account for you and sign all following requests.
openssl genrsa 4096 > account.key
```

or

```
openssl ecparam -name prime256v1 -genkey -noout -out account.key
```

#### Use existing Let's Encrypt key

Alternatively you can convert your key, previously generated by the original
Expand Down Expand Up @@ -66,9 +72,14 @@ The ACME protocol (what Let's Encrypt uses) requires a CSR file to be submitted
to it, even for renewals. You can use the same CSR for multiple renewals. NOTE:
you can't use your account private key as your domain private key!


Generate a domain private key (if you haven't already)
```
#generate a domain private key (if you haven't already)
# If you prefer RSA
openssl genrsa 4096 > domain.key
# If you prefer ECDSA
openssl ecparam -name prime256v1 -genkey -noout -out domain.key
```

```
Expand Down
74 changes: 59 additions & 15 deletions acme_tiny.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,52 @@ def _b64(b):

# parse account key to get public key
log.info("Parsing account key...")
proc = subprocess.Popen(["openssl", "rsa", "-in", account_key, "-noout", "-text"],
account_key_type = None
with open(account_key, "r") as f:
for line in f:
m = re.match(r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line)
if m is not None:
account_key_type = m.group(1).lower()
break
if account_key_type not in ("rsa", "ec"):
raise ValueError("Unknown key type. This tool supports RSA and ECDSA keys only")
proc = subprocess.Popen(["openssl", account_key_type, "-in", account_key, "-noout", "-text"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if proc.returncode != 0:
raise IOError("OpenSSL Error: {0}".format(err))
pub_hex, pub_exp = re.search(
r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
out.decode('utf8'), re.MULTILINE|re.DOTALL).groups()
pub_exp = "{0:x}".format(int(pub_exp))
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
header = {
"alg": "RS256",
"jwk": {
"e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
"kty": "RSA",
"n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
},
}
if account_key_type == "rsa":
pub_hex, pub_exp = re.search(
r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
out.decode('utf8'), re.MULTILINE|re.DOTALL).groups()
pub_exp = "{0:x}".format(int(pub_exp))
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
header = {
"alg": "RS256",
"jwk": {
"e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
"kty": "RSA",
"n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
},
}
else:
pub_hex = re.search(
r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: prime256v1\n",
out.decode('utf8'), re.MULTILINE|re.DOTALL)
if pub_hex is None:
raise ValueError("Invalid or incompatible key. Note, only prime256v1 is supported")
pub_hex = binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex.group(1)))
if len(pub_hex) != 64:
raise ValueError("Key error: public key has incorrect length")
header = {
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": _b64(pub_hex[:32]),
"y": _b64(pub_hex[32:]),
},
}
accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':'))
thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())

Expand All @@ -51,9 +79,25 @@ def _send_signed_request(url, payload):
out, err = proc.communicate("{0}.{1}".format(protected64, payload64).encode('utf8'))
if proc.returncode != 0:
raise IOError("OpenSSL Error: {0}".format(err))

if account_key_type == "ec":
# ECDSA-with-SHA256 signatures consist of just two numbers (R || S),
# however OpenSSL returns them not raw but as a DER-encoded sequence.
proc = subprocess.Popen(["openssl", "asn1parse", "-inform", "DER"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate(out)
if proc.returncode != 0:
raise IOError("OpenSSL Error: {0}".format(err))
signature = re.findall(r"prim:\s+INTEGER\s+:([0-9A-F]{64})\n", out.decode("utf8"))
if len(signature) != 2:
raise ValueError("Failed to generate signature: unexpected DER output")
signature = binascii.unhexlify(signature[0]) + binascii.unhexlify(signature[1])
else:
signature = out

data = json.dumps({
"header": header, "protected": protected64,
"payload": payload64, "signature": _b64(out),
"payload": payload64, "signature": _b64(signature),
})
try:
resp = urlopen(url, data.encode('utf8'))
Expand Down

0 comments on commit f995b09

Please sign in to comment.