pax_global_header00006660000000000000000000000064145251661650014525gustar00rootroot0000000000000052 comment=56ed310a55d24f8d7713c69738dd720702ec54c3 secsipidx-1.3.2/000077500000000000000000000000001452516616500135235ustar00rootroot00000000000000secsipidx-1.3.2/.github/000077500000000000000000000000001452516616500150635ustar00rootroot00000000000000secsipidx-1.3.2/.github/FUNDING.yml000066400000000000000000000000221452516616500166720ustar00rootroot00000000000000github: [miconda] secsipidx-1.3.2/.github/workflows/000077500000000000000000000000001452516616500171205ustar00rootroot00000000000000secsipidx-1.3.2/.github/workflows/go.yml000066400000000000000000000005151452516616500202510ustar00rootroot00000000000000name: Go on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.21 - name: Test run: cd secsipid && GO_TEST_ALL=on go test -v secsipidx-1.3.2/.github/workflows/release.yaml000066400000000000000000000013501452516616500214230ustar00rootroot00000000000000on: release: types: [created] jobs: releases-matrix: name: Release Go Binary runs-on: ubuntu-latest strategy: matrix: # build and publish in parallel: linux/386, linux/amd64, linux/arm64, windows/386, windows/amd64, darwin/amd64, darwin/arm64 goos: [linux, windows, darwin] goarch: ["386", amd64, arm64] exclude: - goarch: "386" goos: darwin - goarch: arm64 goos: windows steps: - uses: actions/checkout@v4 - uses: wangyoucao577/go-release-action@v1.40 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} extra_files: LICENSE README.md secsipidx-1.3.2/.gitignore000066400000000000000000000010651452516616500155150ustar00rootroot00000000000000# Binaries for programs and plugins secsipidx *.exe *.exe~ *.dll *.so *.dylib *.so.* *.o *.a # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # tags tags TAGS .tags* # vi swaps .*.swp .*.swo # ignore dependency files *.d # Emacs backup files *~ # Emacs file locks .#* # Emacs desktop files .emacs.desktop* # MacOSX auto-generated files libiname.lst *.dylib .DS_Store # man pages *.7 # vscode tmp files .vscode secsipidx-1.3.2/LICENSE000066400000000000000000000032271452516616500145340ustar00rootroot00000000000000The Clear BSD License Copyright (c) 2019-2020 asipto.com All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. secsipidx-1.3.2/Makefile000066400000000000000000000017221452516616500151650ustar00rootroot00000000000000# Makefile to build the app and libs # include Makefile.defs .PHONY: all all: tool lib .PHONY: tool tool: GO111MODULE=${GO111MODVAL} ${GO} build -o ${TOOLNAME} . .PHONY: lib lib: GO111MODULE=${GO111MODVAL} $(MAKE) -C csecsipid/ libso GO111MODULE=${GO111MODVAL} $(MAKE) -C csecsipid/ liba .PHONY: install-tool install-tool: cp ${TOOLNAME} ${DESTDIR}${PREFIX}/bin/ .PHONY: install-lib install-lib: $(MAKE) -C csecsipid/ install-libso $(MAKE) -C csecsipid/ install-liba .PHONY: install-dev install-dev: $(MAKE) -C csecsipid/ install-dev .PHONY: install-lib-all install-lib-all: install-lib install-dev .PHONY: install-dirs install-dirs: mkdir -p ${DESTDIR}${PREFIX} mkdir -p ${DESTDIR}${PREFIX}/bin mkdir -p ${DESTDIR}${PREFIX}/lib mkdir -p ${DESTDIR}${PREFIX}/lib/pkgconfig mkdir -p ${DESTDIR}${PREFIX}/include .PHONY: install install: install-dirs install-tool install-lib install-dev .PHONY: clean clean: rm -rf ${TOOLNAME} $(MAKE) -C csecsipid/ clean secsipidx-1.3.2/Makefile.defs000066400000000000000000000006031452516616500161020ustar00rootroot00000000000000# Common defines for Makefile build system # override makefile_defs_included:=1 PREFIX ?= /usr/local BASENAME ?= secsipid TOOLNAME ?= secsipidx LIBVERMAJ ?= 1 LIBVERMIN ?= 3 LIBVERLEV ?= 2 LIBVERSION ?= ${LIBVERMAJ}.${LIBVERMIN}.${LIBVERLEV} GO111MODVAL ?= on GO ?= go OS := $(shell uname -s | sed -e s/SunOS/solaris/ -e s/CYGWIN.*/cygwin/ \ | tr "[A-Z]" "[a-z]" | tr "/" "_") secsipidx-1.3.2/README.md000066400000000000000000000221771452516616500150130ustar00rootroot00000000000000# secsipidx # Secure SIP/Telephony Identity Extensions. Last Version: 1.3.2 Release: Nov 15, 2023 ## Overview ## Applications and libraries implementing STIR/SHAKEN (RFC8224, RFC8588), used in SIP/VoIP services: * RFC8224 - https://tools.ietf.org/html/rfc8224 * RFC8588 - https://tools.ietf.org/html/rfc8588 Components: * `secsipid`: Go STIR/SHAKEN library - common functions for STIR/SHAKEN * `csecsipid`: C STIR/SHAKEN ibrary - wrapper code to build dynamic or static library and .h include files * `secsipidx`: `main.go` - CLI tool and HTTP API server for checking or building SIP identity ## secsipidx ## ### Installation ### Install Go language (golang), guidelines at: * https://golang.org * https://golang.org/doc/install **Note**: When using some specific Go versions (e.g., 1.16), it is necessary to set the environment variable `GO111MODULE` to `off` prior to executing the `go get` or `make` commands. Its value can be specified via `GO111MODVAL` variable for `Makefile`, which is defined inside `Makefile.defs`. It can be also set in the command line, like: ``` GO111MODVAL=off make ``` Deploy the `secsipidx` tool with: ``` go get github.com/asipto/secsipidx ``` The tool is located in `$GOPATH/bin/secsipidx`. If you want to build and run locally: ``` go get -d github.com/asipto/secsipidx cd $GOPATH/src/github.com/asipto/secsipidx/ go build # run from local folder # ./secsipidx ... ``` Install using the `make` command: ``` go get -d github.com/asipto/secsipidx cd $GOPATH/src/github.com/asipto/secsipidx/ make make install ``` The `secsipidx` tool is deployed to `/usr/local/bin/`. The `make install` deploys also the libraries and `C` headers. ## Usage ## To see the available command line options, run: ``` secsipidx -h ``` ### Keys Generation ## The `openssl` tool needs to be installed. The following commands can be used to generate the private and public keys: ``` openssl ecparam -name prime256v1 -genkey -noout -out ec256-private.pem openssl ec -in ec256-private.pem -pubout -out ec256-public.pem ``` ### Usage ### #### CLI - Generate Full Identity Header #### A call from `+493044448888` to `+493055559999` with attestation level `A`, when the public key can be downloaded from `http://asipto.lab/stir/cert.pem`: ``` secsipidx -sign-full -orig-tn 493044448888 -dest-tn 493055559999 -attest A -x5u http://asipto.lab/stir/cert.pem -k ec256-private.pem ``` #### CLI - Check Full Identity Header #### Check the identity header stored in file `identity.txt` using the public key in file `ec256-public.pem` with token expire of 3600 seconds ``` secsipidx -check -fidentity identity.txt -fpubkey ec256-public.pem -expire 3600 ``` #### HTTP Server #### Run `secsipidx` as an HTTP server listening on port `8090` for checking SIP identity with public key from file `ec256-public.pem`: ``` secsipidx -http-srv ":8090" -http-dir /secsipidx/http/public -fprvkey ec256-private.pem -fpubkey ec256-public.pem -expire 3600 -timeout 5 ``` To run `secsipidx` as an HTTPS server on port `8093`, following command line parameters have to be provided: ``` secsipidx -https-srv ":8093" -https-pubkey /keys/secsipidx-public.key -https-prvkey /keys/secsipidx-private.key ... ``` The TLS certificate (public and private keys) can be obtained from services like `Let's Encrypt` or use self-generated ones: ``` openssl genrsa -out secsipidx-private.key 2048 openssl ecparam -genkey -name secp384r1 -out secsipidx-private.key openssl req -new -x509 -sha256 -key secsipidx-private.key -out secsipidx-public.key -days 365 ``` ##### Check Identity ##### If the identity header body is saved in the file `identity.txt`, the next command can be used to check it: ``` curl --data @identity.txt http://127.0.0.1:8090/v1/check ``` If `secsipidx` is started without `-fpubkey` or `-pubkey`, then the public key to check the signature is downloaded from `x5u` URL (or the header `info` parameter). The value of `-timeout` parameter is used to limit the download time of the public key via HTTP. ##### Generate Identity - CSV API ##### Prototype: ``` curl --data 'OrigTN,DestTN,ATTEST,OrigID,X5U' http://127.0.0.1:8090/v1/sign-csv ``` If `OrigID` is missing, then a `UUID` value is generated internally. Example to get the `Identity` header value: ``` curl --data '493044442222,493088886666,A,,https://asipto.lab/v1/pub/cert.pem' http://127.0.0.1:8090/v1/sign-csv ``` ##### HTTP File Server ##### When started with parameter `-httpdir`, the `secsipidx` servers the files from the respective directory on the URL path `/v1/pub/`. ### Certificate Verification ### The certificate retrieved from peers can be verified against system CAs or a list of CAs stored in a file. The path to custom CAs files can be set via `--ca-file` and `--ca-inter` parameters. The verification mode can be set via `--cert-verify` parameter, which represents an integer value build from the bit flags: * `1` (`1<<0`) - verify time validity (not expired and not before validity date) * `2` (`1<<1`) - verify against system root CAs * `4` (`1<<2`) - verify against custom root CAs in the file specified by `--ca-file` * `8` (`1<<3`) - verify against custom intermediate CAs in the file specified by `--ca-inter` * `16` (`1<<4`) - verify against certificate revocation list The value can be combined, so `--cert-verify 7` means that the verification is done against system room CAs and the custom CAs in the file specified by `--ca-file`, together with time validity checks. If `--cert-verify` is `0`, no verification is performed. ## Certificate Caching ## There is support for a basic caching mechanism of the public keys in local files. It can be activated by giving `-cache-dir /path/to/cachedir` cli parameter, how long the cached value is considered valid can be controlled with `-cache-expire`. The C library exports now: ```c int SecSIPIDSetFileCacheOptions(char* dirPath, int expireVal); ``` which can be used to set the two values (the cache dir activates the caching mechanism). The name of the file in the cache directory is created from URL replacing first `://` with `_` and then the rest of `/` also with `_` -- I went this way instead of hashing (or encoding) the url to be human readable. Last modified time of the file is used to determine when the value is considered expired. Kamailio `secsipid` module was also enhanced with two new parameters to set the cache dir and expire values. There is no locking/synchronization on accessing (read/write) cache files for the moment, this can be done externally, for example with Kamailio by using `cfgutils` module: ```c $var(url) = $(hdr(Identity){s.rmws}{param.value,info}{s.unbracket}); lock("$var(url)"); if(secsipid_check_identity("")) { ... } unlock("$var(url)"); ``` ## C API ## The code to get the `C` library is located in the `csecsipid` directory. To generate the `.h` and static library files, run inside the directory: ``` make liba ``` Then the `*.h` and `libsecsipid.a` files can be copied to the folder where it is wanted to be used. The library is used by `secsipid` and `secsipid_proc` modules of Kamailio SIP Server (https://www.kamailio.org): * https://www.kamailio.org/docs/modules/devel/modules/secsipid.html * https://www.kamailio.org/docs/modules/devel/modules/secsipid_proc.html The prototype of functions exported to `C` API and documentation are int the file: * https://github.com/asipto/secsipidx/blob/main/csecsipid/libsecsipid.h ### C Library Options ### The library options that can be set with `SecSIPIDOptSetS()`, `SecSIPIDOptSetN()` or `SecSIPIDOptSetV()`: * `CacheDirPath` (str) - the path to the folder where to store cached certificates that are downloaded from peers * `CacheExpires` (int) - number of seconds after which cached certificates are invalidated * `CertVerify` (int) - the certification verification mode, see the section `Certificate Verification` above * `CertCAFile` (str) - the path with the custom root CA certificates * `CertCAInter` (str) - the path with the custom intermediate CA certificates * `CertCRLFile` (str) - the path with the certificate revocation list ## To-Do ## * external cache (e.g., use of Redis) of downloaded public keys used to verify Identity signatures * blacklisting of unresponsive x5u URLs * support more data formats for HTTP API (e.g., JSON for generating Identity) * configuration file ## Copyright ## License: `BSD 3-Clause Clear License` Copyright: © 2020-2021 asipto.com ## Contributing ## Bug reports and requests for new features have to be filled to: * https://github.com/asipto/secsipidx/issues Code contributions have to be made via pull requests at: * https://github.com/asipto/secsipidx/pulls The code of the pull requests is considered to be licensed under BSD license, unless explicitly requested to be a different license and agreed by the developers before merging. ## Testing ## To test the secsipid library, `cd` into `secsipid/` and run: ```bash go test -v ``` Some of the unit tests take multiple seconds because they're forcing a time out or waiting for something to expire. These tests are skipped by default. To run all tests, including these longer ones, set the environemnt variable: `GO_TEST_ALL` to `on`: ```bash GO_TEST_ALL=on go test -v ``` secsipidx-1.3.2/csecsipid/000077500000000000000000000000001452516616500154715ustar00rootroot00000000000000secsipidx-1.3.2/csecsipid/Makefile000066400000000000000000000025031452516616500171310ustar00rootroot00000000000000# Makefile to build libraries # include ../Makefile.defs SOBASENAME=libsecsipid.so SOLIBNAME=${SOBASENAME}.${LIBVERSIONMAJOR} SOREALNAME=${SOBASENAME}.${LIBVERSION} ifneq ($(OS), darwin) LD_FLAGS += -extldflags=-Wl,-soname,${SOLIBNAME} LD_OPTS = -ldflags="${LD_FLAGS}" else LD_OPTS = endif ABASENAME=libsecsipid.a .PHONY: all all: libso .PHONY: libso libso: rm -rf ${SOLIBNAME} rm -rf ${SOBASENAME} rm -rf ${SOREALNAME} GO111MODULE=${GO111MODVAL} go build ${LD_OPTS} -o ${SOBASENAME} -buildmode=c-shared csecsipid.go mv ${SOBASENAME} ${SOREALNAME} ln -s ${SOREALNAME} ${SOLIBNAME} ln -s ${SOREALNAME} ${SOBASENAME} .PHONY: liba liba: GO111MODULE=${GO111MODVAL} go build -o ${ABASENAME} -buildmode=c-archive csecsipid.go .PHONY: install-libso install-libso: cp ${SOREALNAME} ${DESTDIR}${PREFIX}/lib/ ln -fs ${SOREALNAME} ${DESTDIR}${PREFIX}/lib/${SOBASENAME} ln -fs ${SOREALNAME} ${DESTDIR}${PREFIX}/lib/${SOLIBNAME} .PHONY: install-liba install-liba: cp ${ABASENAME} ${DESTDIR}${PREFIX}/lib/ .PHONY: install-dev install-dev: cp secsipid-1.pc ${DESTDIR}${PREFIX}/lib/pkgconfig/ cp libsecsipid.h ${DESTDIR}${PREFIX}/include/ cp secsipid.h ${DESTDIR}${PREFIX}/include/ .PHONY: install install: install-libso install-liba install-dev .PHONY: clean clean: rm -f ${SOBASENAME} ${SOREALNAME} ${SOLIBNAME} rm -f ${ABASENAME} secsipidx-1.3.2/csecsipid/csecsipid.go000066400000000000000000000214731452516616500177750ustar00rootroot00000000000000package main import "C" import ( "github.com/asipto/secsipidx/secsipid" ) // SecSIPIDSignJSONHP -- // * sign the JSON header and payload with provided private key file path // * headerJSON - header part in JSON forman (0-terminated string) // * payloadJSON - payload part in JSON forman (0-terminated string) // * prvkeyPath - path to private key to be used to generate the signature // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * return: the length of `*outPtr` //export SecSIPIDSignJSONHP func SecSIPIDSignJSONHP(headerJSON *C.char, payloadJSON *C.char, prvkeyPath *C.char, outPtr **C.char) C.int { signature, _, _ := secsipid.SJWTEncodeText(C.GoString(headerJSON), C.GoString(payloadJSON), C.GoString(prvkeyPath)) *outPtr = C.CString(signature) return C.int(len(signature)) } // SecSIPIDSignJSONHPPrvKey -- // * sign the JSON header and payload with provided private key data // * headerJSON - header part in JSON forman (0-terminated string) // * payloadJSON - payload part in JSON forman (0-terminated string) // * prvkeyData - private key data to be used to generate the signature // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * return: the length of `*outPtr` //export SecSIPIDSignJSONHPPrvKey func SecSIPIDSignJSONHPPrvKey(headerJSON *C.char, payloadJSON *C.char, prvkeyData *C.char, outPtr **C.char) C.int { signature, _, _ := secsipid.SJWTEncodeTextWithPrvKey(C.GoString(headerJSON), C.GoString(payloadJSON), C.GoString(prvkeyData)) *outPtr = C.CString(signature) return C.int(len(signature)) } // SecSIPIDGetIdentity -- // Generate the Identity header content using the input attributes // * origTN - calling number // * destTN - called number // * attestVal - attestation level // * origID - unique ID for tracking purposes, if empty string a UUID is generated // * x5uVal - location of public certificate // * prvkeyPath - path to private key to be used to generate the signature // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * return: the length of `*outPtr` on success or error return code (< 0) //export SecSIPIDGetIdentity func SecSIPIDGetIdentity(origTN *C.char, destTN *C.char, attestVal *C.char, origID *C.char, x5uVal *C.char, prvkeyPath *C.char, outPtr **C.char) C.int { signature, ret, _ := secsipid.SJWTGetIdentity(C.GoString(origTN), C.GoString(destTN), C.GoString(attestVal), C.GoString(origID), C.GoString(x5uVal), C.GoString(prvkeyPath)) *outPtr = C.CString(signature) if ret < 0 { return C.int(ret) } return C.int(len(signature)) } // SecSIPIDGetIdentityPrvKey -- // Generate the Identity header content using the input attributes // * origTN - calling number // * destTN - called number // * attestVal - attestation level // * origID - unique ID for tracking purposes, if empty string a UUID is generated // * x5uVal - location of public certificate // * prvkeyData - content of private key to be used to generate the signature // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * return: the length of `*outPtr` on success or error return code (< 0) //export SecSIPIDGetIdentityPrvKey func SecSIPIDGetIdentityPrvKey(origTN *C.char, destTN *C.char, attestVal *C.char, origID *C.char, x5uVal *C.char, prvkeyData *C.char, outPtr **C.char) C.int { signature, ret, _ := secsipid.SJWTGetIdentityPrvKey(C.GoString(origTN), C.GoString(destTN), C.GoString(attestVal), C.GoString(origID), C.GoString(x5uVal), []byte(C.GoString(prvkeyData))) *outPtr = C.CString(signature) if ret < 0 { return C.int(ret) } return C.int(len(signature)) } // SecSIPIDCheck -- // check the Identity header value // * identityVal - identity header value // * identityLen - length of identityVal, if is 0, identityVal is expected // to be 0-terminated // * expireVal - number of seconds until the validity is considered expired // * pubkeyPath - file path or URL to public key // * timeoutVal - timeout in seconds to try to fetch the public key via HTTP // * return: 0 - if validity is ok; <0 - on error or validity is not ok //export SecSIPIDCheck func SecSIPIDCheck(identityVal *C.char, identityLen C.int, expireVal C.int, pubkeyPath *C.char, timeoutVal C.int) C.int { var sIdentity string if identityLen == 0 { sIdentity = C.GoString(identityVal) } else { sIdentity = C.GoStringN(identityVal, identityLen) } ret, _ := secsipid.SJWTCheckIdentity(sIdentity, int(expireVal), C.GoString(pubkeyPath), int(timeoutVal)) return C.int(ret) } // SecSIPIDCheckFull -- //export SecSIPIDCheckFull // check the Identity header value // * identityVal - identity header value with header parameters // * identityLen - length of identityVal, if it is 0, identityVal is expected // to be 0-terminated // * expireVal - number of seconds until the validity is considered expired // * pubkeyPath - file path or URL to public key // * timeoutVal - timeout in seconds to try to fetch the public key via HTTP // * return: 0 - if validity is ok; <0 - on error or validity is not ok func SecSIPIDCheckFull(identityVal *C.char, identityLen C.int, expireVal C.int, pubkeyPath *C.char, timeoutVal C.int) C.int { var sIdentity string if identityLen == 0 { sIdentity = C.GoString(identityVal) } else { sIdentity = C.GoStringN(identityVal, identityLen) } ret, _ := secsipid.SJWTCheckFullIdentity(sIdentity, int(expireVal), C.GoString(pubkeyPath), int(timeoutVal)) return C.int(ret) } // SecSIPIDCheckFullPubKey -- // check the Identity header value // * identityVal - identity header value with header parameters // * identityLen - length of identityVal, if it is 0, identityVal is expected // to be 0-terminated // * expireVal - number of seconds until the validity is considered expired // * pubkeyVal - the value of the public key // * pubkeyLen - the length of the public key, if it is 0, then the pubkeyVal // is expected to be 0-terminated // * return: 0 - if validity is ok; <0 - on error or validity is not ok //export SecSIPIDCheckFullPubKey func SecSIPIDCheckFullPubKey(identityVal *C.char, identityLen C.int, expireVal C.int, pubkeyVal *C.char, pubkeyLen C.int) C.int { var sIdentity string var sPubKeyVal string if identityLen == 0 { sIdentity = C.GoString(identityVal) } else { sIdentity = C.GoStringN(identityVal, identityLen) } if pubkeyLen == 0 { sPubKeyVal = C.GoString(pubkeyVal) } else { sPubKeyVal = C.GoStringN(pubkeyVal, pubkeyLen) } ret, _ := secsipid.SJWTCheckFullIdentityPubKey(sIdentity, int(expireVal), sPubKeyVal) return C.int(ret) } // SecSIPIDSetFileCacheOptions -- // set the options for local file caching of public keys // * dirPath - path to local directory where to store the files // * expireVal - number of the seconds after which to invalidate the cached file // * return: 0 //export SecSIPIDSetFileCacheOptions func SecSIPIDSetFileCacheOptions(dirPath *C.char, expireVal C.int) C.int { secsipid.SetURLFileCacheOptions(C.GoString(dirPath), int(expireVal)) return C.int(0) } // SecSIPIDGetURLContent -- // get the content of an URL // * urlVal - the HTTP or HTTPS URL // * timeoutVal - timeout in seconds to try to get the content of the HTTP URL // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * outLen: to be set to the length of `*outPtr` // * return: 0 - on success; -1 - on failure //export SecSIPIDGetURLContent func SecSIPIDGetURLContent(urlVal *C.char, timeoutVal C.int, outPtr **C.char, outLen *C.int) C.int { content, ret, _ := secsipid.SJWTGetURLContent(C.GoString(urlVal), int(timeoutVal)) if content != nil { *outPtr = C.CString(string(content)) *outLen = C.int(len(string(content))) return C.int(0) } return C.int(ret) } // SecSIPIDOptSetS -- // set a string option for the library // * optName - name of the option // * optVal - value of the option // * return: 0 if option was set, -1 otherwise //export SecSIPIDOptSetS func SecSIPIDOptSetS(optName *C.char, optVal *C.char) C.int { ret := secsipid.SJWTLibOptSetS(C.GoString(optName), C.GoString(optVal)) return C.int(ret) } // SecSIPIDOptSetN -- // set a number (integer) option for the library // * optName - name of the option // * optVal - value of the option // * 0 if option was set, -1 otherwise //export SecSIPIDOptSetN func SecSIPIDOptSetN(optName *C.char, optVal C.int) C.int { ret := secsipid.SJWTLibOptSetN(C.GoString(optName), int(optVal)) return C.int(ret) } // SecSIPIDOptSetV -- // set an option for the library // * optNameVal - string with name=value of the option // * 0 if option was set, -1 otherwise //export SecSIPIDOptSetV func SecSIPIDOptSetV(optNameVal *C.char) C.int { ret := secsipid.SJWTLibOptSetV(C.GoString(optNameVal)) return C.int(ret) } // func main() {} secsipidx-1.3.2/csecsipid/libsecsipid.h000066400000000000000000000167361452516616500201510ustar00rootroot00000000000000/* Code generated by cmd/cgo; DO NOT EDIT. */ /* package command-line-arguments */ #line 1 "cgo-builtin-export-prolog" #include #ifndef GO_CGO_EXPORT_PROLOGUE_H #define GO_CGO_EXPORT_PROLOGUE_H #ifndef GO_CGO_GOSTRING_TYPEDEF typedef struct { const char *p; ptrdiff_t n; } _GoString_; #endif #endif /* Start of preamble from import "C" comments. */ /* End of preamble from import "C" comments. */ /* Start of boilerplate cgo prologue. */ #line 1 "cgo-gcc-export-header-prolog" #ifndef GO_CGO_PROLOGUE_H #define GO_CGO_PROLOGUE_H typedef signed char GoInt8; typedef unsigned char GoUint8; typedef short GoInt16; typedef unsigned short GoUint16; typedef int GoInt32; typedef unsigned int GoUint32; typedef long long GoInt64; typedef unsigned long long GoUint64; typedef GoInt64 GoInt; typedef GoUint64 GoUint; typedef size_t GoUintptr; typedef float GoFloat32; typedef double GoFloat64; #ifdef _MSC_VER #include typedef _Fcomplex GoComplex64; typedef _Dcomplex GoComplex128; #else typedef float _Complex GoComplex64; typedef double _Complex GoComplex128; #endif /* static assertion to make sure the file is being used on architecture at least with matching size of GoInt. */ typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; #ifndef GO_CGO_GOSTRING_TYPEDEF typedef _GoString_ GoString; #endif typedef void *GoMap; typedef void *GoChan; typedef struct { void *t; void *v; } GoInterface; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; #endif /* End of boilerplate cgo prologue. */ #ifdef __cplusplus extern "C" { #endif // SecSIPIDSignJSONHP -- // * sign the JSON header and payload with provided private key file path // * headerJSON - header part in JSON forman (0-terminated string) // * payloadJSON - payload part in JSON forman (0-terminated string) // * prvkeyPath - path to private key to be used to generate the signature // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * return: the length of `*outPtr` extern int SecSIPIDSignJSONHP(char* headerJSON, char* payloadJSON, char* prvkeyPath, char** outPtr); // SecSIPIDSignJSONHPPrvKey -- // * sign the JSON header and payload with provided private key data // * headerJSON - header part in JSON forman (0-terminated string) // * payloadJSON - payload part in JSON forman (0-terminated string) // * prvkeyData - private key data to be used to generate the signature // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * return: the length of `*outPtr` extern int SecSIPIDSignJSONHPPrvKey(char* headerJSON, char* payloadJSON, char* prvkeyData, char** outPtr); // SecSIPIDGetIdentity -- // Generate the Identity header content using the input attributes // * origTN - calling number // * destTN - called number // * attestVal - attestation level // * origID - unique ID for tracking purposes, if empty string a UUID is generated // * x5uVal - location of public certificate // * prvkeyPath - path to private key to be used to generate the signature // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * return: the length of `*outPtr` on success or error return code (< 0) extern int SecSIPIDGetIdentity(char* origTN, char* destTN, char* attestVal, char* origID, char* x5uVal, char* prvkeyPath, char** outPtr); // SecSIPIDGetIdentityPrvKey -- // Generate the Identity header content using the input attributes // * origTN - calling number // * destTN - called number // * attestVal - attestation level // * origID - unique ID for tracking purposes, if empty string a UUID is generated // * x5uVal - location of public certificate // * prvkeyData - content of private key to be used to generate the signature // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * return: the length of `*outPtr` on success or error return code (< 0) extern int SecSIPIDGetIdentityPrvKey(char* origTN, char* destTN, char* attestVal, char* origID, char* x5uVal, char* prvkeyData, char** outPtr); // SecSIPIDCheck -- // check the Identity header value // * identityVal - identity header value // * identityLen - length of identityVal, if is 0, identityVal is expected // to be 0-terminated // * expireVal - number of seconds until the validity is considered expired // * pubkeyPath - file path or URL to public key // * timeoutVal - timeout in seconds to try to fetch the public key via HTTP // * return: 0 - if validity is ok; <0 - on error or validity is not ok extern int SecSIPIDCheck(char* identityVal, int identityLen, int expireVal, char* pubkeyPath, int timeoutVal); // SecSIPIDCheckFull -- // check the Identity header value // * identityVal - identity header value with header parameters // * identityLen - length of identityVal, if it is 0, identityVal is expected // to be 0-terminated // * expireVal - number of seconds until the validity is considered expired // * pubkeyPath - file path or URL to public key // * timeoutVal - timeout in seconds to try to fetch the public key via HTTP // * return: 0 - if validity is ok; <0 - on error or validity is not ok extern int SecSIPIDCheckFull(char* identityVal, int identityLen, int expireVal, char* pubkeyPath, int timeoutVal); // SecSIPIDCheckFullPubKey -- // check the Identity header value // * identityVal - identity header value with header parameters // * identityLen - length of identityVal, if it is 0, identityVal is expected // to be 0-terminated // * expireVal - number of seconds until the validity is considered expired // * pubkeyVal - the value of the public key // * pubkeyLen - the length of the public key, if it is 0, then the pubkeyVal // is expected to be 0-terminated // * return: 0 - if validity is ok; <0 - on error or validity is not ok extern int SecSIPIDCheckFullPubKey(char* identityVal, int identityLen, int expireVal, char* pubkeyVal, int pubkeyLen); // SecSIPIDSetFileCacheOptions -- // set the options for local file caching of public keys // * dirPath - path to local directory where to store the files // * expireVal - number of the seconds after which to invalidate the cached file // * return: 0 extern int SecSIPIDSetFileCacheOptions(char* dirPath, int expireVal); // SecSIPIDGetURLContent -- // get the content of an URL // * urlVal - the HTTP or HTTPS URL // * timeoutVal - timeout in seconds to try to get the content of the HTTP URL // * outPtr - to be set to the pointer containing the output (it is a // 0-terminated string); the `*outPtr` must be freed after use // * outLen: to be set to the length of `*outPtr` // * return: 0 - on success; -1 - on failure extern int SecSIPIDGetURLContent(char* urlVal, int timeoutVal, char** outPtr, int* outLen); // SecSIPIDOptSetS -- // set a string option for the library // * optName - name of the option // * optVal - value of the option // * return: 0 if option was set, -1 otherwise extern int SecSIPIDOptSetS(char* optName, char* optVal); // SecSIPIDOptSetN -- // set a number (integer) option for the library // * optName - name of the option // * optVal - value of the option // * 0 if option was set, -1 otherwise extern int SecSIPIDOptSetN(char* optName, int optVal); // SecSIPIDOptSetV -- // set an option for the library // * optNameVal - string with name=value of the option // * 0 if option was set, -1 otherwise extern int SecSIPIDOptSetV(char* optNameVal); #ifdef __cplusplus } #endif secsipidx-1.3.2/csecsipid/secsipid-1.pc000066400000000000000000000003641452516616500177610ustar00rootroot00000000000000prefix=/usr exec_prefix=${prefix} libdir=${exec_prefix}/lib includedir=${prefix}/include Name: secsipid Description: libraries implementing STIR and SHAKEN (RFC8224, RFC8588) Version: 1.0.0 Libs: -L${libdir} -lsecsipid Cflags: -I${includedir} secsipidx-1.3.2/csecsipid/secsipid.h000066400000000000000000000005541452516616500174510ustar00rootroot00000000000000/* header file for secsipid library */ #ifndef __SECSIPID_H__ #define __SECSIPID_H__ #include "libsecsipid.h" /** * SECSIPID_VERSION = 0xAABBCC00 * - corresponds to version AA.BB.CC * - last two hexdigits are left for patch extensions * - example: 0x1020000 corresponds to version 1.2.0 * - now set to v1.3.0 */ #define SECSIPID_VERSION 0x1030000 #endif secsipidx-1.3.2/go.mod000066400000000000000000000001771452516616500146360ustar00rootroot00000000000000module github.com/asipto/secsipidx go 1.16 require ( github.com/gomagedon/expectate v1.1.0 github.com/google/uuid v1.4.0 ) secsipidx-1.3.2/go.sum000066400000000000000000000014171452516616500146610ustar00rootroot00000000000000github.com/gomagedon/expectate v1.1.0 h1:BhNJNdT1D/NG+3ZuCf+nn5CSsLAoxP/8vTx7WgI5fLI= github.com/gomagedon/expectate v1.1.0/go.mod h1:iynaHs97GMybvVZlkxTF7APDxJJKMLp/cte3lReN5A8= github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= secsipidx-1.3.2/main.go000066400000000000000000000370271452516616500150070ustar00rootroot00000000000000package main import ( "crypto/ecdsa" "encoding/json" "flag" "fmt" "io/ioutil" "log" "net/http" "os" "path/filepath" "strings" "time" "github.com/asipto/secsipidx/secsipid" ) const secsipidxVersion = "1.3.2" // CLIOptions - structure for command line options type CLIOptions struct { httpsrv string httpssrv string httpspubkey string httpsprvkey string httpdir string fprvkey string fpubkey string header string fheader string payload string fpayload string identity string fidentity string alg string ppt string typ string x5u string attest string desttn string origtn string iat int origid string check bool sign bool signfull bool jsonparse bool expire int timeout int ltest bool version bool cachedir string cacheexpire int cafile string cainter string crlfile string certverify int verbosity int } var cliops = CLIOptions{ httpsrv: "", httpssrv: "", httpspubkey: "", httpsprvkey: "", httpdir: "", fprvkey: "", fpubkey: "", header: "", fheader: "", payload: "", fpayload: "", identity: "", fidentity: "", alg: "ES256", ppt: "shaken", typ: "passport", x5u: "", attest: "C", desttn: "", origtn: "", iat: 0, origid: "", check: false, sign: false, signfull: false, jsonparse: false, expire: 0, timeout: 3, ltest: false, version: false, cachedir: "", cacheexpire: 3600, cafile: "", cainter: "", crlfile: "", certverify: 0, verbosity: 0, } // initialize application components func init() { // command line arguments flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s (v%s):\n", filepath.Base(os.Args[0]), secsipidxVersion) fmt.Fprintf(os.Stderr, " (some options have short and long version)\n") flag.PrintDefaults() os.Exit(1) } flag.StringVar(&cliops.httpsrv, "http-srv", cliops.httpsrv, "http server bind address") flag.StringVar(&cliops.httpsrv, "H", cliops.httpsrv, "http server bind address") flag.StringVar(&cliops.httpssrv, "https-srv", cliops.httpssrv, "https server bind address") flag.StringVar(&cliops.httpspubkey, "https-pubkey", cliops.httpspubkey, "https server public key") flag.StringVar(&cliops.httpsprvkey, "https-prvkey", cliops.httpsprvkey, "https server private key") flag.StringVar(&cliops.httpdir, "http-dir", cliops.httpdir, "directory to serve over http") flag.StringVar(&cliops.fprvkey, "fprvkey", cliops.fprvkey, "path to private key") flag.StringVar(&cliops.fprvkey, "k", cliops.fprvkey, "path to private key") flag.StringVar(&cliops.fpubkey, "fpubkey", cliops.fpubkey, "path to public key") flag.StringVar(&cliops.fpubkey, "p", cliops.fpubkey, "path to public key") flag.StringVar(&cliops.fheader, "fheader", cliops.fheader, "path to file with header value in JSON format") flag.StringVar(&cliops.header, "header", cliops.header, "header value in JSON format") flag.StringVar(&cliops.fpayload, "fpayload", cliops.fpayload, "path to file with payload value in JSON format") flag.StringVar(&cliops.payload, "payload", cliops.payload, "payload value in JSON format") flag.StringVar(&cliops.fidentity, "fidentity", cliops.fidentity, "path to file with identity value") flag.StringVar(&cliops.identity, "identity", cliops.identity, "identity value") flag.StringVar(&cliops.alg, "alg", cliops.alg, "encryption algorithm") flag.StringVar(&cliops.ppt, "ppt", cliops.ppt, "used extension") flag.StringVar(&cliops.typ, "typ", cliops.typ, "token type") flag.StringVar(&cliops.x5u, "x5u", cliops.x5u, "value of the field with the location of the certificate used to sign the token (default: '')") flag.StringVar(&cliops.attest, "attest", cliops.attest, "attestation level") flag.StringVar(&cliops.attest, "a", cliops.attest, "attestation level") flag.StringVar(&cliops.desttn, "dest-tn", cliops.desttn, "destination (called) number (default: '')") flag.StringVar(&cliops.desttn, "d", cliops.desttn, "destination (called) number (default: '')") flag.StringVar(&cliops.origtn, "orig-tn", cliops.origtn, "origination (calling) number (default: '')") flag.StringVar(&cliops.origtn, "o", cliops.origtn, "origination (calling) number (default: '')") flag.IntVar(&cliops.iat, "iat", cliops.iat, "timestamp when the token was created") flag.StringVar(&cliops.origid, "orig-id", cliops.origid, "origination identifier (default: '')") flag.BoolVar(&cliops.check, "check", cliops.check, "check validity of the signature") flag.BoolVar(&cliops.check, "c", cliops.check, "check validity of the signature") flag.BoolVar(&cliops.sign, "sign", cliops.sign, "sign the header and payload given as full JSON documents") flag.BoolVar(&cliops.sign, "s", cliops.sign, "sign the header and payload given as full JSON documents") flag.BoolVar(&cliops.signfull, "sign-full", cliops.sign, "sign the header and payload build from the individual parameter values") flag.BoolVar(&cliops.signfull, "S", cliops.sign, "sign the header and payload, with parameters") flag.BoolVar(&cliops.jsonparse, "json-parse", cliops.jsonparse, "parse and re-serialize JSON header and payload values") flag.IntVar(&cliops.expire, "expire", cliops.expire, "duration of token validity (in seconds)") flag.IntVar(&cliops.timeout, "timeout", cliops.timeout, "http get timeout (in seconds)") flag.BoolVar(&cliops.ltest, "ltest", cliops.ltest, "run local basic test") flag.BoolVar(&cliops.ltest, "l", cliops.ltest, "run local basic test") flag.BoolVar(&cliops.version, "version", cliops.version, "print version") flag.StringVar(&cliops.cachedir, "cache-dir", cliops.cachedir, "path to the directory with cached certificates (default: '')") flag.IntVar(&cliops.cacheexpire, "cache-expire", cliops.cacheexpire, "duration of cached certificates (in seconds)") flag.StringVar(&cliops.cafile, "ca-file", cliops.cafile, "file with root CA certificates in pem format") flag.StringVar(&cliops.cainter, "ca-inter", cliops.cainter, "file with intermediate CA certificates in pem format") flag.StringVar(&cliops.crlfile, "crl-file", cliops.crlfile, "file with CRL in pem format") flag.IntVar(&cliops.certverify, "cert-verify", cliops.certverify, "certificate verification mode (default 0)") flag.IntVar(&cliops.verbosity, "verbosity", cliops.verbosity, "verbosity level (default 0)") flag.IntVar(&cliops.verbosity, "vl", cliops.verbosity, "verbosity level (default 0)") } func localTest() { var err error header := secsipid.SJWTHeader{ Alg: "ES256", Ppt: "shaken", Typ: "passport", X5u: "https://certs.kamailio.org/stir-shaken/cert01.crt", } payload := secsipid.SJWTPayload{ ATTest: "A", Dest: secsipid.SJWTDest{ TN: []string{"493044444444"}, }, IAT: time.Now().Unix(), Orig: secsipid.SJWTOrig{ TN: "493055555555", }, OrigID: "32c7e392-33fc-11ea-840b-784f435c76a8", } prvkey, _ := ioutil.ReadFile("../test/certs/ec256-private.pem") var ecdsaPrvKey *ecdsa.PrivateKey if ecdsaPrvKey, _, err = secsipid.SJWTParseECPrivateKeyFromPEM(prvkey); err != nil { fmt.Printf("Unable to parse ECDSA private key: %v\n", err) return } pubkey, _ := ioutil.ReadFile("../test/certs/ec256-public.pem") var ecdsaPubKey *ecdsa.PublicKey if ecdsaPubKey, _, err = secsipid.SJWTParseECPublicKeyFromPEM(pubkey); err != nil { fmt.Printf("Unable to parse ECDSA public key: %v\n", err) return } token := secsipid.SJWTEncode(header, payload, ecdsaPrvKey) fmt.Printf("Result: %s\n", token) payloadOut, _ := secsipid.SJWTDecodeWithPubKey(token, cliops.expire, ecdsaPubKey) jsonPayload, _ := json.Marshal(payloadOut) fmt.Printf("Payload: %s\n", jsonPayload) headerJSON, _ := json.Marshal(header) payloadJSON, _ := json.Marshal(payload) signatureText, _, _ := secsipid.SJWTEncodeText(string(headerJSON), string(payloadJSON), "certs/ec256-private.pem") fmt.Printf("Signature: %s\n", signatureText) } func secsipidxCLISignFull() int { token, _, err := secsipid.SJWTGetIdentity(cliops.origtn, cliops.desttn, cliops.attest, cliops.origid, cliops.x5u, cliops.fprvkey) if err != nil { fmt.Printf("error: %v\n", err) return -1 } fmt.Printf("%s\n", token) return 0 } func secsipidxCLISign() int { var err error var useStruct bool var sHeader string var sPayload string var token string if len(cliops.fprvkey) <= 0 { fmt.Printf("path to private key not provided\n") return -1 } useStruct = false header := secsipid.SJWTHeader{} if len(cliops.fheader) > 0 { vHeader, _ := ioutil.ReadFile(cliops.fheader) if cliops.jsonparse { err = json.Unmarshal(vHeader, &header) if err != nil { fmt.Printf("Failed to parse header json\n") fmt.Println(err) return -1 } useStruct = true } else { sHeader = string(vHeader) } } else if len(cliops.header) > 0 { if cliops.jsonparse { err = json.Unmarshal([]byte(cliops.header), &header) if err != nil { fmt.Printf("Failed to parse header json\n") fmt.Println(err) return -1 } useStruct = true } else { sHeader = cliops.header } } else { header = secsipid.SJWTHeader{ Alg: cliops.alg, Ppt: cliops.ppt, Typ: cliops.typ, X5u: cliops.x5u, } if len(header.X5u) <= 0 { header.X5u = "https://127.0.0.1/cert.pem" } useStruct = true } payload := secsipid.SJWTPayload{} if len(cliops.fpayload) > 0 { vPayload, _ := ioutil.ReadFile(cliops.fpayload) if cliops.jsonparse { err = json.Unmarshal(vPayload, &payload) if err != nil { fmt.Printf("Failed to parse payload json\n") fmt.Println(err) return -1 } useStruct = true } else { sPayload = string(vPayload) } } else if len(cliops.payload) > 0 { if cliops.jsonparse { err = json.Unmarshal([]byte(cliops.payload), &payload) if err != nil { fmt.Printf("Failed to parse payload json\n") fmt.Println(err) return -1 } useStruct = true } else { sPayload = cliops.payload } } else { payload = secsipid.SJWTPayload{ ATTest: cliops.attest, Dest: secsipid.SJWTDest{ TN: []string{cliops.desttn}, }, IAT: int64(cliops.iat), Orig: secsipid.SJWTOrig{ TN: cliops.origtn, }, OrigID: cliops.origid, } if payload.IAT == 0 { payload.IAT = time.Now().Unix() } useStruct = true } if useStruct { if cliops.verbosity > 0 { fmt.Printf("Signing using the structures build from parameter values\n") } prvkey, _ := ioutil.ReadFile(cliops.fprvkey) var ecdsaPrvKey *ecdsa.PrivateKey if ecdsaPrvKey, _, err = secsipid.SJWTParseECPrivateKeyFromPEM(prvkey); err != nil { fmt.Printf("Unable to parse ECDSA private key: %v\n", err) return -1 } token = secsipid.SJWTEncode(header, payload, ecdsaPrvKey) } else { if cliops.verbosity > 0 { fmt.Printf("Signing using the JSON documents from parameters\n") } token, _, _ = secsipid.SJWTEncodeText(sHeader, sPayload, cliops.fprvkey) } fmt.Printf("%s\n", token) return 0 } func secsipidxCLICheck() int { var sIdentity string var ret int var err error if len(cliops.fpubkey) <= 0 { fmt.Printf("path to public key not provided\n") return -1 } if len(cliops.fidentity) > 0 { vIdentity, _ := ioutil.ReadFile(cliops.fidentity) sIdentity = string(vIdentity) } else if len(cliops.identity) > 0 { sIdentity = cliops.identity } else { fmt.Printf("Identity value not provided\n") return -1 } ret, err = secsipid.SJWTCheckFullIdentity(sIdentity, cliops.expire, cliops.fpubkey, cliops.timeout) if err != nil { fmt.Printf("error message: %v\n", err) } return ret } func httpHandleV1Check(w http.ResponseWriter, r *http.Request) { var ret int fmt.Printf("incoming request for identity check ...\n") body, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Printf("error reading body: %v", err) http.Error(w, "cannot read body", http.StatusBadRequest) return } ret, err = secsipid.SJWTCheckFullIdentity(string(body), cliops.expire, cliops.fpubkey, cliops.timeout) if err != nil { fmt.Printf("failed checking identity: %v\n", err) http.Error(w, "FAILED\n", http.StatusInternalServerError) return } fmt.Printf("valid identity - return code: %d\n", ret) fmt.Fprintf(w, "OK\n") } func httpHandleV1SignCSV(w http.ResponseWriter, r *http.Request) { fmt.Printf("incoming request for building identity ...\n") body, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Printf("error reading body: %v\n", err) http.Error(w, "cannot read body", http.StatusBadRequest) return } token := strings.Split(strings.TrimSpace(string(body)), ",") if len(token) < 5 { fmt.Printf("too few tokens in input body: %d\n", len(token)) http.Error(w, "too few tokens", http.StatusBadRequest) return } var hdr string hdr, _, err = secsipid.SJWTGetIdentity(token[0], token[1], token[2], token[3], token[4], cliops.fprvkey) if err != nil { fmt.Printf("error reading body: %v", err) http.Error(w, "cannot read body", http.StatusBadRequest) return } fmt.Fprintf(w, "%s\n", hdr) } func startHTTPServices() chan error { errchan := make(chan error) // starting HTTP server if len(cliops.httpsrv) > 0 { go func() { log.Printf("staring HTTP service on: %s ...", cliops.httpsrv) if err := http.ListenAndServe(cliops.httpsrv, nil); err != nil { errchan <- err } }() } // starting HTTPS server if len(cliops.httpssrv) > 0 && len(cliops.httpspubkey) > 0 && len(cliops.httpsprvkey) > 0 { go func() { log.Printf("Staring HTTPS service on: %s ...", cliops.httpssrv) if err := http.ListenAndServeTLS(cliops.httpssrv, cliops.httpspubkey, cliops.httpsprvkey, nil); err != nil { errchan <- err } }() } return errchan } func main() { var ret int flag.Parse() if cliops.version { fmt.Printf("%s v%s\n", filepath.Base(os.Args[0]), secsipidxVersion) os.Exit(1) } if cliops.ltest { localTest() os.Exit(1) } if len(cliops.cachedir) > 0 { secsipid.SetURLFileCacheOptions(cliops.cachedir, cliops.cacheexpire) } if len(cliops.cafile) > 0 { secsipid.SJWTLibOptSetS("CertCAFile", cliops.cafile) } if len(cliops.cainter) > 0 { secsipid.SJWTLibOptSetS("CertCAInter", cliops.cainter) } if len(cliops.crlfile) > 0 { secsipid.SJWTLibOptSetS("CertCRLFile", cliops.crlfile) } if cliops.certverify > 0 { secsipid.SJWTLibOptSetN("CertVerify", cliops.certverify) } if len(cliops.x5u) > 0 { secsipid.SJWTLibOptSetS("x5u", cliops.x5u) } if (len(cliops.httpsrv) > 0) || (len(cliops.httpssrv) > 0 && len(cliops.httpspubkey) > 0 && len(cliops.httpsprvkey) > 0) { http.HandleFunc("/v1/check", httpHandleV1Check) http.HandleFunc("/v1/sign-csv", httpHandleV1SignCSV) if len(cliops.httpdir) > 0 { fmt.Printf("serving files over http from directory: %s\n", cliops.httpdir) http.Handle("/v1/pub/", http.StripPrefix("/v1/pub/", http.FileServer(http.Dir(cliops.httpdir)))) } fmt.Printf("starting http services ...\n") errchan := startHTTPServices() select { case err := <-errchan: log.Printf("unable to start http services due to (error: %v)", err) } os.Exit(1) } ret = 0 if cliops.check { if cliops.verbosity > 0 { fmt.Printf("Running with check command\n") } ret = secsipidxCLICheck() if ret == 0 { fmt.Printf("ok\n") } else { fmt.Printf("not-ok\n") } os.Exit(ret) } else if cliops.signfull { if cliops.verbosity > 0 { fmt.Printf("Running with sign-full command\n") } ret = secsipidxCLISignFull() os.Exit(ret) } else if cliops.sign { if cliops.verbosity > 0 { fmt.Printf("Running with sign command\n") } ret = secsipidxCLISign() os.Exit(ret) } else { fmt.Printf("%s v%s\n", filepath.Base(os.Args[0]), secsipidxVersion) fmt.Printf("run '%s --help' to see the options\n", filepath.Base(os.Args[0])) } os.Exit(ret) } secsipidx-1.3.2/secsipid/000077500000000000000000000000001452516616500153265ustar00rootroot00000000000000secsipidx-1.3.2/secsipid/.gitignore000066400000000000000000000001331452516616500173130ustar00rootroot00000000000000dummyCA.pem dummyInterCA.pem dummyCRLFile.crl http_example.com_foo http_localhost:5555_foosecsipidx-1.3.2/secsipid/parse_ec_keys_test.go000066400000000000000000000124061452516616500215330ustar00rootroot00000000000000package secsipid_test import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "testing" "github.com/asipto/secsipidx/secsipid" "github.com/gomagedon/expectate" ) type ParseECPrivateKeyTest struct { inputPem []byte expectedKey *ecdsa.PrivateKey expectedErrCode int expectedErrMsg string } func TestParseECPrivateKeyFromPEM(t *testing.T) { runTest := func(t *testing.T, testCase ParseECPrivateKeyTest) { expect := expectate.Expect(t) key, errCode, err := secsipid.SJWTParseECPrivateKeyFromPEM(testCase.inputPem) if testCase.expectedKey == nil { expect(key).ToBe((*ecdsa.PrivateKey)(nil)) } else { expect(key).ToEqual(testCase.expectedKey) } expect(errCode).ToBe(testCase.expectedErrCode) if len(testCase.expectedErrMsg) > 0 { expect(getMsgFromErr(err)).ToBe(testCase.expectedErrMsg) } } t.Run("ErrPrvKeyInvalidFormat with bad key format", func(t *testing.T) { runTest(t, ParseECPrivateKeyTest{ inputPem: []byte("bad key format"), expectedErrCode: secsipid.SJWTRetErrPrvKeyInvalidFormat, expectedErrMsg: "key must be PEM encoded", }) }) t.Run("ErrPrvKeyInvalid with invalid key", func(t *testing.T) { invalidKey, _ := pemEncode(&pem.Block{ Type: "INVALID KEY", Bytes: []byte("invalid key body"), }) runTest(t, ParseECPrivateKeyTest{ inputPem: invalidKey, expectedErrCode: secsipid.SJWTRetErrPrvKeyInvalid, expectedErrMsg: `asn1: structure error: tags don't match (16 vs {class:1 tag:9 length:110 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:0 set:false omitEmpty:false} pkcs8 @2`, }) }) t.Run("ErrPrvKeyInvalidEC with non-EC PKCS8 key", func(t *testing.T) { privateKey, _ := rsa.GenerateKey(rand.Reader, 512) rsaPKCS8Key, _ := x509.MarshalPKCS8PrivateKey(privateKey) rsaKeyPEM, _ := pemEncode(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: rsaPKCS8Key, }) runTest(t, ParseECPrivateKeyTest{ inputPem: rsaKeyPEM, expectedErrCode: secsipid.SJWTRetErrPrvKeyInvalidEC, expectedErrMsg: "not EC private key", }) }) t.Run("Works with EC private key", func(t *testing.T) { privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) privateKeyBytes, _ := x509.MarshalECPrivateKey(privateKey) ecPrivateKeyPEM, _ := pemEncode(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: privateKeyBytes, }) runTest(t, ParseECPrivateKeyTest{ inputPem: ecPrivateKeyPEM, expectedErrCode: secsipid.SJWTRetOK, expectedKey: privateKey, }) }) t.Run("Works with PKCS8 encoded EC private key", func(t *testing.T) { privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) pkcs8Bytes, _ := x509.MarshalPKCS8PrivateKey(privateKey) ecPrivateKeyPEM, _ := pemEncode(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: pkcs8Bytes, }) runTest(t, ParseECPrivateKeyTest{ inputPem: ecPrivateKeyPEM, expectedErrCode: secsipid.SJWTRetOK, expectedKey: privateKey, }) }) } type ParseECPublicKeyTest struct { inputPem []byte expectedKey *ecdsa.PublicKey expectedErrCode int expectedErrMsg string } func TestParseECPublicKeyFromPEM(t *testing.T) { runTest := func(t *testing.T, testCase ParseECPublicKeyTest) { expect := expectate.Expect(t) key, errCode, err := secsipid.SJWTParseECPublicKeyFromPEM(testCase.inputPem) if testCase.expectedKey == nil { expect(key).ToBe((*ecdsa.PublicKey)(nil)) } else { expect(key).ToEqual(testCase.expectedKey) } expect(errCode).ToBe(testCase.expectedErrCode) if len(testCase.expectedErrMsg) > 0 { expect(getMsgFromErr(err)).ToBe(testCase.expectedErrMsg) } } t.Run("ErrCertInvalidFormat with bad key format", func(t *testing.T) { runTest(t, ParseECPublicKeyTest{ inputPem: []byte("bad key format"), expectedErrCode: secsipid.SJWTRetErrCertInvalidFormat, expectedErrMsg: "key must be PEM encoded", }) }) t.Run("ErrCertInvalid with invalid certificate", func(t *testing.T) { invalidCert, _ := pemEncode(&pem.Block{ Type: "CERTIFICATE", Bytes: []byte("invalid certificate"), }) runTest(t, ParseECPublicKeyTest{ inputPem: invalidCert, expectedErrCode: secsipid.SJWTRetErrCertInvalid, expectedErrMsg: "", }) }) t.Run("ErrCertInvalidEC with non-EC public key", func(t *testing.T) { privKey, _ := rsa.GenerateKey(rand.Reader, 512) pubKey := &privKey.PublicKey pubKeyBytes, _ := x509.MarshalPKIXPublicKey(pubKey) rsaPublicKey, _ := pemEncode(&pem.Block{ Type: "CERTIFICATE", Bytes: pubKeyBytes, }) runTest(t, ParseECPublicKeyTest{ inputPem: rsaPublicKey, expectedErrCode: secsipid.SJWTRetErrCertInvalidEC, expectedErrMsg: "not EC public key", }) }) t.Run("OK with EC public key", func(t *testing.T) { privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) pubKey := &privKey.PublicKey pubKeyBytes, _ := x509.MarshalPKIXPublicKey(pubKey) ecPublicKey, _ := pemEncode(&pem.Block{ Type: "CERTIFICATE", Bytes: pubKeyBytes, }) runTest(t, ParseECPublicKeyTest{ inputPem: ecPublicKey, expectedKey: pubKey, }) }) } func pemEncode(block *pem.Block) ([]byte, error) { pemBytes := bytes.NewBufferString("") err := pem.Encode(pemBytes, block) if err != nil { return nil, err } return pemBytes.Bytes(), nil } secsipidx-1.3.2/secsipid/pubkey_verify_test.go000066400000000000000000000336011452516616500216020ustar00rootroot00000000000000package secsipid_test import ( "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "net" "os" "path" "testing" "time" "github.com/asipto/secsipidx/secsipid" "github.com/gomagedon/expectate" ) type PubKeyVerifyTest struct { certVerify int inputKey []byte expectedErrCode int expectedErrMsg string } func TestPubKeyVerify(t *testing.T) { runTest := func(t *testing.T, testCase PubKeyVerifyTest) { expect := expectate.Expect(t) // testing utility secsipid.SJWTLibOptSetN("CertVerify", testCase.certVerify) errCode, err := secsipid.SJWTPubKeyVerify(testCase.inputKey) errMsg := getMsgFromErr(err) expect(errCode).ToBe(testCase.expectedErrCode) if len(testCase.expectedErrMsg) > 0 { expect(errMsg).ToBe(testCase.expectedErrMsg) } } os.Remove("dummyCA.pem") os.Remove("dummyInterCA.pem") os.Remove("dummyCRLFile.crl") // Test t.Run("OK when certVerify is 0", func(t *testing.T) { runTest(t, PubKeyVerifyTest{ certVerify: 0, inputKey: []byte("foo"), expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) }) // Test (for every non-zero value of certVerify) for certVerify := 1; certVerify <= 32; certVerify += 1 { t.Run("ErrCertInvalidFormat when key is invalid format", func(t *testing.T) { runTest(t, PubKeyVerifyTest{ certVerify: certVerify, inputKey: []byte("this is an invalid cert"), expectedErrCode: secsipid.SJWTRetErrCertInvalidFormat, expectedErrMsg: "failed to parse certificate PEM", }) }) } certGenerator := NewDummyCA() t.Run("ErrCertExpired", func(t *testing.T) { cert := certGenerator.generateExpiredCert() runTest(t, PubKeyVerifyTest{ certVerify: 0b00001, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertExpired, expectedErrMsg: "certificate expired", }) }) t.Run("ErrCertBeforeValidity", func(t *testing.T) { cert := certGenerator.generateCertBeforeValidity() runTest(t, PubKeyVerifyTest{ certVerify: 0b00001, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertBeforeValidity, expectedErrMsg: "certificate not valid yet", }) }) t.Run("ErrCertInvalid with no root CAs", func(t *testing.T) { cert := certGenerator.generateValidCert() runTest(t, PubKeyVerifyTest{ certVerify: 0b00001, // haven't enabled system CA or custom CA inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertInvalid, expectedErrMsg: "", }) }) t.Run("ErrCertInvalid with default system CAs", func(t *testing.T) { cert := certGenerator.generateValidCert() runTest(t, PubKeyVerifyTest{ certVerify: 0b00010, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertInvalid, expectedErrMsg: "x509: certificate signed by unknown authority", }) }) t.Run("Cert is valid with dummy system CAs", func(t *testing.T) { oldSSLCertDir := os.Getenv("SSL_CERT_DIR") oldSSLCertFile := os.Getenv("SSL_CERT_FILE") workDir, _ := os.Getwd() os.Setenv("SSL_CERT_DIR", workDir) defer os.Setenv("SSL_CERT_DIR", oldSSLCertDir) os.Setenv("SSL_CERT_FILE", path.Join(workDir, "dummyCA.pem")) defer os.Setenv("SSL_CERT_FILE", oldSSLCertFile) println(os.Getenv("SSL_CERT_DIR")) cert := certGenerator.generateValidCert() os.WriteFile("dummyCA.pem", certGenerator.caPEMBytes, 0777) secsipid.ResetSystemCertPool() runTest(t, PubKeyVerifyTest{ certVerify: 0b00010, inputKey: cert, expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) os.Remove("dummyCA.pem") }) t.Run("ErrCertNoCAFile with no CA file", func(t *testing.T) { cert := certGenerator.generateValidCert() runTest(t, PubKeyVerifyTest{ certVerify: 0b00100, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertNoCAFile, expectedErrMsg: "no CA file", }) }) t.Run("ErrCertReadCAFile with non-existant CA file", func(t *testing.T) { cert := certGenerator.generateValidCert() secsipid.SJWTLibOptSetS("CertCAFile", "nonexistant.pem") runTest(t, PubKeyVerifyTest{ certVerify: 0b00100, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertReadCAFile, expectedErrMsg: "failed to read CA file", }) }) t.Run("ErrCertProcessing with invalid CA file", func(t *testing.T) { cert := certGenerator.generateValidCert() secsipid.SJWTLibOptSetS("CertCAFile", "dummyCA.pem") os.WriteFile("dummyCA.pem", []byte("invalid cert"), 0777) runTest(t, PubKeyVerifyTest{ certVerify: 0b00100, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertProcessing, expectedErrMsg: "failed to append CA file", }) os.Remove("dummyCA.pem") }) t.Run("OK with correct custom CA file", func(t *testing.T) { cert := certGenerator.generateValidCert() os.WriteFile("dummyCA.pem", certGenerator.caPEMBytes, 0777) secsipid.SJWTLibOptSetS("CertCAFile", "dummyCA.pem") runTest(t, PubKeyVerifyTest{ certVerify: 0b00100, inputKey: cert, expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) os.Remove("dummyCA.pem") }) t.Run("ErrCertNoCAInter with no intermediate CA file", func(t *testing.T) { cert := certGenerator.generateValidCert() runTest(t, PubKeyVerifyTest{ certVerify: 0b01000, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertNoCAInter, expectedErrMsg: "no intermediate CA file", }) }) t.Run("ErrCertReadCAInter with non-existant intermediate CA file", func(t *testing.T) { cert := certGenerator.generateValidCert() secsipid.SJWTLibOptSetS("CertCAInter", "nonexistant.pem") runTest(t, PubKeyVerifyTest{ certVerify: 0b01000, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertReadCAInter, expectedErrMsg: "failed to read intermediate CA file", }) }) t.Run("ErrCertProcessing with invalid intermediate CA file", func(t *testing.T) { cert := certGenerator.generateValidCert() os.WriteFile("dummyInterCA.pem", []byte("invalid cert"), 0777) secsipid.SJWTLibOptSetS("CertCAInter", "dummyInterCA.pem") runTest(t, PubKeyVerifyTest{ certVerify: 0b01000, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertProcessing, expectedErrMsg: "failed to append intermediate CA file", }) os.Remove("dummyInterCA.pem") }) t.Run("OK with correct intermediate and root CA files", func(t *testing.T) { interCertGenerator := NewIntermediateCA(certGenerator) cert := interCertGenerator.generateValidCert() os.WriteFile("dummyCA.pem", certGenerator.caPEMBytes, 0777) os.WriteFile("dummyInterCA.pem", interCertGenerator.caPEMBytes, 0777) secsipid.SJWTLibOptSetS("CertCAFile", "dummyCA.pem") secsipid.SJWTLibOptSetS("CertCAInter", "dummyInterCA.pem") runTest(t, PubKeyVerifyTest{ certVerify: 0b01100, inputKey: cert, expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) os.Remove("dummyCA.pem") os.Remove("dummyInterCA.pem") }) t.Run("ErrCertNoCRLFile with no CRL file", func(t *testing.T) { cert := certGenerator.generateValidCert() os.WriteFile("dummyCA.pem", certGenerator.caPEMBytes, 0777) secsipid.SJWTLibOptSetS("CertCAFile", "dummyCA.pem") runTest(t, PubKeyVerifyTest{ certVerify: 0b10100, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertNoCRLFile, expectedErrMsg: "no CRL file", }) os.Remove("dummyCA.pem") }) t.Run("ErrCertReadCRLFile with non-existant CRL file", func(t *testing.T) { cert := certGenerator.generateValidCert() os.WriteFile("dummyCA.pem", certGenerator.caPEMBytes, 0777) secsipid.SJWTLibOptSetS("CertCAFile", "dummyCA.pem") secsipid.SJWTLibOptSetS("CertCRLFile", "dummyCRLFile.crl") runTest(t, PubKeyVerifyTest{ certVerify: 0b10100, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertReadCRLFile, expectedErrMsg: "failed to read CRL file", }) os.Remove("dummyCA.pem") os.Remove("dummyCRLFile.crl") }) t.Run("OK if CRL does not contain cert", func(t *testing.T) { cert := certGenerator.generateValidCert() os.WriteFile("dummyCA.pem", certGenerator.caPEMBytes, 0777) secsipid.SJWTLibOptSetS("CertCAFile", "dummyCA.pem") crl := &x509.RevocationList{ Number: big.NewInt(1), RevokedCertificates: []pkix.RevokedCertificate{ { SerialNumber: big.NewInt(0), RevocationTime: time.Now(), }, }, ThisUpdate: time.Now(), NextUpdate: time.Now().AddDate(1, 0, 0), } crlBytes, _ := x509.CreateRevocationList( rand.Reader, crl, certGenerator.ca, certGenerator.caPrivKey) os.WriteFile("dummyCRLFile.crl", crlBytes, 0777) secsipid.SJWTLibOptSetS("CertCRLFile", "dummyCRLFile.crl") runTest(t, PubKeyVerifyTest{ certVerify: 0b10100, inputKey: cert, expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) os.Remove("dummyCA.pem") }) t.Run("ErrCertRevoked when CRL contains cert", func(t *testing.T) { cert, serialNum := certGenerator.generateCertWithTimes( time.Now(), time.Now().AddDate(1, 0, 0)) os.WriteFile("dummyCA.pem", certGenerator.caPEMBytes, 0777) secsipid.SJWTLibOptSetS("CertCAFile", "dummyCA.pem") crl := &x509.RevocationList{ Number: big.NewInt(1), RevokedCertificates: []pkix.RevokedCertificate{ { SerialNumber: serialNum, RevocationTime: time.Now(), }, }, ThisUpdate: time.Now(), NextUpdate: time.Now().AddDate(1, 0, 0), } crlBytes, _ := x509.CreateRevocationList( rand.Reader, crl, certGenerator.ca, certGenerator.caPrivKey) os.WriteFile("dummyCRLFile.crl", crlBytes, 0777) secsipid.SJWTLibOptSetS("CertCRLFile", "dummyCRLFile.crl") runTest(t, PubKeyVerifyTest{ certVerify: 0b10100, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertRevoked, expectedErrMsg: "serial number match - certificate is revoked", }) os.Remove("dummyCA.pem") os.Remove("dummyCRLFile.crl") }) } func getMsgFromErr(err error) string { if err == nil { return "" } return err.Error() } type DummyCertGenerator struct { ca *x509.Certificate caPEMBytes []byte caPrivKey *rsa.PrivateKey } func NewDummyCA() DummyCertGenerator { caPrivKey, _ := rsa.GenerateKey(rand.Reader, 512) ca := &x509.Certificate{ SerialNumber: big.NewInt(2019), SubjectKeyId: x509.MarshalPKCS1PublicKey(&caPrivKey.PublicKey), Subject: pkix.Name{ Organization: []string{"Foo, Inc."}, Country: []string{"Fantasyland"}, Province: []string{""}, Locality: []string{"Metropolis"}, StreetAddress: []string{"111 Main St."}, PostalCode: []string{"11111"}, }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(10, 0, 0), IsCA: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, } caBytes, _ := x509.CreateCertificate( rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) caPEM := new(bytes.Buffer) pem.Encode(caPEM, &pem.Block{ Type: "CERTIFICATE", Bytes: caBytes, }) return DummyCertGenerator{ ca: ca, caPrivKey: caPrivKey, caPEMBytes: caPEM.Bytes(), } } func NewIntermediateCA(certGenerator DummyCertGenerator) DummyCertGenerator { ca := &x509.Certificate{ SerialNumber: big.NewInt(2020), Subject: pkix.Name{ Organization: []string{"FooBar, Inc."}, Country: []string{"Fantasyland"}, Province: []string{""}, Locality: []string{"Metropolis"}, StreetAddress: []string{"333 Main St."}, PostalCode: []string{"33333"}, }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(10, 0, 0), IsCA: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, } caPrivKey, _ := rsa.GenerateKey(rand.Reader, 512) caBytes, _ := x509.CreateCertificate( rand.Reader, ca, certGenerator.ca, &caPrivKey.PublicKey, certGenerator.caPrivKey) caPEM := new(bytes.Buffer) pem.Encode(caPEM, &pem.Block{ Type: "CERTIFICATE", Bytes: caBytes, }) return DummyCertGenerator{ ca: ca, caPrivKey: caPrivKey, caPEMBytes: caPEM.Bytes(), } } func (gen DummyCertGenerator) generateExpiredCert() []byte { cert, _ := gen.generateCertWithTimes( time.Now().AddDate(-1, 0, 0), // 1 year ago time.Now().AddDate(0, 0, -1), // 1 day ago ) return cert } func (gen DummyCertGenerator) generateCertBeforeValidity() []byte { cert, _ := gen.generateCertWithTimes( time.Now().AddDate(1, 0, 0), // 1 year from now time.Now().AddDate(2, 0, 0), // 2 years from now ) return cert } func (gen DummyCertGenerator) generateValidCert() []byte { cert, _ := gen.generateCertWithTimes( time.Now(), time.Now().AddDate(1, 0, 0), // 1 year from now ) return cert } func (gen DummyCertGenerator) generateCertWithTimes(notBefore time.Time, notAfter time.Time) ([]byte, *big.Int) { serialNum, _ := rand.Int(rand.Reader, big.NewInt(10000)) cert := &x509.Certificate{ SerialNumber: serialNum, Subject: pkix.Name{ Organization: []string{"Bar, Inc."}, Country: []string{"Fantasyland"}, Province: []string{""}, Locality: []string{"Metropolis"}, StreetAddress: []string{"222 Main St."}, PostalCode: []string{"11111"}, }, IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, NotBefore: notBefore, NotAfter: notAfter, SubjectKeyId: []byte{1, 2, 3, 4, 6}, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature, } certPrivKey, _ := rsa.GenerateKey(rand.Reader, 512) certBytes, _ := x509.CreateCertificate( rand.Reader, cert, gen.ca, &certPrivKey.PublicKey, gen.caPrivKey) certPEM := new(bytes.Buffer) pem.Encode(certPEM, &pem.Block{ Type: "CERTIFICATE", Bytes: certBytes, }) certPrivKeyPEM := new(bytes.Buffer) pem.Encode(certPrivKeyPEM, &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), }) return certPEM.Bytes(), serialNum } secsipidx-1.3.2/secsipid/secsipid.go000066400000000000000000000661031452516616500174660ustar00rootroot00000000000000package secsipid import ( "crypto" "crypto/ecdsa" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/json" "encoding/pem" "errors" "fmt" "io/ioutil" "math/big" "net/http" "net/url" "os" "strconv" "strings" "time" "unicode" "github.com/google/uuid" ) // return and error code values const ( SJWTRetOK = 0 // generic errors SJWTRetErr = -1 // public certificate and private key errors: -100..-199 SJWTRetErrCertInvalid = -101 SJWTRetErrCertInvalidFormat = -102 SJWTRetErrCertExpired = -103 SJWTRetErrCertBeforeValidity = -104 SJWTRetErrCertProcessing = -105 SJWTRetErrCertNoCAFile = -106 SJWTRetErrCertReadCAFile = -107 SJWTRetErrCertNoCAInter = -108 SJWTRetErrCertReadCAInter = -109 SJWTRetErrCertNoCRLFile = -110 SJWTRetErrCertReadCRLFile = -111 SJWTRetErrCertRevoked = -112 SJWTRetErrCertInvalidEC = -114 SJWTRetErrPrvKeyInvalid = -151 SJWTRetErrPrvKeyInvalidFormat = -152 SJWTRetErrPrvKeyInvalidEC = -152 // identity JSON header, payload and signature errors: -200..-299 SJWTRetErrJSONHdrParse = -201 SJWTRetErrJSONHdrAlg = -202 SJWTRetErrJSONHdrPpt = -203 SJWTRetErrJSONHdrTyp = -204 SJWTRetErrJSONHdrX5u = -205 SJWTRetErrJSONPayloadParse = -231 SJWTRetErrJSONPayloadIATExpired = -232 SJWTRetErrJSONSignatureInvalid = -251 SJWTRetErrJSONSignatureHashing = -252 SJWTRetErrJSONSignatureSize = -253 SJWTRetErrJSONSignatureFailure = -254 SJWTRetErrJSONSignatureNob64 = -255 // identity SIP header errors: -300..-399 SJWTRetErrSIPHdrParse = -301 SJWTRetErrSIPHdrAlg = -302 SJWTRetErrSIPHdrPpt = -303 SJWTRetErrSIPHdrEmpty = -304 SJWTRetErrSIPHdrInfo = -305 // http and file operations errors: -400..-499 SJWTRetErrHTTPInvalidURL = -401 SJWTRetErrHTTPGet = -402 SJWTRetErrHTTPStatusCode = -403 SJWTRetErrHTTPReadBody = -404 SJWTRetErrFileRead = -451 ) // SJWTHeader - header for JWT type SJWTHeader struct { Alg string `json:"alg"` Ppt string `json:"ppt"` Typ string `json:"typ"` X5u string `json:"x5u"` } // SJWTDest -- type SJWTDest struct { TN []string `json:"tn"` } // SJWTOrig -- type SJWTOrig struct { TN string `json:"tn"` } // SJWTPayload - JWT payload type SJWTPayload struct { ATTest string `json:"attest"` Dest SJWTDest `json:"dest"` IAT int64 `json:"iat"` Orig SJWTOrig `json:"orig"` OrigID string `json:"origid"` } type SJWTLibOptions struct { cacheDirPath string cacheExpire int certCAFile string certCAInter string certCRLFile string certVerify int x5u string } var globalLibOptions = SJWTLibOptions{ cacheDirPath: "", cacheExpire: 3600, certCAFile: "", certCAInter: "", certCRLFile: "", certVerify: 0, x5u: "https://127.0.0.1/cert.pem", } var ( sES256KeyBits = 256 sES256KeySize = 32 ) // SetFileCacheOptions -- func SetURLFileCacheOptions(path string, expire int) { globalLibOptions.cacheDirPath = path globalLibOptions.cacheExpire = expire } // SJWTLibOptSetS -- func SJWTLibOptSetS(optname string, optval string) int { switch optname { case "CacheDirPath": globalLibOptions.cacheDirPath = optval return SJWTRetOK case "CertCAFile": globalLibOptions.certCAFile = optval return SJWTRetOK case "CertCRLFile": globalLibOptions.certCRLFile = optval return SJWTRetOK case "CertCAInter": globalLibOptions.certCAInter = optval return SJWTRetOK case "x5u": globalLibOptions.x5u = optval return SJWTRetOK } return SJWTRetErr } // SJWTLibOptSetN -- func SJWTLibOptSetN(optname string, optval int) int { switch optname { case "CacheExpires": globalLibOptions.cacheExpire = optval return SJWTRetOK case "CertVerify": globalLibOptions.certVerify = optval return SJWTRetOK } return SJWTRetErr } // SJWTLibOptSetV -- func SJWTLibOptSetV(optnameval string) int { optArray := strings.SplitN(optnameval, "=", 2) optName := optArray[0] optVal := optArray[1] switch optName { case "CacheExpires", "CertVerify": intVal, _ := strconv.Atoi(optVal) return SJWTLibOptSetN(optName, intVal) case "CacheDirPath", "CertCAFile", "CertCAInter", "CertCRLFile": return SJWTLibOptSetS(optName, optVal) } return SJWTRetErr } // SJWTRemoveWhiteSpaces -- func SJWTRemoveWhiteSpaces(s string) string { rout := make([]rune, 0, len(s)) for _, r := range s { if !unicode.IsSpace(r) { rout = append(rout, r) } } return string(rout) } // SJWTRemoveWhiteSpaces -- func SJWTGetURLCacheFilePath(urlVal string) string { filePath := strings.Replace(urlVal, "://", "_", -1) filePath = strings.Replace(filePath, "/", "_", -1) if len(globalLibOptions.cacheDirPath) > 0 { filePath = globalLibOptions.cacheDirPath + "/" + filePath } return filePath } // SJWTPubKeyVerify - func SJWTPubKeyVerify(pubKey []byte) (int, error) { if globalLibOptions.certVerify == 0 { return SJWTRetOK, nil } var certVal *x509.Certificate var certInter []*x509.Certificate var rootCAs *x509.CertPool var interCAs *x509.CertPool var err error // The public key may contain multiple intermediate certificates, we must // parse those out and include them when doing the actual validation. var toDecode = pubKey var block *pem.Block for true { // Decode the next block in the public key. If there are no more blocks then // this will return nil. block, toDecode = pem.Decode(toDecode) if block == nil { break } // Parse the block as an x509 certificate. blockCert, err := x509.ParseCertificate(block.Bytes) if blockCert == nil { return SJWTRetErrCertInvalidFormat, err } // If this was the first block then it represents the public certificate, // otherwise it is an intermediate certificate. if certVal == nil { certVal = blockCert } else { certInter = append(certInter, blockCert) } } if certVal == nil { return SJWTRetErrCertInvalidFormat, errors.New("failed to parse certificate PEM") } if (globalLibOptions.certVerify & (1 << 0)) != 0 { if !time.Now().Before(certVal.NotAfter) { return SJWTRetErrCertExpired, errors.New("certificate expired") } else if !time.Now().After(certVal.NotBefore) { return SJWTRetErrCertBeforeValidity, errors.New("certificate not valid yet") } } rootCAs = nil interCAs = nil if (globalLibOptions.certVerify & (1 << 1)) != 0 { // Get the SystemCertPool rootCAs, err = SystemCertPool() if rootCAs == nil { return SJWTRetErrCertProcessing, err } } if (globalLibOptions.certVerify & (1 << 2)) != 0 { if len(globalLibOptions.certCAFile) <= 0 { return SJWTRetErrCertNoCAFile, errors.New("no CA file") } if rootCAs == nil { rootCAs = x509.NewCertPool() if rootCAs == nil { return SJWTRetErrCertProcessing, errors.New("no new ca cert pool") } } var certsCA []byte // Read in the cert file certsCA, err = ioutil.ReadFile(globalLibOptions.certCAFile) if err != nil { return SJWTRetErrCertReadCAFile, errors.New("failed to read CA file") } // Append our cert to the system pool if ok := rootCAs.AppendCertsFromPEM(certsCA); !ok { return SJWTRetErrCertProcessing, errors.New("failed to append CA file") } } if (globalLibOptions.certVerify & (1 << 3)) != 0 { if len(globalLibOptions.certCAInter) <= 0 { return SJWTRetErrCertNoCAInter, errors.New("no intermediate CA file") } interCAs = x509.NewCertPool() if interCAs == nil { return SJWTRetErrCertProcessing, errors.New("no new ca intermediate cert pool") } var certsCA []byte // Read in the cert file certsCA, err = ioutil.ReadFile(globalLibOptions.certCAInter) if err != nil { return SJWTRetErrCertReadCAInter, errors.New("failed to read intermediate CA file") } // Append our cert to the system pool if ok := interCAs.AppendCertsFromPEM(certsCA); !ok { return SJWTRetErrCertProcessing, errors.New("failed to append intermediate CA file") } } // Append any intermediate certificates included in pubKey. if len(certInter) > 0 { if interCAs == nil { interCAs = x509.NewCertPool() } if interCAs == nil { return SJWTRetErrCertProcessing, errors.New("no new ca intermediate cert pool") } // Append our certs for _, iCert := range certInter { interCAs.AddCert(iCert) } } opts := x509.VerifyOptions{ Roots: rootCAs, Intermediates: interCAs, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, } if _, err = certVal.Verify(opts); err != nil { return SJWTRetErrCertInvalid, err } if (globalLibOptions.certVerify & (1 << 4)) != 0 { if len(globalLibOptions.certCRLFile) <= 0 { return SJWTRetErrCertNoCRLFile, errors.New("no CRL file") } var rootCRL *pkix.CertificateList rootCRL = nil var certsCRLData []byte // Read in the cert file certsCRLData, err = ioutil.ReadFile(globalLibOptions.certCRLFile) if err != nil { return SJWTRetErrCertReadCRLFile, errors.New("failed to read CRL file") } rootCRL, err = x509.ParseCRL(certsCRLData) for _, revoked := range rootCRL.TBSCertList.RevokedCertificates { if certVal.SerialNumber.Cmp(revoked.SerialNumber) == 0 { return SJWTRetErrCertRevoked, errors.New("serial number match - certificate is revoked") } } } return SJWTRetOK, nil } // SJWTParseECPrivateKeyFromPEM Parse PEM encoded Elliptic Curve Private Key Structure func SJWTParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, int, error) { var err error var block *pem.Block if block, _ = pem.Decode(key); block == nil { return nil, SJWTRetErrPrvKeyInvalidFormat, errors.New("key must be PEM encoded") } var parsedKey interface{} if parsedKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil { if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { return nil, SJWTRetErrPrvKeyInvalid, err } } var pkey *ecdsa.PrivateKey var ok bool if pkey, ok = parsedKey.(*ecdsa.PrivateKey); !ok { return nil, SJWTRetErrPrvKeyInvalidEC, errors.New("not EC private key") } return pkey, SJWTRetOK, nil } // SJWTParseECPublicKeyFromPEM Parse PEM encoded PKCS1 or PKCS8 public key func SJWTParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, int, error) { var err error var block *pem.Block if block, _ = pem.Decode(key); block == nil { return nil, SJWTRetErrCertInvalidFormat, errors.New("key must be PEM encoded") } var parsedKey interface{} if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { if cert, err := x509.ParseCertificate(block.Bytes); err == nil { parsedKey = cert.PublicKey } else { return nil, SJWTRetErrCertInvalid, err } } var pkey *ecdsa.PublicKey var ok bool if pkey, ok = parsedKey.(*ecdsa.PublicKey); !ok { return nil, SJWTRetErrCertInvalidEC, errors.New("not EC public key") } return pkey, SJWTRetOK, nil } // SJWTBase64EncodeString encode string to base64 with padding stripped func SJWTBase64EncodeString(src string) string { return strings. TrimRight(base64.URLEncoding. EncodeToString([]byte(src)), "=") } // SJWTBase64DecodeString takes in a base 64 encoded string and returns the // actual string or an error of it fails to decode the string func SJWTBase64DecodeString(src string) (string, error) { if l := len(src) % 4; l > 0 { src += strings.Repeat("=", 4-l) } decoded, err := base64.URLEncoding.DecodeString(src) if err != nil { return "", fmt.Errorf("decoding error %s", err) } return string(decoded), nil } // SJWTBase64EncodeBytes encode bytes array to base64 with padding stripped func SJWTBase64EncodeBytes(seg []byte) string { return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=") } // SJWTBase64DecodeBytes takes in a base 64 encoded string and returns the // actual bytes array or an error of it fails to decode the string func SJWTBase64DecodeBytes(seg string) ([]byte, error) { if l := len(seg) % 4; l > 0 { seg += strings.Repeat("=", 4-l) } return base64.URLEncoding.DecodeString(seg) } // SJWTGetURLCachedContent -- func SJWTGetURLCachedContent(urlVal string) ([]byte, error) { filePath := SJWTGetURLCacheFilePath(urlVal) fileStat, err := os.Stat(filePath) if err != nil { return nil, err } tnow := time.Now() if int(tnow.Sub(fileStat.ModTime()).Seconds()) > globalLibOptions.cacheExpire { os.Remove(filePath) return nil, nil } return ioutil.ReadFile(filePath) } // SJWTSetURLCachedContent -- func SJWTSetURLCachedContent(urlVal string, data []byte) error { filePath := SJWTGetURLCacheFilePath(urlVal) return ioutil.WriteFile(filePath, data, 0640) } // SJWTGetURLContent -- func SJWTGetURLContent(urlVal string, timeoutVal int) ([]byte, int, error) { if len(urlVal) == 0 { return nil, SJWTRetErrHTTPInvalidURL, errors.New("no URL value") } if !(strings.HasPrefix(urlVal, "http://") || strings.HasPrefix(urlVal, "https://")) { return nil, SJWTRetErrHTTPInvalidURL, errors.New("invalid URL value") } if len(globalLibOptions.cacheDirPath) > 0 { cdata, cerr := SJWTGetURLCachedContent(urlVal) if cdata != nil { return cdata, SJWTRetOK, cerr } } httpClient := http.Client{ Timeout: time.Duration(timeoutVal) * time.Second, } resp, err := httpClient.Get(urlVal) if err != nil { return nil, SJWTRetErrHTTPGet, fmt.Errorf("http get failure: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, SJWTRetErrHTTPStatusCode, fmt.Errorf("http status error: %v", resp.StatusCode) } data, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, SJWTRetErrHTTPReadBody, fmt.Errorf("read http body failure: %v", err) } if len(globalLibOptions.cacheDirPath) > 0 { SJWTSetURLCachedContent(urlVal, data) } return data, SJWTRetOK, nil } // SJWTGetValidPayload -- func SJWTGetValidPayload(base64Payload string, expireVal int) (*SJWTPayload, int, error) { if len(base64Payload) == 0 { return nil, SJWTRetErrJSONPayloadParse, errors.New("empty payload") } decodedPayload, payloadErr := SJWTBase64DecodeString(base64Payload) if payloadErr != nil { return nil, SJWTRetErrJSONPayloadParse, fmt.Errorf("invalid payload: %s", payloadErr.Error()) } payload := SJWTPayload{} err := json.Unmarshal([]byte(decodedPayload), &payload) if err != nil { return nil, SJWTRetErrJSONPayloadParse, fmt.Errorf("invalid payload: %s", err.Error()) } if payload.IAT == 0 || time.Now().Unix() > payload.IAT+int64(expireVal) { return nil, SJWTRetErrJSONPayloadIATExpired, errors.New("expired token") } return &payload, SJWTRetOK, nil } // SJWTVerifyWithPubKey - implements the verify // For this verify method, key must be an ecdsa.PublicKey struct func SJWTVerifyWithPubKey(signingString string, signature string, key interface{}) (int, error) { var err error var sig []byte if sig, err = SJWTBase64DecodeBytes(signature); err != nil { return SJWTRetErrJSONSignatureNob64, err } var ecdsaKey *ecdsa.PublicKey switch k := key.(type) { case *ecdsa.PublicKey: ecdsaKey = k default: return SJWTRetErrCertInvalidFormat, errors.New("invalid key type") } if len(sig) != 2*sES256KeySize { return SJWTRetErrJSONSignatureSize, errors.New("ECDSA signature size verification failed") } r := big.NewInt(0).SetBytes(sig[:sES256KeySize]) s := big.NewInt(0).SetBytes(sig[sES256KeySize:]) if !crypto.SHA256.Available() { return SJWTRetErrJSONSignatureHashing, errors.New("hashing function unavailable") } hasher := crypto.SHA256.New() hasher.Write([]byte(signingString)) if verifystatus := ecdsa.Verify(ecdsaKey, hasher.Sum(nil), r, s); verifystatus == true { return SJWTRetOK, nil } return SJWTRetErrJSONSignatureInvalid, errors.New("ECDSA verification failed") } // SJWTSignWithPrvKey - implements the signing // For this signing method, key must be an ecdsa.PrivateKey struct func SJWTSignWithPrvKey(signingString string, key interface{}) (string, int, error) { var ecdsaKey *ecdsa.PrivateKey switch k := key.(type) { case *ecdsa.PrivateKey: ecdsaKey = k default: return "", SJWTRetErrPrvKeyInvalidEC, errors.New("invalid key type") } if !crypto.SHA256.Available() { return "", SJWTRetErrJSONSignatureHashing, errors.New("hashing function not available") } hasher := crypto.SHA256.New() hasher.Write([]byte(signingString)) r, s, err := ecdsa.Sign(rand.Reader, ecdsaKey, hasher.Sum(nil)) if err == nil { curveBits := ecdsaKey.Curve.Params().BitSize if sES256KeyBits != curveBits { return "", SJWTRetErrJSONSignatureSize, errors.New("invalid key size") } keyBytes := curveBits / 8 if curveBits%8 > 0 { keyBytes++ } rBytes := r.Bytes() rBytesPadded := make([]byte, keyBytes) copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) sBytes := s.Bytes() sBytesPadded := make([]byte, keyBytes) copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) out := append(rBytesPadded, sBytesPadded...) return SJWTBase64EncodeBytes(out), SJWTRetOK, nil } return "", SJWTRetErrJSONSignatureFailure, err } // SJWTEncode - encode payload to JWT func SJWTEncode(header SJWTHeader, payload SJWTPayload, prvkey interface{}) string { str, _ := json.Marshal(header) jwthdr := SJWTBase64EncodeString(string(str)) encodedPayload, _ := json.Marshal(payload) signingValue := jwthdr + "." + SJWTBase64EncodeString(string(encodedPayload)) signatureValue, _, _ := SJWTSignWithPrvKey(signingValue, prvkey) return signingValue + "." + signatureValue } // SJWTDecodeWithPubKey - decode JWT string func SJWTDecodeWithPubKey(jwt string, expireVal int, pubkey interface{}) (*SJWTPayload, error) { var ret int var err error var payload *SJWTPayload token := strings.Split(strings.TrimSpace(jwt), ".") if len(token) != 3 { splitErr := errors.New("invalid token - must contain header, payload and signature") return nil, splitErr } payload, ret, err = SJWTGetValidPayload(token[1], expireVal) if err != nil { return nil, fmt.Errorf("getting payload failed: (%d) %v", ret, err) } signatureValue := token[0] + "." + token[1] ret, err = SJWTVerifyWithPubKey(signatureValue, token[2], pubkey) if err != nil { return nil, fmt.Errorf("verify failed: (%d) %v", ret, err) } return payload, nil } // SJWTEncodeText - encode header and payload to JWT func SJWTEncodeText(headerJSON string, payloadJSON string, prvkeyPath string) (string, int, error) { var ret int var err error var signatureValue string var ecdsaPrvKey *ecdsa.PrivateKey prvkey, _ := ioutil.ReadFile(prvkeyPath) if ecdsaPrvKey, ret, err = SJWTParseECPrivateKeyFromPEM(prvkey); err != nil { return "", ret, err } signingValue := SJWTBase64EncodeString(strings.TrimSpace(headerJSON)) + "." + SJWTBase64EncodeString(strings.TrimSpace(payloadJSON)) signatureValue, ret, err = SJWTSignWithPrvKey(signingValue, ecdsaPrvKey) if err != nil { return "", ret, fmt.Errorf("failed to build signature: %v", err) } return signingValue + "." + signatureValue, SJWTRetOK, nil } // SJWTEncodeTextWithPrvKey - encode header and payload to JWT with private key data func SJWTEncodeTextWithPrvKey(headerJSON string, payloadJSON string, prvkeyData string) (string, int, error) { var ret int var err error var signatureValue string var ecdsaPrvKey *ecdsa.PrivateKey if ecdsaPrvKey, ret, err = SJWTParseECPrivateKeyFromPEM([]byte(prvkeyData)); err != nil { return "", ret, err } signingValue := SJWTBase64EncodeString(strings.TrimSpace(headerJSON)) + "." + SJWTBase64EncodeString(strings.TrimSpace(payloadJSON)) signatureValue, ret, err = SJWTSignWithPrvKey(signingValue, ecdsaPrvKey) if err != nil { return "", ret, fmt.Errorf("failed to build signature: %v", err) } return signingValue + "." + signatureValue, SJWTRetOK, nil } // SJWTCheckAttributes - implements the verify of attributes func SJWTCheckAttributes(bToken string, paramInfo string) (int, error) { vHeader, err := SJWTBase64DecodeString(bToken) header := SJWTHeader{} err = json.Unmarshal([]byte(vHeader), &header) if err != nil { return SJWTRetErrJSONHdrParse, err } if len(header.Alg) > 0 && header.Alg != "ES256" { return SJWTRetErrJSONHdrAlg, fmt.Errorf("invalid value for alg in json header") } if len(header.Ppt) > 0 && header.Ppt != "shaken" { return SJWTRetErrJSONHdrPpt, fmt.Errorf("invalid value for ppt in json header") } if len(header.Typ) > 0 && header.Typ != "passport" { return SJWTRetErrJSONHdrTyp, fmt.Errorf("invalid value for typ in json header") } if len(header.X5u) > 0 && header.X5u != paramInfo { return SJWTRetErrJSONHdrX5u, fmt.Errorf("mismatching value for x5u and info attributes") } return SJWTRetOK, nil } // SJWTCheckIdentityPKMode - implements the verify of identity func SJWTCheckIdentityPKMode(identityVal string, expireVal int, pubkeyVal string, pubkeyMode int, timeoutVal int) (int, error) { var err error var ret int var ecdsaPubKey *ecdsa.PublicKey var pubkey []byte var payload *SJWTPayload token := strings.Split(strings.TrimSpace(identityVal), ".") if len(token) != 3 { return SJWTRetErrSIPHdrParse, fmt.Errorf("invalid token - must contain header, payload and signature") } payload, ret, err = SJWTGetValidPayload(token[1], expireVal) if err != nil { return ret, err } if pubkeyMode == 1 { pubkey = []byte(pubkeyVal) } else { if strings.HasPrefix(pubkeyVal, "http://") || strings.HasPrefix(pubkeyVal, "https://") { pubkey, ret, err = SJWTGetURLContent(pubkeyVal, timeoutVal) } else if strings.HasPrefix(pubkeyVal, "file://") { fileUrl, _ := url.Parse(pubkeyVal) pubkey, err = ioutil.ReadFile(fileUrl.Path) ret = SJWTRetErrFileRead } else { pubkey, err = ioutil.ReadFile(pubkeyVal) ret = SJWTRetErrFileRead } if err != nil { return ret, err } } ret, err = SJWTPubKeyVerify(pubkey) if ret != SJWTRetOK { return ret, err } if ecdsaPubKey, ret, err = SJWTParseECPublicKeyFromPEM(pubkey); err != nil { return ret, err } ret, err = SJWTVerifyWithPubKey(token[0]+"."+token[1], token[2], ecdsaPubKey) if err == nil { return SJWTRetOK, nil } return ret, fmt.Errorf("failed to verify - origid (%s) (%d) %v", payload.OrigID, ret, err) } // SJWTCheckIdentity - implements the verify of identity func SJWTCheckIdentity(identityVal string, expireVal int, pubkeyPath string, timeoutVal int) (int, error) { return SJWTCheckIdentityPKMode(identityVal, expireVal, pubkeyPath, 0, timeoutVal) } // SJWTGetValidInfoAttr - return info param value of alg and ppt are valid func SJWTGetValidInfoAttr(hdrtoken []string) (string, int, error) { paramInfo := "" for i := 1; i < len(hdrtoken); i++ { ptoken := strings.Split(hdrtoken[i], "=") if len(ptoken) == 2 { if ptoken[0] == "alg" { if ptoken[1] != "ES256" { return "", SJWTRetErrSIPHdrAlg, fmt.Errorf("invalid value for alg header parameter") } } else if ptoken[0] == "ppt" { if ptoken[1] != "shaken" && ptoken[1] != `"shaken"` { return "", SJWTRetErrSIPHdrPpt, fmt.Errorf("invalid value for ppt header parameter") } } else if ptoken[0] == "info" { paramInfo = ptoken[1] } } } if len(paramInfo) <= 2 { return "", SJWTRetErrSIPHdrInfo, fmt.Errorf("invalid value info header parameter") } if paramInfo[0] == '<' && paramInfo[len(paramInfo)-1] == '>' { paramInfo = paramInfo[1 : len(paramInfo)-1] } return paramInfo, SJWTRetOK, nil } // SJWTCheckFullIdentity - implements the verify of identity func SJWTCheckFullIdentity(identityVal string, expireVal int, pubkeyPath string, timeoutVal int) (int, error) { if len(pubkeyPath) == 0 { return SJWTCheckFullIdentityURL(identityVal, expireVal, timeoutVal) } hdrtoken := strings.Split(SJWTRemoveWhiteSpaces(identityVal), ";") ret, err := SJWTCheckIdentity(hdrtoken[0], expireVal, pubkeyPath, timeoutVal) if ret != 0 { return ret, err } if len(hdrtoken) == 1 { return SJWTRetErrSIPHdrParse, nil } paramInfo := "" paramInfo, ret, err = SJWTGetValidInfoAttr(hdrtoken) if err != nil { return ret, err } btoken := strings.Split(strings.TrimSpace(hdrtoken[0]), ".") if len(btoken[0]) == 0 { return SJWTRetErrJSONHdrParse, nil } return SJWTCheckAttributes(btoken[0], paramInfo) } // SJWTCheckFullIdentityURL - implements the verify of identity using URL func SJWTCheckFullIdentityURL(identityVal string, expireVal int, timeoutVal int) (int, error) { var ecdsaPubKey *ecdsa.PublicKey var ret int var err error var pubkey []byte hdrtoken := strings.Split(SJWTRemoveWhiteSpaces(identityVal), ";") if len(hdrtoken) <= 1 { return SJWTRetErrSIPHdrParse, fmt.Errorf("missing parts of the message header") } paramInfo := "" paramInfo, ret, err = SJWTGetValidInfoAttr(hdrtoken) if err != nil { return ret, err } pubkey, ret, err = SJWTGetURLContent(paramInfo, timeoutVal) if pubkey == nil { return ret, err } ret, err = SJWTPubKeyVerify(pubkey) if ret != SJWTRetOK { return ret, err } if ecdsaPubKey, ret, err = SJWTParseECPublicKeyFromPEM(pubkey); err != nil { return ret, err } btoken := strings.Split(strings.TrimSpace(hdrtoken[0]), ".") if len(btoken) != 3 { return SJWTRetErrSIPHdrParse, fmt.Errorf("invalid token - must contain header, payload and signature") } if len(btoken[0]) == 0 { return SJWTRetErrSIPHdrParse, fmt.Errorf("no json header part") } var payload *SJWTPayload payload, ret, err = SJWTGetValidPayload(btoken[1], expireVal) if payload == nil || err != nil { return ret, err } ret, err = SJWTVerifyWithPubKey(btoken[0]+"."+btoken[1], btoken[2], ecdsaPubKey) if err != nil { return ret, err } return SJWTCheckAttributes(btoken[0], paramInfo) } // SJWTCheckFullIdentityPubKey - implements the verify of identity using public key func SJWTCheckFullIdentityPubKey(identityVal string, expireVal int, pubkeyVal string) (int, error) { hdrtoken := strings.Split(SJWTRemoveWhiteSpaces(identityVal), ";") ret, err := SJWTCheckIdentityPKMode(hdrtoken[0], expireVal, pubkeyVal, 1, 5) if ret != 0 { return ret, err } if len(hdrtoken) == 1 { return SJWTRetOK, nil } paramInfo := "" paramInfo, ret, err = SJWTGetValidInfoAttr(hdrtoken) if err != nil { return ret, err } btoken := strings.Split(strings.TrimSpace(hdrtoken[0]), ".") if len(btoken[0]) == 0 { return SJWTRetOK, nil } return SJWTCheckAttributes(btoken[0], paramInfo) } // SJWTGetIdentityPrvKey -- func SJWTGetIdentityPrvKey(origTN string, destTN string, attestVal string, origID string, x5uVal string, prvkeyData []byte) (string, int, error) { var ret int var err error var vOrigID string header := SJWTHeader{ Alg: "ES256", Ppt: "shaken", Typ: "passport", X5u: globalLibOptions.x5u, } if len(x5uVal) > 0 { header.X5u = x5uVal } if len(origID) > 0 { vOrigID = origID } else { vuuid := uuid.New() vOrigID = vuuid.String() } payload := SJWTPayload{ ATTest: attestVal, Dest: SJWTDest{ TN: []string{destTN}, }, IAT: time.Now().Unix(), Orig: SJWTOrig{ TN: origTN, }, OrigID: vOrigID, } var ecdsaPrvKey *ecdsa.PrivateKey if ecdsaPrvKey, ret, err = SJWTParseECPrivateKeyFromPEM(prvkeyData); err != nil { return "", ret, fmt.Errorf("Unable to parse ECDSA private key: %v", err) } token := SJWTEncode(header, payload, ecdsaPrvKey) if len(token) > 0 { return token + ";info=<" + header.X5u + ">;alg=ES256;ppt=shaken", SJWTRetOK, nil } return "", SJWTRetErrSIPHdrEmpty, errors.New("empty result") } // SJWTGetIdentity -- func SJWTGetIdentity(origTN string, destTN string, attestVal string, origID string, x5uVal string, prvkeyPath string) (string, int, error) { var prvkey []byte var err error prvkey, err = ioutil.ReadFile(prvkeyPath) if err != nil { return "", SJWTRetErrFileRead, fmt.Errorf("Unable to read private key file: %v", err) } return SJWTGetIdentityPrvKey(origTN, destTN, attestVal, origID, x5uVal, prvkey) } secsipidx-1.3.2/secsipid/system_cas.go000066400000000000000000000102561452516616500200330ustar00rootroot00000000000000package secsipid /** Copied and pasted from go standard library: "crypto/x509" package!! **/ /* * As of Go version 1.16.6, it is impossible to reload the system CA pool, so * testing whether 'SJWTPubKeyVerify()' actually uses them is impossible * (unless you generate a real certificate signed by a real CA). * * This copy/paste solution allows the system CAs to be reloaded manually, * by calling 'ResetSystemCertPool()' before calling 'SystemCertPool()', * so they can be mocked by writing your own CA certs to a file and changing * the environment variables: SSL_CERT_FILE and SSL_CERT_DIR. */ import ( "crypto/x509" "io/fs" "os" "path/filepath" "strings" ) const ( // certFileEnv is the environment variable which identifies where to locate // the SSL certificate file. If set this overrides the system default. certFileEnv = "SSL_CERT_FILE" // certDirEnv is the environment variable which identifies which directory // to check for SSL certificate files. If set this overrides the system default. // It is a colon separated list of directories. // See https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html. certDirEnv = "SSL_CERT_DIR" ) var certFiles = []string{ "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6 "/etc/ssl/ca-bundle.pem", // OpenSUSE "/etc/pki/tls/cacert.pem", // OpenELEC "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7 "/etc/ssl/cert.pem", // Alpine Linux } var certDirectories = []string{ "/etc/ssl/certs", // SLES10/SLES11, https://golang.org/issue/12139 "/etc/pki/tls/certs", // Fedora/RHEL "/system/etc/security/cacerts", // Android } var systemCertPool *x509.CertPool = nil func SystemCertPool() (*x509.CertPool, error) { if systemCertPool != nil { return systemCertPool, nil } systemRoots, err := loadSystemRoots() if err != nil { return nil, err } systemCertPool = systemRoots return systemCertPool, nil } func ResetSystemCertPool() { systemCertPool = nil } // On Unix systems other than macOS the environment variables SSL_CERT_FILE and // SSL_CERT_DIR can be used to override the system default locations for the SSL // certificate file and SSL certificate files directory, respectively. The // latter can be a colon-separated list. func loadSystemRoots() (*x509.CertPool, error) { roots := x509.NewCertPool() files := certFiles if f := os.Getenv(certFileEnv); f != "" { files = []string{f} } var firstErr error for _, file := range files { data, err := os.ReadFile(file) if err == nil { roots.AppendCertsFromPEM(data) break } if firstErr == nil && !os.IsNotExist(err) { firstErr = err } } dirs := certDirectories if d := os.Getenv(certDirEnv); d != "" { // OpenSSL and BoringSSL both use ":" as the SSL_CERT_DIR separator. // See: // * https://golang.org/issue/35325 // * https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html dirs = strings.Split(d, ":") } for _, directory := range dirs { fis, err := readUniqueDirectoryEntries(directory) if err != nil { if firstErr == nil && !os.IsNotExist(err) { firstErr = err } continue } for _, fi := range fis { data, err := os.ReadFile(directory + "/" + fi.Name()) if err == nil { roots.AppendCertsFromPEM(data) } } } if len(roots.Subjects()) > 0 || firstErr == nil { return roots, nil } return nil, firstErr } // readUniqueDirectoryEntries is like os.ReadDir but omits // symlinks that point within the directory. func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) { files, err := os.ReadDir(dir) if err != nil { return nil, err } uniq := files[:0] for _, f := range files { if !isSameDirSymlink(f, dir) { uniq = append(uniq, f) } } return uniq, nil } // isSameDirSymlink reports whether fi in dir is a symlink with a // target not containing a slash. func isSameDirSymlink(f fs.DirEntry, dir string) bool { if f.Type()&fs.ModeSymlink == 0 { return false } target, err := os.Readlink(filepath.Join(dir, f.Name())) return err == nil && !strings.Contains(target, "/") } secsipidx-1.3.2/secsipid/url_cache_test.go000066400000000000000000000163311452516616500206450ustar00rootroot00000000000000package secsipid_test import ( "context" "fmt" "net" "net/http" "os" "testing" "time" "github.com/asipto/secsipidx/secsipid" "github.com/gomagedon/expectate" ) type GetURLValueTest struct { urlVal string timeoutVal int expectedContent []byte expectedErrCode int expectedErrMsg string } func TestGetURLContent(t *testing.T) { os.Remove("http_example.com_foo") os.Remove("http_localhost:5555_foo") tcpDialErrMsg := getTcpDialErrMsg() runTest := func(t *testing.T, testCase GetURLValueTest) { expect := expectate.Expect(t) // testing utility content, errCode, err := secsipid.SJWTGetURLContent( testCase.urlVal, testCase.timeoutVal) expect(content).ToEqual(testCase.expectedContent) expect(errCode).ToBe(testCase.expectedErrCode) if len(testCase.expectedErrMsg) > 0 { expect(getMsgFromErr(err)).ToBe(testCase.expectedErrMsg) } } t.Run("ErrHTTPInvalidURL with empty urlVal", func(t *testing.T) { runTest(t, GetURLValueTest{ urlVal: "", timeoutVal: 10, expectedContent: nil, expectedErrCode: secsipid.SJWTRetErrHTTPInvalidURL, expectedErrMsg: "no URL value", }) }) t.Run("ErrHTTPInvalidURL with non-http scheme", func(t *testing.T) { badSchemes := []string{"sip", "ftp", "file", "foo", "bar", "invalid"} for _, scheme := range badSchemes { t.Run(scheme, func(t *testing.T) { runTest(t, GetURLValueTest{ urlVal: fmt.Sprintf("%s://example.com/somepath", scheme), timeoutVal: 10, expectedContent: nil, expectedErrCode: secsipid.SJWTRetErrHTTPInvalidURL, expectedErrMsg: "invalid URL value", }) }) } }) t.Run("OK with cached value", func(t *testing.T) { workDir, _ := os.Getwd() secsipid.SetURLFileCacheOptions(workDir, int(time.Hour)) os.WriteFile("http_example.com_foo", []byte("Hello, world"), 0777) runTest(t, GetURLValueTest{ urlVal: "http://example.com/foo", timeoutVal: 10, expectedContent: []byte("Hello, world"), expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) os.Remove("http_example.com_foo") }) t.Run("ErrHTTPGet with no cache file and no running server", func(t *testing.T) { workDir, _ := os.Getwd() secsipid.SetURLFileCacheOptions(workDir, int(time.Hour)) defer secsipid.SetURLFileCacheOptions("", 0) runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: nil, expectedErrCode: secsipid.SJWTRetErrHTTPGet, expectedErrMsg: tcpDialErrMsg, }) }) t.Run("ErrHTTPGet when timeout expires", func(t *testing.T) { if os.Getenv("GO_TEST_ALL") != "on" { t.Skip("This test takes a long time. $GO_TEST_ALL must be set to 'on'") } handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(time.Second * 2) // time out }) stopTestServer := startTestServer(handler) defer stopTestServer() runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 1, expectedContent: nil, expectedErrCode: secsipid.SJWTRetErrHTTPGet, expectedErrMsg: `http get failure: Get "http://localhost:5555/foo": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`, }) }) t.Run("ErrHTTPStatusCode when not 200 OK status code", func(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(400) }) stopTestServer := startTestServer(handler) defer stopTestServer() runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: nil, expectedErrCode: secsipid.SJWTRetErrHTTPStatusCode, expectedErrMsg: "http status error: 400", }) }) t.Run("ErrHTTPReadBody with bad response body", func(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // This will cause the response body to be invalid and return an error when read w.Header().Set("Content-Length", "1") }) stopTestServer := startTestServer(handler) defer stopTestServer() runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: nil, expectedErrCode: secsipid.SJWTRetErrHTTPReadBody, expectedErrMsg: "read http body failure: unexpected EOF", }) }) t.Run("OK but does not cache if cacheDirPath is unset", func(t *testing.T) { secsipid.SetURLFileCacheOptions("", 0) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello from the server!")) }) stopTestServer := startTestServer(handler) runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: []byte("Hello from the server!"), expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) stopTestServer() runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: nil, expectedErrCode: secsipid.SJWTRetErrHTTPGet, expectedErrMsg: tcpDialErrMsg, }) }) t.Run("OK and caches if cacheDirPath is set", func(t *testing.T) { workDir, _ := os.Getwd() secsipid.SetURLFileCacheOptions(workDir, int(time.Hour)) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello from the server!")) }) stopTestServer := startTestServer(handler) runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: []byte("Hello from the server!"), expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) stopTestServer() runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: []byte("Hello from the server!"), expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) os.Remove("http_localhost:5555_foo") }) t.Run("Not OK if cache expires", func(t *testing.T) { if os.Getenv("GO_TEST_ALL") != "on" { t.Skip("This test takes a long time. $GO_TEST_ALL must be set to 'on'") } workDir, _ := os.Getwd() secsipid.SetURLFileCacheOptions(workDir, 1) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello from the server!")) }) stopTestServer := startTestServer(handler) runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: []byte("Hello from the server!"), expectedErrCode: secsipid.SJWTRetOK, expectedErrMsg: "", }) stopTestServer() time.Sleep(time.Second * 2) runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: nil, expectedErrCode: secsipid.SJWTRetErrHTTPGet, expectedErrMsg: tcpDialErrMsg, }) os.Remove("http_localhost:5555_foo") }) } func startTestServer(handler http.Handler) (shutdown func()) { server := http.Server{ Addr: "127.0.0.1:5555", Handler: handler, } listener, _ := net.Listen("tcp", "localhost:5555") go server.Serve(listener) return func() { server.Shutdown(context.Background()) listener.Close() } } func getTcpDialErrMsg() string { // Can't hardcode this error message because localhost resolves differently // on different machines (like a GitHub actions container) _, tcpDialErr := http.Get("http://localhost:5555/foo") return "http get failure: " + tcpDialErr.Error() } secsipidx-1.3.2/secsipidx.1000066400000000000000000000050651452516616500156060ustar00rootroot00000000000000.TH SECSIPIDX 1 "2021-01-07" .\" Please adjust this date whenever revising the manpage. .SH NAME secsipidx \- CLI tool and HTTP API server to check or build SIP identity headers .SH SYNOPSIS .B secsipidx .RI [ options ] .SH DESCRIPTION Command line application to check or build SIP identity headers as per IETF RFC8224 and RFC8588 (STIR and SHAKEN). It also can be run in daemon mode, providing HTTP REST API to ease the adoption of STIR and SHAKEN by external applications. .SH OPTIONS .TP .B \-H, \-http-srv http server bind address .TP .B \-https-srv https server bind address .TP .B \-https-pubkey https server public key .TP .B \-https-prvkey https server private key .TP .B \-http-dir directory to serve over http .TP .B \-k, \-fprvkey path to private key .TP .B \-p, \-fpubkey path to public key .TP .B \-fheader path to file with header value in JSON format .TP .B \-header header value in JSON format .TP .B \-fpayload path to file with payload value in JSON format .TP .B \-payload payload value in JSON format .TP .B \-fidentity path to file with identity value .TP .B \-identity identity value .TP .B \-alg encryption algorithm (default: ES256) .TP .B \-ppt used extension (default: shaken) .TP .B \-typ token type (default: passport) .TP .B \-x5u value of the field with the location of the certificate used to sign the token (default: '') .TP .B \-a, \-attest attestation level (default: 'C') .TP .B \-d, \-dest-tn destination (called) number (default: '') .TP .B \-o, \-orig-th origination (calling) number (default: '') .TP .B \-iat timestamp when the token was created .TP .B \-orig-id origination identifier (default: '') .TP .B \-c, \-check check validity of the signature .TP .B \-s, \-sign sign the header and payload .TP .B \-S, -sign-full sign the header and payload, with parameters .TP .B \-json-parse parse and re-serialize JSON header and payaload values .TP .B \-expire duration of token validity (in seconds) .TP .B \-timeout http get timeout (in seconds, default: 3) .TP .B \-l, \-ltest run local basic test .TP .B \-version print version .TP .B \-cache-dir path to the directory with cached certificates (default: '') .TP .B \-cache-expire duration of cached certificates (in seconds, default 3600) .TP .B \-ca-file file with root CA certificates in pem format .TP .B \-ca-inter file with intermediate CA certificates in pem format .TP .B \-cert-verify certificate verification mode (default: 0) .TP .B \-crl-file file with CRL .TP .SH EXAMPLES TODO .SH AUTHOR .PP This manual page was written by Victor Seva , for the Debian project (and may be used by others).