Compare commits

...

131 commits

Author SHA1 Message Date
Jérémy Dubois a026f2dbd4 README forgot a line 2022-08-09 15:45:06 +02:00
Jérémy Dubois 6870dbbc37 Update README : how to create or revoke many certificates at once 2022-08-09 15:44:03 +02:00
Jérémy Dubois 480ead3ff2 Merge pull request 'rewrite' (#5) from dev into master
Reviewed-on: #5
2022-04-14 17:20:34 +02:00
Jérémy Dubois 754c3455e0 Release 22.04 2022-04-14 17:20:04 +02:00
Jérémy Dubois d614079138 Update CHANGELOG 2022-04-14 17:15:20 +02:00
Jérémy Dubois 7a034a2a17 Some files must be copied to ansible-roles/openvpn 2022-04-14 16:47:33 +02:00
Jérémy Dubois 42de07cb66 Add version to files that will be copied out of this repo so that we easily know if they will need an update 2022-04-14 16:21:38 +02:00
Jérémy Dubois 992fde0930 Precising that the --end-date hour is in UTC +0 2022-04-14 15:53:59 +02:00
Jérémy Dubois 6165ccec6c Generate CRL only if (re)generating CA 2022-04-14 15:51:07 +02:00
Jérémy Dubois 55e02c6a13 Check if CN already exists only after having asked for user password
Otherwise, with "-p", "--replace-existing" and "--non-interactive", with
CA_PASSWORD set but PASSWORD unset, the existing certificate was revoked but
the new one could'nt be created. Now, PASSWORD must be set or the exisiting
certificate won't be revoked
2022-04-14 15:18:57 +02:00
Jérémy Dubois ba2f553ef4 Do not use --password and --password-file together 2022-04-14 15:01:09 +02:00
Jérémy Dubois 97f1affa1b Create crl file after init of PKI 2022-04-04 18:13:37 +02:00
Jérémy Dubois 14a65fa42d Change SUFFIX to use human readable date instead of epoch 2022-04-04 17:55:37 +02:00
Jérémy Dubois c76b7a02ca Split show_usage for each subcommand, add --version and --help in addition to version and help, update VERSION 2022-04-04 17:37:20 +02:00
Jérémy Dubois 1fa4ff205e Parse date in ISO format rather than US format 2022-04-04 17:01:19 +02:00
Jérémy Dubois 554f6166c9 Forget to delete a debug line 2022-03-29 18:59:09 +02:00
Jérémy Dubois 85c3324713 Update Copyright 2022-03-29 18:48:45 +02:00
Jérémy Dubois 9f13a42355 Handle the case where --days argument is not a number or a negative one
Before this test, the error was displayed but ignored and the certificate was
still created depending on the default_days value in openssl.cnf
2022-03-29 18:42:28 +02:00
Jérémy Dubois abf6fb131c Do not use --end-date and --days together 2022-03-29 18:20:16 +02:00
Jérémy Dubois 191ba257d9 Fix parsing options when no option is given 2022-03-29 18:19:33 +02:00
Jérémy Dubois e42af2183c Fix --non-interactive behavior: there were still some prompts to the user 2022-03-29 18:18:01 +02:00
Jérémy Dubois a640892ecb Syntax: no space before ":" 2022-03-29 18:17:03 +02:00
Jérémy Dubois 6d71a5a177 Fix end-date format depending on system 2022-03-29 18:15:57 +02:00
Jérémy Dubois 047c6e334a Improve README and show_usage 2022-03-29 18:10:47 +02:00
Jérémy Dubois 5f27702f17 Delete ovpn.conf.example unnecessary here
shellpki alone is not enough to install OpenVPN, and the openvpn role provides
the openvpn server configuration
2022-03-29 18:01:23 +02:00
Jérémy Dubois 50fc8c2d21 README file : delete unnecessary leading spaces 2022-03-22 18:11:17 +01:00
Jérémy Dubois d0c6a55538 README file and show_usage function : replace "cert" with "certificate" 2022-03-22 18:08:57 +01:00
Jérémy Dubois da7809f3c0 Update README file and show_usage function : forgotten information 2022-03-22 18:04:03 +01:00
Jérémy Dubois 4a2e5c93f1 Update README file and show_usage function 2022-03-22 18:01:22 +01:00
Jérémy Lecour d48dc132be fix replace-existing and non-interactive confict 2022-03-14 14:40:50 +01:00
Jérémy Dubois 69db5a80aa More conventional "list" parsing 2022-03-14 11:03:36 +01:00
Jérémy Dubois c92f7a5a7e Change ovpn example file to match the openvpn ansible role and wiki 2022-03-14 10:55:28 +01:00
Jérémy Dubois af24b1469d Add nobind option to client config 2022-03-14 10:55:06 +01:00
Jérémy Lecour e8ced03988 add .ovpn example 2022-03-11 14:12:27 +01:00
Jérémy Lecour 4bb24707b0 simplify "list" options parsing 2022-03-11 14:10:53 +01:00
Jérémy Lecour 10edbb19fa init can be "non-interactive" 2022-03-11 14:10:32 +01:00
Jérémy Lecour 6cc29fb1f8 reorder functions 2022-03-11 14:09:58 +01:00
Jérémy Lecour 68e4648694 fix shellcheck violations 2022-03-11 11:44:09 +01:00
Jérémy Lecour 41d0ca261d extract get_real_path function to normalize readlink arguments 2022-03-11 11:38:01 +01:00
Jérémy Lecour 593cf4a9f3 show usage if list has no argument, instead of "set -u" error 2022-03-11 11:36:20 +01:00
Jérémy Dubois 4b2b8a95ff cert-expirations.sh: search for valid certificates in the index file rather than in a directory where files could be deleted with the certificates still being valids 2022-02-18 11:45:12 +01:00
Jérémy Dubois 92ee845207 New script cn-validation.sh for OpenVPN 2021-06-14 14:30:34 +02:00
Jérémy Dubois fb22db8dac cert-expirations.sh => certificates names can contain upper case characters 2021-03-02 10:08:32 +01:00
Jérémy Dubois 0bf2bfe60c cert-expirations.sh : warning about UTC hours 2021-02-08 15:36:31 +01:00
Jérémy Dubois 847694339c cert-expirations.sh => certificates names can contain "_" in it 2020-11-06 11:19:38 +01:00
Jérémy Dubois 9deb73b548 cert-expirations.sh => certificates names can contain "@" in it 2020-11-06 10:53:00 +01:00
Jérémy Dubois ff7737e733 Add backup carp check to cert-expirations.sh 2020-11-06 10:14:03 +01:00
Jérémy Lecour 9f3b0a4cd4 list: better options parsing 2020-10-12 23:49:51 +02:00
Jérémy Lecour 83d0ef2449 "shellpki revoke" can be run interactively or not 2020-10-12 23:38:32 +02:00
Jérémy Lecour c83f210387 default values for variables in tests 2020-10-12 23:27:24 +02:00
Jérémy Lecour 75e36189c5 "shellpki init" can be executed interactively or not 2020-10-12 23:27:05 +02:00
Jérémy Lecour 530cd3b333 update changelog 2020-09-07 09:49:53 +02:00
Jérémy Lecour c335b30623 cert-expirations.sh script to print out certificates expiration dates 2020-09-04 14:50:13 +02:00
Jérémy Lecour a6c153b546 Copy files if destination exists 2020-05-06 00:40:36 +02:00
Jérémy Lecour 99e5b8a386 whitespace 2020-05-06 00:39:39 +02:00
Jérémy Lecour fdb9f46e35 Display key file path on success 2020-05-06 00:39:23 +02:00
Jérémy Lecour ab4e3e5de1 Rename --revoke-existing to --replace-existing 2020-05-06 00:38:57 +02:00
Jérémy Lecour 123d5f5c05 split lines 2020-05-06 00:00:00 +02:00
Jérémy Lecour 6bb05a6366 Add --revoke-existing command line option 2020-05-05 23:50:04 +02:00
Jérémy Lecour 1c4b68f571 Use error() and warning() functions in options parsing 2020-05-05 23:49:10 +02:00
Jérémy Lecour 3e2bbe8de5 lowercase variable 2020-05-05 23:20:54 +02:00
Jérémy Lecour e04f686651 Prevent use of uninitialized variables 2020-05-05 23:20:36 +02:00
Jérémy Lecour f94f7d8cd3 Add --non-interactive command line option 2020-05-05 23:19:29 +02:00
Jérémy Lecour 0c4d36cb57 improve error display 2020-05-05 15:24:06 +02:00
Jérémy Lecour d9f866fc3a typo 2020-05-05 15:06:15 +02:00
Jérémy Lecour fa5a344ef4 Remove "set -e" and add many return code checks 2020-05-05 11:45:11 +02:00
Jérémy Lecour 229aab510a Emit errors if files are missing 2020-05-05 11:30:37 +02:00
Jérémy Lecour 3161e93856 Restore forgotten output redirection
It had been removed temporarily to debug an issue
2020-05-05 10:49:33 +02:00
Jérémy Lecour dfeaf77b9f Extract ask_user_password() function 2020-05-05 10:47:09 +02:00
Jérémy Lecour 706608ca4a Use inline pass phrase arguments
It doesn't seem more or less secure to embed the password as an argument
than an environment variable written at the begining of the line.
2020-05-05 10:46:42 +02:00
Jérémy Lecour bb20053ba0 Simplify openssl commands composition 2020-05-05 09:42:54 +02:00
Jérémy Lecour 8e92d46ecd Let OpenSSL read the password file itself 2020-05-05 09:24:09 +02:00
Jérémy Lecour 165c96ca55 Extract variables for files 2020-05-05 00:28:00 +02:00
Jérémy Lecour 7506003f53 Add --days and --end-date command line options 2020-05-05 00:22:35 +02:00
Jérémy Lecour a30be3872f Extract is_user() and is_group() functions 2020-05-04 23:16:19 +02:00
Jérémy Lecour 09c1a7a579 wording 2020-05-04 23:12:56 +02:00
Jérémy Lecour d8a5d04fd0 Extract function cert_end_date() 2020-05-04 23:12:48 +02:00
Jérémy Lecour 7630d8b182 whitespaces 2020-05-04 23:08:19 +02:00
Jérémy Lecour 857bb4b239 explicit checks on exit code 2020-05-04 23:06:51 +02:00
Jérémy Lecour df6d06d848 Add option to revoke the existing certificate when creating one. 2020-05-04 23:02:48 +02:00
Jérémy Lecour a9b2fdd832 verify_ca_password() looks for a previously set password and verifies it 2020-05-04 18:55:10 +02:00
Jérémy Lecour 21182a8dcf CA key length is configurable (minimum 4096) 2020-05-04 18:53:14 +02:00
Jérémy Lecour b03e77d307 More readable variable names 2020-05-04 18:16:39 +02:00
Jérémy Lecour 420fcddb90 whitespaces and if/then normalization 2020-05-04 18:07:20 +02:00
Jérémy Lecour f63caa0779 fix variable name 2020-05-04 17:58:13 +02:00
Jérémy Lecour 480077b600 update CHANGELOG for password-file option 2020-05-04 17:45:28 +02:00
Jérémy Lecour 1443df56bc Rename internal function usage() to show_usage() 2020-05-04 17:44:01 +02:00
Jérémy Lecour 48b282c2df Add a version number and version command 2020-05-04 17:43:09 +02:00
Jérémy Lecour 536de976cc Check on $USER was always true 2020-05-04 17:42:01 +02:00
Jérémy Lecour 2e6c4f541f Create a CHANGELOG 2020-05-04 17:41:21 +02:00
Jérémy Lecour 921cba15b6 accept a password file 2020-05-04 14:21:58 +02:00
Jérémy Lecour f4e53c374a ask for CA password before user password 2020-04-30 16:00:34 +02:00
Victor LABORIE a45a130e60 Don't force Vagrant::DEFAULT_SERVER_URL (doesn't work with recent Vagrant version) 2020-02-10 15:12:30 +01:00
Victor LABORIE b3dcc3ac13 Replace getopts by manual parsing and remove set -u 2019-03-11 11:07:05 +01:00
Victor LABORIE 69948226de Strip .sh extension from shellpki script 2019-03-06 13:42:18 +01:00
Victor LABORIE 93f96968fe Add OpenBSD support to VagrantFile 2019-01-21 14:34:36 +01:00
Victor LABORIE dd383b1015 Fix shell error on vagrant provision 2019-01-16 16:21:35 +01:00
Victor Laborie deca8047f8 Merge branch 'readme-improvements' of evolix/shellpki into dev 2018-12-27 10:46:21 +01:00
Patrick Marchand 332aea981f Improvements to README.md
Changed markdown formatting to be more readable and added some
links.
2018-12-26 14:07:22 -05:00
Victor LABORIE 7ded1befb1 Shellpki is now MIT licensed 2018-12-26 14:17:45 +01:00
Victor LABORIE a01ed24b47 Fix sh compatibility (shellcheck warning) 2018-12-12 18:11:11 +01:00
Victor LABORIE 326664676b Add doc for OpenVPN config file auto-generation 2018-10-24 15:38:31 +02:00
Victor LABORIE 091b32ee73 Add missing param in doc for shellpki init 2018-10-24 15:29:32 +02:00
Victor LABORIE 4092850742 PKIUSER is _shellpki on OpenBSD 2018-10-24 15:28:32 +02:00
Victor LABORIE 39bddf87ae Add documentation for install on OpenBSD 2018-10-24 15:21:43 +02:00
Victor LABORIE 545d251eab Add .swp file to gitignore 2018-10-24 15:20:29 +02:00
Victor LABORIE ba70219688 Add missing arg for cacert creation 2018-08-01 11:03:04 +02:00
Victor LABORIE d176ae28fb Add a delay for auto restart in systemd service 2018-06-27 19:04:59 +02:00
Victor LABORIE 0c7ff3501c Add -ignore_err for openssl ocsp 2018-06-27 19:04:29 +02:00
Victor LABORIE e24f288bfb Use exec for lauch openssl ocsp 2018-06-27 15:29:30 +02:00
Victor LABORIE cf2a667cc3 Add an example of ocspd systemd service 2018-06-27 15:13:42 +02:00
Victor LABORIE 8e9d72071e $USER can be root or $PKIUSER 2018-06-27 14:48:18 +02:00
Victor LABORIE 5f07a5e24c Add an OCSPD responder 2018-06-27 13:48:12 +02:00
Victor LABORIE 75246c956f Force cacert removing when erasing cakey 2018-06-27 12:51:30 +02:00
Victor LABORIE 84bfee7495 TMPDIR must be present 2018-06-27 11:55:19 +02:00
Victor LABORIE e6621cdd9b Init need CommonName for CA 2018-06-27 11:45:03 +02:00
Victor LABORIE 8263ca83e2 Add a Vagrantfile for testing 2018-04-11 14:32:45 +02:00
Victor LABORIE d6469e4e5b Use logger for cn-filter 2018-03-01 16:24:27 +01:00
Victor LABORIE c5ba184692 Add check subcommand for expiration alert 2018-02-21 11:25:00 +01:00
Victor LABORIE 2e2e59790a Fix .ovpn creation, - was missing 2018-02-15 16:33:07 +01:00
Victor LABORIE 3df0ca945b Add cn-filter.sh script for OpenVPN server 2018-02-15 15:22:56 +01:00
Victor LABORIE 0c55b0f285 Add sudo right to README install 2018-01-31 15:15:56 +01:00
Victor LABORIE 690c436dd6 Retrieve vars from openssl.cnf file 2018-01-31 12:43:34 +01:00
Victor LABORIE 8a4b52d1b5 Use -r with read command 2018-01-24 17:29:55 +01:00
Victor LABORIE a4c3a2cf83 Use error function instead of interactive error in main 2018-01-24 15:18:03 +01:00
Victor LABORIE 74cd88c33b List subcommand can filter by valid/revoked cert 2018-01-24 11:43:03 +01:00
Victor LABORIE ef12ea3bb9 Shellpki is no more interactive 2018-01-23 17:22:01 +01:00
Victor LABORIE 1921f9e5e5 Add GPLv2+ licence 2018-01-17 12:32:21 +01:00
Victor LABORIE 771066ff5d Complete refactoring 2018-01-17 12:21:39 +01:00
Victor LABORIE 6684fb4d71 Use 4096 bits for CA key 2018-01-15 17:38:52 +01:00
Jérémy Lecour f8eb22f51e Use available variables 2017-09-11 11:11:22 +02:00
12 changed files with 1471 additions and 261 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.vagrant
*.swp

