From ed5d2f170cfcfd565682028a9f44caa6ba1217ef Mon Sep 17 00:00:00 2001 From: Kaarle Ritvanen Date: Thu, 16 Aug 2018 00:28:54 +0300 Subject: separate CRL signing keys --- dmvpn-ca | 212 +++++++++++++++++++++++++++++++++++-------------------- dmvpn-pfx-decode | 22 ++++-- syntax.txt | 4 +- 3 files changed, 153 insertions(+), 85 deletions(-) diff --git a/dmvpn-ca b/dmvpn-ca index 79c14c2..cf1692a 100755 --- a/dmvpn-ca +++ b/dmvpn-ca @@ -262,9 +262,11 @@ function sign(object, hash_alg, cert, key) end function issue_cert(attrs, func) - local ca = attrs.serial == 0 local key = pkey.new(attrs.params.key) + local ca = attrs.usage and attrs.usage.keyCertSign + attrs.serial = ca and 0 or next_key('certificate', 'serial') + local cert = x509.new() cert:setVersion(3) cert:setSerial(attrs.serial) @@ -278,7 +280,7 @@ function issue_cert(attrs, func) end cert:setSubject(dn) - cert:setBasicConstraints{CA=ca} + cert:setBasicConstraints{CA=attrs.usage} cert:setBasicConstraintsCritical(true) local issued = cert:getLifetime() @@ -287,7 +289,8 @@ function issue_cert(attrs, func) attrs.issued = issued attrs.expires = expires - attrs.privateKey = encrypt_key(key, ca) + attrs.privateKey = (ca or not attrs.usage) and encrypt_key(key, ca) or + key:getPrivateKey() cert:addExtension( x509ext.new( @@ -297,16 +300,15 @@ function issue_cert(attrs, func) ) ) - if ca then + if attrs.usage then cert:addExtension( x509ext.new( 'keyUsage', 'DER', - rfc5280.KeyUsage.encode{ - ['keyCertSign']=true, ['cRLSign']=true - } + rfc5280.KeyUsage.encode(attrs.usage) ) ) + attrs.usage = nil end local crl_dp = config.crl['dist-point'] @@ -339,6 +341,10 @@ function issue_cert(attrs, func) return attrs end +function issue_ca_cert(usage) + return issue_cert{dn=config.ca.dn, params=config.ca, usage=usage} +end + function export_cert(cert) local password = {} for i=1,config['password-length'] do @@ -352,6 +358,9 @@ function export_cert(cert) local chain = x509ch.new() chain:add(load_ca_cert()) + + if config.db['encrypt-keys'] then chain:add(load_crl_cert()) end + chain:add(x509.new(cert.data, 'PEM')) local file = io.open( @@ -648,6 +657,10 @@ function get_certs() end, get_cert_by_serial(serial) end + if arg[1] == 'crl' then + return select_certs('site IS NULL AND serial > 0') + end + return select_certs( vpnc_filter(scan_vpnc{multiple=true, retired=true}) or 'serial > 0' @@ -655,6 +668,33 @@ function get_certs() end +function try_load_crl_cert() + if not config.db['encrypt-keys'] then + return table.unpack{load_ca_cert()} + end + + if not crl_cert then + for row in select_certs( + 'site IS NULL and serial > 0', 'serial DESC' + ) do + if not crl_cert then + local cert, key = load_cert(row) + if is_valid(row) then + crl_cert = cert + crl_key = key + end + end + end + end + return crl_cert, crl_key +end + +function load_crl_cert() + local cert, key = try_load_crl_cert() + if not cert then error('No valid CRL signing key found') end + return cert, key +end + function generate_crl() local crl = x509crl.new() crl:setVersion(2) @@ -678,7 +718,7 @@ function generate_crl() end end - sign(crl, config.crl['hash-alg']) + sign(crl, config.crl['hash-alg'], table.unpack{load_crl_cert()}) insert('crl', {serial=new_serial, expires=expires, data=tostring(crl)}) if old_serial then delete('crl', {serial=old_serial}) end @@ -1047,10 +1087,9 @@ output = scan_choice( add_subnet('', subnet) end - issue_cert{ - dn=config.ca.dn, - serial=0, - params=config.ca + issue_ca_cert{ + keyCertSign=true, + cRLSign=not config.db['encrypt-keys'] } end, show=function() @@ -1059,78 +1098,94 @@ output = scan_choice( }, cert={ generate=function() - local vpnc = vpnc_filter( + local crl = arg[1] == 'crl' + local vpnc = not crl and vpnc_filter( scan_vpnc{multiple=true}, 's.code', 'v.id' ) - local filter = vpnc or {} - filter['s.active'] = '1' - filter['v.active'] = '1' - - local subjects = {} local dns = {} - - for row in select_many( - 's.code, v.id, s.asn, s.name, v.name', - 'site s INNER JOIN vpnc v ON s.code = v.site', - filter, - 'n' - ) do - local attrs = { - site=row[1], - vpnc=row[2], - asn=row[3], - sname=row[4], - vname=row[5] - } - - local function insert() - attrs.params = config[ - attrs.site == '' and - 'hub' or - 'spoke' - ] - - attrs.dn = attrs.params.dn:gsub( - '%$(%u+)', - { - ROOT=config.ca.dn, - SITE=attrs.sname or - attrs.site, - NAME=attrs.vname or - attrs.params[ - 'default-name' - ]:gsub( - '$ID', - attrs.vpnc - ) - } - ) - - table.insert( - subjects, attrs - ) + local gen_crl_cert + if not vpnc then + gen_crl_cert = not try_load_crl_cert() + if gen_crl_cert then table.insert( dns, - {tostring(attrs.dn)} + {config.ca.dn} ) end + end + + local subjects = {} - if vpnc then insert() + if not crl then + local filter = vpnc or {} + filter['s.active'] = '1' + filter['v.active'] = '1' + + for row in select_many( + 's.code, v.id, s.asn, s.name, v.name', + 'site s INNER JOIN vpnc v ON s.code = v.site', + filter, + 'n' + ) do + local attrs = { + site=row[1], + vpnc=row[2], + asn=row[3], + sname=row[4], + vname=row[5] + } + + local function insert() + attrs.params = config[ + attrs.site == '' and + 'hub' or + 'spoke' + ] + + attrs.dn = attrs.params.dn:gsub( + '%$(%u+)', + { + ROOT=config.ca.dn, + SITE=attrs.sname or + attrs.site, + NAME=attrs.vname or + attrs.params[ + 'default-name' + ]:gsub( + '$ID', + attrs.vpnc + ) + } + ) - else - local valid - for cert in select_certs{ - site=row[1], vpnc=row[2] - } do - if is_valid(cert) then - valid = true - end + table.insert( + subjects, + attrs + ) + table.insert( + dns, + {tostring(attrs.dn)} + ) end - if not valid then - insert() + + if vpnc then insert() + + else + local valid + for cert in select_certs{ + site=row[1], + vpnc=row[2] + } do + if is_valid(cert) then + valid = true + end + end + if not valid then + insert() + end end end end @@ -1140,11 +1195,16 @@ output = scan_choice( local issued = {} - for _, attrs in ipairs(subjects) do - attrs.serial = next_key( - 'certificate', 'serial' + if gen_crl_cert then + table.insert( + issued, + issue_ca_cert{ + cRLSign=true + } ) + end + for _, attrs in ipairs(subjects) do local asn = attrs.asn attrs.asn = nil @@ -1298,8 +1358,8 @@ output = scan_choice( 'serial', 'selector' ) local cert = get_cert_by_serial(serial) - if cert.serial == 0 then - error('Cannot export root certificate') + if not cert.site then + error('Cannot export CA certificate') end export_cert(cert) end diff --git a/dmvpn-pfx-decode b/dmvpn-pfx-decode index eecd3f5..1ec2830 100755 --- a/dmvpn-pfx-decode +++ b/dmvpn-pfx-decode @@ -7,6 +7,7 @@ See LICENSE file for license details dmvpn = require('dmvpn') pkcs12 = require('openssl.pkcs12') +rfc5280 = require('asn1.rfc5280') name = arg[1] file = io.open(name) @@ -22,17 +23,24 @@ if not success then key, cert, chain = pkcs12.parse(data, dmvpn.get_password()) end -function write_pem_file(dir, data) - local file = io.open('/etc/swanctl/'..dir..'/dmvpn.pem', 'w') +function write_pem_file(data, dir, suffix) + local file = io.open( + '/etc/swanctl/'..dir..'/dmvpn'..(suffix or '')..'.pem', 'w' + ) file:write(data) file:close() end -write_pem_file('private', key:toPEM('private')) -write_pem_file('x509', tostring(cert)) -for i, ca_cert in pairs(chain) do - assert(i == 1) - write_pem_file('x509ca', tostring(ca_cert)) +write_pem_file(key:toPEM('private'), 'private') +write_pem_file(tostring(cert), 'x509') +for _, ca_cert in pairs(chain) do + local suffix + local usage = rfc5280.KeyUsage.decode( + ca_cert:getExtension('keyUsage'):getData() + ) + if usage.keyCertSign then suffix = '' + elseif usage.cRLSign then suffix = '-crl' end + if suffix then write_pem_file(tostring(ca_cert), 'x509ca', suffix) end end function print_var(name, value) diff --git a/syntax.txt b/syntax.txt index f946404..51ff432 100644 --- a/syntax.txt +++ b/syntax.txt @@ -26,8 +26,8 @@ dmvpn-ca gre-addr del dmvpn-ca root-cert {generate|show} -dmvpn-ca cert generate [hubs|hub |site [vpnc ]] -dmvpn-ca cert {list|show|revoke} [serial |hubs|hub |site [vpnc ]] +dmvpn-ca cert generate [hubs|hub |site [vpnc ]|crl] +dmvpn-ca cert {list|show|revoke} [serial |hubs|hub |site [vpnc ]|crl] dmvpn-ca cert export serial dmvpn-ca crl {generate|show|export} -- cgit v1.2.3