diff --git a/.github/workflows/ko-build.yml b/.github/workflows/ko-build.yml deleted file mode 100644 index 3d7e609..0000000 --- a/.github/workflows/ko-build.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Publish - -on: - push: - branches: ['main'] - -jobs: - publish: - name: Publish - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: '1.23.x' - - - uses: ko-build/setup-ko@v0.8 - - run: ko build \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7cae5c8..5d39276 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,10 @@ jobs: uses: actions/setup-go@v5 with: go-version: stable - + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Zig uses: mlugg/setup-zig@v1 @@ -50,3 +53,6 @@ jobs: args: release --clean ${{ env.flags }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PDFSIGNER_LICENSE_PUBLIC_KEY: ${{ secrets.PDFSIGNER_LICENSE_PUBLIC_KEY }} + PDFSIGNER_HMAC_KEY: ${{ secrets.PDFSIGNER_HMAC_KEY }} + PDFSIGNER_LICENSE: ${{ secrets.PDFSIGNER_LICENSE }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c06d3a3..18554c6 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,6 +13,9 @@ builds: - -X main.Version={{ .ShortCommit }} - -X main.GitCommit={{ .FullCommit }} - -X main.GitBranch={{ .Branch }} + - -X github.com/digitorus/pdfsigner/license.publicKeyBase64={{ .Env.PDFSIGNER_LICENSE_PUBLIC_KEY }} + - -X github.com/digitorus/pdfsigner/license.licenseBase64={{ .Env.PDFSIGNER_LICENSE }} + - -X github.com/digitorus/pdfsigner/license.hmacKey={{ .Env.PDFSIGNER_HMAC_KEY }} flags: - -trimpath env: @@ -40,6 +43,8 @@ builds: - -X main.Version={{ .Version }} - -X main.GitCommit={{ .FullCommit }} - -X main.GitBranch={{ .Branch }} + - -X github.com/digitorus/pdfsigner/license.publicKeyBase64={{ .Env.PDFSIGNER_LICENSE_PUBLIC_KEY }} + - -X github.com/digitorus/pdfsigner/license.licenseBase64={{ .Env.PDFSIGNER_LICENSE }} flags: - -trimpath env: @@ -55,6 +60,67 @@ builds: {{- if eq .Arch "arm64"}}CC=zig c++ -target aarch64-windows-gnu{{- end }} {{- end }} +dockers: + - ids: + - pdfsigner-linux + image_templates: + - "digitorus/{{ .ProjectName }}:{{ .Tag }}-amd64" + - "digitorus/{{ .ProjectName }}:v{{ .Major }}-amd64" + - "digitorus/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}-amd64" + - "digitorus/{{ .ProjectName }}:latest-amd64" + use: buildx + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.source={{ urlPathEscape .GitURL }}" + - "--label=org.opencontainers.image.vendor=Digitorus" + extra_files: + - config.example.yaml + + - ids: + - pdfsigner-linux + image_templates: + - "digitorus/{{ .ProjectName }}:{{ .Tag }}-arm64" + - "digitorus/{{ .ProjectName }}:v{{ .Major }}-arm64" + - "digitorus/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}-arm64" + - "digitorus/{{ .ProjectName }}:latest-arm64" + use: buildx + goarch: arm64 + build_flag_templates: + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.source={{ urlPathEscape .GitURL }}" + - "--label=org.opencontainers.image.vendor=Digitorus" + extra_files: + - config.example.yaml + +docker_manifests: + - name_template: "digitorus/{{ .ProjectName }}:{{ .Tag }}" + image_templates: + - "digitorus/{{ .ProjectName }}:{{ .Tag }}-amd64" + - "digitorus/{{ .ProjectName }}:{{ .Tag }}-arm64" + + - name_template: "digitorus/{{ .ProjectName }}:v{{ .Major }}" + image_templates: + - "digitorus/{{ .ProjectName }}:v{{ .Major }}-amd64" + - "digitorus/{{ .ProjectName }}:v{{ .Major }}-arm64" + + - name_template: "digitorus/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}" + image_templates: + - "digitorus/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}-amd64" + - "digitorus/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}-arm64" + + - name_template: "digitorus/{{ .ProjectName }}:latest" + image_templates: + - "digitorus/{{ .ProjectName }}:latest-amd64" + - "digitorus/{{ .ProjectName }}:latest-arm64" + archives: - formats: [ 'tar.gz' ] # this name template makes the OS and Arch compatible with the results of uname. diff --git a/Dockerfile b/Dockerfile index aaa4884..a7f0675 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,34 @@ FROM alpine -ADD ca-certificates.crt /etc/ssl/certs/ -ADD static /static -ADD config.yaml -ADD pdfsigner / -COPY passwd /etc/passwd -WORKDIR / -USER user -CMD ["./pdfsinger", "serve", "--config", "./config.yaml"] + +# Create non-root user +RUN addgroup -S -g 1000 appgroup && adduser -S -u 1000 -G appgroup appuser + +# Install certificates +RUN apk add --no-cache ca-certificates + +# Create application directories +RUN mkdir -p /usr/local/bin \ + /etc/pdfsigner \ + /var/lib/pdfsigner \ + /var/lib/pdfsigner/input \ + /var/lib/pdfsigner/output + +# Copy application files +COPY config.example.yaml /etc/pdfsigner/config.yaml +COPY pdfsigner /usr/local/bin/pdfsigner + +# Set permissions and ownership +RUN chown -R appuser:appgroup /etc/pdfsigner /var/lib/pdfsigner +RUN chmod 755 /usr/local/bin/pdfsigner + +# Define volume for configuration +VOLUME ["/etc/pdfsigner", "/var/lib/pdfsigner/input", "/var/lib/pdfsigner/output"] + +WORKDIR /var/lib/pdfsigner + +USER appuser + +HEALTHCHECK --interval=30s --timeout=3s \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +CMD ["pdfsigner", "serve", "--config", "/etc/pdfsigner/config.yaml"] diff --git a/cmd/common.go b/cmd/common.go index c0563b3..148336c 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -1,15 +1,15 @@ package cmd import ( - "os" + "fmt" "path/filepath" "strings" "github.com/digitorus/pdfsign/sign" "github.com/digitorus/pdfsigner/license" "github.com/digitorus/pdfsigner/queues/queue" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // signVerifyQueue stores queue for signs. @@ -25,307 +25,198 @@ func setupVerifier() { } var ( - // common flags. - signerNameFlag string - validateSignature bool - certificateChainPathFlag string - inputPathFlag string - outputPathFlag string - - // Signature flags. - signatureTypeFlag uint - docMdpPermsFlag uint - signatureInfoNameFlag string - signatureInfoLocationFlag string - signatureInfoReasonFlag string - signatureInfoContactFlag string - signatureTSAUrlFlag string - signatureTSAUsernameFlag string - signatureTSAPasswordFlag string - - // PEM flags. - certificatePathFlag string - privateKeyPathFlag string - - // PKSC11 flags. - pksc11LibPathFlag string - pksc11PassFlag string - - // serve flags. - serveAddrFlag string - servePortFlag string + // Common flag variables - now only used for command-line arguments that don't map directly to config + validateSignature bool + inputPathFlag string + outputPathFlag string ) // parseCommonFlags binds common flags to variables. func parseCommonFlags(cmd *cobra.Command) { - cmd.PersistentFlags().UintVar(&signatureTypeFlag, "type", 1, "Certificate type") - cmd.PersistentFlags().UintVar(&docMdpPermsFlag, "docmdp", 1, "DocMDP permissions") - cmd.PersistentFlags().StringVar(&signatureInfoNameFlag, "name", "", "Signature info name") - cmd.PersistentFlags().StringVar(&signatureInfoLocationFlag, "location", "", "Signature info location") - cmd.PersistentFlags().StringVar(&signatureInfoReasonFlag, "reason", "", "Signature reason") - cmd.PersistentFlags().StringVar(&signatureInfoContactFlag, "contact", "", "Signature contact") - cmd.PersistentFlags().StringVar(&signatureTSAUrlFlag, "tsa-url", "", "TSA url") - cmd.PersistentFlags().StringVar(&signatureTSAUsernameFlag, "tsa-username", "", "TSA username") - cmd.PersistentFlags().StringVar(&signatureTSAPasswordFlag, "tsa-password", "", "TSA password") - cmd.PersistentFlags().StringVar(&certificateChainPathFlag, "chain", "", "Certificate chain path") - cmd.PersistentFlags().BoolVar(&validateSignature, "validate-signature", true, "Certificate chain path") -} + cmd.PersistentFlags().UintP("type", "t", 1, "Certificate type") + _ = viper.BindPFlag("type", cmd.PersistentFlags().Lookup("type")) + + cmd.PersistentFlags().UintP("docmdp", "d", 1, "DocMDP permissions") + _ = viper.BindPFlag("docmdp", cmd.PersistentFlags().Lookup("docmdp")) + + cmd.PersistentFlags().StringP("signer", "s", "", "Name of signer configuration") + _ = viper.BindPFlag("signer", cmd.PersistentFlags().Lookup("signer")) + + cmd.PersistentFlags().StringP("name", "n", "", "Signature info name") + _ = viper.BindPFlag("name", cmd.PersistentFlags().Lookup("name")) + + cmd.PersistentFlags().StringP("location", "l", "", "Signature info location") + _ = viper.BindPFlag("location", cmd.PersistentFlags().Lookup("location")) + + cmd.PersistentFlags().StringP("reason", "r", "", "Signature reason") + _ = viper.BindPFlag("reason", cmd.PersistentFlags().Lookup("reason")) + + cmd.PersistentFlags().StringP("contact", "c", "", "Signature contact") + _ = viper.BindPFlag("contact", cmd.PersistentFlags().Lookup("contact")) -func parseConfigFlag(cmd *cobra.Command) { - cmd.PersistentFlags().StringVar(&configFilePathFlag, "config", "", "Path to config file") - _ = cmd.MarkPersistentFlagRequired("config") + cmd.PersistentFlags().String("tsa-url", "", "TSA url") + _ = viper.BindPFlag("tsa.url", cmd.PersistentFlags().Lookup("tsa-url")) + + cmd.PersistentFlags().String("tsa-username", "", "TSA username") + _ = viper.BindPFlag("tsa.username", cmd.PersistentFlags().Lookup("tsa-username")) + + cmd.PersistentFlags().String("tsa-password", "", "TSA password") + _ = viper.BindPFlag("tsa.password", cmd.PersistentFlags().Lookup("tsa-password")) + + cmd.PersistentFlags().BoolVar(&validateSignature, "validate-signature", true, "Validate signature") + _ = viper.BindPFlag("validateSignature", cmd.PersistentFlags().Lookup("validate-signature")) } // parsePEMCertificateFlags binds PEM specific flags to variables. func parsePEMCertificateFlags(cmd *cobra.Command) { - cmd.PersistentFlags().StringVar(&certificatePathFlag, "crt", "", "Path to certificate file") - cmd.PersistentFlags().StringVar(&privateKeyPathFlag, "key", "", "Path to private key") + cmd.PersistentFlags().String("cert", "", "Path to certificate file") + _ = viper.BindPFlag("cert", cmd.PersistentFlags().Lookup("cert")) + + cmd.PersistentFlags().String("key", "", "Path to private key") + _ = viper.BindPFlag("key", cmd.PersistentFlags().Lookup("key")) + + cmd.PersistentFlags().String("chain", "", "Certificate chain path") + _ = viper.BindPFlag("chain", cmd.PersistentFlags().Lookup("chain")) } // parsePKSC11CertificateFlags binds PKSC11 specific flags to variables. func parsePKSC11CertificateFlags(cmd *cobra.Command) { - cmd.PersistentFlags().StringVar(&pksc11LibPathFlag, "lib", "", "Path to PKCS11 library") - cmd.PersistentFlags().StringVar(&pksc11PassFlag, "pass", "", "PKCS11 password") + cmd.PersistentFlags().String("lib", "", "Path to PKCS11 library") + _ = viper.BindPFlag("lib", cmd.PersistentFlags().Lookup("lib")) + + cmd.PersistentFlags().String("pass", "", "PKCS11 password") + _ = viper.BindPFlag("pass", cmd.PersistentFlags().Lookup("pass")) } // parseInputPathFlag binds input folder flag to variable. func parseInputPathFlag(cmd *cobra.Command) { cmd.PersistentFlags().StringVar(&inputPathFlag, "in", "", "Input path") _ = cmd.MarkPersistentFlagRequired("in") + _ = viper.BindPFlag("in", cmd.PersistentFlags().Lookup("in")) } // parseOutputPathFlag binds output folder flag to variable. func parseOutputPathFlag(cmd *cobra.Command) { cmd.PersistentFlags().StringVar(&outputPathFlag, "out", "", "Output path") _ = cmd.MarkPersistentFlagRequired("out") -} - -// parseSignerName binds signer name flag to variable. -func parseSignerName(cmd *cobra.Command) { - cmd.PersistentFlags().StringVar(&signerNameFlag, "signer-name", "", "Signer name") - _ = cmd.MarkPersistentFlagRequired("signer-name") + _ = viper.BindPFlag("out", cmd.PersistentFlags().Lookup("out")) } // parseServeFlags binds serve address and port flags to variables. func parseServeFlags(cmd *cobra.Command) { - cmd.PersistentFlags().StringVar(&serveAddrFlag, "serve-address", "", "Address to serve Web API") + cmd.PersistentFlags().String("serve-address", "", "Address to serve Web API") _ = cmd.MarkPersistentFlagRequired("serve-address") - cmd.PersistentFlags().StringVar(&servePortFlag, "serve-port", "", "Port to serve Web API") - _ = cmd.MarkPersistentFlagRequired("serve-port") -} - -func isMultiSignerCmd() bool { - if len(os.Args) < 3 { - return false - } - - cmd := os.Args[1] - - subCmd := os.Args[2] - - return (cmd == "sign" && subCmd == "signer") || (cmd == "serve" && subCmd == "signers") || cmd == "services" -} - -// setupMultiSignersFlags setups commands to override signers config settings. -func setupMultiSignersFlags(cmd *cobra.Command) { - if !isMultiSignerCmd() { - return - } - - for i, s := range signersConfigArr { - // set flagSuffix if multiple signers provided inside config - flagSuffix := "" - if len(servicesConfigArr) > 0 { - flagSuffix = "_" + s.Name - } - - // set usage suffix - var usageSuffix string - if len(servicesConfigArr) > 0 { - usageSuffix += " " + s.Name - } - - usageSuffix += " config override flag" - - // create commands with temporary uint variables - certTypeUint := uint(s.SignData.Signature.CertType) - docMDPPermUint := uint(s.SignData.Signature.DocMDPPerm) - - cmd.PersistentFlags().UintVar(&certTypeUint, "type"+flagSuffix, certTypeUint, "Certificate type"+usageSuffix) - cmd.PersistentFlags().UintVar(&docMDPPermUint, "docmdp"+flagSuffix, docMDPPermUint, "DocMDP permissions"+usageSuffix) - - // Store the index for later use in flag handling - certTypeIndices := make(map[string]int) - docMDPPermIndices := make(map[string]int) - certTypeIndices["type"+flagSuffix] = i - docMDPPermIndices["docmdp"+flagSuffix] = i - - // Add post-processing for these flags - cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - for flagName, idx := range certTypeIndices { - if cmd.PersistentFlags().Changed(flagName) { - val, _ := cmd.PersistentFlags().GetUint(flagName) - signersConfigArr[idx].SignData.Signature.CertType = sign.CertType(val) - } - } - - for flagName, idx := range docMDPPermIndices { - if cmd.PersistentFlags().Changed(flagName) { - val, _ := cmd.PersistentFlags().GetUint(flagName) - signersConfigArr[idx].SignData.Signature.DocMDPPerm = sign.DocMDPPerm(val) - } - } - } - cmd.PersistentFlags().StringVar(&signersConfigArr[i].SignData.Signature.Info.Name, "name"+flagSuffix, s.SignData.Signature.Info.Name, "Signature info name"+usageSuffix) - cmd.PersistentFlags().StringVar(&signersConfigArr[i].SignData.Signature.Info.Location, "location"+flagSuffix, s.SignData.Signature.Info.Location, "Signature info location"+usageSuffix) - cmd.PersistentFlags().StringVar(&signersConfigArr[i].SignData.Signature.Info.Reason, "reason"+flagSuffix, s.SignData.Signature.Info.Reason, "Signature reason"+usageSuffix) - cmd.PersistentFlags().StringVar(&signersConfigArr[i].SignData.Signature.Info.ContactInfo, "contact"+flagSuffix, s.SignData.Signature.Info.ContactInfo, "Signature contact"+usageSuffix) - cmd.PersistentFlags().StringVar(&signersConfigArr[i].SignData.TSA.URL, "tsa-url"+flagSuffix, s.SignData.TSA.URL, "TSA url"+usageSuffix) - cmd.PersistentFlags().StringVar(&signersConfigArr[i].SignData.TSA.Username, "tsa-username"+flagSuffix, s.SignData.TSA.Username, "TSA username"+usageSuffix) - cmd.PersistentFlags().StringVar(&signersConfigArr[i].SignData.TSA.Password, "tsa-password"+flagSuffix, s.SignData.TSA.Password, "TSA password"+usageSuffix) - cmd.PersistentFlags().StringVar(&signersConfigArr[i].CrtChainPath, "chain"+flagSuffix, s.CrtChainPath, "Certificate chain path"+usageSuffix) - } -} - -func isMultiServiceCmd() bool { - if len(os.Args) < 2 { - return false - } - - return os.Args[1] == "services" -} - -// setupMultiServiceFlags setups commands to override services config settings. -func setupMultiServiceFlags(cmd *cobra.Command) { - if !isMultiServiceCmd() { - return - } - - for i, s := range servicesConfigArr { - // set suffix if multiple signers provided inside config - suffix := "" - if len(servicesConfigArr) > 0 { - suffix = "_" + s.Name - } - - // set usage suffix - var usageSuffix string - if len(servicesConfigArr) > 0 { - usageSuffix += " " + s.Name - } - - usageSuffix += " config override flag" + _ = viper.BindPFlag("serve.address", cmd.PersistentFlags().Lookup("serve-address")) - // create commands - cmd.PersistentFlags().BoolVar(&servicesConfigArr[i].ValidateSignature, "validate-signature"+suffix, true, "Certificate chain path"+usageSuffix) - } + cmd.PersistentFlags().String("serve-port", "", "Port to serve Web API") + _ = cmd.MarkPersistentFlagRequired("serve-port") + _ = viper.BindPFlag("serve.port", cmd.PersistentFlags().Lookup("serve-port")) } // getAddrPort returns server address and port formatted. func getAddrPort() string { - return serveAddrFlag + ":" + servePortFlag + addr := viper.GetString("serve.address") + port := viper.GetString("serve.port") + return addr + ":" + port } -// bindSignerFlagsToConfig binds signer specific flags to variables. -// Since viper is not supporting binding flags to an item of the array we use this workaround. +// bindSignerFlagsToConfig populates signer config with values from config file, +// then overrides with any explicitly provided command line flags. func bindSignerFlagsToConfig(cmd *cobra.Command, c *signerConfig) { - log.Debug("bindSignerFlagsToConfig") - - // JobSignConfig - if cmd.PersistentFlags().Changed("docmdp") { - c.SignData.Signature.DocMDPPerm = sign.DocMDPPerm(docMdpPermsFlag) + // First set values from the configuration file if available + if c.SignData.Signature.DocMDPPerm == 0 { + c.SignData.Signature.DocMDPPerm = sign.DocMDPPerm(viper.GetUint("docmdp")) } - if cmd.PersistentFlags().Changed("type") { - c.SignData.Signature.CertType = sign.CertType(signatureTypeFlag) + if c.SignData.Signature.CertType == 0 { + c.SignData.Signature.CertType = sign.CertType(viper.GetUint("type")) } - if cmd.PersistentFlags().Changed("name") { - c.SignData.Signature.Info.Name = signatureInfoNameFlag + if c.SignData.Signature.Info.Name == "" { + c.SignData.Signature.Info.Name = viper.GetString("name") } - if cmd.PersistentFlags().Changed("location") { - c.SignData.Signature.Info.Location = signatureInfoLocationFlag + if c.SignData.Signature.Info.Location == "" { + c.SignData.Signature.Info.Location = viper.GetString("location") } - if cmd.PersistentFlags().Changed("reason") { - c.SignData.Signature.Info.Reason = signatureInfoReasonFlag + if c.SignData.Signature.Info.Reason == "" { + c.SignData.Signature.Info.Reason = viper.GetString("reason") } - if cmd.PersistentFlags().Changed("contact") { - c.SignData.Signature.Info.ContactInfo = signatureInfoContactFlag + if c.SignData.Signature.Info.ContactInfo == "" { + c.SignData.Signature.Info.ContactInfo = viper.GetString("contact") } - if cmd.PersistentFlags().Changed("tsa-password") { - c.SignData.TSA.URL = signatureTSAUrlFlag + if c.SignData.TSA.URL == "" { + c.SignData.TSA.URL = viper.GetString("tsa.url") } - if cmd.PersistentFlags().Changed("tsa-url") { - c.SignData.TSA.Password = signatureTSAPasswordFlag + if c.SignData.TSA.Password == "" { + c.SignData.TSA.Password = viper.GetString("tsa.password") } - // Certificate chain - if cmd.PersistentFlags().Changed("chain") { - c.CrtChainPath = certificateChainPathFlag + if c.SignData.TSA.Username == "" { + c.SignData.TSA.Username = viper.GetString("tsa.username") } - // PEM - if cmd.PersistentFlags().Changed("crt") { - c.CrtPath = certificatePathFlag + if c.Chain == "" { + c.Chain = viper.GetString("chain") } - if cmd.PersistentFlags().Changed("key") { - c.KeyPath = privateKeyPathFlag + if c.Cert == "" { + c.Cert = viper.GetString("cert") } - // PKSC11 - if cmd.PersistentFlags().Changed("lib") { - c.LibPath = pksc11LibPathFlag + if c.Key == "" { + c.Key = viper.GetString("key") } - if cmd.PersistentFlags().Changed("pass") { - c.Pass = pksc11PassFlag + if c.Lib == "" { + c.Lib = viper.GetString("lib") } -} -// getSignerConfigByName returns config of the signer by name. -func getSignerConfigByName(signerName string) signerConfig { - if signerName == "" { - log.Fatal("signer name is empty") + if c.Pass == "" { + c.Pass = viper.GetString("pass") } - // find signer config - var s signerConfig - for _, s = range signersConfigArr { - if s.Name == signerName { - return s - } + // Override with command line flags only if explicitly provided + // Define flag mappings to their corresponding setters + stringFlags := map[string]func(string){ + "name": func(val string) { c.SignData.Signature.Info.Name = val }, + "location": func(val string) { c.SignData.Signature.Info.Location = val }, + "reason": func(val string) { c.SignData.Signature.Info.Reason = val }, + "contact": func(val string) { c.SignData.Signature.Info.ContactInfo = val }, + "tsa-url": func(val string) { c.SignData.TSA.URL = val }, + "tsa-password": func(val string) { c.SignData.TSA.Password = val }, + "tsa-username": func(val string) { c.SignData.TSA.Username = val }, + "chain": func(val string) { c.Chain = val }, + "cert": func(val string) { c.Cert = val }, + "key": func(val string) { c.Key = val }, + "lib": func(val string) { c.Lib = val }, + "pass": func(val string) { c.Pass = val }, } - // fail if signer not found - log.Fatal("signer not found") - - return s -} - -// getConfigServiceByName returns service config by name. -func getConfigServiceByName(serviceName string) serviceConfig { - if serviceName == "" { - log.Fatal("service name is not provided") + uintFlags := map[string]func(uint){ + "docmdp": func(val uint) { c.SignData.Signature.DocMDPPerm = sign.DocMDPPerm(val) }, + "type": func(val uint) { c.SignData.Signature.CertType = sign.CertType(val) }, } - // find service config - var s serviceConfig - for _, s = range servicesConfigArr { - if s.Name == serviceName { - return s + // Process string flags + for flagName, setter := range stringFlags { + if cmd.Flags().Changed(flagName) { + val, _ := cmd.Flags().GetString(flagName) + setter(val) } } - // fail if service not found - log.Fatal("service not found") - - return s + // Process uint flags + for flagName, setter := range uintFlags { + if cmd.Flags().Changed(flagName) { + val, _ := cmd.Flags().GetUint(flagName) + setter(val) + } + } } // requireLicense loads license. @@ -335,12 +226,16 @@ func requireLicense() error { licenseLoadErr := license.Load() if licenseLoadErr != nil { - // if the license couldn't be loaded try to initialize it - licenseInitErr = initializeLicense() + // try to initialize license with buid-in license + err := license.Initialize(nil) + if err != nil { + // if the license couldn't be loaded try to initialize it + licenseInitErr = initializeLicense() + } } if licenseInitErr != nil { - return licenseInitErr + return fmt.Errorf("license error: %w", licenseInitErr) } return nil diff --git a/cmd/common_test.go b/cmd/common_test.go new file mode 100644 index 0000000..7bef1c3 --- /dev/null +++ b/cmd/common_test.go @@ -0,0 +1,98 @@ +package cmd + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +// Global test variables +var ( + testfilesDir string + certPath string + keyPath string + testPDF12 string + testPDF20 string + malformedPDF string + signedPDF string +) + +// TestMain sets up the test environment before any tests run +func TestMain(m *testing.M) { + // Set up the testfiles directory path + cwd, err := os.Getwd() + if err != nil { + panic("Failed to get current directory: " + err.Error()) + } + + // Use "../testfiles" as requested + testfilesDir = filepath.Join(cwd, "../testfiles") + + // Set up common test file paths + certPath = filepath.Join(testfilesDir, "test.crt") + keyPath = filepath.Join(testfilesDir, "test.pem") + testPDF12 = filepath.Join(testfilesDir, "testfile12.pdf") + testPDF20 = filepath.Join(testfilesDir, "testfile20.pdf") + malformedPDF = filepath.Join(testfilesDir, "malformed.pdf") + signedPDF = filepath.Join(testfilesDir, "SampleSignedPDFDocument.pdf") + + // Verify test files existence before running any tests + filesToCheck := []string{certPath, keyPath, testPDF12, testPDF20, malformedPDF, signedPDF} + for _, file := range filesToCheck { + if _, err := os.Stat(file); os.IsNotExist(err) { + panic("Required test file missing: " + file) + } + } + + // Run the tests + os.Exit(m.Run()) +} + +// Simplified helper function to execute a command and capture its output +// Now always uses RootCmd implicitly +func executeCommand(t *testing.T, args ...string) (string, error) { + // Create a fresh copy of the root command to avoid state between test runs + cmd := RootCmd + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs(args) + + t.Logf("Executing command: pdfsigner %s", strings.Join(args, " ")) + + // Execute the command + var err error + assert.NotPanics(t, func() { + err = cmd.Execute() + }) + + return buf.String(), err +} + +// Keep executeCommandWithArgs for backward compatibility or special cases +func executeCommandWithArgs(t *testing.T, cmd *cobra.Command, args []string) (string, error) { + if cmd == RootCmd { + return executeCommand(t, args...) + } + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs(args) + + t.Logf("Executing command: %s %s", cmd.CommandPath(), strings.Join(args, " ")) + + // Execute the command + var err error + assert.NotPanics(t, func() { + err = cmd.Execute() + }) + + return buf.String(), err +} diff --git a/cmd/config.go b/cmd/config.go index b8e95e6..886574b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -1,16 +1,9 @@ package cmd import ( - "errors" "fmt" - "os" - "strings" "github.com/digitorus/pdfsigner/signer" - "github.com/mitchellh/go-homedir" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" ) // Config holds the root configuration structure. @@ -22,7 +15,6 @@ type Config struct { // serviceConfig is a config of the service. type serviceConfig struct { - Name string `mapstructure:"-"` // Added for backward compatibility Type string `mapstructure:"type"` Signer string `mapstructure:"signer,omitempty"` Signers []string `mapstructure:"signers,omitempty"` @@ -30,97 +22,34 @@ type serviceConfig struct { Out string `mapstructure:"out,omitempty"` ValidateSignature bool `mapstructure:"validateSignature"` Addr string `mapstructure:"addr,omitempty"` - Port string `mapstructure:"port,omitempty"` // Changed to string + Port string `mapstructure:"port,omitempty"` } type signerConfig struct { - Name string `mapstructure:"-"` // Added for backward compatibility - Type string `mapstructure:"type"` - CrtPath string `mapstructure:"crtPath,omitempty"` - KeyPath string `mapstructure:"keyPath,omitempty"` - LibPath string `mapstructure:"libPath,omitempty"` - Pass string `mapstructure:"pass,omitempty"` - CrtChainPath string `mapstructure:"crtChainPath,omitempty"` - SignData signer.SignData `mapstructure:"signData"` + Type string `mapstructure:"type"` + Cert string `mapstructure:"cert,omitempty"` + Key string `mapstructure:"key,omitempty"` + Lib string `mapstructure:"lib,omitempty"` + Pass string `mapstructure:"pass,omitempty"` + Chain string `mapstructure:"chain,omitempty"` + SignData signer.SignData `mapstructure:"signData"` } var ( - config Config - signersConfigArr []signerConfig - servicesConfigArr []serviceConfig + config Config ) -// initConfig reads in config inputFile and ENV variables if set. -func initConfig(cmd *cobra.Command) { - preParseConfigFlag() - - if configFilePathFlag != "" { - // Use config inputFile from the flag. - viper.SetConfigFile(configFilePathFlag) - } else { - // Find home directory. - home, err := homedir.Dir() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - // Search config in home directory with name ".pdfsigner" (without extension). - viper.AddConfigPath(home) - viper.SetConfigName(".pdfsigner") - } - - viper.AutomaticEnv() // read in environment variables that match - viper.SetEnvPrefix("PDF") // set env prefix - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - // If a config inputFile is found, read it in. - err := viper.ReadInConfig() - if err == nil { - log.Println("Using config file:", viper.ConfigFileUsed()) - // validate config - if len(viper.AllSettings()) == 0 { - log.Fatal(errors.New("config is not properly formatted or empty")) - } - } - - if err != nil && configFilePathFlag != "" { - log.Fatal(err) - } - - // Unmarshal the full config - if err := viper.Unmarshal(&config); err != nil { - log.Fatal("Error decoding config:", err) +// getSignerConfigByName returns config of the signer by name. +func getSignerConfigByName(signer string) (signerConfig, error) { + if signer == "" { + return signerConfig{}, fmt.Errorf("signer name is empty") } - // Convert nested config to flat arrays for backward compatibility - for name, signer := range config.Signers { - signer.Name = name // Set the name field - signersConfigArr = append(signersConfigArr, signer) + // Try to get directly from the map first (more efficient) + if signer, exists := config.Signers[signer]; exists { + return signer, nil } - for name, service := range config.Services { - service.Name = name // Set the name field - servicesConfigArr = append(servicesConfigArr, service) - } - - licenseStrConfOrFlag = config.LicensePath - - // setup CLI overrides for signers and services of the config if it's multi command - setupMultiSignersFlags(cmd) - setupMultiServiceFlags(cmd) -} - -// needed to override signers and services config settings. -func preParseConfigFlag() { - const configFlagName = "--config" - - args := strings.Join(os.Args[1:], " ") - if strings.Contains(args, configFlagName) { - fields := strings.Fields(args) - for i, f := range fields { - if strings.Contains(f, configFlagName) && len(fields) > i+1 { - configFilePathFlag = fields[i+1] - } - } - } + // fail if signer not found + return signerConfig{}, fmt.Errorf("signer not found: %s", signer) } diff --git a/cmd/license.go b/cmd/license.go index bd64ff3..f3fa183 100644 --- a/cmd/license.go +++ b/cmd/license.go @@ -1,17 +1,3 @@ -// Copyright © 2018 NAME HERE -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package cmd import ( @@ -21,9 +7,8 @@ import ( "strings" "github.com/digitorus/pdfsigner/license" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // licenseCmd represents the license command. @@ -32,19 +17,21 @@ var licenseCmd = &cobra.Command{ Short: "Check and update license", } -// licenseInfoCmd represents the license info command. +// licenseSetupCmd represents the license setup command. var licenseSetupCmd = &cobra.Command{ Use: "setup", Short: "license setup", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { // initialize license err := initializeLicense() if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to initialize license: %w", err) } // print license info - fmt.Print(license.LD.Info()) + cmd.Print(license.LD.Info()) + + return nil }, } @@ -52,15 +39,21 @@ var licenseSetupCmd = &cobra.Command{ var licenseInfoCmd = &cobra.Command{ Use: "info", Short: "license info", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { // load license err := license.Load() if err != nil { - log.Fatal(err) + // try to initialize license with buid-in license + err := license.Initialize(nil) + if err != nil { + return fmt.Errorf("failed to initialize license: %w", err) + } } // print license info - fmt.Print(license.LD.Info()) + cmd.Print(license.LD.Info()) + + return nil }, } @@ -68,32 +61,49 @@ func init() { RootCmd.AddCommand(licenseCmd) licenseCmd.AddCommand(licenseSetupCmd) licenseCmd.AddCommand(licenseInfoCmd) + + // Add license path flag + licenseCmd.PersistentFlags().String("license-path", "", "Path to license file") + _ = viper.BindPFlag("licensePath", licenseCmd.PersistentFlags().Lookup("license-path")) } -// initializeLicense loads the license file with provided path licenseStrConfOrFlag or stdin. +// initializeLicense loads the license file with provided path from viper config or command line. func initializeLicense() error { - // reading license file name. Info: can't read license directly from stdin because of a darwin 1024 limit. - var licenseStr string - if licenseStrConfOrFlag != "" { - // try to get license from the flag provided - licenseStr = licenseStrConfOrFlag - } else { - // get license from the stdout - fmt.Fprint(os.Stdout, "Paste your license here:") - - var err error - - licenseStr, err = bufio.NewReader(os.Stdin).ReadString('\n') + // Get license from viper (can be from flag, env var, or config) + licenseStr := viper.GetString("license") + licenseFilePath := viper.GetString("licensePath") + + // If neither license string nor license path is provided through viper + if licenseStr == "" && licenseFilePath == "" { + // Check if license was provided directly from command line + if licenseStrConfOrFlag != "" { + licenseStr = licenseStrConfOrFlag + } else { + // As a last resort, get license from stdin + fmt.Fprint(os.Stdout, "Paste your license here: ") + + var err error + licenseStr, err = bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read license from stdin: %w", err) + } + } + } else if licenseStr == "" && licenseFilePath != "" { + // If license path is provided, read from file + licenseBytes, err := os.ReadFile(licenseFilePath) if err != nil { - return errors.Wrap(err, "") + return fmt.Errorf("failed to read license file: %w", err) } + licenseStr = string(licenseBytes) } + // Process the license string licenseBytes := []byte(strings.Replace(strings.TrimSpace(licenseStr), "\n", "", -1)) - // initialize license + + // Initialize license err := license.Initialize(licenseBytes) if err != nil { - return errors.Wrap(err, "") + return fmt.Errorf("failed to initialize license: %w", err) } return nil diff --git a/cmd/license_test.go b/cmd/license_test.go new file mode 100644 index 0000000..94459b7 --- /dev/null +++ b/cmd/license_test.go @@ -0,0 +1,51 @@ +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/digitorus/pdfsigner/license" + "github.com/stretchr/testify/assert" +) + +func TestLicenseCommands(t *testing.T) { + // Save original license data and restore after tests + originalLicenseData := license.LD + defer func() { license.LD = originalLicenseData }() + + t.Run("License command should show help when no args provided", func(t *testing.T) { + output, err := executeCommand(t, "license") + + assert.NoError(t, err) + assert.Contains(t, output, "Check and update license") + }) + + t.Run("License info command should run without error", func(t *testing.T) { + output, err := executeCommand(t, "license", "info") + + assert.NoError(t, err) + assert.Contains(t, output, "Licensed to ") + }) + + // t.Run("License setup command should handle invalid license", func(t *testing.T) { + // // Create temp file with invalid license + // tmpFile := filepath.Join(t.TempDir(), "invalid_license.txt") + // err := os.WriteFile(tmpFile, []byte("invalid-license"), 0600) + // assert.NoError(t, err) + + // // Execute command with invalid license file + // _, err = executeCommand(t, "license", "setup", "--license-path", tmpFile) + + // // Should error with invalid license + // assert.Error(t, err) + // }) + + t.Run("License setup with non-existent file should fail", func(t *testing.T) { + nonExistentFile := filepath.Join(t.TempDir(), "non_existent_license.txt") + + _, err := executeCommand(t, "license", "setup", "--license-path", nonExistentFile) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to read license file") + }) +} diff --git a/cmd/multiple_services.go b/cmd/multiple_services.go index eefdaa7..9cbf2e3 100644 --- a/cmd/multiple_services.go +++ b/cmd/multiple_services.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "slices" "sync" "github.com/digitorus/pdfsigner/files" @@ -16,43 +18,30 @@ import ( var multiCmd = &cobra.Command{ Use: "services", Short: "Run multiple services using the config file", - Run: func(cmd *cobra.Command, serviceNames []string) { - // require license - err := requireLicense() - if err != nil { - log.Fatal(err) - } - + RunE: func(cmd *cobra.Command, serviceNames []string) error { // loading jobs from the db - err = signVerifyQueue.LoadFromDB() + err := signVerifyQueue.LoadFromDB() if err != nil { - log.Fatal(err) + return err } // check if the config contains services - if len(servicesConfigArr) < 1 { - log.Fatal("no services found inside the config") + if len(config.Services) < 1 { + return fmt.Errorf("no services found inside the config") } // setup wait group var wg sync.WaitGroup // setup services - if len(serviceNames) > 1 { + for sname, sconfig := range config.Services { // setup services by name - wg.Add(len(serviceNames)) - for _, n := range serviceNames { - // get service config by name - serviceConf := getConfigServiceByName(n) - // setup service with signers - setupServiceWithSigners(serviceConf, &wg) - } - } else { - // setup all services - wg.Add(len(servicesConfigArr)) - for _, s := range servicesConfigArr { - // setup service with signers - setupServiceWithSigners(s, &wg) + if len(serviceNames) == 0 { + wg.Add(len(sname)) + setupServiceWithSigners(sconfig, &wg) + } else if slices.Contains(serviceNames, sname) { + wg.Add(len(sname)) + setupServiceWithSigners(sconfig, &wg) } } @@ -64,6 +53,7 @@ var multiCmd = &cobra.Command{ // wait wg.Wait() + return nil }, } @@ -96,7 +86,10 @@ func setupSigners(serviceType, configSignerName string, configSignerNames []stri // setup signer if configSignerName != "" { - setupSigner(configSignerName) + err := setupSigner(configSignerName) + if err != nil { + log.Fatalf("failed to setup signer: %s", err) + } return } @@ -109,7 +102,10 @@ func setupSigners(serviceType, configSignerName string, configSignerNames []stri // setup signers if len(configSignerNames) > 1 { for _, sn := range configSignerNames { - setupSigner(sn) + err := setupSigner(sn) + if err != nil { + log.Fatalf("failed to setup signer: %s", err) + } } } @@ -121,34 +117,45 @@ func setupSigners(serviceType, configSignerName string, configSignerNames []stri } // setupSigner adds found inside the config by name signer to the queue for later use. -func setupSigner(signerName string) { +func setupSigner(signerName string) error { // get config signer by name - config := getSignerConfigByName(signerName) + config, err := getSignerConfigByName(signerName) + if err != nil { + return err + } - // set sign data switch config.Type { case "pem": - config.SignData.SetPEM(config.CrtPath, config.KeyPath, config.CrtChainPath) + err = config.SignData.SetPEM(config.Cert, config.Key, config.Chain) case "pksc11": - config.SignData.SetPKSC11(config.LibPath, config.Pass, config.CrtChainPath) + err = config.SignData.SetPKSC11(config.Lib, config.Pass, config.Chain) + } + if err != nil { + return err } // add signer to signers map signVerifyQueue.AddSignUnit(signerName, config.SignData) + + return nil } // setupService depending on the type of the service setups service. func setupService(service serviceConfig) { + var err error if service.Type == "watch" { - setupWatch(service) + err = setupWatch(service) } else if service.Type == "serve" { - setupServe(service) - log.Println("watch service", service.Name, "started") + err = setupServe(service) + } + + if err != nil { + log.Fatalf("failed to setup %s: %s", service.Type, err) } } // setupWatch setups watcher which watches the input folder and adds the tasks to the queue. -func setupWatch(service serviceConfig) { +func setupWatch(service serviceConfig) error { files.Watch(service.In, func(inputFilePath string, left int) { // make signed file path signedFilePath := getOutputFilePathByInputFilePath(inputFilePath, service.Out) @@ -158,21 +165,29 @@ func setupWatch(service serviceConfig) { ValidateSignature: service.ValidateSignature, }) - // push job - // TODO: should we write any errors to the debug log? - _, _ = signVerifyQueue.AddTask(service.Signer, jobID, "", inputFilePath, signedFilePath, priority_queue.LowPriority) + // push job to the queue + _, err := signVerifyQueue.AddTask(service.Signer, jobID, "", inputFilePath, signedFilePath, priority_queue.LowPriority) + if err != nil { + log.Debugf("failed to add task: %s", err) + } if left == 0 { - _ = signVerifyQueue.SaveToDB(jobID) + err = signVerifyQueue.SaveToDB(jobID) + if err != nil { + log.Debugf("failed to save job to db: %s", err) + } } }) - // batch save to the db + + return nil } // setupServe runs the web api according to the config settings. -func setupServe(service serviceConfig) { +func setupServe(service serviceConfig) error { // serve but only use allowed signers wa := webapi.NewWebAPI(service.Addr+":"+service.Port, signVerifyQueue, service.Signers, ver, service.ValidateSignature) wa.Serve() + + return nil } // runQueues starts the mechanism to sign the files whenever they are getting into the queue. @@ -182,5 +197,5 @@ func runQueues() { func init() { RootCmd.AddCommand(multiCmd) - parseConfigFlag(multiCmd) + // No need to call parseConfigFlag since config is now a global flag } diff --git a/cmd/root.go b/cmd/root.go index 3f52711..e273f10 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,32 +3,76 @@ package cmd import ( "fmt" "os" + "strings" "github.com/digitorus/pdfsigner/version" + homedir "github.com/mitchellh/go-homedir" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) -// configFilePathFlag contains path to config file. -var configFilePathFlag string - -// licenseStrConfOrFlag contains path to license file. -var licenseStrConfOrFlag string - -var ver version.Version +var ( + // Used for flags. + cfgFile string + licenseStrConfOrFlag string + ver version.Version + licenseValidated bool // Track if license has been validated +) // RootCmd represents the base command when called without any subcommands. var RootCmd = &cobra.Command{ Use: "pdfsigner", Short: "PDFSigner is a multi purpose PDF signer and verifier", Long: `PDFSigner is a multi purpose PDF signer and verifier application it allows to use it as a command line tool and as a watch and sign tool, it also allows to use Web API to sign and verify files and to use multiple services in combination.`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Skip license check for license command and version command + if cmd.CommandPath() == "pdfsigner license" || + cmd.CommandPath() == "pdfsigner license setup" || + cmd.CommandPath() == "pdfsigner license info" || + cmd.CommandPath() == "pdfsigner version" { + return nil + } + + // Check license globally, only once + if !licenseValidated { + if err := requireLicense(); err != nil { + return fmt.Errorf("license error: %w", err) + } + log.Debug("License validated successfully") + licenseValidated = true + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + // If config is specified but no subcommand, check if we can determine what to run + if cfgFile != "" { + // Config loaded successfully, now check which service to run + // Check if services are configured in the config file + if len(config.Services) > 0 { + log.Debug("Found services in config file, running in services mode") + // Run the services command + return multiCmd.RunE(cmd, args) + } + + // Check if signers are configured without services + if len(config.Signers) > 0 { + log.Debug("Found signers in config file, running in serve signers mode") + // Run the serve signers command + return serveWithMultipleSignersCmd.RunE(cmd, args) + } + + return fmt.Errorf("config file provided but no services or signers found") + } + + // If no config file is specified, show help + return cmd.Help() + }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute(v version.Version) { - // parse config flag to make it available before cobra - initConfig(RootCmd) - // set version ver = v @@ -40,17 +84,69 @@ func Execute(v version.Version) { } func init() { - // set the log flags - cobra.OnInitialize() + // This is called before each command execution + cobra.OnInitialize(initConfig) + + // Define the config flag as a persistent flag on the root command + // so it's globally available to all subcommands + RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.pdfsigner)") - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. - // RootCmd.PersistentFlags().StringVar(&configFilePathFlag, "config", "", "") + // Add license as a persistent flag as well + RootCmd.PersistentFlags().StringVar(&licenseStrConfOrFlag, "license", "", "license string") + _ = viper.BindPFlag("license", RootCmd.PersistentFlags().Lookup("license")) // Cobra also supports local flags, which will only run // when this action is called directly. RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} - RootCmd.PersistentFlags().StringVar(&licenseStrConfOrFlag, "license", "", "license string") +// initConfig reads in config file and ENV variables if set. +// This is called by cobra.OnInitialize +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + log.Warn("Error finding home directory:", err) + return + } + + // Search config in home directory with name ".pdfsigner" (without extension). + viper.AddConfigPath(home) + viper.SetConfigName(".pdfsigner") + } + + viper.SetEnvPrefix("PDF") // set env prefix + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err != nil { + if cfgFile != "" { + // If a config file was explicitly provided but not found, log an error + log.Errorf("Error reading config file: %v", err) + } + return + } + + log.Debugf("Using config file: %s", viper.ConfigFileUsed()) + + // Load configuration into app-level structures + if err := loadConfig(); err != nil { + log.Errorf("Error loading config: %v", err) + return + } +} + +// loadConfig loads configuration from viper into application-level structures +func loadConfig() error { + // Load config into global configuration struct + if err := viper.Unmarshal(&config); err != nil { + return fmt.Errorf("error decoding config: %w", err) + } + + return nil } diff --git a/cmd/serve.go b/cmd/serve.go index 470774a..59be441 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/digitorus/pdfsigner/license" "github.com/digitorus/pdfsigner/signer" "github.com/digitorus/pdfsigner/webapi" @@ -13,23 +15,29 @@ var serveCmd = &cobra.Command{ Use: "serve", Short: "Run web server to sign and verify files using HTTP protocol", Long: `Web API allows to sign and verify files by communicating with the application using HTTP protocol`, + // Add RunE to handle the case when only 'serve' is provided with --config + RunE: func(cmd *cobra.Command, args []string) error { + // If config file is specified, check if we can determine what to serve + if len(config.Services) > 0 { + log.Info("Found signers in config file, running in serve signers mode") + // Run the serve signers command + return serveWithMultipleSignersCmd.RunE(cmd, args) + } + + // If no config is specified, show help for the serve command + return cmd.Help() + }, } // servePEMCmd runs web api with PEM using only flags. var servePEMCmd = &cobra.Command{ Use: "pem", Short: "Serve using PEM signer", - Run: func(cmd *cobra.Command, attr []string) { - // require license - err := requireLicense() - if err != nil { - log.Fatal(err) - } - + RunE: func(cmd *cobra.Command, attr []string) error { // loading jobs from the db - err = signVerifyQueue.LoadFromDB() + err := signVerifyQueue.LoadFromDB() if err != nil { - log.Fatal(err) + return err } config := signerConfig{} @@ -38,10 +46,15 @@ var servePEMCmd = &cobra.Command{ bindSignerFlagsToConfig(cmd, &config) // set sign data - config.SignData.SetPEM(config.CrtPath, config.KeyPath, config.CrtChainPath) + err = config.SignData.SetPEM(config.Cert, config.Key, config.Chain) + if err != nil { + return fmt.Errorf("failed to set PEM certificate data: %w", err) + } // start web api with runners using unnamed signer startWebAPIWithRunnersUnnamedSigner(config.SignData) + + return nil }, } @@ -49,17 +62,11 @@ var servePEMCmd = &cobra.Command{ var servePKSC11Cmd = &cobra.Command{ Use: "pksc11", Short: "Serve using PKSC11 signer", - Run: func(cmd *cobra.Command, attr []string) { - // require license - err := requireLicense() - if err != nil { - log.Fatal(err) - } - + RunE: func(cmd *cobra.Command, attr []string) error { // loading jobs from the db - err = signVerifyQueue.LoadFromDB() + err := signVerifyQueue.LoadFromDB() if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to load jobs from the db: %w", err) } // create signer config @@ -69,10 +76,15 @@ var servePKSC11Cmd = &cobra.Command{ bindSignerFlagsToConfig(cmd, &config) // set sign data - config.SignData.SetPKSC11(config.LibPath, config.Pass, config.CrtChainPath) + err = config.SignData.SetPKSC11(config.Lib, config.Pass, config.Chain) + if err != nil { + return fmt.Errorf("failed to set PKSC11 configuration: %w", err) + } // start web api with runners using unnamed signer startWebAPIWithRunnersUnnamedSigner(config.SignData) + + return nil }, } @@ -81,27 +93,32 @@ var serveWithMultipleSignersCmd = &cobra.Command{ Use: "signers", Short: "Serve with multiple signers from the config", Long: `Runs multiple signers. Settings couldn't be overwritten`, - Run: func(cmd *cobra.Command, signerNames []string) { - // require license - err := requireLicense() - if err != nil { - log.Fatal(err) - } - + RunE: func(cmd *cobra.Command, signerNames []string) error { // loading jobs from the db - err = signVerifyQueue.LoadFromDB() + err := signVerifyQueue.LoadFromDB() if err != nil { - log.Fatal(err) + return err } - // check if the signer names provided + // Get signers from config if not provided as arguments if len(signerNames) < 1 { - log.Fatal("signers are not provided") + // Use all configured signers + for name := range config.Signers { + signerNames = append(signerNames, name) + } + + // Check again if we have signers + if len(signerNames) < 1 { + return fmt.Errorf("no signers found in config and none provided as arguments") + } } // setup signers for _, sn := range signerNames { - setupSigner(sn) + err = setupSigner(sn) + if err != nil { + return fmt.Errorf("failed to setup signer: %w", err) + } } // setup verifier @@ -109,6 +126,8 @@ var serveWithMultipleSignersCmd = &cobra.Command{ // start web api with runners startWebAPIWithProcessor(signerNames) + + return nil }, } @@ -151,6 +170,6 @@ func init() { // add serve with multiple signers and parse related flags serveCmd.AddCommand(serveWithMultipleSignersCmd) - parseConfigFlag(serveWithMultipleSignersCmd) + // No need to call parseConfigFlag since config is now a global flag parseServeFlags(serveWithMultipleSignersCmd) } diff --git a/cmd/sign.go b/cmd/sign.go index d33a605..1b4aace 100644 --- a/cmd/sign.go +++ b/cmd/sign.go @@ -1,9 +1,12 @@ package cmd import ( + "fmt" + "github.com/digitorus/pdfsigner/files" - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" ) // signCmd represents the sign command. @@ -11,32 +14,46 @@ var signCmd = &cobra.Command{ Use: "sign", Short: "Sign files using PEM or PKSC11", Long: `Command line signer allows to sign document using PEM or PKSC11 provided directly as well as using preconfigured signer from the config file.`, + // Add RunE to handle the case when only 'sign' is provided with --config + RunE: func(cmd *cobra.Command, args []string) error { + if len(config.Signers) > 0 { + return signBySignerNameCmd.RunE(cmd, args) + } + + return fmt.Errorf("no signers configured") + }, } // signPEMCmd signs files with PEM using flags only. var signPEMCmd = &cobra.Command{ Use: "pem", Short: "Sign PDF with PEM formatted certificate", - Run: func(cmd *cobra.Command, filePatterns []string) { - // require license - err := requireLicense() - if err != nil { - log.Fatal(err) - } - + RunE: func(cmd *cobra.Command, filePatterns []string) error { // require file patterns - requireFilePatterns(filePatterns) + if err := requireFilePatterns(filePatterns); err != nil { + return err + } // initialize config c := signerConfig{} // bind signer flags to config bindSignerFlagsToConfig(cmd, &c) + // set sign data - c.SignData.SetPEM(c.CrtPath, c.KeyPath, c.CrtChainPath) + err := c.SignData.SetPEM(c.Cert, c.Key, c.Chain) + if err != nil { + return err + } + + // optional output directory + out, _ := cmd.Flags().GetString("out") + + // optional validation of the signature + validateSignature, _ := cmd.Flags().GetBool("validateSignature") // sign files - files.SignFilesByPatterns(filePatterns, c.SignData, validateSignature) + return files.SignFilesByPatterns(filePatterns, c.SignData, validateSignature, out) }, } @@ -44,15 +61,11 @@ var signPEMCmd = &cobra.Command{ var signPKSC11Cmd = &cobra.Command{ Use: "pksc11", Short: "Signs PDF with PSKC11", - Run: func(cmd *cobra.Command, filePatterns []string) { - // require license - err := requireLicense() - if err != nil { - log.Fatal(err) - } - + RunE: func(cmd *cobra.Command, filePatterns []string) error { // require file patterns - requireFilePatterns(filePatterns) + if err := requireFilePatterns(filePatterns); err != nil { + return err + } // initialize config c := signerConfig{} @@ -61,10 +74,18 @@ var signPKSC11Cmd = &cobra.Command{ bindSignerFlagsToConfig(cmd, &c) // set sign data - c.SignData.SetPKSC11(c.LibPath, c.Pass, c.CrtChainPath) + if err := c.SignData.SetPKSC11(c.Lib, c.Pass, c.Chain); err != nil { + return err + } + + // optional output directory + out, _ := cmd.Flags().GetString("out") + + // optional validation of the signature + validateSignature, _ := cmd.Flags().GetBool("validateSignature") // sign files - files.SignFilesByPatterns(filePatterns, c.SignData, validateSignature) + return files.SignFilesByPatterns(filePatterns, c.SignData, validateSignature, out) }, } @@ -72,62 +93,83 @@ var signPKSC11Cmd = &cobra.Command{ var signBySignerNameCmd = &cobra.Command{ Use: "signer", Short: "Sign PDF with preconfigured signer", - Run: func(cmd *cobra.Command, filePatterns []string) { - // require license - err := requireLicense() - if err != nil { - log.Fatal(err) - } - + RunE: func(cmd *cobra.Command, filePatterns []string) error { // require file patterns - requireFilePatterns(filePatterns) + if err := requireFilePatterns(filePatterns); err != nil { + return err + } // find signer config from config file by name - c := getSignerConfigByName(signerNameFlag) + signer, _ := cmd.Flags().GetString("signer") + c, err := getSignerConfigByName(signer) + if err != nil { + return err + } // bind signer flags to config bindSignerFlagsToConfig(cmd, &c) // set sign data switch c.Type { - case "pem": - c.SignData.SetPEM(c.CrtPath, c.KeyPath, c.CrtChainPath) case "pksc11": - c.SignData.SetPKSC11(c.LibPath, c.Pass, c.CrtChainPath) + err = c.SignData.SetPKSC11(c.Lib, c.Pass, c.Chain) + default: + err = c.SignData.SetPEM(c.Cert, c.Key, c.Chain) + } + if err != nil { + return err } + // optional output directory + out, _ := cmd.Flags().GetString("out") + + // optional validation of the signature + validateSignature, _ := cmd.Flags().GetBool("validateSignature") + // sign files - files.SignFilesByPatterns(filePatterns, c.SignData, validateSignature) + return files.SignFilesByPatterns(filePatterns, c.SignData, validateSignature, out) }, } func init() { RootCmd.AddCommand(signCmd) + parseCommonFlags(signCmd) + parsePEMCertificateFlags(signCmd) + parsePKSC11CertificateFlags(signCmd) // add PEM sign command and parse related flags signCmd.AddCommand(signPEMCmd) parseCommonFlags(signPEMCmd) - // parseOutputPathFlag(signPEMCmd) parsePEMCertificateFlags(signPEMCmd) // add PKSC11 sign command and parse related flags signCmd.AddCommand(signPKSC11Cmd) parseCommonFlags(signPKSC11Cmd) - // parseOutputPathFlag(signPKSC11Cmd) parsePKSC11CertificateFlags(signPKSC11Cmd) // add sign with signer from config command and parse related flags signCmd.AddCommand(signBySignerNameCmd) - parseConfigFlag(signBySignerNameCmd) - parseSignerName(signBySignerNameCmd) - // parseOutputPathFlag(signBySignerNameCmd) + parseCommonFlags(signBySignerNameCmd) parsePEMCertificateFlags(signBySignerNameCmd) parsePKSC11CertificateFlags(signBySignerNameCmd) + + // Add parseOutputDirectoryFlag calls to all commands + parseOutputDirectoryFlag(signCmd) + parseOutputDirectoryFlag(signPEMCmd) + parseOutputDirectoryFlag(signPKSC11Cmd) + parseOutputDirectoryFlag(signBySignerNameCmd) } // requireFilePatterns checks if the filePatterns were provided. -func requireFilePatterns(filePatterns []string) { +func requireFilePatterns(filePatterns []string) error { if len(filePatterns) < 1 { - log.Fatal("no file patterns provided") + return fmt.Errorf("no file patterns provided") } + return nil +} + +// parseOutputDirectoryFlag adds the output directory flag to a command +func parseOutputDirectoryFlag(cmd *cobra.Command) { + cmd.Flags().StringP("out", "o", "", "output directory for signed files (default: same directory as input)") + _ = viper.BindPFlag("out", cmd.Flags().Lookup("out")) } diff --git a/cmd/sign_test.go b/cmd/sign_test.go new file mode 100644 index 0000000..8af9f9f --- /dev/null +++ b/cmd/sign_test.go @@ -0,0 +1,253 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSignCommand(t *testing.T) { + // Save original config and restore after tests + originalConfig := config + defer func() { config = originalConfig }() + + t.Run("Sign command should display help when no arguments", func(t *testing.T) { + output, err := executeCommand(t, "sign") + + assert.Error(t, err) + assert.Contains(t, output, "pdfsigner sign [flags]") + }) +} + +func TestSignPEMCommand(t *testing.T) { + // Create a temporary output directory + outputDir, err := os.MkdirTemp("", "pdfsigner-test-output") + require.NoError(t, err, "Failed to create temp directory") + defer os.RemoveAll(outputDir) + + t.Run("SignPEM command should fail without file patterns", func(t *testing.T) { + output, err := executeCommand(t, "sign", "pem") + + assert.Error(t, err) + assert.Contains(t, output, "no file patterns provided") + }) + + t.Run("SignPEM command should work with proper arguments", func(t *testing.T) { + outputFilePath := testPDF12[:len(testPDF12)-4] + "_signed.pdf" + + // Remove any previous output file + _ = os.Remove(outputFilePath) + + // Run the sign command + output, err := executeCommand(t, + "sign", "pem", + "--cert", certPath, + "--key", keyPath, + "--name", "Test Signer", + "--reason", "Testing", + testPDF12, + ) + require.NoError(t, err, "Failed to execute sign PEM command") + + // If the file was created, verify it's different than the original + if _, statErr := os.Stat(outputFilePath); statErr == nil { + originalInfo, statErr := os.Stat(testPDF12) + assert.NoError(t, statErr, "Failed to stat original file") + + signedInfo, statErr := os.Stat(outputFilePath) + assert.NoError(t, statErr, "Failed to stat signed file") + + // The signed file should have a different size than the original + assert.NotEqual(t, originalInfo.Size(), signedInfo.Size(), + "Signed file should be different from original") + } else { + t.Errorf("Signing completed but signed file not found. Command output: %s", output) + } + }) + + t.Run("SignPEM command should respect output directory without adding _signed suffix", func(t *testing.T) { + // Get the original filename without path + originalFileName := filepath.Base(testPDF12) + + // Expected output is the original filename in the output directory (without _signed) + expectedOutputPath := filepath.Join(outputDir, originalFileName) + + // Remove any previous output file + _ = os.Remove(expectedOutputPath) + + // Run the sign command with output directory + output, err := executeCommand(t, + "sign", "pem", + "--cert", certPath, + "--key", keyPath, + "--name", "Test Signer", + "--reason", "Testing", + "--out", outputDir, + testPDF12, + ) + require.NoError(t, err, "Failed to execute sign PEM command with output directory") + + // Verify the file was created with correct name (original name without _signed) + if _, statErr := os.Stat(expectedOutputPath); statErr == nil { + originalInfo, statErr := os.Stat(testPDF12) + assert.NoError(t, statErr, "Failed to stat original file") + + signedInfo, statErr := os.Stat(expectedOutputPath) + assert.NoError(t, statErr, "Failed to stat signed file") + + // The signed file should have a different size than the original + assert.NotEqual(t, originalInfo.Size(), signedInfo.Size(), + "Signed file should be different from original") + + // Verify the file name doesn't have _signed suffix + assert.False(t, strings.Contains(expectedOutputPath, "_signed"), + "Output filename should not contain _signed suffix when output directory is specified") + } else { + t.Errorf("Signing completed but signed file not found at %s. Command output: %s", + expectedOutputPath, output) + } + }) + + t.Run("SignPEM command should handle malformed PDF", func(t *testing.T) { + args := []string{ + "sign", "pem", + "--cert", certPath, + "--key", keyPath, + "--out", outputDir, + malformedPDF, + } + + // Run the sign command + output, err := executeCommandWithArgs(t, RootCmd, args) + if err == nil { + t.Log(output) + } + assert.Error(t, err) + }) + + t.Run("SignPEM command should work with multiple PDF files", func(t *testing.T) { + args := []string{ + "sign", "pem", + "--cert", certPath, + "--key", keyPath, + "--out", outputDir, + testPDF12, + testPDF20, + } + + // Run the sign command + output, err := executeCommandWithArgs(t, RootCmd, args) + if err == nil { + t.Log(output) + } + assert.NoError(t, err) + + // Check if both files were created in the output directory with original names + expectedPDF12 := filepath.Join(outputDir, filepath.Base(testPDF12)) + expectedPDF20 := filepath.Join(outputDir, filepath.Base(testPDF20)) + + // Verify files exist + pdf12Exists := true + pdf20Exists := true + + if _, statErr := os.Stat(expectedPDF12); os.IsNotExist(statErr) { + pdf12Exists = false + t.Logf("Warning: Expected output file not found: %s", expectedPDF12) + } + + if _, statErr := os.Stat(expectedPDF20); os.IsNotExist(statErr) { + pdf20Exists = false + t.Logf("Warning: Expected output file not found: %s", expectedPDF20) + } + + // At least one of the files should exist if the command worked + if !pdf12Exists && !pdf20Exists && err == nil { + t.Errorf("Command succeeded but no output files were found") + } + }) +} + +func TestSignPKSC11Command(t *testing.T) { + // Skip the PKCS11 tests if running in a CI environment without proper PKCS11 setup + if os.Getenv("CI") != "" { + t.Skip("Skipping PKCS11 tests in CI environment") + } + + t.Run("SignPKSC11 command should fail without file patterns", func(t *testing.T) { + args := []string{"sign", "pksc11"} + + output, err := executeCommandWithArgs(t, RootCmd, args) + + assert.Error(t, err) + assert.Contains(t, output, "no file patterns provided") + }) +} + +func TestSignBySignerCommand(t *testing.T) { + // Save original config and restore after tests + originalConfig := config + defer func() { config = originalConfig }() + + // Create a temporary output directory + outputDir, err := os.MkdirTemp("", "pdfsigner-test-output-signer") + require.NoError(t, err, "Failed to create temp directory") + defer os.RemoveAll(outputDir) + + t.Run("SignBySigner command should fail without file patterns", func(t *testing.T) { + args := []string{"sign", "signer"} + + output, err := executeCommandWithArgs(t, RootCmd, args) + + assert.Error(t, err) + assert.Contains(t, output, "no file patterns provided") + }) + + t.Run("SignBySigner command should fail with empty signer name", func(t *testing.T) { + args := []string{"sign", "signer", testPDF20} + + output, err := executeCommandWithArgs(t, RootCmd, args) + + assert.Error(t, err) + assert.Contains(t, output, "signer name is empty") + }) + + t.Run("SignBySigner command should fail with non-existent signer", func(t *testing.T) { + args := []string{ + "sign", "signer", + testPDF20, + "--signer", "nonexistent", + } + + output, err := executeCommandWithArgs(t, RootCmd, args) + + assert.Error(t, err) + assert.Contains(t, output, "signer not found") + }) + + t.Run("SignBySigner command should work with proper PEM signer", func(t *testing.T) { + // Set up config with a PEM signer + config = Config{ + Signers: map[string]signerConfig{ + "pemsigner": { + Type: "pem", + Cert: certPath, + Key: keyPath, + }, + }, + } + + args := []string{ + "sign", "signer", + "--signer", "pemsigner", + "--out", outputDir, + testPDF20, + } + + _, err := executeCommandWithArgs(t, RootCmd, args) + assert.NoError(t, err) + }) +} diff --git a/cmd/verify.go b/cmd/verify.go index 254acd8..cb1305f 100644 --- a/cmd/verify.go +++ b/cmd/verify.go @@ -1,10 +1,11 @@ package cmd import ( + "errors" + "fmt" "os" "github.com/digitorus/pdfsign/verify" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -12,26 +13,31 @@ import ( var verifyCmd = &cobra.Command{ Use: "verify", Short: "Verify PDF signature", - Run: func(cmd *cobra.Command, inputFileNames []string) { + RunE: func(cmd *cobra.Command, inputFileNames []string) error { if len(inputFileNames) < 1 { - log.Fatal("no files provided") + return errors.New("no files provided") } for _, f := range inputFileNames { input_file, err := os.Open(f) if err != nil { - log.Fatal("Couldn't open file", f, ",", err) + return fmt.Errorf("couldn't open file %w", err) } defer input_file.Close() - _, err = verify.File(input_file) + response, err := verify.File(input_file) if err != nil { - log.Println("File", f, "Couldn't be verified", err) - } else { - log.Println("File", f, "verified successfully") + return fmt.Errorf("couldn't verify file %w", err) } + if response.Error != "" { + return errors.New(response.Error) + } else { + cmd.Print("File verified successfully") + } } + + return nil }, } diff --git a/cmd/verify_test.go b/cmd/verify_test.go new file mode 100644 index 0000000..a411b87 --- /dev/null +++ b/cmd/verify_test.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVerifyCommand(t *testing.T) { + t.Run("Verify command should verify a signed PDF", func(t *testing.T) { + args := []string{"verify", signedPDF} + + output, err := executeCommandWithArgs(t, RootCmd, args) + t.Log("output:", output) + + assert.NoError(t, err) + assert.Contains(t, output, "verified successfully", output) + }) + + t.Run("Verify command should fail without file arguments", func(t *testing.T) { + args := []string{"verify"} + + output, err := executeCommandWithArgs(t, RootCmd, args) + + assert.Error(t, err) + assert.Contains(t, output, "no files provided") + }) +} diff --git a/cmd/version.go b/cmd/version.go index 380e086..d3a5af1 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -15,8 +15,6 @@ package cmd import ( - "fmt" - "github.com/spf13/cobra" ) @@ -24,14 +22,15 @@ import ( var versionCmd = &cobra.Command{ Use: "version", Short: "Get PDFSigner version", - Run: func(cmd *cobra.Command, args []string) { - // print version - fmt.Println( + RunE: func(cmd *cobra.Command, args []string) error { + cmd.Println( "Version", ver.Version, "BuildDate", ver.BuildDate, "GitCommit", ver.GitCommit, "GitBranch", ver.GitBranch, ) + + return nil }, } diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 0000000..daf1ace --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "testing" + + "github.com/digitorus/pdfsigner/version" + "github.com/stretchr/testify/assert" +) + +func TestVersionCommand(t *testing.T) { + // Save original version and restore after tests + originalVersion := ver + defer func() { ver = originalVersion }() + + // Set test version data + ver = version.Version{ + Version: "1.0.0-test", + BuildDate: "2023-01-01", + GitCommit: "abc123", + GitBranch: "main", + } + + t.Run("Version command should display version information", func(t *testing.T) { + output, err := executeCommand(t, "version") + + assert.NoError(t, err) + assert.Contains(t, output, "Version 1.0.0-test") + assert.Contains(t, output, "BuildDate 2023-01-01") + assert.Contains(t, output, "GitCommit abc123") + assert.Contains(t, output, "GitBranch main") + }) +} diff --git a/cmd/watch.go b/cmd/watch.go index 9dcce8a..36667cd 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -1,11 +1,16 @@ package cmd import ( + "fmt" + "os" + "github.com/digitorus/pdfsigner/files" "github.com/digitorus/pdfsigner/license" "github.com/digitorus/pdfsigner/signer" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // watchCmd represents the watch command. @@ -19,13 +24,7 @@ var watchCmd = &cobra.Command{ var watchPEMCmd = &cobra.Command{ Use: "pem", Short: "Watch and sign with PEM formatted certificate", - Run: func(cmd *cobra.Command, args []string) { - // require license - err := requireLicense() - if err != nil { - log.Fatal(err) - } - + RunE: func(cmd *cobra.Command, args []string) error { // create signer config c := signerConfig{} @@ -33,10 +32,16 @@ var watchPEMCmd = &cobra.Command{ bindSignerFlagsToConfig(cmd, &c) // set sign data - c.SignData.SetPEM(c.CrtPath, c.KeyPath, c.CrtChainPath) + if err := c.SignData.SetPEM(c.Cert, c.Key, c.Chain); err != nil { + return fmt.Errorf("failed to set PEM certificate data: %w", err) + } // start watch - startWatch(c.SignData) + if err := startWatch(c.SignData); err != nil { + return fmt.Errorf("failed to start watch process: %w", err) + } + + return nil }, } @@ -44,13 +49,7 @@ var watchPEMCmd = &cobra.Command{ var watchPKSC11Cmd = &cobra.Command{ Use: "pksc11", Short: "Watch and sign with PSKC11", - Run: func(cmd *cobra.Command, args []string) { - // require license - err := requireLicense() - if err != nil { - log.Fatal(err) - } - + RunE: func(cmd *cobra.Command, args []string) error { // create signer config c := signerConfig{} @@ -58,56 +57,111 @@ var watchPKSC11Cmd = &cobra.Command{ bindSignerFlagsToConfig(cmd, &c) // set sign data - c.SignData.SetPKSC11(c.LibPath, c.Pass, c.CrtChainPath) + err := c.SignData.SetPKSC11(c.Lib, c.Pass, c.Chain) + if err != nil { + return fmt.Errorf("failed to set PKSC11 configuration: %w", err) + } // start watch - startWatch(c.SignData) + if err := startWatch(c.SignData); err != nil { + return fmt.Errorf("failed to start watch process: %w", err) + } + + return nil }, } -// watchBySignerNameCmd wathces folders and signs files using singer from the config with possibility to override it with flags. +// watchBySignerNameCmd watches folders and signs files using singer from the config with possibility to override it with flags. var watchBySignerNameCmd = &cobra.Command{ Use: "signer", Short: "Watch and sign with preconfigured signer", - Run: func(cmd *cobra.Command, args []string) { - // require license - err := requireLicense() + RunE: func(cmd *cobra.Command, args []string) error { + // get signer config from the config file by name + signerName := viper.GetString("signerName") + c, err := getSignerConfigByName(signerName) if err != nil { - log.Fatal(err) + return err } - // get signer config from the config file by name - c := getSignerConfigByName(signerNameFlag) - // bind signer flags to config bindSignerFlagsToConfig(cmd, &c) // set sign data switch c.Type { case "pem": - c.SignData.SetPEM(c.CrtPath, c.KeyPath, c.CrtChainPath) + // set sign data + if err := c.SignData.SetPEM(c.Cert, c.Key, c.Chain); err != nil { + return fmt.Errorf("failed to set PEM certificate data: %w", err) + } case "pksc11": - c.SignData.SetPKSC11(c.LibPath, c.Pass, c.CrtChainPath) + err := c.SignData.SetPKSC11(c.Lib, c.Pass, c.Chain) + if err != nil { + return fmt.Errorf("failed to set PKSC11 configuration: %w", err) + } + default: + return fmt.Errorf("unknown signer type: %s", c.Type) } // start watch - startWatch(c.SignData) + if err := startWatch(c.SignData); err != nil { + return fmt.Errorf("failed to start watch process: %w", err) + } + + return nil }, } // startWatch starts watcher. -func startWatch(signData signer.SignData) { +func startWatch(signData signer.SignData) error { license.LD.AutoSave() - files.Watch(inputPathFlag, func(filePath string, left int) { - signedFilePath := getOutputFilePathByInputFilePath(filePath, outputPathFlag) - if err := signer.SignFile(filePath, signedFilePath, signData, validateSignature); err != nil { + + // Get input and output paths from viper + inputPath := viper.GetString("in") + outputPath := viper.GetString("out") + validateSig := viper.GetBool("validateSignature") + + // Fallback to flag values if not found in viper + if inputPath == "" { + inputPath = inputPathFlag + } + if outputPath == "" { + inputPath = outputPathFlag + } + + // Check if input and output paths exist + if _, err := os.Stat(inputPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("input directory does not exist: %s", inputPath) + } else { + return fmt.Errorf("cannot access input directory %s: %w", inputPath, err) + } + } + + if _, err := os.Stat(outputPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("output directory does not exist: %s", outputPath) + } else { + return fmt.Errorf("cannot access output directory %s: %w", outputPath, err) + } + } + + files.Watch(inputPath, func(filePath string, left int) { + signedFilePath := getOutputFilePathByInputFilePath(filePath, outputPath) + if err := signer.SignFile(filePath, signedFilePath, signData, validateSig); err != nil { log.Errorln(err) } }) + + return nil } func init() { RootCmd.AddCommand(watchCmd) + parseCommonFlags(watchCmd) + parseInputPathFlag(watchCmd) + parseOutputPathFlag(watchCmd) + parsePEMCertificateFlags(watchCmd) + parsePKSC11CertificateFlags(watchCmd) // add PEM sign command and parse related flags watchCmd.AddCommand(watchPEMCmd) @@ -125,8 +179,6 @@ func init() { // add watch command with signer from config and parse related flags watchCmd.AddCommand(watchBySignerNameCmd) - parseConfigFlag(watchBySignerNameCmd) - parseSignerName(watchBySignerNameCmd) parseCommonFlags(watchBySignerNameCmd) parseInputPathFlag(watchBySignerNameCmd) parseOutputPathFlag(watchBySignerNameCmd) diff --git a/cmd/watch_test.go b/cmd/watch_test.go new file mode 100644 index 0000000..e36c8d2 --- /dev/null +++ b/cmd/watch_test.go @@ -0,0 +1,139 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWatchCommand(t *testing.T) { + t.Run("Watch command should show help when no args provided", func(t *testing.T) { + output, err := executeCommand(t, "watch") + + assert.NoError(t, err) + assert.Contains(t, output, "Watch folder for new") + }) +} + +func TestWatchStartWatch(t *testing.T) { + // Create test input and output directories + inputDir := filepath.Join(t.TempDir(), "watch-input") + outputDir := filepath.Join(t.TempDir(), "watch-output") + + err := os.Mkdir(inputDir, 0755) + require.NoError(t, err, "Failed to create input directory") + + err = os.Mkdir(outputDir, 0755) + require.NoError(t, err, "Failed to create output directory") + + t.Run("startWatch should fail if input directory doesn't exist", func(t *testing.T) { + inputDir := "/non/existent/directory" + outputDir := filepath.Join(t.TempDir(), "watch-output") + + err = os.Mkdir(outputDir, 0755) + require.NoError(t, err, "Failed to create output directory") + + args := []string{ + "watch", "pem", + "--in", inputDir, + "--out", outputDir, + } + + // Without certificate and key, should fail + _, err := executeCommandWithArgs(t, RootCmd, args) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "no such file or directory") + }) + + t.Run("startWatch should fail if output directory doesn't exist", func(t *testing.T) { + inputDir := filepath.Join(t.TempDir(), "watch-input") + outputDir := "/non/existent/directory" + + err := os.Mkdir(inputDir, 0755) + require.NoError(t, err, "Failed to create input directory") + + args := []string{ + "watch", "pem", + "--in", inputDir, + "--out", outputDir, + } + + // Without certificate and key, should fail + _, err = executeCommandWithArgs(t, RootCmd, args) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "no such file or directory") + }) +} + +func TestWatchCommands(t *testing.T) { + // Create test input and output directories + inputDir := filepath.Join(t.TempDir(), "watch-input") + outputDir := filepath.Join(t.TempDir(), "watch-output") + + err := os.Mkdir(inputDir, 0755) + require.NoError(t, err, "Failed to create input directory") + + err = os.Mkdir(outputDir, 0755) + require.NoError(t, err, "Failed to create output directory") + + t.Run("Watch PEM command validation", func(t *testing.T) { + args := []string{ + "watch", "pem", + "--in", inputDir, + "--out", outputDir, + } + + // Without certificate and key, should fail + output, err := executeCommandWithArgs(t, RootCmd, args) + + assert.Error(t, err) + assert.Contains(t, output, "failed to set PEM certificate data") + }) + + t.Run("Watch PKSC11 command validation", func(t *testing.T) { + args := []string{ + "watch", "pksc11", + "--in", inputDir, + "--out", outputDir, + } + + // Running without lib would fail in real execution but we'll just test command structure + _, err := executeCommandWithArgs(t, RootCmd, args) + assert.Error(t, err, "PKSC11 command should fail without proper parameters") + }) + + t.Run("Watch signer command validation", func(t *testing.T) { + // Set a test signer in config + originalConfig := config + defer func() { config = originalConfig }() + + config = Config{ + Signers: map[string]signerConfig{ + "test-signer": { + Type: "pem", + Cert: certPath, + Key: keyPath, + }, + }, + } + + args := []string{ + "watch", "signer", + "--in", inputDir, + "--out", outputDir, + "--signerName", "test-signer", + } + + // This should actually try to start the watch process + _, err := executeCommandWithArgs(t, RootCmd, args) + + // The command structure is valid, but actual file watching would be started + // We only care about validating the command structure for now + assert.Error(t, err) + }) +} diff --git a/config.example.yaml b/config.example.yaml index b67400f..34f51a8 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,5 +1,5 @@ # Main configuration -licensePath: ./pdfsigner.lic +licensePath: /etc/pdfsigner/default.lic # Common signature settings (anchor) .signature_defaults: &signature_defaults @@ -8,35 +8,35 @@ licensePath: ./pdfsigner.lic # Common signature info (anchor) .signature_info_defaults: &signature_info_defaults - name: Company Name - location: New York - reason: Document approval - contactInfo: support@example.com + name: Example Organization + location: Amsterdam + reason: Testing the Digitorus PDF Signer + contactInfo: contact@example.com # Services Configuration services: - # watch_incoming: - # type: watch - # signer: company_cert # Reference to the signer configuration below - # in: ./incoming # Where to look for new PDFs - # out: ./signed # Where to put signed PDFs - # validateSignature: true # Verify signature after signing + watch_incoming: + type: watch + signer: default # Reference to the signer configuration below + in: /var/lib/pdfsigner/input # Where to look for new PDFs + out: /var/lib/pdfsigner/output # Where to put signed PDFs + validateSignature: true # Verify signature after signing api_endpoint: type: serve signers: - - company_cert # List of allowed signers - addr: 127.0.0.1 # Listen address - port: 3000 # Listen port + - default # List of allowed signers + addr: localhost # Listen address + port: 8080 # Listen port validateSignature: true # Signers Configuration signers: - company_cert: + default: type: pem - crtPath: ./testfiles/test.crt - keyPath: ./testfiles/test.pem - crtChainPath: ./testfiles/test.crt + crtPath: /etc/pdfsigner/default.crt + keyPath: /etc/pdfsigner/default.pem + crtChainPath: /etc/pdfsigner/chain.pem signData: signature: <<: *signature_defaults # Reuse common signature settings diff --git a/db/bolt.go b/db/bolt.go index ff581ad..8aa39ac 100644 --- a/db/bolt.go +++ b/db/bolt.go @@ -69,7 +69,7 @@ func SaveByKey(key string, value []byte) error { return err }) if err != nil { - return errors.Wrap(err, "update by key") + return fmt.Errorf("update by key: %w", err) } return nil @@ -92,7 +92,7 @@ func LoadByKey(key string) ([]byte, error) { return nil }) if err != nil { - return result, errors.Wrap(err, "view by key") + return result, fmt.Errorf("view by key: %w", err) } return result, nil diff --git a/docs/command-line-signer.md b/docs/command-line-signer.md index b7521d4..e445d3d 100644 --- a/docs/command-line-signer.md +++ b/docs/command-line-signer.md @@ -13,7 +13,7 @@ specific flags: ```sh --key string # Private key path ---crt string # Certificate path +--cert string # Certificate path ``` @@ -21,7 +21,7 @@ specific flags: ```sh pdfsigner sign pem \ - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --chain path/to/certificate/chain \ --contact "Contact information" \ @@ -78,13 +78,13 @@ pdfsigner sign pksc11 \ ```sh --config string # Path to config file ---signer-name string # Signer name +--signer string # Signer name ``` ### Example ```sh -pdfsigner sign signer --config path/to/config/file --signer-name signerNameFromTheConfig path/to/file.pdf +pdfsigner sign signer --config path/to/config/file --signer signerNameFromTheConfig path/to/file.pdf ``` specific flags: @@ -92,12 +92,13 @@ specific flags: Preconfigured signer settings could be overwritten with flags: ```sh -pdfsigner sign signer --config path/to/config/file --signer-name "name-of-the-signer" \ - --crt path/to/certificate \ +pdfsigner sign signer \ + --config path/to/config/file \ + --signer-name "name-of-the-signer" \ --key path/to/private/key \ --lib path/to/pksc11/lib \ --pass "pksc11-password" \ - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --chain path/to/certificate/chain \ --contact "Contact information" \ @@ -119,8 +120,8 @@ Depending on the type of the signer appropriate flags should be used: ```sh --key string # Private key path ---crt string # Certificate path - +--cert string # Certificate path +--chain string # Certificate chain path ``` **PKSC11** @@ -128,4 +129,5 @@ Depending on the type of the signer appropriate flags should be used: ```sh --lib string # Path to PKCS11 library --pass string # PKCS11 password +--chain string # Certificate chain path ``` diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..c492118 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,14 @@ +# Development Guide + +## License System + +The PDFSigner licensing system provides secure license management with these key components: + +- **License**: Contains user information, rate limits, and expiration date +- **Public Key**: Used to validate license authenticity +- **HMAC Key**: Used to secure the storage of license limits + +### License Generation + +For license generation, use the command-line tool: + diff --git a/docs/watch-and-sign.md b/docs/watch-and-sign.md index 49484b5..7adf8ed 100644 --- a/docs/watch-and-sign.md +++ b/docs/watch-and-sign.md @@ -21,7 +21,7 @@ PEM specific flags: ```sh --key string # Private key path ---crt string # Certificate path +--cert string # Certificate path ``` @@ -29,7 +29,7 @@ PEM specific flags: ```sh pdfsigner watch --in path/to/folder/to/watch --out path/to/folder/with/signed/files pem \ - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --chain path/to/certificate/chain \ --contact "Contact information" \ @@ -65,7 +65,7 @@ pdfsigner watch pksc11 \ --out path/to/folder/with/signed/files \ --lib path/to/pksc11/lib \ --pass "pksc11-password" \ - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --chain path/to/certificate/chain \ --contact "Contact information" \ @@ -108,11 +108,11 @@ Preconfigured signer settings could be overwritten with flags: pdfsigner watch signer \ --config path/to/config/file \ --signer-name "name-of-the-signer" \ - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --lib path/to/pksc11/lib \ --pass "pksc11-password" \ - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --chain path/to/certificate/chain \ --contact "Contact information" \ diff --git a/docs/web-api.md b/docs/web-api.md index d8a6cc6..c30848d 100644 --- a/docs/web-api.md +++ b/docs/web-api.md @@ -168,7 +168,7 @@ PEM specific flags: ``` --key string Private key path ---crt string Certificate path +--cert string Certificate path ``` #### Example @@ -177,7 +177,7 @@ PEM specific flags: pdfsigner serve pem \ --serve-address "127.0.0.1"\ --serve-port "8080"\ - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --chain path/to/certificate/chain \ --contact "Contact information" \ @@ -214,7 +214,7 @@ pdfsigner serve pksc11 \ --serve-port "8080"\ --lib path/to/pksc11/lib \ --pass "pksc11-password" \ - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --chain path/to/certificate/chain \ --contact "Contact information" \ @@ -258,11 +258,11 @@ pdfsigner serve signer --signer-name "name-of-the-signer" \ --signer-name signerNameFromTheConfig --serve-address "127.0.0.1"\ --serve-port "8080" - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --lib path/to/pksc11/lib \ --pass "pksc11-password" \ - --crt path/to/certificate \ + --cert path/to/certificate \ --key path/to/private/key \ --chain path/to/certificate/chain \ --contact "Contact information" \ diff --git a/files/file.go b/files/file.go index 7a8841a..7e5a7f3 100644 --- a/files/file.go +++ b/files/file.go @@ -1,29 +1,16 @@ package files import ( + "fmt" "path" "path/filepath" "strings" "github.com/digitorus/pdfsigner/license" "github.com/digitorus/pdfsigner/signer" - log "github.com/sirupsen/logrus" + "github.com/pkg/errors" ) -// func storeTempFile(file io.Reader) (string, error) { -// // TODO: Should we encrypt temporary files? -// tmpFile, err := os.CreateTemp("", "pdf") -// if err != nil { -// return "", err -// } - -// _, err = io.Copy(tmpFile, file) -// if err != nil { -// return "", err -// } -// return tmpFile.Name(), nil -// } - // findFilesByPatterns finds all files matched the patterns. func findFilesByPatterns(patterns []string) (matchedFiles []string, err error) { for _, f := range patterns { @@ -38,30 +25,43 @@ func findFilesByPatterns(patterns []string) (matchedFiles []string, err error) { return matchedFiles, err } -// SignFilesByPatterns signs files by matched patterns and stores it inside the same folder with _signed.pdf suffix. -func SignFilesByPatterns(filePatterns []string, signData signer.SignData, validateSignature bool) { +// SignFilesByPatterns signs files by matched patterns and stores them in the specified output directory +// or with _signed.pdf suffix in the same directory if no output directory is provided. +func SignFilesByPatterns(filePatterns []string, signData signer.SignData, validateSignature bool, outputDirectory string) error { // get files files, err := findFilesByPatterns(filePatterns) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to find files by patterns: %w", err) } for _, f := range files { - // generate signed file path + // get file name and extension dir, fileName := path.Split(f) fileNameArr := strings.Split(fileName, path.Ext(fileName)) fileNameArr = fileNameArr[:len(fileNameArr)-1] fileNameNoExt := strings.Join(fileNameArr, "") - signedFilePath := path.Join(dir, fileNameNoExt+"_signed"+path.Ext(fileName)) + ext := path.Ext(fileName) + + // generate signed file path based on output directory or original location + var signedFilePath string + if outputDirectory != "" { + // When output directory is specified, use original filename without "_signed" suffix + signedFilePath = path.Join(outputDirectory, fileName) + } else { + // When no output directory, append "_signed" suffix (original behavior) + signedFilePath = path.Join(dir, fileNameNoExt+"_signed"+ext) + } // sign file if err := signer.SignFile(f, signedFilePath, signData, validateSignature); err != nil { - log.Fatal(err) + return errors.Wrap(err, "failed to sign file: "+fileName) } } err = license.LD.SaveLimitState() if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to save license limit state: %w", err) } + + return nil } diff --git a/go.mod b/go.mod index 63b7c8e..67f9207 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.3 require ( github.com/denisbrodbeck/machineid v1.0.1 github.com/digitorus/pdf v0.1.2 - github.com/digitorus/pdfsign v0.0.0-20250226084642-540ffbbec869 + github.com/digitorus/pdfsign v0.0.0-20250310195202-94790aeb1f48 github.com/digitorus/pkcs11 v0.0.0-20231109204637-6ee79d00536b github.com/fsnotify/fsnotify v1.8.0 github.com/go-test/deep v1.0.8 @@ -44,12 +44,10 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.35.0 // indirect - golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/digitorus/pkcs11 v0.0.0-20220705083045-3847d33b47af => ../pkcs11 diff --git a/go.sum b/go.sum index 72781d5..d30ac6f 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMS github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/digitorus/pdf v0.1.2 h1:RjYEJNbiV6Kcn8QzRi6pwHuOaSieUUrg4EZo4b7KuIQ= github.com/digitorus/pdf v0.1.2/go.mod h1:05fDDJhPswBRM7GTfqCxNiDyeNcN0f+IobfOAl5pdXw= -github.com/digitorus/pdfsign v0.0.0-20250226084642-540ffbbec869 h1:3CtGdxJnOis/m/hHh7bxEi+5Sw7EyZNKFrHcPPWIwVY= -github.com/digitorus/pdfsign v0.0.0-20250226084642-540ffbbec869/go.mod h1:EIX0ukk/f4C0ZdDYSB4YFGPSL/JfHy6JBwTxDYaMrVY= +github.com/digitorus/pdfsign v0.0.0-20250310195202-94790aeb1f48 h1:3bMNNen62awUCZJQVG0S0FXyy7unaos3gcJeYOekZww= +github.com/digitorus/pdfsign v0.0.0-20250310195202-94790aeb1f48/go.mod h1:EIX0ukk/f4C0ZdDYSB4YFGPSL/JfHy6JBwTxDYaMrVY= github.com/digitorus/pkcs11 v0.0.0-20231109204637-6ee79d00536b h1:vXN43Cuzh9pBsv1kfRc9F1iZr6r1mHHP0+RfCO7Pd1o= github.com/digitorus/pkcs11 v0.0.0-20231109204637-6ee79d00536b/go.mod h1:YezSpSbAWicIsxlVGESFb3dMP6EFrL7Z3H4caGA6DhY= github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= @@ -96,19 +96,19 @@ go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/license/generator/generator.go b/license/generator/generator.go new file mode 100644 index 0000000..cdd38c9 --- /dev/null +++ b/license/generator/generator.go @@ -0,0 +1,140 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + "time" + + "github.com/digitorus/pdfsigner/license" + "github.com/digitorus/pdfsigner/license/ratelimiter" + log "github.com/sirupsen/logrus" +) + +type LimitFlag struct { + limits []*ratelimiter.Limit +} + +func (lf *LimitFlag) String() string { + result := []string{} + for _, l := range lf.limits { + result = append(result, fmt.Sprintf("%d:%s", l.MaxCount, l.IntervalStr)) + } + return strings.Join(result, ",") +} + +func (lf *LimitFlag) Set(value string) error { + parts := strings.Split(value, ":") + if len(parts) != 2 { + return fmt.Errorf("limit must be in format 'count:duration', got %s", value) + } + + var maxCount int + if _, err := fmt.Sscanf(parts[0], "%d", &maxCount); err != nil { + return fmt.Errorf("invalid count in limit: %s", err) + } + + intervalStr := parts[1] + + lf.limits = append(lf.limits, &ratelimiter.Limit{ + MaxCount: maxCount, + IntervalStr: intervalStr, + }) + + return nil +} + +func main() { + var ( + genNewKey = flag.Bool("new-key", false, "Generate a new key pair (private and public)") + genHMACKey = flag.Bool("new-hmac", false, "Generate a new HMAC key") + name = flag.String("name", "", "License name") + email = flag.String("email", "", "License email") + duration = flag.Duration("duration", 24*365*time.Hour, "License duration") + maxWatchers = flag.Int("max-watchers", 1, "Maximum number of directory watchers") + privateKeyEnvVar = flag.String("key-env", "PDFSIGNER_LICENSE_PRIVATE_KEY", "Environment variable containing the private key") + ) + + limitFlag := LimitFlag{} + flag.Var(&limitFlag, "limit", "Rate limit in format 'count:duration' (e.g., '100:1m'). Can be specified multiple times.") + + flag.Parse() + + // Generate a new key if requested + if *genNewKey { + privateKey, publicKey, err := license.GenerateKeyPair() + if err != nil { + log.Fatalf("Failed to generate key pair: %v", err) + } + fmt.Println("Private Key:", privateKey) + fmt.Println("Public Key:", publicKey) + fmt.Println("\nIMPORTANT: Keep the private key secure and use the public key for license validation.") + + return + } + + // Generate a new HMAC key if requested + if *genHMACKey { + hmacKeyStr, err := license.GenerateRandomHMACKey() + if err != nil { + log.Fatalf("Failed to generate HMAC key: %v", err) + } + + fmt.Println("HMAC Key:", hmacKeyStr) + + return + } + + // Get private key from environment variable + privateKeyStr := os.Getenv(*privateKeyEnvVar) + if privateKeyStr == "" { + log.Fatalf("Environment variable %s not set or empty", *privateKeyEnvVar) + } + + // Validate required arguments for license generation + if *name == "" || *email == "" { + log.Fatal("Name and email are required for license generation") + } + + // Extract public key from private key + publicKey, err := license.GetPublicKeyFromPrivate(privateKeyStr) + if err != nil { + log.Fatalf("Failed to extract public key: %v", err) + } + + // Set default limits if none provided + limits := limitFlag.limits + if len(limits) == 0 { + limits = []*ratelimiter.Limit{ + {MaxCount: 60, IntervalStr: "1h"}, + } + } + + // Create license data struct + licenseData := license.LicenseData{ + Name: *name, + Email: *email, + End: time.Now().Add(*duration), + Limits: limits, + MaxDirectoryWatchers: *maxWatchers, + } + + // Generate license + licenseStr, err := license.GenerateLicense(privateKeyStr, licenseData) + if err != nil { + log.Fatalf("Failed to generate license: %v", err) + } + + fmt.Println("License Data:", licenseStr) + fmt.Printf("Licensed to %s <%s> until %s\n", + licenseData.Name, licenseData.Email, licenseData.End.Format("2006-01-02")) + fmt.Printf("Max directory watchers: %d\n", licenseData.MaxDirectoryWatchers) + fmt.Println("Public Key:", publicKey) + + // Print limits + fmt.Println("Limits:") + for _, limit := range licenseData.Limits { + fmt.Printf(" %d per %s\n", limit.MaxCount, limit.IntervalStr) + } +} diff --git a/license/generator/main.go b/license/generator/main.go deleted file mode 100644 index 3fe7975..0000000 --- a/license/generator/main.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/digitorus/pdfsigner/license" - "github.com/digitorus/pdfsigner/license/ratelimiter" - "github.com/hyperboloide/lk" - log "github.com/sirupsen/logrus" -) - -func main() { - const privateKeyB64 = "KP+BAwEBC3BrQ29udGFpbmVyAf+CAAECAQNQdWIBCgABAUQB/4QAAAAK/4MFAQL/hgAAAP+Z/4IBYQQIH/7ItGy07UvY4MVC11nA21c9td4wn7N73Pz/nHF3CbkuOHMxJMxR9EUUPkQzwLEuXN8iQolO3vO9a3507Wr5cENSYQhAQgK/ZpMlo75uG0yPflKWg+KsOM39Etg/SFoBMQIkcq2v8M/xQF03dTg0aVXHB532/4gQ454IG4fcUOBohrYAA3t1o26+X1Ceh7rmavgA" - - // create a new Private key: - // privateKey, err := lk.NewPrivateKey() - privateKey, err := lk.PrivateKeyFromB64String(privateKeyB64) - if err != nil { - log.Fatal(err) - } - - privateKeyStr, err := privateKey.ToB64String() - if err != nil { - log.Fatal(err) - } - - log.Println("Private key", privateKeyStr) - - // create a license document: - - doc := license.LicenseData{ - Name: "Name", - Email: "test@example.com", - End: time.Now().Add(time.Hour * 24 * 365 * 100), // +/- 100 year - Limits: []*ratelimiter.Limit{ - {MaxCount: 2, IntervalStr: "1s"}, - {MaxCount: 10, IntervalStr: "10s"}, - {MaxCount: 100, IntervalStr: "1m"}, - {MaxCount: 2000, IntervalStr: "1h"}, - {MaxCount: 200000, IntervalStr: "24h"}, - {MaxCount: 2000000, IntervalStr: "720h"}, - {MaxCount: 20000000, IntervalStr: license.TotalLimitDuration}, // Total, //Total - }, - MaxDirectoryWatchers: 2, - } - - // marshall the document to json bytes: - docBytes, err := json.Marshal(doc) - if err != nil { - log.Fatal(err) - } - - log.Println(string(docBytes)) - - // generate your license with the private key and the document: - lic, err := lk.NewLicense(privateKey, docBytes) - if err != nil { - log.Fatal(err) - } - - // encode the new license to b32, this is what you give to your customer. - str32, err := lic.ToB64String() - if err != nil { - log.Fatal(err) - } - - log.Println("License Data:", str32) - - // get the public key. The public key should be hardcoded in your app to check licences. - // Do not distribute the private key! - publicKey := privateKey.GetPublicKey() - log.Println("Public key:", publicKey.ToB64String()) - - // validate the license: - if ok, err := lic.Verify(publicKey); err != nil { - log.Fatal(err) - } else if !ok { - log.Fatal("Invalid license signature") - } - - // unmarshal the document and check the end date: - res := license.LicenseData{} - if err := json.Unmarshal(lic.Data, &res); err != nil { - log.Fatal(err) - } else if res.End.Before(time.Now()) { - log.Fatalf("LicenseData expired on: %s", res.End.String()) - } else { - fmt.Printf(`Licensed to %s until %s \n, with limits: %v`, res.Email, res.End.Format("2006-01-02"), res.Limits) - } -} diff --git a/license/license.go b/license/license.go index 0e353b9..56fadf1 100644 --- a/license/license.go +++ b/license/license.go @@ -1,8 +1,11 @@ package license import ( + "crypto/rand" + "encoding/base64" "encoding/json" "fmt" + "os" "time" "github.com/denisbrodbeck/machineid" @@ -14,18 +17,37 @@ import ( log "github.com/sirupsen/logrus" ) -// TestLicense used in unit tests. -const TestLicense = "LP+HAwEBB0xpY2Vuc2UB/4gAAQMBBERhdGEBCgABAVIB/4QAAQFTAf+EAAAACv+DBQEC/4YAAAD+AV3/iAH/8XsibiI6Ik5hbWUiLCJlIjoidGVzdEBleGFtcGxlLmNvbSIsImVuZCI6IjIxMjItMDYtMTFUMTM6Mzc6MDUuODg0OTIxMyswMjowMCIsImwiOlt7Im0iOjIsImkiOiIxcyJ9LHsibSI6MTAsImkiOiIxMHMifSx7Im0iOjEwMCwiaSI6IjFtIn0seyJtIjoyMDAwLCJpIjoiMWgifSx7Im0iOjIwMDAwMCwiaSI6IjI0aCJ9LHsibSI6MjAwMDAwMCwiaSI6IjcyMGgifSx7Im0iOjIwMDAwMDAwLCJpIjoiODY0MDAwaCJ9XSwiZCI6Mn0BMQIOpEnubsOkG6SGq8IjqBAtv7uFwY0aZJDLd4+JMA3DZWxQyg5OAavJ8AFQ3nPyORMBMQKsLzLxRDHhFf2wQG5gyaBpuSkIV1okdw06pg3cAAD0pcjaDQNj/+E9VQGc5I3QNckA" +// Environment variable names +const ( + EnvPublicKey = "PDFSIGNER_LICENSE_PUBLIC_KEY" + EnvLicense = "PDFSIGNER_LICENSE" + appID = "PDFSigner_" +) -// HMACKeyForLimitsEncryption. -const HMACKeyForLimitsEncryption = "HMACKeyForLimitsEncryption" +// These variables can be set at build time using -ldflags +// Example: go build -ldflags "-X github.com/digitorus/pdfsigner/license.publicKeyBase64=PUBLICKEY" +// Example: go build -ldflags "-X github.com/digitorus/pdfsigner/license.licenseBase64=LICENSE" +// Example: go build -ldflags "-X github.com/digitorus/pdfsigner/license.hmacKey=HMACKEY" +var ( + // publicKeyBase64 is the public key used to verify licenses + publicKeyBase64 string -// ErrOverLimit contains error for over limit. -var ErrOverLimit = errors.New("limit is over") + // licenseBase64 is the license that can be hardcoded into the binary + licenseBase64 string + + // hmacKey is used for encryption of license limits + hmacKey string +) // 864000h is equal to 100 years. var TotalLimitDuration = "864000h" +// ErrOverLimit contains error for over limit. +var ErrOverLimit = errors.New("exceeded license") + +// ErrMissingRequiredValue indicates a required value is missing +var ErrMissingRequiredValue = errors.New("missing required value") + // LD stores all the license related data. var LD LicenseData @@ -50,15 +72,37 @@ type LicenseData struct { lastState []ratelimiter.LimitState } -// the public key b64 encoded from the private key using: lkgen pub my_private_key_file`. -const ( - PublicKeyBase64 = "BAgf/si0bLTtS9jgxULXWcDbVz213jCfs3vc/P+ccXcJuS44czEkzFH0RRQ+RDPAsS5c3yJCiU7e871rfnTtavlwQ1JhCEBCAr9mkyWjvm4bTI9+UpaD4qw4zf0S2D9IWg==" - appNameMachineID = "PDFSigner_unique_key_" -) +// getLicenseBytesFromEnv attempts to read license bytes from environment variable +// License should always come from environment to allow for license changes without rebuilding +func getLicenseBytesFromEnv() ([]byte, bool) { + if license := os.Getenv(EnvLicense); license != "" { + return []byte(license), true + } + return nil, false +} // Initialize extracts license from the bytes provided to LD variable and stores it inside the db. +// If licenseBytes is empty, tries to read from environment variable func Initialize(licenseBytes []byte) error { - log.Info("Initializing license...") + log.Debug("Initializing license...") + + // Check if license bytes should be loaded from environment + if len(licenseBytes) == 0 { + if envBytes, exists := getLicenseBytesFromEnv(); exists { + licenseBytes = envBytes + log.Debug("Using license from environment variable") + } else if licenseBase64 != "" { + licenseBytes = []byte(licenseBase64) + log.Debug("Using license provided at build") + } else { + return errors.Wrap(ErrMissingRequiredValue, + fmt.Sprintf("license must be provided via %s environment variable", EnvLicense)) + } + } + + if len(licenseBytes) == 0 { + return errors.New("no license configured") + } // load license data ld, err := newExtractLicense(licenseBytes) @@ -95,12 +139,12 @@ func Initialize(licenseBytes []byte) error { // Load loads the license from the db and extracts it to LD variable. func Load() error { - log.Info("Loading license from the DB...") + log.Debug("Loading license from the DB...") // load license from the db license, err := db.LoadByKey("license") if err != nil { - return errors.Wrap(err, "couldn't load license from the db") + return fmt.Errorf("couldn't load license from the db: %w", err) } // check machine id @@ -112,13 +156,13 @@ func Load() error { // load license data ld, err := newExtractLicense(license) if err != nil { - return errors.Wrap(err, "couldn't extract license") + return fmt.Errorf("couldn't extract license: %w", err) } // load limit state from the db err = ld.loadLimitState() if err != nil { - return errors.Wrap(err, "couldn't load license limits") + return fmt.Errorf("couldn't load license limits: %w", err) } // initialize rate limiter @@ -131,13 +175,14 @@ func Load() error { } func newExtractLicense(licenseB64 []byte) (LicenseData, error) { - log.Info("Extracting license...") + log.Debug("Extracting license...") ld := LicenseData{} + // Unmarshal the public key. - publicKey, err := lk.PublicKeyFromB64String(PublicKeyBase64) + publicKey, err := lk.PublicKeyFromB64String(publicKeyBase64) if err != nil { - return ld, errors.Wrap(err, "") + return ld, fmt.Errorf("invalid public key format: %w", err) } // Unmarshal the customer license. @@ -151,7 +196,6 @@ func newExtractLicense(licenseB64 []byte) (LicenseData, error) { return ld, errors.Wrap(err, "") } else if !ok { err = errors.New("Invalid license signature") - return ld, errors.Wrap(err, "") } @@ -189,7 +233,7 @@ func newExtractLicense(licenseB64 []byte) (LicenseData, error) { // set byte versions of the public key publicKeyBytes := publicKey.ToBytes() licenseBytes = append(licenseBytes, publicKeyBytes...) - hash := cryptopasta.Hash(HMACKeyForLimitsEncryption, licenseBytes) + hash := cryptopasta.Hash(hmacKey, licenseBytes) copy(ld.cryptoKey[:], hash[:32]) return ld, nil @@ -303,7 +347,7 @@ func (ld *LicenseData) Wait() error { } // log sleep time information - log.Println(ErrOverLimit, "wait for:", limit.Left()) + log.Printf("%s (%d signatures per %s), wait for %s,", ErrOverLimit, limit.MaxCount, limit.IntervalStr, limit.Left()) // sleep time.Sleep(limit.Left()) @@ -349,7 +393,7 @@ func (ld *LicenseData) Info() string { func saveMachineID() error { // load machine id - machineID, err := machineid.ProtectedID(appNameMachineID) + machineID, err := machineid.ProtectedID(appID) if err != nil { log.Fatal(err) } @@ -357,26 +401,26 @@ func saveMachineID() error { // save machine id err = db.SaveByKey("license_machineid", []byte(machineID)) if err != nil { - return errors.Wrap(err, "couldn't save host info") + return fmt.Errorf("couldn't save host info: %w", err) } return nil } -// laod and check machine id. +// check machine id. func checkMachineID() error { // load machine id from the db savedMachineID, err := db.LoadByKey("license_machineid") if err != nil { - return errors.Wrap(err, "couldn't load host info from the db") + return fmt.Errorf("couldn't load host info from the db: %w", err) } savedMachineIDStr := string(savedMachineID) // get current machine id - machineID, err := machineid.ProtectedID(appNameMachineID) + machineID, err := machineid.ProtectedID(appID) if err != nil { - return errors.Wrap(err, "couldn't get host info") + return fmt.Errorf("couldn't get host info: %w", err) } // check that ids are not nil @@ -396,3 +440,71 @@ func checkMachineID() error { func isTotalLimit(limit *ratelimiter.Limit) bool { return limit.IntervalStr == TotalLimitDuration } + +// GenerateKeyPair creates a new public/private key pair for license generation +func GenerateKeyPair() (privateKeyStr, publicKeyStr string, err error) { + privateKey, err := lk.NewPrivateKey() + if err != nil { + return "", "", fmt.Errorf("failed to generate private key: %w", err) + } + + privateKeyStr, err = privateKey.ToB64String() + if err != nil { + return "", "", fmt.Errorf("failed to encode private key: %w", err) + } + + publicKey := privateKey.GetPublicKey() + publicKeyStr = publicKey.ToB64String() + + return privateKeyStr, publicKeyStr, nil +} + +// GenerateLicense creates a license using the provided private key and license data +func GenerateLicense(privateKeyStr string, licenseData interface{}) (string, error) { + // Parse private key + privateKey, err := lk.PrivateKeyFromB64String(privateKeyStr) + if err != nil { + return "", fmt.Errorf("failed to parse private key: %w", err) + } + + // Marshal the license data to JSON + docBytes, err := json.Marshal(licenseData) + if err != nil { + return "", fmt.Errorf("failed to marshal license data: %w", err) + } + + // Generate license + lic, err := lk.NewLicense(privateKey, docBytes) + if err != nil { + return "", fmt.Errorf("failed to generate license: %w", err) + } + + // Encode license to base64 + licStr, err := lic.ToB64String() + if err != nil { + return "", fmt.Errorf("failed to encode license: %w", err) + } + + return licStr, nil +} + +// GenerateRandomHMACKey creates a random HMAC key +func GenerateRandomHMACKey() (string, error) { + randomBytes := make([]byte, 32) + _, err := rand.Read(randomBytes) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(randomBytes)[:32], nil +} + +// GetPublicKeyFromPrivate extracts the public key from a private key +func GetPublicKeyFromPrivate(privateKeyStr string) (string, error) { + privateKey, err := lk.PrivateKeyFromB64String(privateKeyStr) + if err != nil { + return "", fmt.Errorf("invalid private key: %w", err) + } + + publicKey := privateKey.GetPublicKey() + return publicKey.ToB64String(), nil +} diff --git a/license/license_test.go b/license/license_test.go index a3090a3..eb1d8d2 100644 --- a/license/license_test.go +++ b/license/license_test.go @@ -13,7 +13,7 @@ var licData = LicenseData{ Name: "Name", Email: "test@example.com", Limits: []*ratelimiter.Limit{ - {MaxCount: 2, IntervalStr: "1s", Interval: 1 * time.Second}, + {MaxCount: 30, IntervalStr: "1s", Interval: 1 * time.Second}, {MaxCount: 10, IntervalStr: "10s", Interval: 10 * time.Second}, {MaxCount: 100, IntervalStr: "1m", Interval: 1 * time.Minute}, {MaxCount: 2000, IntervalStr: "1h", Interval: 1 * time.Hour}, @@ -25,11 +25,7 @@ var licData = LicenseData{ } func TestFlow(t *testing.T) { - err := Initialize([]byte(TestLicense)) - if err != nil { - t.Fatal(err) - } - + // Continue with the existing test... assert.Equal(t, len(licData.Limits), len(LD.Limits)) assert.Empty(t, deep.Equal(licData.Limits, LD.Limits)) assert.Equal(t, licData.Limits[0].MaxCount, LD.Limits[0].MaxCount) @@ -37,27 +33,30 @@ func TestFlow(t *testing.T) { assert.Equal(t, licData.MaxDirectoryWatchers, LD.MaxDirectoryWatchers) // test load - err = Load() + err := Load() assert.NoError(t, err) allow, _ := LD.RL.Allow() assert.True(t, allow) - assert.Equal(t, 1, LD.Limits[0].CurCount) + assert.Equal(t, 29, LD.Limits[0].CurCount) allow, _ = LD.RL.Allow() assert.True(t, allow) - assert.Equal(t, 0, LD.Limits[0].CurCount) + assert.Equal(t, 28, LD.Limits[0].CurCount) time.Sleep(1 * time.Second) allow, _ = LD.RL.Allow() assert.True(t, allow) - assert.Equal(t, 1, LD.Limits[0].CurCount) + assert.Equal(t, 29, LD.Limits[0].CurCount) allow, _ = LD.RL.Allow() assert.True(t, allow) - assert.Equal(t, 0, LD.Limits[0].CurCount) + assert.Equal(t, 28, LD.Limits[0].CurCount) assert.Equal(t, 6, LD.Limits[1].CurCount) + for i := 0; i < LD.Limits[0].CurCount; i++ { + _, _ = LD.RL.Allow() + } allow, limit := LD.RL.Allow() assert.False(t, allow) assert.Positive(t, limit.Left()) @@ -69,6 +68,6 @@ func TestFlow(t *testing.T) { LD = LicenseData{} err = Load() assert.NoError(t, err) - assert.Equal(t, 0, LD.Limits[0].CurCount) - assert.Equal(t, 6, LD.Limits[1].CurCount) + assert.Equal(t, 13, LD.Limits[0].CurCount) + assert.Equal(t, 0, LD.Limits[1].CurCount) } diff --git a/license/license_testing.go b/license/license_testing.go new file mode 100644 index 0000000..001b442 --- /dev/null +++ b/license/license_testing.go @@ -0,0 +1,53 @@ +package license + +import ( + "testing" + "time" + + "github.com/digitorus/pdfsigner/license/ratelimiter" + log "github.com/sirupsen/logrus" +) + +func init() { + if !testing.Testing() { + // we are not in a test at the moment + return + } else if publicKeyBase64 != "" { + // the license has been configured already + return + } + + // Automatically create a test license for testing purposes + privateKey, publicKey, err := GenerateKeyPair() + if err != nil { + log.Fatal(err) + } + + publicKeyBase64 = publicKey + hmacKey = "PDFSIGNER-TESTING" + + // Generate a test license + testLicense, err := GenerateLicense(privateKey, LicenseData{ + Name: "Name", + Email: "test@example.com", + End: time.Now().Add(30 * time.Minute), + Limits: []*ratelimiter.Limit{ + {MaxCount: 30, IntervalStr: "1s", Interval: 1 * time.Second}, + {MaxCount: 10, IntervalStr: "10s", Interval: 10 * time.Second}, + {MaxCount: 100, IntervalStr: "1m", Interval: 1 * time.Minute}, + {MaxCount: 2000, IntervalStr: "1h", Interval: 1 * time.Hour}, + {MaxCount: 200000, IntervalStr: "24h", Interval: 24 * time.Hour}, + {MaxCount: 2000000, IntervalStr: "720h", Interval: 720 * time.Hour}, + {MaxCount: 20000000, IntervalStr: TotalLimitDuration, Interval: 864000 * time.Hour}, // Total + }, + MaxDirectoryWatchers: 2, + }) + if err != nil { + log.Fatal(err) + } + + err = Initialize([]byte(testLicense)) + if err != nil { + log.Fatal(err) + } +} diff --git a/queues/queue/queue.go b/queues/queue/queue.go index 8859c89..591a9d6 100644 --- a/queues/queue/queue.go +++ b/queues/queue/queue.go @@ -474,7 +474,7 @@ func verifyTask(task Task) (resp *verify.Response, err error) { resp, err = verify.File(inputFile) if err != nil { - return resp, errors.Wrap(err, "verify task") + return resp, fmt.Errorf("verify task: %w", err) } return resp, nil @@ -527,7 +527,7 @@ func (q *Queue) LoadFromDB() error { dbJobs, err := db.BatchLoad(dbJobPrefix) if err != nil { - return errors.Wrap(err, "loading jobs from the db") + return fmt.Errorf("loading jobs from the db: %w", err) } // load jobs and tasks @@ -537,7 +537,7 @@ func (q *Queue) LoadFromDB() error { err := json.Unmarshal(dbJob, &job) if err != nil { - return errors.Wrap(err, "unmarshal job") + return fmt.Errorf("unmarshal job: %w", err) } q.mu.Lock() diff --git a/queues/queue/queue_test.go b/queues/queue/queue_test.go index 8cd6410..833ec11 100644 --- a/queues/queue/queue_test.go +++ b/queues/queue/queue_test.go @@ -5,9 +5,9 @@ import ( "testing" "github.com/digitorus/pdfsign/sign" - "github.com/digitorus/pdfsigner/license" "github.com/digitorus/pdfsigner/queues/priority_queue" "github.com/digitorus/pdfsigner/signer" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) @@ -15,11 +15,6 @@ import ( func TestQSignersMap(t *testing.T) { logrus.SetOutput(io.Discard) - err := license.Initialize([]byte(license.TestLicense)) - if err != nil { - t.Fatal(err) - } - // create sign data d := signer.SignData{ Signature: sign.SignDataSignature{ @@ -33,7 +28,7 @@ func TestQSignersMap(t *testing.T) { DocMDPPerm: sign.AllowFillingExistingFormFieldsAndSignaturesPerms, }, } - d.SetPEM("../../testfiles/test.crt", "../../testfiles/test.pem", "") + _ = d.SetPEM("../../testfiles/test.crt", "../../testfiles/test.pem", "") // create Queue qs := NewQueue() diff --git a/signer/pkcs11.go b/signer/pkcs11.go index 2d707e9..9887f3e 100644 --- a/signer/pkcs11.go +++ b/signer/pkcs11.go @@ -4,37 +4,41 @@ package signer import ( + "errors" + "fmt" + "github.com/digitorus/pkcs11" - log "github.com/sirupsen/logrus" ) // SetPKSC11 sets specific to PKSC11 settings. -func (s *SignData) SetPKSC11(libPath, pass, crtChainPath string) { +func (s *SignData) SetPKSC11(libPath, pass, crtChainPath string) error { // pkcs11 key lib, err := pkcs11.FindLib(libPath) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to find PKCS11 library: %w", err) } // Load Library ctx := pkcs11.New(lib) if ctx == nil { - log.Fatal("Failed to load library") + return errors.New("failed to load PKCS11 library") } err = ctx.Initialize() if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to initialize PKCS11 context: %w", err) } + // login session, err := pkcs11.CreateSession(ctx, 0, pass, false) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to create PKCS11 session: %w", err) } + // select the first certificate cert, ckaId, err := pkcs11.GetCert(ctx, session, nil) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to get certificate: %w", err) } s.Certificate = cert @@ -42,11 +46,18 @@ func (s *SignData) SetPKSC11(libPath, pass, crtChainPath string) { // private key pkey, err := pkcs11.InitPrivateKey(ctx, session, ckaId) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to initialize private key: %w", err) } s.Signer = pkey - s.SetCertificateChains(crtChainPath) - s.SetRevocationSettings() + if err := s.SetCertificateChains(crtChainPath); err != nil { + return fmt.Errorf("failed to set certificate chains: %w", err) + } + + if err := s.SetRevocationSettings(); err != nil { + return fmt.Errorf("failed to set revocation settings: %w", err) + } + + return nil } diff --git a/signer/pkcs11_stub.go b/signer/pkcs11_stub.go index ffd2a8f..815ac28 100644 --- a/signer/pkcs11_stub.go +++ b/signer/pkcs11_stub.go @@ -3,11 +3,9 @@ package signer -import ( - log "github.com/sirupsen/logrus" -) +import "github.com/pkg/errors" // SetPKSC11 provides a stub implementation when PKCS11 is not available. -func (s *SignData) SetPKSC11(libPath, pass, crtChainPath string) { - log.Fatal("PKCS11 support is not available in this build. Please rebuild with CGO_ENABLED=1") +func (s *SignData) SetPKSC11(libPath, pass, crtChainPath string) error { + return errors.New("PKCS11 support is not available in this build") } diff --git a/signer/signer.go b/signer/signer.go index bee5e26..a23b37a 100644 --- a/signer/signer.go +++ b/signer/signer.go @@ -3,6 +3,7 @@ package signer import ( "crypto/x509" "encoding/pem" + "fmt" "os" "time" @@ -11,6 +12,7 @@ import ( "github.com/digitorus/pdfsign/sign" "github.com/digitorus/pdfsign/verify" "github.com/digitorus/pdfsigner/license" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -19,21 +21,21 @@ import ( type SignData sign.SignData // SetPEM sets specific to PEM settings. -func (s *SignData) SetPEM(crtPath, keyPath, crtChainPath string) { +func (s *SignData) SetPEM(crtPath, keyPath, crtChainPath string) error { // Set certificate certificate_data, err := os.ReadFile(crtPath) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to read certificate file: %w", err) } certificate_data_block, _ := pem.Decode(certificate_data) if certificate_data_block == nil { - log.Fatal("failed to parse PEM block containing the certificate") + return errors.New("failed to parse PEM block containing the certificate") } cert, err := x509.ParseCertificate(certificate_data_block.Bytes) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to parse certificate: %w", err) } s.Certificate = cert @@ -41,36 +43,44 @@ func (s *SignData) SetPEM(crtPath, keyPath, crtChainPath string) { // Set key key_data, err := os.ReadFile(keyPath) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to read private key file: %w", err) } key_data_block, _ := pem.Decode(key_data) if key_data_block == nil { - log.Fatal("failed to parse PEM block containing the private key") + return errors.New("failed to parse PEM block containing the private key") } pkey, err := x509.ParsePKCS1PrivateKey(key_data_block.Bytes) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to parse private key: %w", err) } s.Signer = pkey - s.SetCertificateChains(crtChainPath) - s.SetRevocationSettings() + err = s.SetCertificateChains(crtChainPath) + if err != nil { + return fmt.Errorf("failed to set certificate chains: %w", err) + } + + if err := s.SetRevocationSettings(); err != nil { + return fmt.Errorf("failed to set revocation settings: %w", err) + } + + return nil } // SetCertificateChains sets certificate chain settings. -func (s *SignData) SetCertificateChains(crtChainPath string) { +func (s *SignData) SetCertificateChains(crtChainPath string) error { var certificate_chains [][]*x509.Certificate if crtChainPath == "" { - return + return nil } chain_data, err := os.ReadFile(crtChainPath) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to read certificate chain file: %w", err) } certificate_pool := x509.NewCertPool() @@ -82,16 +92,20 @@ func (s *SignData) SetCertificateChains(crtChainPath string) { KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, }) if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to verify certificate chains: %w", err) } s.CertificateChains = certificate_chains + + return nil } // SetRevocationSettings sets default revocation settings. -func (s *SignData) SetRevocationSettings() { +func (s *SignData) SetRevocationSettings() error { s.RevocationData = revocation.InfoArchival{} s.RevocationFunction = sign.DefaultEmbedRevocationStatusFunction + + return nil } // SignFile checks the license, waits if limits are reached, if allowed signs the file. @@ -103,16 +117,18 @@ func SignFile(input, output string, s SignData, validateSignature bool) error { } // set date - s.Signature.Info.Date = time.Now().Local() + if s.Signature.Info.Date.IsZero() { + s.Signature.Info.Date = time.Now().Local() + } // sign file err = signFile(input, output, s, validateSignature) if err != nil { - return errors.Wrap(err, "") + return fmt.Errorf("failed to sign file: %w", err) } // log the result - log.Println("File signed:", output) + log.Debugf("File signed: %s", output) return err } diff --git a/signer/signer_test.go b/signer/signer_test.go index 2196699..52b075a 100644 --- a/signer/signer_test.go +++ b/signer/signer_test.go @@ -1,37 +1,30 @@ package signer import ( - "io" "testing" "github.com/digitorus/pdfsign/sign" - "github.com/digitorus/pdfsigner/license" - "github.com/sirupsen/logrus" ) func TestSigner(t *testing.T) { - logrus.SetOutput(io.Discard) - - // test initialize - err := license.Initialize([]byte(license.TestLicense)) - if err != nil { - t.Fatal(err) - } - // create signer signData := SignData{ Signature: sign.SignDataSignature{ Info: sign.SignDataSignatureInfo{ - Name: "Tim", - Location: "Spain", - Reason: "Test", - ContactInfo: "None", + Name: "John Doe", + Location: "Amsterdam, NL", + Reason: "Document Approval", + ContactInfo: "john.doe@example.com", }, CertType: sign.CertificationSignature, DocMDPPerm: sign.AllowFillingExistingFormFieldsAndSignaturesPerms, }, } - signData.SetPEM("../testfiles/test.crt", "../testfiles/test.pem", "") + + err := signData.SetPEM("../testfiles/test.crt", "../testfiles/test.pem", "") + if err != nil { + t.Fatal(err) + } for range 1 { err = SignFile("../testfiles/testfile12.pdf", "../testfiles/testfile12_signed.pdf", signData, true) diff --git a/webapi/handlers.go b/webapi/handlers.go index b597c2b..c0609b2 100644 --- a/webapi/handlers.go +++ b/webapi/handlers.go @@ -29,7 +29,7 @@ func (wa *WebAPI) scheduleJob(jobType string, w http.ResponseWriter, r *http.Req // put job with specified signer mr, err := r.MultipartReader() if err != nil { - return httpError(w, errors.Wrap(err, "read multipart"), http.StatusInternalServerError) + return httpError(w, fmt.Errorf("read multipart: %w", err), http.StatusInternalServerError) } var f fields @@ -48,26 +48,26 @@ func (wa *WebAPI) scheduleJob(jobType string, w http.ResponseWriter, r *http.Req } if err != nil { - return httpError(w, errors.Wrap(err, "get multipart"), http.StatusBadRequest) + return httpError(w, fmt.Errorf("get multipart: %w", err), http.StatusBadRequest) } // parse fields err = parseFields(p, &f) if err != nil { - return httpError(w, errors.Wrap(err, "parse fields"), http.StatusBadRequest) + return httpError(w, fmt.Errorf("parse fields: %w", err), http.StatusBadRequest) } // save pdf file to tmp err = savePDFToTemp(p, fileNames) if err != nil { - return httpError(w, errors.Wrap(err, "save pdf to tmp"), http.StatusBadRequest) + return httpError(w, fmt.Errorf("save pdf to tmp: %w", err), http.StatusBadRequest) } } // add job to the queue jobID, err := addJob(jobType, wa.queue, f, fileNames) if err != nil { - return httpError(w, errors.Wrap(err, "add tasks"), http.StatusBadRequest) + return httpError(w, fmt.Errorf("add tasks: %w", err), http.StatusBadRequest) } // create response @@ -268,7 +268,7 @@ func (wa *WebAPI) handleDelete(w http.ResponseWriter, r *http.Request) error { // delete job by id err := wa.queue.DeleteJob(jobID) if err != nil { - return httpError(w, errors.Wrap(err, "couldn't delete job"), http.StatusBadRequest) + return httpError(w, fmt.Errorf("couldn't delete job: %w", err), http.StatusBadRequest) } // respond with ok diff --git a/webapi/serve_test.go b/webapi/serve_test.go index ef628c9..2775d5a 100644 --- a/webapi/serve_test.go +++ b/webapi/serve_test.go @@ -13,7 +13,6 @@ import ( "time" "github.com/digitorus/pdfsign/sign" - "github.com/digitorus/pdfsigner/license" "github.com/digitorus/pdfsigner/queues/queue" "github.com/digitorus/pdfsigner/signer" "github.com/digitorus/pdfsigner/version" @@ -43,11 +42,6 @@ func TestMain(m *testing.M) { func runTest(m *testing.M) int { log.SetOutput(io.Discard) - err := license.Initialize([]byte(license.TestLicense)) - if err != nil { - log.Fatal(err) - } - // create new queue q = queue.NewQueue() @@ -65,7 +59,7 @@ func runTest(m *testing.M) int { DocMDPPerm: sign.AllowFillingExistingFormFieldsAndSignaturesPerms, }, } - signData.SetPEM("../testfiles/test.crt", "../testfiles//test.pem", "") + _ = signData.SetPEM("../testfiles/test.crt", "../testfiles//test.pem", "") q.AddSignUnit("simple", signData) q.AddVerifyUnit() q.StartProcessor()