pax_global_header00006660000000000000000000000064152725671240014525gustar00rootroot0000000000000052 comment=bfe9fa1d5b558bd27d227e4832c86dc21494aefa asipto-secsipidx-bfe9fa1/000077500000000000000000000000001527256712400155745ustar00rootroot00000000000000asipto-secsipidx-bfe9fa1/.github/000077500000000000000000000000001527256712400171345ustar00rootroot00000000000000asipto-secsipidx-bfe9fa1/.github/FUNDING.yml000066400000000000000000000000221527256712400207430ustar00rootroot00000000000000github: [miconda] asipto-secsipidx-bfe9fa1/.github/workflows/000077500000000000000000000000001527256712400211715ustar00rootroot00000000000000asipto-secsipidx-bfe9fa1/.github/workflows/go.yml000066400000000000000000000005061527256712400223220ustar00rootroot00000000000000name: 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@v7 with: go-version: "1.26" - name: Test run: GO_TEST_ALL=on go test -v ./... asipto-secsipidx-bfe9fa1/.github/workflows/release.yaml000066400000000000000000000041611527256712400234770ustar00rootroot00000000000000name: Release on: release: types: [created] workflow_dispatch: inputs: release_tag: description: Existing release tag to build and upload assets for required: true type: string permissions: contents: write jobs: releases-matrix: name: Release Go Binary (${{ matrix.goos }}, ${{ matrix.goarch }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: goos: [linux, windows, darwin] goarch: ["386", amd64, arm64] exclude: - goos: darwin goarch: "386" - goos: windows goarch: arm64 steps: - uses: actions/checkout@v7 with: ref: ${{ github.event.release.tag_name || inputs.release_tag }} - name: Set up Go uses: actions/setup-go@v7 with: go-version-file: go.mod cache: true - name: Build and package id: package env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} CGO_ENABLED: "0" RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }} run: | binary_name="secsipidx" if [ "$GOOS" = "windows" ]; then binary_name="${binary_name}.exe" fi package_dir="dist/package" archive_base="secsipidx-${RELEASE_TAG}-${GOOS}-${GOARCH}" mkdir -p "$package_dir" go build -trimpath -ldflags="-s -w" -o "$package_dir/$binary_name" . cp LICENSE README.md secsipidx.1 "$package_dir/" if [ "$GOOS" = "windows" ]; then archive="dist/${archive_base}.zip" (cd "$package_dir" && zip -q -r "../../$archive" .) else archive="dist/${archive_base}.tar.gz" tar -C "$package_dir" -czf "$archive" . fi echo "asset=$archive" >> "$GITHUB_OUTPUT" - name: Upload release asset env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: >- gh release upload "${{ github.event.release.tag_name || inputs.release_tag }}" "${{ steps.package.outputs.asset }}" --clobber asipto-secsipidx-bfe9fa1/.gitignore000066400000000000000000000010651527256712400175660ustar00rootroot00000000000000# 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 asipto-secsipidx-bfe9fa1/LICENSE000066400000000000000000000032271527256712400166050ustar00rootroot00000000000000The 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. asipto-secsipidx-bfe9fa1/Makefile000066400000000000000000000017221527256712400172360ustar00rootroot00000000000000# 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 asipto-secsipidx-bfe9fa1/Makefile.defs000066400000000000000000000006031527256712400201530ustar00rootroot00000000000000# Common defines for Makefile build system # override makefile_defs_included:=1 PREFIX ?= /usr/local BASENAME ?= secsipid TOOLNAME ?= secsipidx LIBVERMAJ ?= 2 LIBVERMIN ?= 0 LIBVERLEV ?= 0 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 "/" "_") asipto-secsipidx-bfe9fa1/README.md000066400000000000000000000315431527256712400170610ustar00rootroot00000000000000# SecSIPIdX Project Secure SIP/Telephony Identity Extensions. Last Version: 2.0.0 Release: Nov 15, 2023 ## Table Of Content - [secsipidx](#secsipidx-project) * [Overview](#overview) * [secsipidx](#secsipidx) + [Installation](#installation) * [Usage](#usage) + [Keys Generation](#keys-generation) + [Tools Usage](#tools-usage) - [CLI - Generate Full Identity Header](#cli-generate-full-identity-header) - [CLI - Check Full Identity Header](#cli-check-full-identity-header) - [HTTP Server](#http-server) * [Check Identity](#check-identity) * [Generate Identity - CSV API](#generate-identity-csv-api) * [HTTP File Server](#http-file-server) + [Certificate Verification](#certificate-verification) * [Certificate Caching](#certificate-caching) * [C API](#c-api) + [C Library Options](#c-library-options) * [To-Do](#to-do) * [Copyright](#copyright) * [Contributing](#contributing) * [Testing](#testing) ## 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 ## 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 ``` With newer Go versions, `go install` may have to be used instead of `go get`: ``` go install 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 ``` ### Tools 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 * `32` (`1<<5`) - verify time validity only (not expired and not before validity date) 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 Downloaded public certificates can be cached in local files or in Redis. The cache lifetime is controlled with `-cache-expire` and defaults to 3600 seconds. The filesystem cache keeps its existing configuration and can be activated with: ```bash secsipidx -cache-dir /path/to/cachedir -cache-file-mode sha256 ``` Specifying `-cache-backend file` is optional when `-cache-dir` is set. The filename mode can be `plain` (the default) or `sha256`. The Redis cache is configured with: ```bash secsipidx -cache-backend redis \ -redis-url redis://localhost:6379/0 \ -redis-key-prefix secsipidx:cert: \ -redis-timeout 3 \ -cache-store-url \ -cache-expire 3600 ``` Redis URLs can use `redis://`, `rediss://`, or `unix://` and can include authentication and database selection. Certificate URLs are stored as SHA-256 keys below `-redis-key-prefix`; the original URL is not exposed in the Redis key. Redis controls expiration using the configured cache lifetime. Cache read or write failures do not prevent certificate downloading or verification. Use `-cache-store-url` to store the original certificate URL as optional metadata. This is disabled by default because URLs can contain credentials or sensitive query parameters. The filesystem cache stores the URL in `.url`. Redis stores it in a companion key named `:url` with the same TTL as the certificate. Restrict access to the cache when this option is enabled. The Go library exposes the same settings through `SJWTOptions`: `CacheBackend`, `RedisURL`, `RedisKeyPrefix`, `RedisTimeout`, `CacheExpire`, and `CacheStoreURL`. `SJWTVerifier` and `SJWTSigner` instances should be closed when no longer needed so Redis connection-pool resources are released. Certificate URL downloads allow private and local destinations by default. Use `-deny-private-x5u` to reject loopback, private, link-local, multicast, unspecified, and carrier-grade NAT destinations. Redirect destinations are checked as well when this restriction is enabled. Downloaded certificate bodies are limited to 1 MiB by default. The limit can be configured with `-max-url-content-bytes`. Responses exceeding it are rejected and are not written to the certificate cache. 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 * `CacheBackend` (str) - cache backend: `none`, `file`, or `redis`; an empty value keeps the legacy behavior where `CacheDirPath` enables the file cache * `CacheNameMode` (str) - filesystem cache filename mode: `plain` or `sha256` * `CacheStoreURL` (int) - set to `1` to store the original certificate URL in companion cache metadata; the default is `0` * `CacheExpires` (int) - number of seconds after which cached certificates are invalidated * `RedisURL` (str) - Redis connection URL using `redis://`, `rediss://`, or `unix://` * `RedisKeyPrefix` (str) - prefix for Redis certificate keys; the default is `secsipidx:cert:` * `RedisTimeout` (int) - Redis operation timeout in seconds; the default is `3` * `DenyPrivateX5U` (int) - set to `1` to reject certificate URLs resolving to private or local addresses; the default is `0` * `MaxURLContentBytes` (int) - maximum downloaded certificate size in bytes; the default is `1048576` * `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 * 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 ``` asipto-secsipidx-bfe9fa1/csecsipid/000077500000000000000000000000001527256712400175425ustar00rootroot00000000000000asipto-secsipidx-bfe9fa1/csecsipid/Makefile000066400000000000000000000024751527256712400212120ustar00rootroot00000000000000# Makefile to build libraries # include ../Makefile.defs SOBASENAME=libsecsipid.so SOLIBNAME=${SOBASENAME}.${LIBVERMAJ} 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} asipto-secsipidx-bfe9fa1/csecsipid/csecsipid.go000066400000000000000000000224151527256712400220430ustar00rootroot00000000000000package 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 -- // 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 // //export SecSIPIDCheckFull 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) } // SecSIPIDOptGetN -- // get the number (integer) option from the library // * optName - name of the option // * return: option value if it was found, -1 otherwise // //export SecSIPIDOptGetN func SecSIPIDOptGetN(optName *C.char) C.int { ret := secsipid.SJWTLibOptGetN(C.GoString(optName)) 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() {} asipto-secsipidx-bfe9fa1/csecsipid/libsecsipid.h000066400000000000000000000175151527256712400222160ustar00rootroot00000000000000/* 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); // SecSIPIDOptGetN -- // get the number (integer) option from the library // * optName - name of the option // * return: option value if it was found, -1 otherwise // extern int SecSIPIDOptGetN(char* optName); // 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 asipto-secsipidx-bfe9fa1/csecsipid/secsipid-1.pc000066400000000000000000000003641527256712400220320ustar00rootroot00000000000000prefix=/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} asipto-secsipidx-bfe9fa1/csecsipid/secsipid.h000066400000000000000000000005541527256712400215220ustar00rootroot00000000000000/* 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 v2.0.0 */ #define SECSIPID_VERSION 0x2000000 #endif asipto-secsipidx-bfe9fa1/go.mod000066400000000000000000000005401527256712400167010ustar00rootroot00000000000000module github.com/asipto/secsipidx go 1.26.0 require ( github.com/gomagedon/expectate v1.1.0 github.com/google/uuid v1.4.0 github.com/redis/go-redis/v9 v9.23.0 ) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/google/go-cmp v0.5.4 // indirect go.uber.org/atomic v1.12.0 // indirect golang.org/x/sys v0.48.0 // indirect ) asipto-secsipidx-bfe9fa1/go.sum000066400000000000000000000045241527256712400167340ustar00rootroot00000000000000github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.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.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/redis/go-redis/v9 v9.23.0 h1:/TTVdJa8BRNsybwwGGnmiYktHNlUXtbOQxlWpHBPL3s= github.com/redis/go-redis/v9 v9.23.0/go.mod h1:EWP2UUk+XDaI1s5nQB5mablZN6vccXlmBQTyfnF87D0= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE= go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= asipto-secsipidx-bfe9fa1/main.go000066400000000000000000000530211527256712400170500ustar00rootroot00000000000000package main import ( "crypto/ecdsa" "encoding/json" "flag" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "time" "github.com/asipto/secsipidx/secsipid" ) const secsipidxVersion = "2.0.0" const ( maxHTTPRequestBodyBytes = 1 << 20 httpReadHeaderTimeout = 5 * time.Second httpReadTimeout = 10 * time.Second httpWriteTimeout = 10 * time.Second httpIdleTimeout = 30 * time.Second ) // 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 cachebackend string cachedir string cacheexpire int cachefilemode string cachestoreurl bool redisurl string rediskeyprefix string redistimeout int denyprivateX5U bool maxURLContentBytes 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, cachebackend: "", cachedir: "", cacheexpire: 3600, cachefilemode: "plain", cachestoreurl: false, redisurl: "", rediskeyprefix: "secsipidx:cert:", redistimeout: 3, denyprivateX5U: false, maxURLContentBytes: 1 << 20, cafile: "", cainter: "", crlfile: "", certverify: 0, verbosity: 0, } func registerFlags(fs *flag.FlagSet, opts *CLIOptions) { fs.StringVar(&opts.httpsrv, "http-srv", opts.httpsrv, "http server bind address") fs.StringVar(&opts.httpsrv, "H", opts.httpsrv, "http server bind address") fs.StringVar(&opts.httpssrv, "https-srv", opts.httpssrv, "https server bind address") fs.StringVar(&opts.httpspubkey, "https-pubkey", opts.httpspubkey, "https server public key") fs.StringVar(&opts.httpsprvkey, "https-prvkey", opts.httpsprvkey, "https server private key") fs.StringVar(&opts.httpdir, "http-dir", opts.httpdir, "directory to serve over http") fs.StringVar(&opts.fprvkey, "fprvkey", opts.fprvkey, "path to private key") fs.StringVar(&opts.fprvkey, "k", opts.fprvkey, "path to private key") fs.StringVar(&opts.fpubkey, "fpubkey", opts.fpubkey, "path to public key") fs.StringVar(&opts.fpubkey, "p", opts.fpubkey, "path to public key") fs.StringVar(&opts.fheader, "fheader", opts.fheader, "path to file with header value in JSON format") fs.StringVar(&opts.header, "header", opts.header, "header value in JSON format") fs.StringVar(&opts.fpayload, "fpayload", opts.fpayload, "path to file with payload value in JSON format") fs.StringVar(&opts.payload, "payload", opts.payload, "payload value in JSON format") fs.StringVar(&opts.fidentity, "fidentity", opts.fidentity, "path to file with identity value") fs.StringVar(&opts.identity, "identity", opts.identity, "identity value") fs.StringVar(&opts.alg, "alg", opts.alg, "encryption algorithm") fs.StringVar(&opts.ppt, "ppt", opts.ppt, "used extension") fs.StringVar(&opts.typ, "typ", opts.typ, "token type") fs.StringVar(&opts.x5u, "x5u", opts.x5u, "value of the field with the location of the certificate used to sign the token (default: '')") fs.StringVar(&opts.attest, "attest", opts.attest, "attestation level") fs.StringVar(&opts.attest, "a", opts.attest, "attestation level") fs.StringVar(&opts.desttn, "dest-tn", opts.desttn, "destination (called) number (default: '')") fs.StringVar(&opts.desttn, "d", opts.desttn, "destination (called) number (default: '')") fs.StringVar(&opts.origtn, "orig-tn", opts.origtn, "origination (calling) number (default: '')") fs.StringVar(&opts.origtn, "o", opts.origtn, "origination (calling) number (default: '')") fs.IntVar(&opts.iat, "iat", opts.iat, "timestamp when the token was created") fs.StringVar(&opts.origid, "orig-id", opts.origid, "origination identifier (default: '')") fs.BoolVar(&opts.check, "check", opts.check, "check validity of the signature") fs.BoolVar(&opts.check, "c", opts.check, "check validity of the signature") fs.BoolVar(&opts.sign, "sign", opts.sign, "sign the header and payload given as full JSON documents") fs.BoolVar(&opts.sign, "s", opts.sign, "sign the header and payload given as full JSON documents") fs.BoolVar(&opts.signfull, "sign-full", opts.signfull, "sign the header and payload build from the individual parameter values") fs.BoolVar(&opts.signfull, "S", opts.signfull, "sign the header and payload, with parameters") fs.BoolVar(&opts.jsonparse, "json-parse", opts.jsonparse, "parse and re-serialize JSON header and payload values") fs.IntVar(&opts.expire, "expire", opts.expire, "duration of token validity (in seconds)") fs.IntVar(&opts.timeout, "timeout", opts.timeout, "http get timeout (in seconds)") fs.BoolVar(&opts.ltest, "ltest", opts.ltest, "run local basic test") fs.BoolVar(&opts.ltest, "l", opts.ltest, "run local basic test") fs.BoolVar(&opts.version, "version", opts.version, "print version") fs.StringVar(&opts.cachebackend, "cache-backend", opts.cachebackend, "certificate cache backend: none, file, or redis") fs.StringVar(&opts.cachedir, "cache-dir", opts.cachedir, "path to the directory with cached certificates (default: '')") fs.IntVar(&opts.cacheexpire, "cache-expire", opts.cacheexpire, "duration of cached certificates (in seconds)") fs.StringVar(&opts.cachefilemode, "cache-file-mode", opts.cachefilemode, "cache filename mode: plain or sha256") fs.BoolVar(&opts.cachestoreurl, "cache-store-url", opts.cachestoreurl, "store certificate URL metadata alongside cached content") fs.StringVar(&opts.redisurl, "redis-url", opts.redisurl, "Redis cache URL (redis://, rediss://, or unix://)") fs.StringVar(&opts.rediskeyprefix, "redis-key-prefix", opts.rediskeyprefix, "Redis cache key prefix") fs.IntVar(&opts.redistimeout, "redis-timeout", opts.redistimeout, "Redis operation timeout (in seconds)") fs.BoolVar(&opts.denyprivateX5U, "deny-private-x5u", opts.denyprivateX5U, "deny certificate URLs resolving to private or local addresses") fs.IntVar(&opts.maxURLContentBytes, "max-url-content-bytes", opts.maxURLContentBytes, "maximum downloaded certificate size in bytes") fs.StringVar(&opts.cafile, "ca-file", opts.cafile, "file with root CA certificates in pem format") fs.StringVar(&opts.cainter, "ca-inter", opts.cainter, "file with intermediate CA certificates in pem format") fs.StringVar(&opts.crlfile, "crl-file", opts.crlfile, "file with CRL in pem format") fs.IntVar(&opts.certverify, "cert-verify", opts.certverify, "certificate verification mode (default 0)") fs.IntVar(&opts.verbosity, "verbosity", opts.verbosity, "verbosity level (default 0)") fs.IntVar(&opts.verbosity, "vl", opts.verbosity, "verbosity level (default 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) } registerFlags(flag.CommandLine, &cliops) } 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, err := os.ReadFile("../test/certs/ec256-private.pem") if err != nil { fmt.Printf("Unable to read ECDSA private key: %v\n", err) return } 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, err := os.ReadFile("../test/certs/ec256-public.pem") if err != nil { fmt.Printf("Unable to read ECDSA public key: %v\n", err) return } 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 readCLIFile(path string, label string) ([]byte, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("unable to read %s file %q: %w", label, path, err) } return data, nil } 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, err := readCLIFile(cliops.fheader, "header") if err != nil { fmt.Printf("%v\n", err) return -1 } 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, err := readCLIFile(cliops.fpayload, "payload") if err != nil { fmt.Printf("%v\n", err) return -1 } 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, err := readCLIFile(cliops.fprvkey, "private key") if err != nil { fmt.Printf("%v\n", err) return -1 } 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.fidentity) > 0 { vIdentity, err := readCLIFile(cliops.fidentity, "identity") if err != nil { fmt.Printf("%v\n", err) return -1 } 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 readHTTPRequestBody(w http.ResponseWriter, r *http.Request) ([]byte, bool) { r.Body = http.MaxBytesReader(w, r.Body, maxHTTPRequestBodyBytes) defer r.Body.Close() body, err := io.ReadAll(r.Body) if err != nil { if _, ok := err.(*http.MaxBytesError); ok { http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) return nil, false } http.Error(w, "cannot read body", http.StatusBadRequest) return nil, false } return body, true } func httpHandleV1Check(w http.ResponseWriter, r *http.Request) { var ret int fmt.Printf("incoming request for identity check ...\n") body, ok := readHTTPRequestBody(w, r) if !ok { return } var err error ret, err = secsipid.SJWTCheckFullIdentity(string(body), cliops.expire, cliops.fpubkey, cliops.timeout) if err != nil || ret != secsipid.SJWTRetOK { fmt.Printf("failed checking identity - return code: %d, error: %v\n", ret, 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, ok := readHTTPRequestBody(w, r) if !ok { return } var err error 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 buildHTTPHandler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/v1/check", httpHandleV1Check) mux.HandleFunc("/v1/sign-csv", httpHandleV1SignCSV) if len(cliops.httpdir) > 0 { fmt.Printf("serving files over http from directory: %s\n", cliops.httpdir) mux.Handle("/v1/pub/", http.StripPrefix("/v1/pub/", http.FileServer(http.Dir(cliops.httpdir)))) } return mux } func startHTTPServices(handler http.Handler) chan error { errchan := make(chan error) // starting HTTP server if len(cliops.httpsrv) > 0 { go func() { log.Printf("starting HTTP service on: %s ...", cliops.httpsrv) server := &http.Server{ Addr: cliops.httpsrv, Handler: handler, ReadHeaderTimeout: httpReadHeaderTimeout, ReadTimeout: httpReadTimeout, WriteTimeout: httpWriteTimeout, IdleTimeout: httpIdleTimeout, } if err := server.ListenAndServe(); err != nil { errchan <- err } }() } // starting HTTPS server if len(cliops.httpssrv) > 0 && len(cliops.httpspubkey) > 0 && len(cliops.httpsprvkey) > 0 { go func() { log.Printf("Starting HTTPS service on: %s ...", cliops.httpssrv) server := &http.Server{ Addr: cliops.httpssrv, Handler: handler, ReadHeaderTimeout: httpReadHeaderTimeout, ReadTimeout: httpReadTimeout, WriteTimeout: httpWriteTimeout, IdleTimeout: httpIdleTimeout, } if err := server.ListenAndServeTLS(cliops.httpspubkey, cliops.httpsprvkey); err != nil { errchan <- err } }() } return errchan } func httpServiceExitCode(err error) int { if err == nil || err == http.ErrServerClosed { return 0 } return 1 } func configureCache(opts CLIOptions) error { switch opts.cachebackend { case "", "none", "file", "redis": default: return fmt.Errorf("invalid cache backend: %s", opts.cachebackend) } if opts.cachebackend == "redis" { if opts.redisurl == "" { return fmt.Errorf("Redis URL is required for the redis cache backend") } if opts.cachedir != "" { return fmt.Errorf("cache directory cannot be used with the redis cache backend") } if opts.cacheexpire <= 0 { return fmt.Errorf("Redis cache expiration must be positive: %d", opts.cacheexpire) } } else if opts.redisurl != "" { return fmt.Errorf("Redis URL requires the redis cache backend") } // Keep caching disabled while applying settings, then activate the backend // only after every setter has accepted its value. if secsipid.SJWTLibOptSetS("CacheBackend", "none") != secsipid.SJWTRetOK { return fmt.Errorf("unable to disable cache while configuring it") } if secsipid.SJWTLibOptSetS("CacheDirPath", opts.cachedir) != secsipid.SJWTRetOK { return fmt.Errorf("invalid cache directory: %s", opts.cachedir) } if secsipid.SJWTLibOptSetN("CacheExpires", opts.cacheexpire) != secsipid.SJWTRetOK { return fmt.Errorf("invalid cache expiration: %d", opts.cacheexpire) } if secsipid.SJWTLibOptSetS("CacheNameMode", opts.cachefilemode) != secsipid.SJWTRetOK { return fmt.Errorf("invalid cache file mode: %s", opts.cachefilemode) } if secsipid.SJWTLibOptSetN("CacheStoreURL", boolToInt(opts.cachestoreurl)) != secsipid.SJWTRetOK { return fmt.Errorf("invalid cache URL metadata setting") } if secsipid.SJWTLibOptSetS("RedisURL", opts.redisurl) != secsipid.SJWTRetOK { return fmt.Errorf("invalid Redis URL") } if secsipid.SJWTLibOptSetS("RedisKeyPrefix", opts.rediskeyprefix) != secsipid.SJWTRetOK { return fmt.Errorf("invalid Redis key prefix") } if secsipid.SJWTLibOptSetN("RedisTimeout", opts.redistimeout) != secsipid.SJWTRetOK { return fmt.Errorf("invalid Redis timeout: %d", opts.redistimeout) } if secsipid.SJWTLibOptSetS("CacheBackend", opts.cachebackend) != secsipid.SJWTRetOK { return fmt.Errorf("invalid cache backend: %s", opts.cachebackend) } return nil } func boolToInt(value bool) int { if value { return 1 } return 0 } func main() { var ret int flag.Parse() if cliops.version { fmt.Printf("%s v%s\n", filepath.Base(os.Args[0]), secsipidxVersion) os.Exit(0) } if cliops.ltest { localTest() os.Exit(0) } if err := configureCache(cliops); err != nil { fmt.Printf("%v\n", err) os.Exit(1) } if cliops.denyprivateX5U { secsipid.SJWTLibOptSetN("DenyPrivateX5U", 1) } if secsipid.SJWTLibOptSetN("MaxURLContentBytes", cliops.maxURLContentBytes) != secsipid.SJWTRetOK { fmt.Printf("invalid maximum URL content size: %d\n", cliops.maxURLContentBytes) os.Exit(1) } 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) { fmt.Printf("starting http services ...\n") errchan := startHTTPServices(buildHTTPHandler()) select { case err := <-errchan: log.Printf("unable to start http services due to (error: %v)", err) os.Exit(httpServiceExitCode(err)) } } 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) } asipto-secsipidx-bfe9fa1/main_test.go000066400000000000000000000237761527256712400201250ustar00rootroot00000000000000package main import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/pem" "errors" "flag" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/asipto/secsipidx/secsipid" ) func TestHTTPServiceExitCode(t *testing.T) { testCases := []struct { name string err error want int }{ {name: "nil error is success", err: nil, want: 0}, {name: "server closed is success", err: http.ErrServerClosed, want: 0}, {name: "other error is failure", err: errors.New("bind failed"), want: 1}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if got := httpServiceExitCode(tc.err); got != tc.want { t.Fatalf("expected exit code %d, got %d", tc.want, got) } }) } } func TestRegisterFlagsParsesSignFullFlagsIndependently(t *testing.T) { t.Run("long flag sets signfull", func(t *testing.T) { opts := cliops fs := flag.NewFlagSet("test", flag.ContinueOnError) registerFlags(fs, &opts) if err := fs.Parse([]string{"-sign-full"}); err != nil { t.Fatalf("parse flags: %v", err) } if !opts.signfull { t.Fatalf("expected signfull to be true") } if opts.sign { t.Fatalf("expected sign to remain false") } }) t.Run("short flag sets signfull", func(t *testing.T) { opts := cliops fs := flag.NewFlagSet("test", flag.ContinueOnError) registerFlags(fs, &opts) if err := fs.Parse([]string{"-S"}); err != nil { t.Fatalf("parse flags: %v", err) } if !opts.signfull { t.Fatalf("expected signfull to be true") } if opts.sign { t.Fatalf("expected sign to remain false") } }) } func TestRegisterFlagsDeniesPrivateCertificateURLsExplicitly(t *testing.T) { opts := cliops fs := flag.NewFlagSet("test", flag.ContinueOnError) registerFlags(fs, &opts) if err := fs.Parse([]string{"-deny-private-x5u"}); err != nil { t.Fatalf("parse flags: %v", err) } if !opts.denyprivateX5U { t.Fatalf("expected deny-private-x5u to be true") } } func TestRegisterFlagsSetsMaximumURLContentSize(t *testing.T) { opts := cliops fs := flag.NewFlagSet("test", flag.ContinueOnError) registerFlags(fs, &opts) if err := fs.Parse([]string{"-max-url-content-bytes", "2048"}); err != nil { t.Fatalf("parse flags: %v", err) } if opts.maxURLContentBytes != 2048 { t.Fatalf("expected max URL content size 2048, got %d", opts.maxURLContentBytes) } } func TestRegisterFlagsSetsRedisCacheOptions(t *testing.T) { opts := cliops fs := flag.NewFlagSet("test", flag.ContinueOnError) registerFlags(fs, &opts) err := fs.Parse([]string{ "-cache-backend", "redis", "-cache-expire", "900", "-cache-store-url", "-redis-url", "rediss://user:password@example.com:6380/2", "-redis-key-prefix", "tenant-a:cert:", "-redis-timeout", "5", }) if err != nil { t.Fatalf("parse flags: %v", err) } if opts.cachebackend != "redis" || opts.cacheexpire != 900 || !opts.cachestoreurl || opts.redisurl != "rediss://user:password@example.com:6380/2" || opts.rediskeyprefix != "tenant-a:cert:" || opts.redistimeout != 5 { t.Fatalf("unexpected Redis CLI options: %+v", opts) } } func restoreMainCacheOptions(options secsipid.SJWTOptions) { secsipid.SJWTLibOptSetS("CacheBackend", "none") secsipid.SJWTLibOptSetS("CacheDirPath", options.CacheDirPath) secsipid.SJWTLibOptSetN("CacheExpires", options.CacheExpire) secsipid.SJWTLibOptSetS("CacheNameMode", options.CacheNameMode) secsipid.SJWTLibOptSetN("CacheStoreURL", boolToInt(options.CacheStoreURL)) secsipid.SJWTLibOptSetS("RedisURL", options.RedisURL) secsipid.SJWTLibOptSetS("RedisKeyPrefix", options.RedisKeyPrefix) secsipid.SJWTLibOptSetN("RedisTimeout", options.RedisTimeout) secsipid.SJWTLibOptSetS("CacheBackend", options.CacheBackend) } func TestConfigureCache(t *testing.T) { t.Run("configures Redis", func(t *testing.T) { original := secsipid.DefaultSJWTOptions() defer restoreMainCacheOptions(original) opts := cliops opts.cachebackend = "redis" opts.cacheexpire = 900 opts.cachestoreurl = true opts.redisurl = "redis://localhost:6379/2" opts.rediskeyprefix = "tenant-a:cert:" opts.redistimeout = 5 if err := configureCache(opts); err != nil { t.Fatalf("configure Redis cache: %v", err) } configured := secsipid.DefaultSJWTOptions() if configured.CacheBackend != "redis" || configured.CacheExpire != 900 || !configured.CacheStoreURL || configured.RedisURL != opts.redisurl || configured.RedisKeyPrefix != opts.rediskeyprefix || configured.RedisTimeout != 5 { t.Fatalf("unexpected configured Redis options: %+v", configured) } }) t.Run("preserves legacy file activation", func(t *testing.T) { original := secsipid.DefaultSJWTOptions() defer restoreMainCacheOptions(original) opts := cliops opts.cachebackend = "" opts.cachedir = t.TempDir() opts.cachefilemode = "sha256" if err := configureCache(opts); err != nil { t.Fatalf("configure file cache: %v", err) } configured := secsipid.DefaultSJWTOptions() if configured.CacheBackend != "" || configured.CacheDirPath != opts.cachedir || configured.CacheNameMode != "sha256" { t.Fatalf("unexpected configured file options: %+v", configured) } }) testCases := []struct { name string configure func(*CLIOptions) errorSubstr string }{ { name: "invalid backend", configure: func(opts *CLIOptions) { opts.cachebackend = "invalid" }, errorSubstr: "invalid cache backend", }, { name: "missing Redis URL", configure: func(opts *CLIOptions) { opts.cachebackend = "redis" opts.redisurl = "" }, errorSubstr: "Redis URL is required", }, { name: "Redis and file conflict", configure: func(opts *CLIOptions) { opts.cachebackend = "redis" opts.redisurl = "redis://localhost:6379" opts.cachedir = t.TempDir() }, errorSubstr: "cache directory cannot be used", }, { name: "malformed Redis URL", configure: func(opts *CLIOptions) { opts.cachebackend = "redis" opts.redisurl = "://invalid" }, errorSubstr: "invalid Redis URL", }, { name: "invalid Redis timeout", configure: func(opts *CLIOptions) { opts.redistimeout = 0 }, errorSubstr: "invalid Redis timeout", }, { name: "non-Redis backend with Redis URL", configure: func(opts *CLIOptions) { opts.cachebackend = "file" opts.redisurl = "redis://localhost:6379" }, errorSubstr: "Redis URL requires", }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { original := secsipid.DefaultSJWTOptions() defer restoreMainCacheOptions(original) opts := cliops testCase.configure(&opts) err := configureCache(opts) if err == nil || !strings.Contains(err.Error(), testCase.errorSubstr) { t.Fatalf("expected error containing %q, got %v", testCase.errorSubstr, err) } }) } } func TestCLICommandsFailOnMissingInputFiles(t *testing.T) { t.Run("sign fails when header file is missing", func(t *testing.T) { prev := cliops defer func() { cliops = prev }() cliops = CLIOptions{ fprvkey: "/does/not/matter.pem", fheader: "/definitely/missing-header.json", } if ret := secsipidxCLISign(); ret == 0 { t.Fatalf("expected sign command to fail for missing header file") } }) t.Run("check fails when identity file is missing", func(t *testing.T) { prev := cliops defer func() { cliops = prev }() cliops = CLIOptions{ fidentity: "/definitely/missing-identity.txt", } if ret := secsipidxCLICheck(); ret == 0 { t.Fatalf("expected check command to fail for missing identity file") } }) } func TestReadHTTPRequestBodyRejectsLargePayloads(t *testing.T) { oversizedBody := strings.Repeat("a", maxHTTPRequestBodyBytes+1) req := httptest.NewRequest(http.MethodPost, "/v1/check", strings.NewReader(oversizedBody)) rec := httptest.NewRecorder() body, ok := readHTTPRequestBody(rec, req) if ok { t.Fatalf("expected oversized body to be rejected") } if body != nil { t.Fatalf("expected no body to be returned for oversized payload") } if rec.Code != http.StatusRequestEntityTooLarge { t.Fatalf("expected status %d, got %d", http.StatusRequestEntityTooLarge, rec.Code) } } func TestReadHTTPRequestBodyAllowsSmallPayloads(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/v1/check", strings.NewReader("ok")) rec := httptest.NewRecorder() body, ok := readHTTPRequestBody(rec, req) if !ok { t.Fatalf("expected small body to be accepted, got status %d", rec.Code) } if string(body) != "ok" { t.Fatalf("expected body %q, got %q", "ok", string(body)) } } func TestHTTPHandleV1CheckRejectsNonzeroResultWithoutError(t *testing.T) { privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("generate private key: %v", err) } privateKeyDER, err := x509.MarshalECPrivateKey(privateKey) if err != nil { t.Fatalf("marshal private key: %v", err) } privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKeyDER}) publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) if err != nil { t.Fatalf("marshal public key: %v", err) } publicKeyPath := filepath.Join(t.TempDir(), "public.pem") if err := os.WriteFile(publicKeyPath, pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyDER}), 0600); err != nil { t.Fatalf("write public key: %v", err) } identity, ret, err := secsipid.SJWTGetIdentityPrvKey( "12025550100", "12025550101", "A", "", "https://example.com/cert.pem", privateKeyPEM) if err != nil || ret != secsipid.SJWTRetOK { t.Fatalf("build identity: ret=%d err=%v", ret, err) } identityToken := strings.SplitN(identity, ";", 2)[0] previousOptions := cliops defer func() { cliops = previousOptions }() cliops.fpubkey = publicKeyPath cliops.expire = 3600 req := httptest.NewRequest(http.MethodPost, "/v1/check", strings.NewReader(identityToken)) rec := httptest.NewRecorder() httpHandleV1Check(rec, req) if rec.Code != http.StatusInternalServerError { t.Fatalf("expected status %d, got %d", http.StatusInternalServerError, rec.Code) } if strings.TrimSpace(rec.Body.String()) != "FAILED" { t.Fatalf("expected failed response, got %q", rec.Body.String()) } } asipto-secsipidx-bfe9fa1/secsipid/000077500000000000000000000000001527256712400173775ustar00rootroot00000000000000asipto-secsipidx-bfe9fa1/secsipid/.gitignore000066400000000000000000000001331527256712400213640ustar00rootroot00000000000000dummyCA.pem dummyInterCA.pem dummyCRLFile.crl http_example.com_foo http_localhost:5555_fooasipto-secsipidx-bfe9fa1/secsipid/cache.go000066400000000000000000000102311527256712400207660ustar00rootroot00000000000000package secsipid import ( "context" "crypto/sha256" "encoding/hex" "fmt" "os" "path/filepath" "strings" "time" ) type urlCache interface { Get(ctx context.Context, urlVal string) ([]byte, error) Set(ctx context.Context, urlVal string, data []byte) error Close() error } type fileURLCache struct { dirPath string expire int nameMode string storeURL bool } var _ urlCache = (*fileURLCache)(nil) func newFileURLCache(dirPath string, expire int, nameMode string, storeURL bool) *fileURLCache { return &fileURLCache{ dirPath: dirPath, expire: expire, nameMode: nameMode, storeURL: storeURL, } } func (c *fileURLCache) filePath(urlVal string) string { var fileName string switch c.nameMode { case "sha256": hashVal := sha256.Sum256([]byte(urlVal)) fileName = "url_" + hex.EncodeToString(hashVal[:]) default: fileName = strings.ReplaceAll(urlVal, "://", "_") fileName = strings.ReplaceAll(fileName, "/", "_") } if c.dirPath != "" { return filepath.Join(c.dirPath, fileName) } return fileName } func (c *fileURLCache) Get(_ context.Context, urlVal string) ([]byte, error) { filePath := c.filePath(urlVal) fileStat, err := os.Stat(filePath) if err != nil { return nil, err } if int(time.Since(fileStat.ModTime()).Seconds()) > c.expire { _ = os.Remove(filePath) _ = os.Remove(filePath + ".url") return nil, nil } return os.ReadFile(filePath) } func (c *fileURLCache) Set(_ context.Context, urlVal string, data []byte) error { filePath := c.filePath(urlVal) if err := writeFileAtomic(filePath, data); err != nil { return err } urlFilePath := filePath + ".url" if !c.storeURL { _ = os.Remove(urlFilePath) return nil } return writeFileAtomic(urlFilePath, []byte(urlVal)) } func writeFileAtomic(filePath string, data []byte) error { cacheDir := filepath.Dir(filePath) if err := os.MkdirAll(cacheDir, 0750); err != nil { return err } tempFile, err := os.CreateTemp(cacheDir, ".secsipid-cache-*") if err != nil { return err } tempPath := tempFile.Name() defer os.Remove(tempPath) if _, err = tempFile.Write(data); err != nil { tempFile.Close() return err } if err = tempFile.Chmod(0640); err != nil { tempFile.Close() return err } if err = tempFile.Close(); err != nil { return err } return os.Rename(tempPath, filePath) } func (c *fileURLCache) Close() error { return nil } type errorURLCache struct { err error } func (c *errorURLCache) Get(_ context.Context, _ string) ([]byte, error) { return nil, c.err } func (c *errorURLCache) Set(_ context.Context, _ string, _ []byte) error { return c.err } func (c *errorURLCache) Close() error { return nil } func (o *SJWTLibOptions) refreshURLCache() { switch o.cacheBackend { case "", "file": if o.cacheDirPath == "" { o.cache = nil return } o.cache = newFileURLCache(o.cacheDirPath, o.cacheExpire, o.cacheNameMode, o.cacheStoreURL) case "none": o.cache = nil case "redis": cache, err := newRedisURLCache(o.redisURL, o.redisKeyPrefix, o.cacheExpire, o.redisTimeout, o.cacheStoreURL) if err != nil { o.cache = &errorURLCache{err: err} return } o.cache = cache default: o.cache = &errorURLCache{err: fmt.Errorf("unsupported cache backend %q", o.cacheBackend)} } } func (o *SJWTLibOptions) fileURLCache() *fileURLCache { return newFileURLCache(o.cacheDirPath, o.cacheExpire, o.cacheNameMode, o.cacheStoreURL) } // SJWTGetURLCacheFilePath returns the configured filesystem cache path for a URL. func SJWTGetURLCacheFilePath(urlVal string) string { options := globalLibOptionsSnapshot() return options.fileURLCache().filePath(urlVal) } // SJWTGetURLCachedContent reads URL content from the configured cache. func SJWTGetURLCachedContent(urlVal string) ([]byte, error) { options := globalLibOptionsSnapshot() cache := options.cache if cache == nil { cache = options.fileURLCache() } return cache.Get(context.Background(), urlVal) } // SJWTSetURLCachedContent writes URL content to the configured cache. func SJWTSetURLCachedContent(urlVal string, data []byte) error { options := globalLibOptionsSnapshot() cache := options.cache if cache == nil { cache = options.fileURLCache() } return cache.Set(context.Background(), urlVal, data) } asipto-secsipidx-bfe9fa1/secsipid/cache_redis.go000066400000000000000000000047521527256712400221670ustar00rootroot00000000000000package secsipid import ( "context" "crypto/sha256" "encoding/hex" "errors" "time" "github.com/redis/go-redis/v9" ) const ( defaultRedisKeyPrefix = "secsipidx:cert:" defaultRedisTimeout = 3 ) type redisURLCache struct { client *redis.Client expire time.Duration keyPrefix string timeout time.Duration storeURL bool } var _ urlCache = (*redisURLCache)(nil) func newRedisURLCache(redisURL string, keyPrefix string, expire int, timeout int, storeURL bool) (*redisURLCache, error) { if redisURL == "" { return nil, errors.New("Redis URL is required") } if expire <= 0 { return nil, errors.New("Redis cache expiration must be positive") } if timeout <= 0 { return nil, errors.New("Redis timeout must be positive") } options, err := redis.ParseURL(redisURL) if err != nil { return nil, errors.New("invalid Redis URL") } timeoutDuration := time.Duration(timeout) * time.Second options.DialTimeout = timeoutDuration options.ReadTimeout = timeoutDuration options.WriteTimeout = timeoutDuration options.ContextTimeoutEnabled = true if keyPrefix == "" { keyPrefix = defaultRedisKeyPrefix } return &redisURLCache{ client: redis.NewClient(options), expire: time.Duration(expire) * time.Second, keyPrefix: keyPrefix, timeout: timeoutDuration, storeURL: storeURL, }, nil } func validRedisURL(redisURL string) bool { if redisURL == "" { return true } _, err := redis.ParseURL(redisURL) return err == nil } func (c *redisURLCache) key(urlVal string) string { hashVal := sha256.Sum256([]byte(urlVal)) return c.keyPrefix + hex.EncodeToString(hashVal[:]) } func (c *redisURLCache) operationContext(ctx context.Context) (context.Context, context.CancelFunc) { return context.WithTimeout(ctx, c.timeout) } func (c *redisURLCache) Get(ctx context.Context, urlVal string) ([]byte, error) { ctx, cancel := c.operationContext(ctx) defer cancel() data, err := c.client.Get(ctx, c.key(urlVal)).Bytes() if errors.Is(err, redis.Nil) { return nil, nil } return data, err } func (c *redisURLCache) Set(ctx context.Context, urlVal string, data []byte) error { ctx, cancel := c.operationContext(ctx) defer cancel() key := c.key(urlVal) if !c.storeURL { return c.client.Set(ctx, key, data, c.expire).Err() } _, err := c.client.TxPipelined(ctx, func(pipe redis.Pipeliner) error { pipe.Set(ctx, key, data, c.expire) pipe.Set(ctx, key+":url", urlVal, c.expire) return nil }) return err } func (c *redisURLCache) Close() error { return c.client.Close() } asipto-secsipidx-bfe9fa1/secsipid/secsipid.go000066400000000000000000001173451527256712400215440ustar00rootroot00000000000000package secsipid import ( "context" "crypto" "crypto/ecdsa" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/json" "encoding/pem" "errors" "fmt" "io" "math/big" "net" "net/http" "net/url" "os" "strconv" "strings" "sync" "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 ) const defaultMaxURLContentBytes = 1 << 20 // 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 { cache urlCache cacheBackend string cacheDirPath string cacheExpire int cacheNameMode string cacheStoreURL bool redisURL string redisKeyPrefix string redisTimeout int certCAFile string certCAInter string certCRLFile string certVerify int attrsVerify int denyPrivateX5U bool maxURLContentBytes int x5u string } // SJWTOptions - configuration for signer and verifier instances without relying // on process-wide mutable state. type SJWTOptions struct { CacheBackend string CacheDirPath string CacheExpire int CacheNameMode string CacheStoreURL bool RedisURL string RedisKeyPrefix string RedisTimeout int CertCAFile string CertCAInter string CertCRLFile string CertVerify int AttrsVerify int DenyPrivateX5U bool MaxURLContentBytes int X5U string } // SJWTVerifier verifies identities using instance-local options. type SJWTVerifier struct { options SJWTLibOptions } // SJWTSigner builds identities using instance-local options. type SJWTSigner struct { options SJWTLibOptions } const ( CertVerifyOptTime = (1 << 0) CertVerifyOptSysCA = (1 << 1) CertVerifyOptCustCA = (1 << 2) CertVerifyOptInterCA = (1 << 3) CertVerifyOptCRL = (1 << 4) CertVerifyOptTimeOnly = (1 << 5) ) var ( globalLibOptionsMu sync.RWMutex globalLibOptions = SJWTLibOptions{ cacheBackend: "", cacheDirPath: "", cacheExpire: 3600, cacheNameMode: "plain", cacheStoreURL: false, redisURL: "", redisKeyPrefix: defaultRedisKeyPrefix, redisTimeout: defaultRedisTimeout, certCAFile: "", certCAInter: "", certCRLFile: "", certVerify: 0, attrsVerify: 1, denyPrivateX5U: false, maxURLContentBytes: defaultMaxURLContentBytes, x5u: "https://127.0.0.1/cert.pem", } ) func globalLibOptionsSnapshot() SJWTLibOptions { globalLibOptionsMu.RLock() defer globalLibOptionsMu.RUnlock() return globalLibOptions } func defaultSJWTLibOptions() SJWTLibOptions { return globalLibOptionsSnapshot() } func (o SJWTOptions) toLibOptions() SJWTLibOptions { libOptions := defaultSJWTLibOptions() if len(o.CacheBackend) > 0 { libOptions.cacheBackend = o.CacheBackend } if len(o.CacheDirPath) > 0 { libOptions.cacheDirPath = o.CacheDirPath } if o.CacheExpire != 0 { libOptions.cacheExpire = o.CacheExpire } if len(o.CacheNameMode) > 0 { libOptions.cacheNameMode = o.CacheNameMode } libOptions.cacheStoreURL = o.CacheStoreURL if len(o.RedisURL) > 0 { libOptions.redisURL = o.RedisURL } if len(o.RedisKeyPrefix) > 0 { libOptions.redisKeyPrefix = o.RedisKeyPrefix } if o.RedisTimeout != 0 { libOptions.redisTimeout = o.RedisTimeout } if len(o.CertCAFile) > 0 { libOptions.certCAFile = o.CertCAFile } if len(o.CertCAInter) > 0 { libOptions.certCAInter = o.CertCAInter } if len(o.CertCRLFile) > 0 { libOptions.certCRLFile = o.CertCRLFile } libOptions.certVerify = o.CertVerify libOptions.attrsVerify = o.AttrsVerify libOptions.denyPrivateX5U = o.DenyPrivateX5U if o.MaxURLContentBytes > 0 { libOptions.maxURLContentBytes = o.MaxURLContentBytes } if len(o.X5U) > 0 { libOptions.x5u = o.X5U } libOptions.refreshURLCache() return libOptions } // DefaultSJWTOptions returns the current process-wide defaults as an explicit config. func DefaultSJWTOptions() SJWTOptions { options := globalLibOptionsSnapshot() return SJWTOptions{ CacheBackend: options.cacheBackend, CacheDirPath: options.cacheDirPath, CacheExpire: options.cacheExpire, CacheNameMode: options.cacheNameMode, CacheStoreURL: options.cacheStoreURL, RedisURL: options.redisURL, RedisKeyPrefix: options.redisKeyPrefix, RedisTimeout: options.redisTimeout, CertCAFile: options.certCAFile, CertCAInter: options.certCAInter, CertCRLFile: options.certCRLFile, CertVerify: options.certVerify, AttrsVerify: options.attrsVerify, DenyPrivateX5U: options.denyPrivateX5U, MaxURLContentBytes: options.maxURLContentBytes, X5U: options.x5u, } } // NewSJWTVerifier builds a verifier with explicit options instead of global state. func NewSJWTVerifier(options SJWTOptions) *SJWTVerifier { return &SJWTVerifier{options: options.toLibOptions()} } // NewSJWTSigner builds a signer with explicit options instead of global state. func NewSJWTSigner(options SJWTOptions) *SJWTSigner { return &SJWTSigner{options: options.toLibOptions()} } // Close releases resources owned by the verifier cache backend. func (v *SJWTVerifier) Close() error { if v.options.cache == nil { return nil } return v.options.cache.Close() } // Close releases resources owned by the signer cache backend. func (s *SJWTSigner) Close() error { if s.options.cache == nil { return nil } return s.options.cache.Close() } // CheckFullIdentity verifies a full Identity header with instance-local options. func (v *SJWTVerifier) CheckFullIdentity(identityVal string, expireVal int, pubkeyPath string, timeoutVal int) (int, error) { return v.options.checkFullIdentity(identityVal, expireVal, pubkeyPath, timeoutVal) } // CheckFullIdentityURL verifies a full Identity header using the x5u/info URL. func (v *SJWTVerifier) CheckFullIdentityURL(identityVal string, expireVal int, timeoutVal int) (int, error) { return v.options.checkFullIdentityURL(identityVal, expireVal, timeoutVal) } // CheckFullIdentityPubKey verifies a full Identity header against the provided public key data. func (v *SJWTVerifier) CheckFullIdentityPubKey(identityVal string, expireVal int, pubkeyVal string) (int, error) { return v.options.checkFullIdentityPubKey(identityVal, expireVal, pubkeyVal) } // GetIdentity signs an Identity header using a private key path and instance-local defaults. func (s *SJWTSigner) GetIdentity(origTN string, destTN string, attestVal string, origID string, x5uVal string, prvkeyPath string) (string, int, error) { return s.options.getIdentity(origTN, destTN, attestVal, origID, x5uVal, prvkeyPath) } // GetIdentityPrvKey signs an Identity header using private key bytes and instance-local defaults. func (s *SJWTSigner) GetIdentityPrvKey(origTN string, destTN string, attestVal string, origID string, x5uVal string, prvkeyData []byte) (string, int, error) { return s.options.getIdentityPrvKey(origTN, destTN, attestVal, origID, x5uVal, prvkeyData) } var ( sES256KeyBits = 256 sES256KeySize = 32 ) // SetFileCacheOptions -- func SetURLFileCacheOptions(path string, expire int) { globalLibOptionsMu.Lock() defer globalLibOptionsMu.Unlock() globalLibOptions.cacheDirPath = path globalLibOptions.cacheExpire = expire globalLibOptions.refreshURLCache() } // SJWTLibOptSetS -- func SJWTLibOptSetS(optname string, optval string) int { globalLibOptionsMu.Lock() defer globalLibOptionsMu.Unlock() switch optname { case "CacheBackend": if optval != "" && optval != "none" && optval != "file" && optval != "redis" { return SJWTRetErr } globalLibOptions.cacheBackend = optval globalLibOptions.refreshURLCache() return SJWTRetOK case "CacheDirPath": globalLibOptions.cacheDirPath = optval globalLibOptions.refreshURLCache() return SJWTRetOK case "RedisURL": if !validRedisURL(optval) { return SJWTRetErr } globalLibOptions.redisURL = optval globalLibOptions.refreshURLCache() return SJWTRetOK case "RedisKeyPrefix": globalLibOptions.redisKeyPrefix = optval globalLibOptions.refreshURLCache() return SJWTRetOK case "CacheNameMode": if optval != "plain" && optval != "sha256" { return SJWTRetErr } globalLibOptions.cacheNameMode = optval globalLibOptions.refreshURLCache() 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 { globalLibOptionsMu.Lock() defer globalLibOptionsMu.Unlock() switch optname { case "CacheExpires": globalLibOptions.cacheExpire = optval globalLibOptions.refreshURLCache() return SJWTRetOK case "CacheStoreURL": globalLibOptions.cacheStoreURL = optval != 0 globalLibOptions.refreshURLCache() return SJWTRetOK case "RedisTimeout": if optval <= 0 { return SJWTRetErr } globalLibOptions.redisTimeout = optval globalLibOptions.refreshURLCache() return SJWTRetOK case "CertVerify": globalLibOptions.certVerify = optval return SJWTRetOK case "AttrsVerify": globalLibOptions.attrsVerify = optval return SJWTRetOK case "DenyPrivateX5U": globalLibOptions.denyPrivateX5U = optval != 0 return SJWTRetOK case "MaxURLContentBytes": if optval <= 0 { return SJWTRetErr } globalLibOptions.maxURLContentBytes = optval return SJWTRetOK } return SJWTRetErr } // SJWTLibOptGetN -- func SJWTLibOptGetN(optname string) int { globalLibOptionsMu.RLock() defer globalLibOptionsMu.RUnlock() switch optname { case "CacheExpires": return globalLibOptions.cacheExpire case "CacheStoreURL": if globalLibOptions.cacheStoreURL { return 1 } return 0 case "RedisTimeout": return globalLibOptions.redisTimeout case "CertVerify": return globalLibOptions.certVerify case "AttrsVerify": return globalLibOptions.attrsVerify case "DenyPrivateX5U": if globalLibOptions.denyPrivateX5U { return 1 } return 0 case "MaxURLContentBytes": return globalLibOptions.maxURLContentBytes } return SJWTRetErr } // SJWTLibOptSetV -- func SJWTLibOptSetV(optnameval string) int { optArray := strings.SplitN(optnameval, "=", 2) if len(optArray) != 2 || len(optArray[0]) == 0 { return SJWTRetErr } optName := optArray[0] optVal := optArray[1] switch optName { case "CacheExpires", "CacheStoreURL", "RedisTimeout", "CertVerify", "DenyPrivateX5U", "MaxURLContentBytes": intVal, err := strconv.Atoi(optVal) if err != nil { return SJWTRetErr } return SJWTLibOptSetN(optName, intVal) case "CacheBackend", "CacheDirPath", "CacheNameMode", "RedisURL", "RedisKeyPrefix", "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) } // SJWTPubKeyVerify - func SJWTPubKeyVerify(pubKey []byte) (int, error) { options := globalLibOptionsSnapshot() return options.pubKeyVerify(pubKey) } func (o *SJWTLibOptions) pubKeyVerify(pubKey []byte) (int, error) { if o.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 (o.certVerify & (CertVerifyOptTime | CertVerifyOptTimeOnly)) != 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") } } if (o.certVerify & CertVerifyOptTimeOnly) != 0 { return SJWTRetOK, nil } rootCAs = nil interCAs = nil if (o.certVerify & CertVerifyOptSysCA) != 0 { // Get the SystemCertPool rootCAs, err = SystemCertPool() if rootCAs == nil { return SJWTRetErrCertProcessing, err } } if (o.certVerify & CertVerifyOptCustCA) != 0 { if len(o.certCAFile) <= 0 { return SJWTRetErrCertNoCAFile, errors.New("no custom 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 = os.ReadFile(o.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 (o.certVerify & CertVerifyOptInterCA) != 0 { if len(o.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 = os.ReadFile(o.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 (o.certVerify & CertVerifyOptCRL) != 0 { if len(o.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 = os.ReadFile(o.certCRLFile) if err != nil { return SJWTRetErrCertReadCRLFile, errors.New("failed to read CRL file") } rootCRL, err = x509.ParseCRL(certsCRLData) if err != nil || rootCRL == nil { return SJWTRetErrCertReadCRLFile, errors.New("failed to parse CRL file") } 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) } // SJWTGetURLContent -- func SJWTGetURLContent(urlVal string, timeoutVal int) ([]byte, int, error) { options := globalLibOptionsSnapshot() return options.getURLContent(urlVal, timeoutVal) } func validateRemoteURL(parsedURL *url.URL) error { if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { return errors.New("invalid URL value") } if parsedURL.Hostname() == "" { return errors.New("invalid URL value") } return nil } func isRestrictedRemoteIP(ip net.IP) bool { if ip == nil || !ip.IsGlobalUnicast() || ip.IsLoopback() || ip.IsUnspecified() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsMulticast() { return true } if ip4 := ip.To4(); ip4 != nil { return ip4[0] == 10 || (ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) || (ip4[0] == 192 && ip4[1] == 168) || (ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127) } ip16 := ip.To16() return ip16 != nil && ip16[0]&0xfe == 0xfc } func dialPublicRemote(ctx context.Context, network string, address string) (net.Conn, error) { host, port, err := net.SplitHostPort(address) if err != nil { return nil, err } addresses, err := net.DefaultResolver.LookupIPAddr(ctx, host) if err != nil { return nil, err } if len(addresses) == 0 { return nil, fmt.Errorf("no addresses found for %s", host) } for _, resolvedAddress := range addresses { if isRestrictedRemoteIP(resolvedAddress.IP) { return nil, fmt.Errorf("blocked non-public address %s for %s", resolvedAddress.IP, host) } } dialer := net.Dialer{} var lastErr error for _, resolvedAddress := range addresses { conn, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(resolvedAddress.IP.String(), port)) if dialErr == nil { return conn, nil } lastErr = dialErr } return nil, lastErr } func (o *SJWTLibOptions) getURLContent(urlVal string, timeoutVal int) ([]byte, int, error) { if len(urlVal) == 0 { return nil, SJWTRetErrHTTPInvalidURL, errors.New("no URL value") } parsedURL, err := url.Parse(urlVal) if err != nil || validateRemoteURL(parsedURL) != nil { return nil, SJWTRetErrHTTPInvalidURL, errors.New("invalid URL value") } if o.cache != nil { cdata, cerr := o.cache.Get(context.Background(), urlVal) if cdata != nil { return cdata, SJWTRetOK, cerr } } transport := &http.Transport{} if defaultTransport, ok := http.DefaultTransport.(*http.Transport); ok { transport = defaultTransport.Clone() } transport.Proxy = nil if o.denyPrivateX5U { transport.DialContext = dialPublicRemote } defer transport.CloseIdleConnections() httpClient := http.Client{ Transport: transport, Timeout: time.Duration(timeoutVal) * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) >= 10 { return errors.New("stopped after 10 redirects") } return validateRemoteURL(req.URL) }, } resp, err := httpClient.Get(parsedURL.String()) 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 := io.ReadAll(io.LimitReader(resp.Body, int64(o.maxURLContentBytes))) if err != nil { return nil, SJWTRetErrHTTPReadBody, fmt.Errorf("read http body failure: %v", err) } if len(data) == o.maxURLContentBytes { var extraByte [1]byte bytesRead, readErr := io.ReadFull(resp.Body, extraByte[:]) if bytesRead > 0 { return nil, SJWTRetErrHTTPReadBody, fmt.Errorf("read http body failure: content exceeds %d byte limit", o.maxURLContentBytes) } if readErr != nil && readErr != io.EOF { return nil, SJWTRetErrHTTPReadBody, fmt.Errorf("read http body failure: %v", readErr) } } if o.cache != nil { _ = o.cache.Set(context.Background(), 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, _ := os.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) { options := globalLibOptionsSnapshot() return options.checkAttributes(bToken, paramInfo) } func (o *SJWTLibOptions) checkAttributes(bToken string, paramInfo string) (int, error) { if o.attrsVerify == 0 { return SJWTRetOK, nil } 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) { options := globalLibOptionsSnapshot() return options.checkIdentityPKMode(identityVal, expireVal, pubkeyVal, pubkeyMode, timeoutVal) } func (o *SJWTLibOptions) checkIdentityPKMode(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 = o.getURLContent(pubkeyVal, timeoutVal) } else if strings.HasPrefix(pubkeyVal, "file://") { fileUrl, _ := url.Parse(pubkeyVal) pubkey, err = os.ReadFile(fileUrl.Path) ret = SJWTRetErrFileRead } else { pubkey, err = os.ReadFile(pubkeyVal) ret = SJWTRetErrFileRead } if err != nil { return ret, err } } ret, err = o.pubKeyVerify(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) { options := globalLibOptionsSnapshot() return options.checkIdentity(identityVal, expireVal, pubkeyPath, timeoutVal) } func (o *SJWTLibOptions) checkIdentity(identityVal string, expireVal int, pubkeyPath string, timeoutVal int) (int, error) { return o.checkIdentityPKMode(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) { options := globalLibOptionsSnapshot() return options.checkFullIdentity(identityVal, expireVal, pubkeyPath, timeoutVal) } func (o *SJWTLibOptions) checkFullIdentity(identityVal string, expireVal int, pubkeyPath string, timeoutVal int) (int, error) { if len(pubkeyPath) == 0 { return o.checkFullIdentityURL(identityVal, expireVal, timeoutVal) } hdrtoken := strings.Split(SJWTRemoveWhiteSpaces(identityVal), ";") ret, err := o.checkIdentity(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 o.checkAttributes(btoken[0], paramInfo) } // SJWTCheckFullIdentityURL - implements the verify of identity using URL func SJWTCheckFullIdentityURL(identityVal string, expireVal int, timeoutVal int) (int, error) { options := globalLibOptionsSnapshot() return options.checkFullIdentityURL(identityVal, expireVal, timeoutVal) } func (o *SJWTLibOptions) checkFullIdentityURL(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 = o.getURLContent(paramInfo, timeoutVal) if pubkey == nil { return ret, err } ret, err = o.pubKeyVerify(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 o.checkAttributes(btoken[0], paramInfo) } // SJWTCheckFullIdentityPubKey - implements the verify of identity using public key func SJWTCheckFullIdentityPubKey(identityVal string, expireVal int, pubkeyVal string) (int, error) { options := globalLibOptionsSnapshot() return options.checkFullIdentityPubKey(identityVal, expireVal, pubkeyVal) } func (o *SJWTLibOptions) checkFullIdentityPubKey(identityVal string, expireVal int, pubkeyVal string) (int, error) { hdrtoken := strings.Split(SJWTRemoveWhiteSpaces(identityVal), ";") ret, err := o.checkIdentityPKMode(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 o.checkAttributes(btoken[0], paramInfo) } // SJWTGetIdentityPrvKey -- func SJWTGetIdentityPrvKey(origTN string, destTN string, attestVal string, origID string, x5uVal string, prvkeyData []byte) (string, int, error) { options := globalLibOptionsSnapshot() return options.getIdentityPrvKey(origTN, destTN, attestVal, origID, x5uVal, prvkeyData) } func (o *SJWTLibOptions) getIdentityPrvKey(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: o.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) { options := globalLibOptionsSnapshot() return options.getIdentity(origTN, destTN, attestVal, origID, x5uVal, prvkeyPath) } func (o *SJWTLibOptions) getIdentity(origTN string, destTN string, attestVal string, origID string, x5uVal string, prvkeyPath string) (string, int, error) { var prvkey []byte var err error prvkey, err = os.ReadFile(prvkeyPath) if err != nil { return "", SJWTRetErrFileRead, fmt.Errorf("Unable to read private key file: %v", err) } return o.getIdentityPrvKey(origTN, destTN, attestVal, origID, x5uVal, prvkey) } asipto-secsipidx-bfe9fa1/secsipid/secsipid_test.go000066400000000000000000001320531527256712400225740ustar00rootroot00000000000000package secsipid_test import ( "bytes" "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/hex" "encoding/pem" "errors" "fmt" "math/big" "net" "net/http" "os" "path" "path/filepath" "strings" "sync" "testing" "time" "github.com/asipto/secsipidx/secsipid" "github.com/gomagedon/expectate" "github.com/redis/go-redis/v9" ) 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 custom 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("ErrCertReadCRLFile with invalid CRL file", func(t *testing.T) { cert := certGenerator.generateValidCert() os.WriteFile("dummyCA.pem", certGenerator.caPEMBytes, 0777) secsipid.SJWTLibOptSetS("CertCAFile", "dummyCA.pem") os.WriteFile("dummyCRLFile.crl", []byte("invalid CRL"), 0777) secsipid.SJWTLibOptSetS("CertCRLFile", "dummyCRLFile.crl") runTest(t, PubKeyVerifyTest{ certVerify: 0b10100, inputKey: cert, expectedErrCode: secsipid.SJWTRetErrCertReadCRLFile, expectedErrMsg: "failed to parse 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, 1024) 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, 1024) 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, 1024) 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 } 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, 1024) 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, 1024) 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 } func TestSJWTLibOptSetV(t *testing.T) { t.Run("rejects malformed input without panicking", func(t *testing.T) { expect := expectate.Expect(t) testCases := []string{ "", "CacheExpires", "=", "=123", } for _, input := range testCases { expect(secsipid.SJWTLibOptSetV(input)).ToBe(secsipid.SJWTRetErr) } }) t.Run("rejects invalid numeric values without changing options", func(t *testing.T) { testCases := []struct { name string option string value string baseline int }{ {name: "empty", option: "CacheExpires", value: "", baseline: 42}, {name: "text", option: "CertVerify", value: "invalid", baseline: 7}, {name: "decimal", option: "DenyPrivateX5U", value: "1.5", baseline: 1}, {name: "overflow", option: "MaxURLContentBytes", value: "999999999999999999999999", baseline: testMaxURLContentBytes}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { previous := secsipid.SJWTLibOptGetN(testCase.option) defer secsipid.SJWTLibOptSetN(testCase.option, previous) if ret := secsipid.SJWTLibOptSetN(testCase.option, testCase.baseline); ret != secsipid.SJWTRetOK { t.Fatalf("set baseline for %s: %d", testCase.option, ret) } if ret := secsipid.SJWTLibOptSetV(testCase.option + "=" + testCase.value); ret != secsipid.SJWTRetErr { t.Fatalf("expected invalid value for %s to fail, got %d", testCase.option, ret) } if value := secsipid.SJWTLibOptGetN(testCase.option); value != testCase.baseline { t.Fatalf("expected %s to remain %d, got %d", testCase.option, testCase.baseline, value) } }) } }) t.Run("sets numeric options from valid key value input", func(t *testing.T) { expect := expectate.Expect(t) defer secsipid.SJWTLibOptSetN("MaxURLContentBytes", testMaxURLContentBytes) expect(secsipid.SJWTLibOptSetV("CacheExpires=42")).ToBe(secsipid.SJWTRetOK) expect(secsipid.SJWTLibOptGetN("CacheExpires")).ToBe(42) expect(secsipid.SJWTLibOptSetV("DenyPrivateX5U=1")).ToBe(secsipid.SJWTRetOK) expect(secsipid.SJWTLibOptGetN("DenyPrivateX5U")).ToBe(1) expect(secsipid.SJWTLibOptSetN("DenyPrivateX5U", 0)).ToBe(secsipid.SJWTRetOK) expect(secsipid.SJWTLibOptSetV("MaxURLContentBytes=2048")).ToBe(secsipid.SJWTRetOK) expect(secsipid.SJWTLibOptGetN("MaxURLContentBytes")).ToBe(2048) expect(secsipid.SJWTLibOptSetN("MaxURLContentBytes", 0)).ToBe(secsipid.SJWTRetErr) }) t.Run("sets string options from valid key value input", func(t *testing.T) { expect := expectate.Expect(t) expect(secsipid.SJWTLibOptSetV("CertCAFile=dummy-ca.pem")).ToBe(secsipid.SJWTRetOK) expect(secsipid.SJWTLibOptSetS("CertCAFile", "")).ToBe(secsipid.SJWTRetOK) }) } func restoreURLCacheOptions(options secsipid.SJWTOptions) { // Disable the active backend while restoring its individual settings. secsipid.SJWTLibOptSetS("CacheBackend", "none") secsipid.SJWTLibOptSetS("CacheDirPath", options.CacheDirPath) secsipid.SJWTLibOptSetN("CacheExpires", options.CacheExpire) secsipid.SJWTLibOptSetS("CacheNameMode", options.CacheNameMode) if options.CacheStoreURL { secsipid.SJWTLibOptSetN("CacheStoreURL", 1) } else { secsipid.SJWTLibOptSetN("CacheStoreURL", 0) } secsipid.SJWTLibOptSetS("RedisURL", options.RedisURL) secsipid.SJWTLibOptSetS("RedisKeyPrefix", options.RedisKeyPrefix) secsipid.SJWTLibOptSetN("RedisTimeout", options.RedisTimeout) secsipid.SJWTLibOptSetS("CacheBackend", options.CacheBackend) } func TestRedisCacheOptions(t *testing.T) { original := secsipid.DefaultSJWTOptions() defer restoreURLCacheOptions(original) if ret := secsipid.SJWTLibOptSetS("CacheBackend", "invalid"); ret != secsipid.SJWTRetErr { t.Fatalf("expected invalid cache backend to fail, got %d", ret) } if ret := secsipid.SJWTLibOptSetN("RedisTimeout", 0); ret != secsipid.SJWTRetErr { t.Fatalf("expected invalid Redis timeout to fail, got %d", ret) } if ret := secsipid.SJWTLibOptSetS("RedisURL", "://invalid"); ret != secsipid.SJWTRetErr { t.Fatalf("expected invalid Redis URL to fail, got %d", ret) } secsipid.SJWTLibOptSetS("RedisURL", "redis://localhost:6379/2") secsipid.SJWTLibOptSetS("RedisKeyPrefix", "test-prefix:") secsipid.SJWTLibOptSetN("RedisTimeout", 7) secsipid.SJWTLibOptSetN("CacheStoreURL", 1) secsipid.SJWTLibOptSetS("CacheBackend", "redis") options := secsipid.DefaultSJWTOptions() if options.CacheBackend != "redis" || options.RedisURL != "redis://localhost:6379/2" || options.RedisKeyPrefix != "test-prefix:" || options.RedisTimeout != 7 || !options.CacheStoreURL { t.Fatalf("unexpected Redis options: %+v", options) } } func TestRedisCacheFailureIsFailOpen(t *testing.T) { original := secsipid.DefaultSJWTOptions() defer restoreURLCacheOptions(original) secsipid.SJWTLibOptSetS("CacheBackend", "none") secsipid.SJWTLibOptSetN("CacheExpires", 60) secsipid.SJWTLibOptSetN("RedisTimeout", 1) secsipid.SJWTLibOptSetS("RedisURL", "redis://127.0.0.1:1/0") secsipid.SJWTLibOptSetS("CacheBackend", "redis") handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("downloaded without Redis")) }) stopTestServer := startTestServer(handler) defer stopTestServer() content, ret, err := secsipid.SJWTGetURLContent("http://localhost:5555/redis-fail-open", 10) if ret != secsipid.SJWTRetOK || err != nil { t.Fatalf("expected HTTP fallback to succeed, ret=%d err=%v", ret, err) } if string(content) != "downloaded without Redis" { t.Fatalf("unexpected downloaded content: %q", content) } } func TestRedisCacheIntegration(t *testing.T) { redisURL := os.Getenv("SECSIPIDX_REDIS_TEST_URL") if redisURL == "" { t.Skip("SECSIPIDX_REDIS_TEST_URL is not set") } redisOptions, err := redis.ParseURL(redisURL) if err != nil { t.Fatalf("parse Redis test URL: %v", err) } redisClient := redis.NewClient(redisOptions) defer redisClient.Close() if err = redisClient.Ping(context.Background()).Err(); err != nil { t.Fatalf("connect to Redis test server: %v", err) } original := secsipid.DefaultSJWTOptions() defer restoreURLCacheOptions(original) prefix := fmt.Sprintf("secsipidx:test:%d:", time.Now().UnixNano()) urlVal := "https://example.com/redis-cert.pem" hashVal := sha256.Sum256([]byte(urlVal)) redisKey := prefix + hex.EncodeToString(hashVal[:]) redisURLKey := redisKey + ":url" defer redisClient.Del(context.Background(), redisKey, redisURLKey) secsipid.SJWTLibOptSetS("CacheBackend", "none") secsipid.SJWTLibOptSetN("CacheExpires", 1) secsipid.SJWTLibOptSetN("RedisTimeout", 2) secsipid.SJWTLibOptSetS("RedisURL", redisURL) secsipid.SJWTLibOptSetS("RedisKeyPrefix", prefix) secsipid.SJWTLibOptSetN("CacheStoreURL", 1) secsipid.SJWTLibOptSetS("CacheBackend", "redis") if err = secsipid.SJWTSetURLCachedContent(urlVal, []byte("certificate")); err != nil { t.Fatalf("write Redis cache: %v", err) } content, err := secsipid.SJWTGetURLCachedContent(urlVal) if err != nil || string(content) != "certificate" { t.Fatalf("read Redis cache: content=%q err=%v", content, err) } stored, err := redisClient.Get(context.Background(), redisKey).Bytes() if err != nil || string(stored) != "certificate" { t.Fatalf("inspect Redis cache key: content=%q err=%v", stored, err) } if ttl := redisClient.TTL(context.Background(), redisKey).Val(); ttl <= 0 || ttl > time.Second { t.Fatalf("unexpected Redis cache TTL: %v", ttl) } storedURL, err := redisClient.Get(context.Background(), redisURLKey).Result() if err != nil || storedURL != urlVal { t.Fatalf("inspect Redis URL metadata: url=%q err=%v", storedURL, err) } if ttl := redisClient.TTL(context.Background(), redisURLKey).Val(); ttl <= 0 || ttl > time.Second { t.Fatalf("unexpected Redis URL metadata TTL: %v", ttl) } time.Sleep(1100 * time.Millisecond) content, err = secsipid.SJWTGetURLCachedContent(urlVal) if err != nil || content != nil { t.Fatalf("expected expired Redis cache miss: content=%q err=%v", content, err) } if err = redisClient.Get(context.Background(), redisURLKey).Err(); !errors.Is(err, redis.Nil) { t.Fatalf("expected expired Redis URL metadata, got %v", err) } } func TestLegacyGlobalOptionsConcurrentAccess(t *testing.T) { original := secsipid.DefaultSJWTOptions() defer secsipid.SetURLFileCacheOptions(original.CacheDirPath, original.CacheExpire) secsipid.SetURLFileCacheOptions("cache-a", 1) start := make(chan struct{}) errors := make(chan string, 1) var waitGroup sync.WaitGroup reportError := func(message string) { select { case errors <- message: default: } } waitGroup.Add(1) go func() { defer waitGroup.Done() <-start for i := 0; i < 1000; i++ { secsipid.SetURLFileCacheOptions("cache-a", 1) secsipid.SetURLFileCacheOptions("cache-b", 2) } }() for i := 0; i < 4; i++ { waitGroup.Add(1) go func() { defer waitGroup.Done() <-start for j := 0; j < 1000; j++ { options := secsipid.DefaultSJWTOptions() validPair := options.CacheDirPath == "cache-a" && options.CacheExpire == 1 || options.CacheDirPath == "cache-b" && options.CacheExpire == 2 if !validPair { reportError(fmt.Sprintf("inconsistent cache options: path=%q expire=%d", options.CacheDirPath, options.CacheExpire)) return } secsipid.SJWTGetURLCacheFilePath("https://example.com/cert.pem") secsipid.NewSJWTVerifier(secsipid.SJWTOptions{}) secsipid.SJWTLibOptGetN("CacheExpires") } }() } close(start) waitGroup.Wait() select { case message := <-errors: t.Fatal(message) default: } } func TestSignerAndVerifierUseInstanceLocalOptions(t *testing.T) { expect := expectate.Expect(t) privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("generate key: %v", err) } privateKeyBytes, err := x509.MarshalECPrivateKey(privateKey) if err != nil { t.Fatalf("marshal private key: %v", err) } privateKeyPEM, err := pemEncode(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: privateKeyBytes, }) if err != nil { t.Fatalf("encode private key: %v", err) } publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) if err != nil { t.Fatalf("marshal public key: %v", err) } publicKeyPEM := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: publicKeyBytes, }) signerA := secsipid.NewSJWTSigner(secsipid.SJWTOptions{ CacheExpire: 3600, X5U: "https://signer-a.example/cert.pem", }) signerB := secsipid.NewSJWTSigner(secsipid.SJWTOptions{ CacheExpire: 3600, X5U: "https://signer-b.example/cert.pem", }) identityA, ret, err := signerA.GetIdentityPrvKey("12025550100", "12025550101", "A", "", "", privateKeyPEM) expect(ret).ToBe(secsipid.SJWTRetOK) if err != nil { t.Fatalf("sign identity A: %v", err) } expect(strings.Contains(identityA, "https://signer-a.example/cert.pem")).ToBe(true) expect(strings.Contains(identityA, "https://signer-b.example/cert.pem")).ToBe(false) identityB, ret, err := signerB.GetIdentityPrvKey("12025550100", "12025550101", "A", "", "", privateKeyPEM) expect(ret).ToBe(secsipid.SJWTRetOK) if err != nil { t.Fatalf("sign identity B: %v", err) } expect(strings.Contains(identityB, "https://signer-b.example/cert.pem")).ToBe(true) expect(strings.Contains(identityB, "https://signer-a.example/cert.pem")).ToBe(false) strictVerifier := secsipid.NewSJWTVerifier(secsipid.SJWTOptions{ CacheExpire: 3600, CertVerify: 0, AttrsVerify: 1, }) relaxedVerifier := secsipid.NewSJWTVerifier(secsipid.SJWTOptions{ CacheExpire: 3600, CertVerify: 0, AttrsVerify: 0, }) tamperedIdentity := strings.Replace(identityA, ";info=", ";info=", 1) ret, err = strictVerifier.CheckFullIdentityPubKey(tamperedIdentity, 3600, string(publicKeyPEM)) expect(ret).ToBe(secsipid.SJWTRetErrJSONHdrX5u) expect(getMsgFromErr(err)).ToBe("mismatching value for x5u and info attributes") ret, err = relaxedVerifier.CheckFullIdentityPubKey(tamperedIdentity, 3600, string(publicKeyPEM)) expect(ret).ToBe(secsipid.SJWTRetOK) expect(err).ToBe(error(nil)) } type GetURLValueTest struct { urlVal string timeoutVal int expectedContent []byte expectedErrCode int expectedErrMsg string } const testMaxURLContentBytes = 1 << 20 func TestGetURLContent(t *testing.T) { os.Remove("http_example.com_foo") os.Remove("http_localhost:5555_foo") secsipid.SJWTLibOptSetS("CacheNameMode", "plain") secsipid.SJWTLibOptSetN("MaxURLContentBytes", testMaxURLContentBytes) defer secsipid.SJWTLibOptSetN("MaxURLContentBytes", testMaxURLContentBytes) tcpDialErrMsg := getTcpDialErrMsg() runTest := func(t *testing.T, testCase GetURLValueTest) { expect := expectate.Expect(t) 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, 3600) 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, 3600) 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) }) 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) { 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 when response body is at the size limit", func(t *testing.T) { responseBody := strings.Repeat("a", testMaxURLContentBytes) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(responseBody)) }) stopTestServer := startTestServer(handler) defer stopTestServer() runTest(t, GetURLValueTest{ urlVal: "http://localhost:5555/foo", timeoutVal: 10, expectedContent: []byte(responseBody), expectedErrCode: secsipid.SJWTRetOK, }) }) t.Run("ErrHTTPReadBody when response body exceeds the size limit", func(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(strings.Repeat("a", testMaxURLContentBytes+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: content exceeds 1048576 byte limit", }) }) t.Run("ErrHTTPReadBody uses configured response body size limit", func(t *testing.T) { const configuredLimit = 16 secsipid.SJWTLibOptSetN("MaxURLContentBytes", configuredLimit) defer secsipid.SJWTLibOptSetN("MaxURLContentBytes", testMaxURLContentBytes) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(strings.Repeat("a", configuredLimit+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: content exceeds 16 byte limit", }) }) 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, 3600) 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 TestURLCacheFilePathModes(t *testing.T) { expect := expectate.Expect(t) workDir := t.TempDir() urlVal := "http://example.com/foo" secsipid.SetURLFileCacheOptions(workDir, 3600) defer secsipid.SetURLFileCacheOptions("", 0) defer secsipid.SJWTLibOptSetS("CacheNameMode", "plain") secsipid.SJWTLibOptSetS("CacheNameMode", "plain") expect(secsipid.SJWTGetURLCacheFilePath(urlVal)).ToBe(filepath.Join(workDir, "http_example.com_foo")) secsipid.SJWTLibOptSetS("CacheNameMode", "sha256") hashVal := sha256.Sum256([]byte(urlVal)) expect(secsipid.SJWTGetURLCacheFilePath(urlVal)).ToBe(filepath.Join(workDir, "url_"+hex.EncodeToString(hashVal[:]))) } func TestFileCacheStoresURLMetadata(t *testing.T) { original := secsipid.DefaultSJWTOptions() defer restoreURLCacheOptions(original) cacheDir := t.TempDir() urlVal := "https://example.com/cert.pem?tenant=example" secsipid.SJWTLibOptSetS("CacheBackend", "none") secsipid.SJWTLibOptSetS("CacheDirPath", cacheDir) secsipid.SJWTLibOptSetN("CacheExpires", 3600) secsipid.SJWTLibOptSetS("CacheNameMode", "sha256") secsipid.SJWTLibOptSetN("CacheStoreURL", 1) secsipid.SJWTLibOptSetS("CacheBackend", "file") if err := secsipid.SJWTSetURLCachedContent(urlVal, []byte("certificate")); err != nil { t.Fatalf("write file cache: %v", err) } cacheFilePath := secsipid.SJWTGetURLCacheFilePath(urlVal) urlFilePath := cacheFilePath + ".url" storedURL, err := os.ReadFile(urlFilePath) if err != nil || string(storedURL) != urlVal { t.Fatalf("read URL metadata: url=%q err=%v", storedURL, err) } secsipid.SJWTLibOptSetN("CacheStoreURL", 0) if err = secsipid.SJWTSetURLCachedContent(urlVal, []byte("updated certificate")); err != nil { t.Fatalf("rewrite file cache without URL metadata: %v", err) } if _, err = os.Stat(urlFilePath); !os.IsNotExist(err) { t.Fatalf("expected disabled URL metadata to be removed, got %v", err) } secsipid.SJWTLibOptSetN("CacheStoreURL", 1) if err = secsipid.SJWTSetURLCachedContent(urlVal, []byte("certificate")); err != nil { t.Fatalf("rewrite file cache with URL metadata: %v", err) } expiredTime := time.Now().Add(-2 * time.Second) if err = os.Chtimes(cacheFilePath, expiredTime, expiredTime); err != nil { t.Fatalf("age cache file: %v", err) } if err = os.Chtimes(urlFilePath, expiredTime, expiredTime); err != nil { t.Fatalf("age URL metadata file: %v", err) } secsipid.SJWTLibOptSetN("CacheExpires", 1) content, err := secsipid.SJWTGetURLCachedContent(urlVal) if err != nil || content != nil { t.Fatalf("expected expired cache miss: content=%q err=%v", content, err) } for _, filePath := range []string{cacheFilePath, urlFilePath} { if _, err = os.Stat(filePath); !os.IsNotExist(err) { t.Fatalf("expected expired cache file %q to be removed, got %v", filePath, err) } } } func TestGetURLContentCreatesCacheDirectoryAndWritesHashedFile(t *testing.T) { expect := expectate.Expect(t) cacheDir := filepath.Join(t.TempDir(), "nested", "cache") secsipid.SetURLFileCacheOptions(cacheDir, 3600) secsipid.SJWTLibOptSetS("CacheNameMode", "sha256") defer secsipid.SetURLFileCacheOptions("", 0) defer secsipid.SJWTLibOptSetS("CacheNameMode", "plain") handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello from the server!")) }) stopTestServer := startTestServer(handler) defer stopTestServer() content, errCode, err := secsipid.SJWTGetURLContent("http://localhost:5555/foo", 10) expect(content).ToEqual([]byte("Hello from the server!")) expect(errCode).ToBe(secsipid.SJWTRetOK) expect(err).ToBe(error(nil)) cacheFilePath := secsipid.SJWTGetURLCacheFilePath("http://localhost:5555/foo") _, statErr := os.Stat(cacheFilePath) expect(statErr).ToBe(error(nil)) } func TestGetURLContentBlocksPrivateAddressesWhenEnabled(t *testing.T) { secsipid.SetURLFileCacheOptions("", 0) secsipid.SJWTLibOptSetN("DenyPrivateX5U", 1) defer secsipid.SJWTLibOptSetN("DenyPrivateX5U", 0) urls := []string{ "http://127.0.0.1:1/cert.pem", "http://10.0.0.1/cert.pem", "http://172.16.0.1/cert.pem", "http://192.168.0.1/cert.pem", "http://100.64.0.1/cert.pem", "http://169.254.1.1/cert.pem", "http://224.0.0.1/cert.pem", "http://0.0.0.0/cert.pem", "http://[::1]/cert.pem", "http://[fc00::1]/cert.pem", "http://[fe80::1]/cert.pem", } for _, urlVal := range urls { t.Run(urlVal, func(t *testing.T) { content, errCode, err := secsipid.SJWTGetURLContent(urlVal, 10) if content != nil { t.Fatalf("expected no content, got %q", content) } if errCode != secsipid.SJWTRetErrHTTPGet { t.Fatalf("expected HTTP get error code, got %d", errCode) } if err == nil || !strings.Contains(err.Error(), "blocked non-public address") { t.Fatalf("expected blocked private address error, got %v", err) } }) } } 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 { _, tcpDialErr := http.Get("http://localhost:5555/foo") return "http get failure: " + tcpDialErr.Error() } asipto-secsipidx-bfe9fa1/secsipid/system_cas.go000066400000000000000000000102561527256712400221040ustar00rootroot00000000000000package 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, "/") } asipto-secsipidx-bfe9fa1/secsipidx.1000066400000000000000000000066251527256712400176620ustar00rootroot00000000000000.TH SECSIPIDX 1 "2026-10-20" .\" 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-tn 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 payload 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-backend certificate cache backend: none, file, or redis. If omitted, setting cache-dir keeps the legacy filesystem cache behavior .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 \-cache-file-mode filesystem cache filename mode: plain or sha256 (default: plain) .TP .B \-cache-store-url store the original certificate URL in companion cache metadata. Disabled by default because URLs can contain sensitive data .TP .B \-redis-url Redis cache URL using redis://, rediss://, or unix:// .TP .B \-redis-key-prefix Redis certificate key prefix (default: secsipidx:cert:) .TP .B \-redis-timeout Redis operation timeout in seconds (default: 3) .TP .B \-deny-private-x5u deny certificate URLs resolving to private or local addresses (disabled by default) .TP .B \-max-url-content-bytes maximum downloaded certificate size in bytes (default 1048576) .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 .B \-vl, \-verbosity verbosity level (default: 0) .SH EXAMPLES TODO .SH AUTHOR .PP This manual page was written by Victor Seva , for the Debian project (and may be used by others).