51
CHANGELOG.md Normal file
View file

@ -0,0 +1,51 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
### Changed
### Fixed
### Removed
### Security
## [22.04] 2022-04-14
### Added
* Create a changelog
* Add a version number and `version` command
* Accept a `password-file` command line option to read password from a file
* Accept `--days` and `--end-date` command line options
* CA key length is configurable (minimum 4096)
* Add `--non-interactive` command line option
* Add `--replace-existing` command line option
* Copy files if destination exists
* Generate the CRL file after initialization of the CA
* `cert-expirations.sh` script to print out certificates expiration dates
### Changed
* Rename internal function usage() to show_usage()
* Split show_usage() for each subcommand
* More readable variable names
* verify_ca_password() looks for a previously set password and verifies it
* Extract cert_end_date() function
* Extract is_user() and is_group() functions
* Extract ask_user_password() function
* Extract variables for files
* Use inline pass phrase arguments
* Create files with a human readable date instead of epoch
* Remove "set -e" and add many return code checks
* Prevent use of uninitialized variables
### Fixed
* Check on $USER was always true

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Evolix
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

167
README.md
View file

@ -1,24 +1,161 @@
# shellpki
# ShellPKI
This script is a wrapper around openssl to manage all the pki stuff
for openvpn.
This script is a wrapper around OpenSSL to manage a small
[PKI](https://en.wikipedia.org/wiki/Public_key_infrastructure).
# Usage
## Contribution
First create the directory, put the script in it and the openssl
configuration file. You may certainly need to edit the configuration.
After an update of this repo and if everything is working fine, some files must
be copied to [ansible-roles/openvpn](https://gitea.evolix.org/evolix/ansible-roles/src/branch/unstable/openvpn/files/shellpki)
mkdir -p /etc/openvpn/ssl
cp /path/to/shellpki.sh /etc/openvpn/ssl/
cp /path/to/openssl.cnf /etc/openvpn/ssl/
$EDITOR /etc/openvpn/ssl/openssl.cnf
## Install
Then you'll need to initialize the pki.
### Debian
cd /etc/openvpn/ssl
sh shellpki.sh init
~~~
useradd shellpki --system -M --home-dir /etc/shellpki --shell /usr/sbin/nologin
mkdir /etc/shellpki
install -m 0640 openssl.cnf /etc/shellpki/
install -m 0755 shellpki /usr/local/sbin/shellpki
chown -R shellpki: /etc/shellpki
~~~
Once it's done, you can create all the certificates you need.
~~~
# visudo -f /etc/sudoers.d/shellpki
%shellpki ALL = (root) /usr/local/sbin/shellpki
~~~
sh shellpki.sh create
### OpenBSD
~~~
useradd -r 1..1000 -d /etc/shellpki -s /sbin/nologin _shellpki
mkdir /etc/shellpki
install -m 0640 openssl.cnf /etc/shellpki/
install -m 0755 shellpki /usr/local/sbin/shellpki
chown -R _shellpki:_shellpki /etc/shellpki
~~~
~~~
# visudo -f /etc/sudoers
%_shellpki ALL = (root) /usr/local/sbin/shellpki
~~~
## OpenVPN
If you want auto-generation of the OpenVPN config file in
/etc/shellpki/openvpn, you need to create a template file in
/etc/shellpki/ovpn.conf, eg. :
~~~
client
dev tun
tls-client
proto udp
remote ovpn.example.com 1194
nobind
user nobody
group nogroup
persist-key
persist-tun
cipher AES-256-GCM
~~~
## Usage
~~~
Usage: shellpki <subcommand> [options] [CommonName]
~~~
Initialize PKI (create CA key and self-signed certificate) :
~~~
shellpki init [options] <commonName_for_CA>
Options
--non-interactive do not prompt the user, and exit if an error occurs
~~~
Create a client certificate with key and CSR directly generated on server :
~~~
shellpki create [options] <commonName>
Options
-f, --file, --csr-file create a client certificate from a CSR (doesn't need key)
-p, --password prompt the user for a password to set on the client key
--password-file if provided with a path to a readable file, the first line is read and set as password on the client key
--days specify how many days the certificate should be valid
--end-date specify until which date the certificate should be valid, in YYYY/MM/DD hh:mm:ss format, UTC +0
--non-interactive do not prompt the user, and exit if an error occurs
--replace-existing if the certificate already exists, revoke it before creating a new one
~~~
Revoke a client certificate :
~~~
shellpki revoke [options] <commonName>
Options
--non-interactive do not prompt the user, and exit if an error occurs
~~~
List all certificates :
~~~
shellpki list <options>
Options
-a, --all list all certificates : valid and revoked ones
-v, --valid list all valid certificates
-r, --revoked list all revoked certificates
~~~
Check expiration date of valid certificates :
~~~
shellpki check
~~~
Run OCSP_D server :
~~~
shellpki ocsp <ocsp_uri:ocsp_port>
~~~
Show version :
~~~
shellpki version
~~~
Show help :
~~~
shellpki help
~~~
## Loop
We can loop over a file to revoke or create many certificates at once.
To revoke :
~~~
$ read CA_PASS
$ for cert_name in $(cat /path/to/file_certs_to_revoke); do CA_PASSWORD=$CA_PASS shellpki revoke $cert_name --non-interactive ; done
~~~
To create (without `--replace-existing`) or renew (with `--replace-existing`), with a password on the client key :
~~~
$ read CA_PASS
$ for cert_name in $(cat /path/to/file_certs_to_create); do apg -n 1 -m 16 -M lcN > /path/to/folder/to/store/${cert_name}.passwd; CA_PASSWORD=$CA_PASS shellpki create --replace-existing --non-interactive --password-file /path/to/folder/to/store/${cert_name}.passwd ${cert_name}; done
~~~
## License
ShellPKI is an [Evolix](https://evolix.com) project and is licensed
under the [MIT license](LICENSE).

39
Vagrantfile vendored Normal file
View file

@ -0,0 +1,39 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Load ~/.VagrantFile if exist, permit local config provider
vagrantfile = File.join("#{Dir.home}", '.VagrantFile')
load File.expand_path(vagrantfile) if File.exists?(vagrantfile)
Vagrant.configure('2') do |config|
config.vm.synced_folder "./", "/vagrant", type: "rsync", rsync__exclude: [ '.vagrant', '.git' ]
config.ssh.shell="/bin/sh"
$deps = <<SCRIPT
mkdir -p /etc/shellpki
if [ "$(uname)" = "Linux" ]; then
id shellpki 2>&1 >/dev/null || useradd shellpki --system -M --home-dir /etc/shellpki --shell /usr/sbin/nologin
fi
if [ "$(uname)" = "OpenBSD" ]; then
id _shellpki 2>&1 >/dev/null || useradd -r 1..1000 -d /etc/shellpki -s /sbin/nologin _shellpki
fi
ln -sf /vagrant/openssl.cnf /etc/shellpki/
ln -sf /vagrant/shellpki /usr/local/sbin/shellpki
SCRIPT
nodes = [
{ :name => "debian", :box => "debian/stretch64" },
{ :name => "openbsd", :box => "generic/openbsd6" }
]
nodes.each do |i|
config.vm.define "#{i[:name]}" do |node|
node.vm.hostname = "shellpki-#{i[:name]}"
node.vm.box = "#{i[:box]}"
config.vm.provision "deps", type: "shell", :inline => $deps
end
end
end

28
cert-expirations.sh Normal file
View file

@ -0,0 +1,28 @@
#!/bin/sh
VERSION="22.04"
carp=$(/sbin/ifconfig carp0 2>/dev/null | grep 'status' | cut -d' ' -f2)
if [ "$carp" = "backup" ]; then
exit 0
fi
echo "Warning : all times are in UTC !\n"
echo "CA certificate:"
openssl x509 -enddate -noout -in /etc/shellpki/cacert.pem \
| cut -d '=' -f 2 \
| sed -e "s/^\(.*\)\ \(20..\).*/- \2 \1/"
echo ""
echo "Client certificates:"
cat /etc/shellpki/index.txt \
| grep ^V \
| awk -F "/" '{print $1,$5}' \
| awk '{print $2,$5}' \
| sed 's/CN=//' \
| sed -E 's/([[:digit:]]{2})([[:digit:]]{2})([[:digit:]]{2})([[:digit:]]{2})([[:digit:]]{2})([[:digit:]]{2})Z (.*)/- 20\1 \2 \3 \4:\5:\6 \7/' \
| awk '{if ($3 == "01") $3="Jan"; else if ($3 == "02") $3="Feb"; else if ($3 == "03") $3="Mar"; else if ($3 == "04") $3="Apr"; else if ($3 == "05") $3="May"; else if ($3 == "06") $3="Jun"; else if ($3 == "07") $3="Jul"; else if ($3 == "08") $3="Aug"; else if ($3 == "09") $3="Sep"; else if ($3 == "10") $3="Oct"; else if ($3 == "11") $3="Nov"; else if ($3 == "12") $3="Dec"; print $0;}' \
| sort -n -k 2 -k 3M -k 4

24
cn-filter.sh Normal file
View file

@ -0,0 +1,24 @@
#!/bin/sh
#
# cn-filter.sh is a client-connect script for OpenVPN server
# It allow clients to connect only if their CN is in $AUTH_FILE
#
# You need this parameters in your's server config :
#
# script-security 3
# client-connect <path-to-cn-filter>/cn-filter.sh
#
set -u
AUTH_FILE="/etc/openvpn/authorized_cns"
grep -qE "^${common_name}$" "${AUTH_FILE}"
if [ "$?" -eq 0 ]; then
logger -i -t openvpn-cn-filter -p auth.info "Accepted login for ${common_name} from ${trusted_ip} port ${trusted_port}"
exit 0
else
logger -i -t openvpn-cn-filter -p auth.notice "Failed login for ${common_name} from ${trusted_ip} port ${trusted_port}"
fi
exit 1

21
cn-validation.sh Normal file
View file

@ -0,0 +1,21 @@
#!/bin/sh
#
# cn-validation.sh is a client-connect script for OpenVPN server
# When connecting using the PAM plugin, it allow clients to connect only if their CN is equal to their UNIX username
#
# You need this parameters in your's server config :
#
# script-security 2
# client-connect <path-to-cn-filter>/cn-validation.sh
#
set -u
if [ "${common_name}" = "${username}" ]; then
logger -i -t openvpn-cn-validation -p auth.info "Accepted login for ${common_name} from ${trusted_ip} port ${trusted_port}"
exit 0
else
logger -i -t openvpn-cn-validation -p auth.notice "Failed login for CN ${common_name} / username ${username} from ${trusted_ip} port ${trusted_port}"
fi
exit 1

15
ocspd.service Normal file
View file

@ -0,0 +1,15 @@
[Unit]
Description=Shellpki OCSP responder
After=network.target
[Service]
User=shellpki
Group=shellpki
Type=simple
ExecStart=/usr/local/sbin/shellpki ocsp ocsp.example.com:8888
KillMode=process
Restart=always
RestartSec=2s
[Install]
WantedBy=multi-user.target

View file

@ -1,15 +1,17 @@
# VERSION="22.04"
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /etc/openvpn/ssl/ca
certs = /etc/openvpn/ssl/certs
new_certs_dir = /etc/openvpn/ssl/ca/tmp
dir = /etc/shellpki
certs = $dir/certs
new_certs_dir = $dir/tmp
database = $dir/index.txt
certificate = $dir/cacert.pem
serial = $dir/serial
crl = /etc/openvpn/ssl/crl.pem
private_key = $dir/private.key
crl = $dir/crl.pem
private_key = $dir/cakey.key
RANDFILE = $dir/.rand
default_days = 365
default_crl_days= 365
@ -34,6 +36,11 @@ subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = CA:true
[ v3_ocsp ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = OCSPSigning
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = FR
@ -51,5 +58,3 @@ commonName_max = 64
emailAddress = Email Address
emailAddress_default = security@evolix.net
emailAddress_max = 40

1106
shellpki Executable file
View file

@ -0,0 +1,1106 @@
#!/bin/sh
#
# shellpki is a wrapper around OpenSSL to manage a small PKI
#
set -u
VERSION="22.04"
show_version() {
cat <<END
shellpki version ${VERSION}
Copyright 2010-2022 Evolix <info@evolix.fr>,
Thomas Martin <tmartin@evolix.fr>,
Gregory Colpart <reg@evolix.fr>,
Romain Dessort <rdessort@evolix.fr>,
Benoit Série <bserie@evolix.fr>,
Victor Laborie <vlaborie@evolix.fr>,
Daniel Jakots <djakots@evolix.fr>,
Patrick Marchand <pmarchand@evolix.fr>,
Jérémy Lecour <jlecour@evolix.fr>,
Jérémy Dubois <jdubois@evolix.fr>
and others.
shellpki comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
See the MIT Licence for details.
END
}
show_usage() {
cat <<EOF
Usage: ${0} <subcommand> [options] [CommonName]
Warning: [options] always must be before [CommonName] and after <subcommand>
EOF
show_usage_init
show_usage_create
show_usage_revoke
show_usage_list
show_usage_check
show_usage_ocsp
cat <<EOF
Show version :
${0} --version
Show help :
${0} --help
EOF
}
show_usage_init() {
cat <<EOF
Initialize PKI (create CA key and self-signed certificate) :
${0} init [options] <commonName_for_CA>
Options
--non-interactive do not prompt the user, and exit if an error occurs
EOF
}
show_usage_create() {
cat <<EOF
Create a client certificate with key and CSR directly generated on server :
${0} create [options] <commonName>
Options
-f, --file, --csr-file create a client certificate from a CSR (doesn't need key)
-p, --password prompt the user for a password to set on the client key
--password-file if provided with a path to a readable file, the first line is read and set as password on the client key
--days specify how many days the certificate should be valid
--end-date specify until which date the certificate should be valid, in "YYYY/MM/DD hh:mm:ss" format, UTC +0
--non-interactive do not prompt the user, and exit if an error occurs
--replace-existing if the certificate already exists, revoke it before creating a new one
EOF
}
show_usage_revoke() {
cat <<EOF
Revoke a client certificate :
${0} revoke [options] <commonName>
Options
--non-interactive do not prompt the user, and exit if an error occurs
EOF
}
show_usage_list() {
cat <<EOF
List certificates :
${0} list <options>
Options
-a, --all list all certificates: valid and revoked ones
-v, --valid list all valid certificates
-r, --revoked list all revoked certificates
EOF
}
show_usage_check() {
cat <<EOF
Check expiration date of valid certificates :
${0} check
EOF
}
show_usage_ocsp() {
cat <<EOF
Run OCSP_D server :
${0} ocsp <ocsp_uri:ocsp_port>
EOF
}
error() {
echo "${1}" >&2
exit 1
}
warning() {
echo "${1}" >&2
}
verify_ca_password() {
"${OPENSSL_BIN}" rsa \
-in "${CA_KEY}" \
-passin pass:"${CA_PASSWORD}" \
>/dev/null 2>&1
}
get_real_path() {
# --canonicalize is supported on Linux
# -f is supported on Linux and OpenBSD
readlink -f -- "${1}"
}
ask_ca_password() {
attempt=${1:-0}
max_attempts=3
trap 'unset CA_PASSWORD' 0
if [ ! -f "${CA_KEY}" ]; then
error "You must initialize your PKI with \`shellpki init' !"
fi
if [ "${attempt}" -gt 0 ]; then
warning "Invalid password, retry."
fi
if [ "${attempt}" -ge "${max_attempts}" ]; then
error "Maximum number of attempts reached (${max_attempts})."
fi
if [ -z "${CA_PASSWORD:-}" ]; then
if [ "${non_interactive}" -eq 1 ]; then
error "In non-interactive mode, you must pass CA_PASSWORD as environment variable"
fi
stty -echo
printf "Password for CA key: "
read -r CA_PASSWORD
stty echo
printf "\n"
fi
if [ -z "${CA_PASSWORD:-}" ] || ! verify_ca_password; then
unset CA_PASSWORD
attempt=$(( attempt + 1 ))
ask_ca_password "${attempt}"
fi
}
ask_user_password() {
trap 'unset PASSWORD' 0
if [ -z "${PASSWORD:-}" ]; then
if [ "${non_interactive}" -eq 1 ]; then
error "In non-interactive mode, you must pass PASSWORD as environment variable or use --password-file"
fi
stty -echo
printf "Password for user key: "
read -r PASSWORD
stty echo
printf "\n"
fi
if [ -z "${PASSWORD:-}" ]; then
warning "Warning: empty password from input"
fi
}
replace_existing_or_abort() {
cn=${1:?}
if [ "${non_interactive}" -eq 1 ]; then
if [ "${replace_existing}" -eq 1 ]; then
revoke --non-interactive "${cn}"
else
error "${cn} already exists, use \`--replace-existing' to force"
fi
else
if [ "${replace_existing}" -eq 1 ]; then
revoke "${cn}"
else
printf "%s already exists, do you want to revoke and recreate it ? [y/N] " "${cn}"
read -r REPLY
resp=$(echo "${REPLY}" | tr 'Y' 'y')
if [ "${resp}" = "y" ]; then
revoke "${cn}"
else
error "Aborted"
fi
fi
fi
}
init() {
umask 0177
[ -d "${CA_DIR}" ] || mkdir -m 0750 "${CA_DIR}"
[ -d "${CRT_DIR}" ] || mkdir -m 0750 "${CRT_DIR}"
[ -f "${INDEX_FILE}" ] || touch "${INDEX_FILE}"
[ -f "${CRL}" ] || touch "${CRL}"
[ -f "${SERIAL}" ] || echo "01" > "${SERIAL}"
non_interactive=0
# Parse options
# based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-} in
--non-interactive)
non_interactive=1
;;
--)
# End of all options.
shift
break
;;
-?*)
# ignore unknown options
warning "Warning: unknown option (ignored): \`$1'"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
cn="${1:-}"
if [ -z "${cn}" ]; then
show_usage_init >&2
exit 1
fi
if [ -f "${CA_KEY}" ]; then
if [ "${non_interactive}" -eq 1 ]; then
error "${CA_KEY} already exists, erase it manually if you want to start over."
else
printf "%s already exists, do you really want to erase it ? [y/N] " "${CA_KEY}"
read -r REPLY
resp=$(echo "${REPLY}" | tr 'Y' 'y')
if [ "${resp}" = "y" ]; then
rm -f "${CA_KEY}" "${CA_CERT}"
fi
fi
fi
passout_arg=""
if [ -n "${CA_PASSWORD:-}" ]; then
passout_arg="-passout pass:${CA_PASSWORD}"
elif [ "${non_interactive}" -eq 1 ]; then
error "In non-interactive mode, you must pass CA_PASSWORD as environment variable."
fi
if [ ! -f "${CA_KEY}" ]; then
"${OPENSSL_BIN}" genrsa \
-out "${CA_KEY}" \
${passout_arg} \
-aes256 \
"${CA_KEY_LENGTH}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the CA key"
fi
fi
if [ -f "${CA_CERT}" ]; then
if [ "${non_interactive}" -eq 1 ]; then
error "${CA_CERT} already exists, erase it manually if you want to start over."
else
printf "%s already exists, do you really want to erase it ? [y/N] " "${CA_CERT}"
read -r REPLY
resp=$(echo "${REPLY}" | tr 'Y' 'y')
if [ "${resp}" = "y" ]; then
rm "${CA_CERT}"
fi
fi
fi
if [ ! -f "${CA_CERT}" ]; then
ask_ca_password 0
fi
if [ ! -f "${CA_CERT}" ]; then
"${OPENSSL_BIN}" req \
-new \
-batch \
-sha512 \
-x509 \
-days 3650 \
-extensions v3_ca \
-passin pass:"${CA_PASSWORD}" \
-key "${CA_KEY}" \
-out "${CA_CERT}" \
-config /dev/stdin <<EOF
$(cat "${CONF_FILE}")
commonName_default = ${cn}
EOF
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the CA certificate"
fi
"${OPENSSL_BIN}" ca \
-config "${CONF_FILE}" \
-passin pass:${CA_PASSWORD} \
-gencrl \
-out "${CRL}"
fi
}
ocsp() {
umask 0177
ocsp_uri="${1:-}"
if [ -z "${ocsp_uri}" ]; then
show_usage_ocsp >&2
exit 1
fi
ocsp_csr_file="${CSR_DIR}/ocsp.csr"
url=$(echo "${ocsp_uri}" | cut -d':' -f1)
port=$(echo "${ocsp_uri}" | cut -d':' -f2)
if [ ! -f "${OCSP_KEY}" ]; then
"${OPENSSL_BIN}" genrsa \
-out "${OCSP_KEY}" \
"${KEY_LENGTH}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the OCSP key"
fi
fi
"${OPENSSL_BIN}" req \
-batch \
-new \
-key "${OCSP_KEY}" \
-out "${ocsp_csr_file}" \
-config /dev/stdin <<EOF
$(cat "${CONF_FILE}")
commonName_default = ${url}
[ usr_cert ]
authorityInfoAccess = OCSP;URI:http://${ocsp_uri}
EOF
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the OCSP request"
fi
if [ ! -f "${OCSP_CERT}" ]; then
ask_ca_password 0
fi
if [ ! -f "${OCSP_CERT}" ]; then
"${OPENSSL_BIN}" ca \
-extensions v3_ocsp \
-in "${ocsp_csr_file}" \
-out "${OCSP_CERT}" \
-passin pass:"${CA_PASSWORD}" \
-config "${CONF_FILE}"
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the OCSP certificate"
fi
fi
exec "${OPENSSL_BIN}" ocsp \
-ignore_err \
-index "${INDEX_FILE}" \
-port "${port}" \
-rsigner "${OCSP_CERT}" \
-rkey "${OCSP_KEY}" \
-CA "${CA_CERT}" \
-text
}
create() {
from_csr=0
ask_pass=0
non_interactive=0
replace_existing=0
days=""
end_date=""
days_set=0
end_date_set=0
password_set=0
password_file_set=0
# Parse options
# based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-} in
-f|--file|--csr-file)
# csr-file option, with value separated by space
if [ -n "$2" ]; then
from_csr=1
csr_file=$(get_real_path "${2}")
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error accessing file \`${2}'"
fi
shift
else
error "Argument error: \`--csr-file' requires a value"
fi
;;
--file=?*|--csr-file=?*)
from_csr=1
# csr-file option, with value separated by =
csr_file=$(get_real_path "${1#*=}")
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error accessing file \`${1#*=}'"
fi
;;
--file=|--csr-file=)
# csr-file options, without value
error "Argument error: \`--csr-file' requires a value"
;;
-p|--password)
ask_pass=1
password_set=1
;;
--password-file)
# password-file option, with value separated by space
if [ -n "$2" ]; then
password_file=$(get_real_path "${2}")
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error accessing file \`${2}'"
fi
password_file_set=1
shift
else
error "Argument error: \`--password-file' requires a value"
fi
;;
--password-file=?*)
# password-file option, with value separated by =
password_file=$(get_real_path "${1#*=}")
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error accessing file \`${1#*=}'"
fi
password_file_set=1
;;
--password-file=)
# password-file options, without value
error "Argument error: \`--password-file' requires a value"
;;
--days)
# days option, with value separated by space
if [ -n "$2" ]; then
days=${2}
days_set=1
shift
else
error "Argument error: \`--days' requires a value"
fi
;;
--days=?*)
# days option, with value separated by =
days=${1#*=}
days_set=1
;;
--days=)
# days options, without value
error "Argument error: \`--days' requires a value"
;;
--end-date)
# end-date option, with value separated by space
if [ -n "$2" ]; then
end_date=${2}
end_date_set=1
shift
else
error "Argument error: \`--end-date' requires a value"
fi
;;
--end-date=?*)
# end-date option, with value separated by =
end_date=${1#*=}
end_date_set=1
;;
--end-date=)
# end-date options, without value
error "Argument error: \`--end-date' requires a value"
;;
--non-interactive)
non_interactive=1
;;
--replace-existing)
replace_existing=1
;;
--)
# End of all options.
shift
break
;;
-?*)
# ignore unknown options
warning "Warning: unknown option (ignored): \`$1'"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
if [ "${days_set}" -eq 1 ] && [ "${end_date_set}" -eq 1 ]; then
error "Argument error: \`--end-date' and \`--days' cannot be used together."
fi
if [ "${password_set}" -eq 1 ] && [ "${password_file_set}" -eq 1 ]; then
error "Argument error: \`--password' and \`--password-file' cannot be used together."
fi
# The name of the certificate
cn="${1:-}"
# Set expiration argument
crt_expiration_arg=""
if [ -n "${days}" ]; then
if [ "${days}" -gt 0 ]; then
crt_expiration_arg="-days ${days}"
else
error "Argument error: \"${days}\" is not a valid value for \`--days'."
fi
fi
if [ -n "${end_date}" ]; then
if [ "${SYSTEM}" = "linux" ]; then
cert_end_date=$(TZ=:Zulu date --date "${end_date}" +"%Y%m%d%H%M%SZ" 2> /dev/null)
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Invalid end date format: \`${end_date}' can't be parsed by date(1). Expected format: YYYY/MM/DD [hh[:mm[:ss]]]."
else
crt_expiration_arg="-enddate ${cert_end_date}"
fi
elif [ "${SYSTEM}" = "openbsd" ]; then
cert_end_date=$(TZ=:Zulu date -f "%C%y/%m/%d %H:%M:%S" -j "${end_date}" +"%Y%m%d%H%M%SZ" 2> /dev/null)
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Invalid end date format: \`${end_date}' can't be parsed by date(1). Expected format: YYYY/MM/DD hh:mm:ss."
else
crt_expiration_arg="-enddate ${cert_end_date}"
fi
else
error "System ${SYSTEM} not supported."
fi
fi
if [ "${non_interactive}" -eq 1 ]; then
batch_arg="-batch"
else
batch_arg=""
fi
if [ "${from_csr}" -eq 1 ]; then
if [ "${ask_pass}" -eq 1 ]; then
warning "Warning: -p|--password is ignored with -f|--file|--crt-file"
fi
if [ -n "${password_file:-}" ]; then
warning "Warning: --password-file is ignored with -f|--file|--crt-file"
fi
crt_file="${CRT_DIR}/${cn}.crt"
# ask for CA passphrase
ask_ca_password 0
# check if csr_file is a CSR
"${OPENSSL_BIN}" req \
-noout \
-subject \
-in "${csr_file}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "${csr_file} is not a valid CSR !"
fi
# check if csr_file contain a CN
"${OPENSSL_BIN}" req \
-noout \
-subject \
-in "${csr_file}" \
| grep -Eo "CN\s*=[^,/]*" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "${csr_file} doesn't contain a CommonName !"
fi
# get CN from CSR
cn=$("${OPENSSL_BIN}" req -noout -subject -in "${csr_file}" | grep -Eo "CN\s*=[^,/]*" | cut -d'=' -f2 | xargs)
# check if CN already exists
if [ -f "${crt_file}" ]; then
replace_existing_or_abort "${cn}"
fi
# ca sign and generate cert
if [ "${non_interactive}" -eq 1 ]; then
batch_arg="-batch"
else
batch_arg=""
fi
"${OPENSSL_BIN}" ca \
${batch_arg} \
-config "${CONF_FILE}" \
-in "${csr_file}" \
-passin pass:"${CA_PASSWORD}" \
-out "${crt_file}" \
${crt_expiration_arg}
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the certificate"
else
echo "The certificate file is available at \`${crt_file}'"
fi
else
if [ -z "${cn}" ]; then
show_usage_create >&2
exit 1
fi
csr_file="${CSR_DIR}/${cn}-${SUFFIX}.csr"
crt_file="${CRT_DIR}/${cn}.crt"
key_file="${KEY_DIR}/${cn}-${SUFFIX}.key"
ovpn_file="${OVPN_DIR}/${cn}-${SUFFIX}.ovpn"
pkcs12_file="${PKCS12_DIR}/${cn}-${SUFFIX}.p12"
# ask for CA passphrase
ask_ca_password 0
if [ "${ask_pass}" -eq 1 ]; then
ask_user_password
fi
# check if CN already exists
if [ -f "${crt_file}" ]; then
replace_existing_or_abort "${cn}"
fi
# generate private key
pass_args=""
if [ -n "${password_file:-}" ]; then
pass_args="-aes256 -passout file:${password_file}"
elif [ -n "${PASSWORD:-}" ]; then
pass_args="-aes256 -passout pass:${PASSWORD}"
fi
"${OPENSSL_BIN}" genrsa \
-out "${key_file}" \
${pass_args} \
"${KEY_LENGTH}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -eq 0 ]; then
echo "The KEY file is available at \`${key_file}'"
else
error "Error generating the private key"
fi
# generate csr req
pass_args=""
if [ -n "${password_file:-}" ]; then
pass_args="-passin file:${password_file}"
elif [ -n "${PASSWORD:-}" ]; then
pass_args="-passin pass:${PASSWORD}"
fi
"${OPENSSL_BIN}" req \
-batch \
-new \
-key "${key_file}" \
-out "${csr_file}" \
${pass_args} \
-config /dev/stdin <<EOF
$(cat "${CONF_FILE}")
commonName_default = ${cn}
EOF
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the CSR"
fi
# ca sign and generate cert
"${OPENSSL_BIN}" ca \
${batch_arg} \
-config "${CONF_FILE}" \
-passin pass:${CA_PASSWORD} \
-in "${csr_file}" \
-out "${crt_file}" \
${crt_expiration_arg}
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the certificate"
fi
# check if CRT is a valid
"${OPENSSL_BIN}" x509 \
-noout \
-subject \
-in "${crt_file}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
rm -f "${crt_file}"
fi
if [ ! -f "${crt_file}" ]; then
error "Error in CSR creation"
fi
chmod 640 "${crt_file}"
echo "The CRT file is available in ${crt_file}"
# generate pkcs12 format
pass_args=""
if [ -n "${password_file:-}" ]; then
# Hack for pkcs12 :
# If passin and passout files are the same path, it expects 2 lines
# so we make a temporary copy of the password file
password_file_out=$(mktemp)
cp "${password_file}" "${password_file_out}"
pass_args="-passin file:${password_file} -passout file:${password_file_out}"
elif [ -n "${PASSWORD:-}" ]; then
pass_args="-passin pass:${PASSWORD} -passout pass:${PASSWORD}"
else
pass_args="-passout pass:"
fi
"${OPENSSL_BIN}" pkcs12 \
-export \
-nodes \
-inkey "${key_file}" \
-in "${crt_file}" \
-out "${pkcs12_file}" \
${pass_args}
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the pkcs12 file"
fi
if [ -n "${password_file_out:-}" ]; then
# Hack for pkcs12 :
# Destroy the temporary file
rm -f "${password_file_out}"
fi
chmod 640 "${pkcs12_file}"
echo "The PKCS12 config file is available at \`${pkcs12_file}'"
# generate openvpn format
if [ -e "${CA_DIR}/ovpn.conf" ]; then
cat "${CA_DIR}/ovpn.conf" - > "${ovpn_file}" <<EOF
<ca>
$(cat "${CA_CERT}")
</ca>
<cert>
$(cat "${crt_file}")
</cert>
<key>
$(cat "${key_file}")
</key>
EOF
chmod 640 "${ovpn_file}"
echo "The OpenVPN config file is available at \`${ovpn_file}'"
fi
# Copy files if destination exists
if [ -d "${COPY_DIR}" ]; then
for file in "${crt_file}" "${key_file}" "${pkcs12_file}" "${ovpn_file}"; do
if [ -f "${file}" ]; then
new_file="${COPY_DIR}/$(basename "${file}")"
if [ "${replace_existing}" -eq 1 ]; then
cp -f "${file}" "${COPY_DIR}/"
else
if [ "${non_interactive}" -eq 1 ]; then
if [ -f "${new_file}" ]; then
echo "File \`${file}' has not been copied to \`${new_file}', it already exists" >&2
continue
else
cp "${file}" "${COPY_DIR}/"
fi
else
cp -i "${file}" "${COPY_DIR}/"
fi
fi
echo "File \`${file}' has been copied to \`${new_file}'"
fi
done
# shellcheck disable=SC2086
chown -R ${PKI_USER}:${PKI_USER} "${COPY_DIR}/"
chmod -R u=rwX,g=rwX,o= "${COPY_DIR}/"
fi
fi
}
revoke() {
non_interactive=0
# Parse options
# based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-} in
--non-interactive)
non_interactive=1
;;
--)
# End of all options.
shift
break
;;
-?*)
# ignore unknown options
warning "Warning: unknown option (ignored): \`$1'"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
# The name of the certificate
cn="${1:-}"
if [ -z "${cn}" ]; then
show_usage_revoke >&2
exit 1
fi
crt_file="${CRT_DIR}/${cn}.crt"
# check if CRT exists
if [ ! -f "${crt_file}" ]; then
error "Unknow CN: ${cn} (\`${crt_file}' not found)"
fi
# check if CRT is a valid
"${OPENSSL_BIN}" x509 \
-noout \
-subject \
-in "${crt_file}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "${crt_file} is not a valid CRT, you must delete it !"
fi
# ask for CA passphrase
ask_ca_password 0
echo "Revoke certificate ${crt_file} :"
"${OPENSSL_BIN}" ca \
-config "${CONF_FILE}" \
-passin pass:"${CA_PASSWORD}" \
-revoke "${crt_file}"
# shellcheck disable=SC2181
if [ "$?" -eq 0 ]; then
rm "${crt_file}"
fi
"${OPENSSL_BIN}" ca \
-config "${CONF_FILE}" \
-passin pass:"${CA_PASSWORD}" \
-gencrl \
-out "${CRL}"
}
list() {
if [ ! -f "${INDEX_FILE}" ]; then
exit 0
fi
if [ -z "${1:-}" ]; then
show_usage_list >&2
exit 1
fi
while :; do
case "${1:-}" in
-a|--all)
list_valid=0
list_revoked=0
;;
-v|--valid)
list_valid=0
list_revoked=1
;;
-r|--revoked)
list_valid=1
list_revoked=0
;;
-?*)
warning "unknow option ${1} (ignored)"
;;
*)
break
;;
esac
shift
done
if [ "${list_valid}" -eq 0 ]; then
certs=$(grep "^V" "${INDEX_FILE}")
fi
if [ "${list_revoked}" -eq 0 ]; then
certs=$(grep "^R" "${INDEX_FILE}")
fi
if [ "${list_valid}" -eq 0 ] && [ "${list_revoked}" -eq 0 ]; then
certs=$(cat "${INDEX_FILE}")
fi
echo "${certs}" | grep -Eo "CN\s*=[^,/]*" | cut -d'=' -f2 | xargs -n1
}
cert_end_date() {
"${OPENSSL_BIN}" x509 -noout -enddate -in "${1}" | cut -d'=' -f2
}
check() {
# default expiration alert
# TODO: permit override with parameters
min_day=90
cur_epoch=$(date -u +'%s')
for cert in "${CRT_DIR}"/*; do
end_date=$(cert_end_date "${cert}")
end_epoch=$(date -ud "${end_date}" +'%s')
diff_epoch=$(( end_epoch - cur_epoch ))
diff_day=$(( diff_epoch / 60 / 60 / 24 ))
if [ "${diff_day}" -lt "${min_day}" ]; then
if [ "${diff_day}" -le 0 ]; then
echo "${cert} has expired"
else
echo "${cert} expire in ${diff_day} days"
fi
fi
done
}
is_user() {
getent passwd "${1}" >/dev/null
}
is_group() {
getent group "${1}" >/dev/null
}
main() {
# Know what system we are on, because OpenBSD and Linux do not implement date(1) in the same way
SYSTEM=$(uname | tr '[:upper:]' '[:lower:]')
# default config
# TODO: override with /etc/default/shellpki
CONF_FILE="/etc/shellpki/openssl.cnf"
if [ "$(uname)" = "OpenBSD" ]; then
PKI_USER="_shellpki"
else
PKI_USER="shellpki"
fi
if [ "${USER}" != "root" ] && [ "${USER}" != "${PKI_USER}" ]; then
error "Please become root before running ${0} !"
fi
# retrieve CA path from config file
CA_DIR=$(grep -E "^dir" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1)
CA_KEY=$(grep -E "^private_key" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~")
CA_CERT=$(grep -E "^certificate" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~")
OCSP_KEY="${CA_DIR}/ocsp.key"
OCSP_CERT="${CA_DIR}/ocsp.pem"
CRT_DIR=$(grep -E "^certs" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~")
TMP_DIR=$(grep -E "^new_certs_dir" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~")
INDEX_FILE=$(grep -E "^database" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~")
SERIAL=$(grep -E "^serial" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~")
CRL=$(grep -E "^crl" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~")
# directories for clients key, csr, crt
KEY_DIR="${CA_DIR}/private"
CSR_DIR="${CA_DIR}/requests"
PKCS12_DIR="${CA_DIR}/pkcs12"
OVPN_DIR="${CA_DIR}/openvpn"
COPY_DIR="$(dirname "${CONF_FILE}")/copy_output"
CA_KEY_LENGTH=4096
if [ "${CA_KEY_LENGTH}" -lt 4096 ]; then
error "CA key must be at least 4096 bits long."
fi
KEY_LENGTH=2048
if [ "${KEY_LENGTH}" -lt 2048 ]; then
error "User key must be at least 2048 bits long."
fi
OPENSSL_BIN=$(command -v openssl)
SUFFIX=$(TZ=:Zulu /bin/date +"%Y%m%d%H%M%SZ")
if ! is_user "${PKI_USER}" || ! is_group "${PKI_USER}"; then
error "You must create ${PKI_USER} user and group !"
fi
if [ ! -e "${CONF_FILE}" ]; then
error "${CONF_FILE} is missing"
fi
mkdir -p "${CA_DIR}" "${CRT_DIR}" "${KEY_DIR}" "${CSR_DIR}" "${PKCS12_DIR}" "${OVPN_DIR}" "${TMP_DIR}"
command=${1:-help}
case "${command}" in
init)
shift
init "$@"
;;
ocsp)
shift
ocsp "$@"
;;
create)
shift
create "$@"
;;
revoke)
shift
revoke "$@"
;;
list)
shift
list "$@"
;;
check)
shift
check "$@"
;;
version|--version)
show_version
exit 0
;;
help|--help)
show_usage
exit 0
;;
*)
show_usage >&2
exit 1
;;
esac
# fix right
chown -R "${PKI_USER}":"${PKI_USER}" "${CA_DIR}"
chmod 750 "${CA_DIR}" "${CRT_DIR}" "${KEY_DIR}" "${CSR_DIR}" "${PKCS12_DIR}" "${OVPN_DIR}" "${TMP_DIR}"
chmod 600 "${INDEX_FILE}"* "${SERIAL}"* "${CA_KEY}" "${CRL}"
chmod 640 "${CA_CERT}"
}
main "$@"

View file

@ -1,239 +0,0 @@
#!/bin/sh
PREFIX=/etc/openvpn/ssl
CONFFILE=$PREFIX/openssl.cnf
OPENSSL=$(which openssl)
TIMESTAMP=$(/bin/date +"%s")
WWWDIR=/var/www/htdocs/vpn/ssl
if [ "$(id -u)" != "0" ]; then
echo "Please become root before running ${0##*/}!" >&2
echo >&2
echo "Press return to continue..." >&2
read REPLY
exit 1
fi
init() {
echo "Do you confirm ${0##*/} initialization?"
echo
echo "Press return to continue..."
read REPLY
echo
if [ ! -d $PREFIX/ca ]; then mkdir -p $PREFIX/ca; fi
if [ ! -d $PREFIX/ca/tmp ]; then mkdir -p $PREFIX/ca/tmp; fi
if [ ! -d $PREFIX/certs ]; then mkdir -p $PREFIX/certs; fi
if [ ! -d $PREFIX/files ]; then mkdir -p $PREFIX/files; fi
if [ ! -f $PREFIX/ca/index.txt ]; then touch $PREFIX/ca/index.txt; fi
if [ ! -f $PREFIX/files/ca/serial ]; then echo 01 > $PREFIX/ca/serial; fi
if [ ! -e "$CONFFILE" ]; then
echo "$CONFFILE is missing" >&2
echo >&2
echo "Press return to continue..." >&2
read REPLY
exit 1
fi
$OPENSSL dhparam -out $PREFIX/ca/dh2048.pem 2048
$OPENSSL genrsa -out $PREFIX/ca/private.key 2048
$OPENSSL req \
-config $CONFFILE \
-new -x509 -days 3650 \
-extensions v3_ca \
-keyout $PREFIX/ca/private.key \
-out $PREFIX/ca/cacert.pem
}
create() {
echo "Please enter your CN (Common Name)"
read cn
echo
echo "Your CN is '$cn'"
echo "Press return to continue..."
read REPLY
echo
if [ -e $PREFIX/certs/$cn.crt ]; then
echo "Please revoke actual $cn cert before creating one"
echo
echo "Press return to continue..."
read REPLY
exit 1
fi
DIR=$PREFIX/files/$cn-$TIMESTAMP
mkdir $DIR
# generate private key
echo -n "Should private key be protected by a passphrase? [y/N] "
read REPLY
if [ "$REPLY" = "y" ] || [ "$REPLY" = "Y" ]; then
$OPENSSL genrsa -aes128 -out $DIR/$cn.key 2048
else
$OPENSSL genrsa -out $DIR/$cn.key 2048
fi
# generate csr req
$OPENSSL req \
-new \
-key $DIR/$cn.key \
-config $CONFFILE \
-out $DIR/$cn.csr
# ca sign and generate cert
$OPENSSL ca \
-config $CONFFILE \
-in $DIR/$cn.csr \
-out $DIR/$cn.crt
# pem cert style
cp $DIR/$cn.key $DIR/$cn.pem
cat $DIR/$cn.crt >> $DIR/$cn.pem
# copy to public certs dir
if [ -d "$WWWDIR" ]; then
echo
echo "copy cert to public certs dir"
echo
cp -i $DIR/$cn.crt $PREFIX/certs/
cp -i $DIR/$cn.crt $WWWDIR/
cp -i $DIR/$cn.key $WWWDIR/
chown -R root:www $WWWDIR
chmod -R u=rwX,g=rwX,o= $WWWDIR
echo
fi
# generate client configuration
if [ -e $PREFIX/template.conf ]; then
CA=/etc/openvpn/ssl/ca/cacert.pem
CERT=/var/www/htdocs/vpn/ssl/$cn.crt
KEY=/var/www/htdocs/vpn/ssl/$cn.key
REP=/tmp
cp $PREFIX/template.conf $REP/$cn.conf
echo "
<ca>
$(cat $CA)
</ca>
<cert>
$(cat $CERT)
</cert>
<key>
$(cat $KEY)
</key>
" >> $REP/$cn.conf
echo "The configuration file is available in $REP/$cn.conf"
fi
}
revoke() {
echo "Please enter CN (Common Name) to revoke"
read cn
echo
echo "CN '$cn' will be revoked"
echo "Press return to continue..."
read REPLY
echo
$OPENSSL ca \
-config $CONFFILE \
-revoke $PREFIX/certs/$cn.crt
rm -i $PREFIX/certs/$cn.crt
if [ -d "$WWWDIR" ]; then
rm -i $WWWDIR/$cn.crt
rm -i $WWWDIR/$cn.key
fi
}
fromcsr() {
echo "Please enter path for your CSR request file"
read path
echo
if [ ! -e $path ]; then
echo "Error in path..." >&2
echo >&2
echo "Press return to continue..." >&2
read REPLY
exit 1
fi
echo "Please enter the CN (Common Name)"
read cn
echo
echo "Your CN is '$cn'"
echo "Press return to continue..."
read REPLY
echo
DIR=$PREFIX/files/req_$cn-$TIMESTAMP
mkdir $DIR
cp $path $DIR
# ca sign and generate cert
$OPENSSL ca \
-config $CONFFILE \
-in $path \
-out $DIR/$cn.crt
# copy to public certs dir
echo
echo "copy cert to public certs dir"
echo
cp -i $DIR/$cn.crt $PREFIX/certs/
echo
}
crl() {
$OPENSSL ca -gencrl \
-config $CONFFILE \
-out crl.pem
# TODO : a voir pour l'importation pdts Mozilla, Apple et Microsoft
#openssl crl2pkcs7 -in crl.pem -certfile /etc/ssl/certs/cacert.pem -out p7.pem
}
case "$1" in
init)
init
;;
create)
create
;;
fromcsr)
fromcsr
;;
revoke)
revoke
;;
crl)
crl
;;
*)
echo "Usage: ${0##*/} {init|create|fromcsr|revoke|crl}" >&2
exit 1
;;
esac