diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23abe13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +pdfsign +certs/ +.realize/ \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..15bd5d7 --- /dev/null +++ b/config/config.go @@ -0,0 +1,34 @@ +package config + +import ( + "log" + "os" + + "bitbucket.org/digitorus/pdfsign/sign" + "github.com/BurntSushi/toml" +) + +var ( + DefaultLocation string = "./pdfsign.conf" // Default location of the config file + Settings Config // Initialized once inside Read method Settings are stored in memory. +) + +// Config is the root of the config +type Config struct { + Info sign.SignDataSignatureInfo + TSA sign.TSA +} + +func Read(configfile string) { + + _, err := os.Stat(configfile) + if err != nil { + log.Fatal("Config file is missing: ", configfile) + } + + var c Config + if _, err := toml.DecodeFile(configfile, &c); err != nil { + } + + Settings = c +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..3000fa0 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,24 @@ +package config_test + +import ( + "testing" + + "bitbucket.org/digitorus/littlewatcher/src/config" + "github.com/BurntSushi/toml" + "github.com/stretchr/testify/assert" +) + +func TestConfig(t *testing.T) { + const configContent = ` +staticPath = "../static" +` + + var c config.Config + + if _, err := toml.Decode(configContent, &c); err != nil { + t.Error(err) + } + + // Root + assert.Equal(t, "../static", c) +} diff --git a/p11sign.go b/p11sign.go_ similarity index 100% rename from p11sign.go rename to p11sign.go_ diff --git a/sign.go b/sign.go index 22e8931..0980cfc 100644 --- a/sign.go +++ b/sign.go @@ -4,141 +4,179 @@ import ( "flag" "log" "os" - "time" - "crypto/x509" - "encoding/pem" - "errors" - "io/ioutil" + "fmt" - "bitbucket.org/digitorus/pdfsign/revocation" + "bitbucket.org/digitorus/pdfsign/config" "bitbucket.org/digitorus/pdfsign/sign" "bitbucket.org/digitorus/pdfsign/verify" ) +// usage is a usage function for the flags package. func usage() { - log.Fatal("Usage: sign input.pdf output.pdf certificate.crt private_key.key [chain.crt] OR verify input.pdf") + fmt.Fprintf(os.Stderr, "Pdfsign is a tool to sign and verify PDF digital signatures\n\n") + fmt.Fprintf(os.Stderr, "Usage:\n\n") + fmt.Fprintf(os.Stderr, "\tpdfsign command [arguments]\n\n") + fmt.Fprintf(os.Stderr, "The commands are:\n\n") + fmt.Fprintf(os.Stderr, "\tsign \t\tsign single PDF document\n") + fmt.Fprintf(os.Stderr, "\tverify \t\tverify signature of single PDF document\n") + fmt.Fprintf(os.Stderr, "\tserve \t\tserve web API with signing capabilities. API documentation url\n") + fmt.Fprintf(os.Stderr, "\twatch \t\tautomatically sign PDF files inside a folder\n") + fmt.Fprintf(os.Stderr, "\n\n") + flag.PrintDefaults() + os.Exit(2) } func main() { - flag.Parse() - - if len(flag.Args()) < 2 { + // if no flags provided print usage + if len(os.Args) == 1 { usage() + return } - method := flag.Arg(0) - if method != "sign" && method != "verify" { - usage() - } - - input := flag.Arg(1) - if len(input) == 0 { - usage() - } - - if method == "verify" { - input_file, err := os.Open(input) - if err != nil { - log.Fatal(err) - } - defer input_file.Close() - - resp, err := verify.Verify(input_file) - log.Println(resp) - if err != nil { - log.Println(err) - } - } - - if method == "sign" { - if len(flag.Args()) < 5 { - usage() - } - - output := flag.Arg(2) - if len(output) == 0 { - usage() - } - - certificate_data, err := ioutil.ReadFile(flag.Arg(3)) - if err != nil { - log.Fatal(err) - - } - certificate_data_block, _ := pem.Decode(certificate_data) - if certificate_data_block == nil { - log.Fatal(errors.New("failed to parse PEM block containing the certificate")) - } - - cert, err := x509.ParseCertificate(certificate_data_block.Bytes) - if err != nil { - log.Fatal(err) - } - - key_data, err := ioutil.ReadFile(flag.Arg(4)) - if err != nil { - log.Fatal(err) - } - key_data_block, _ := pem.Decode(key_data) - if key_data_block == nil { - log.Fatal(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) - } - - certificate_chains := make([][]*x509.Certificate, 0) - - if flag.Arg(5) != "" { - certificate_pool := x509.NewCertPool() - if err != nil { - log.Fatal(err) - } - - chain_data, err := ioutil.ReadFile(flag.Arg(5)) - if err != nil { - log.Fatal(err) - } - - certificate_pool.AppendCertsFromPEM(chain_data) - certificate_chains, err = cert.Verify(x509.VerifyOptions{ - Intermediates: certificate_pool, - CurrentTime: cert.NotBefore, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - }) - if err != nil { - log.Fatal(err) - } - } - - err = sign.SignFile(input, output, sign.SignData{ - Signature: sign.SignDataSignature{ - Info: sign.SignDataSignatureInfo{ - Name: "Jeroen Bobbeldijk", - Location: "Rotterdam", - Reason: "Test", - ContactInfo: "Geen", - Date: time.Now().Local(), - }, - CertType: 2, - Approval: false, - }, - Signer: pkey, - Certificate: cert, - CertificateChains: certificate_chains, - TSA: sign.TSA{ - URL: "http://aatl-timestamp.globalsign.com/tsa/aohfewat2389535fnasgnlg5m23", - }, - RevocationData: revocation.InfoArchival{}, - RevocationFunction: sign.DefaultEmbedRevocationStatusFunction, - }) - if err != nil { - log.Println(err) - } else { - log.Println("Signed PDF written to " + output) - } + switch os.Args[1] { + case "sign": + simpleSign() + case "verify": + verifyPDF() + case "serve": + case "watch": + default: + fmt.Printf("%q is not valid command.\n", os.Args[1]) + os.Exit(2) + } +} + +func verifyPDF() { + verifyCommand := flag.NewFlagSet("verify", flag.ExitOnError) + input := verifyCommand.String("in", "", "") + + input_file, err := os.Open(*input) + if err != nil { + log.Fatal(err) + } + defer input_file.Close() + + resp, err := verify.Verify(input_file) + log.Println(resp) + if err != nil { + log.Println(err) + } +} + +func getSignDataFlags(f *flag.FlagSet) sign.SignData { + // Signature info + name := f.String("info-name", config.Settings.Info.Name, "Signature info name") + location := f.String("info-location", config.Settings.Info.Location, "Signature info location") + reason := f.String("info-reason", config.Settings.Info.Reason, "Signature info reason") + contact := f.String("info-contact", config.Settings.Info.ContactInfo, "Signature info contact") + // Signature other + approval := f.Bool("approval", false, "Approval") + certType := f.Uint("type", 0, "Certificate type") + + // TSA + tsaUrl := f.String("tsa-url", "", "tsaUrl") + tsaUsername := f.String("tsa-username", "", "tsaUsername") + tsaPassword := f.String("tsa-password", "", "tsaPassword") + + var sd sign.SignData + sd.Signature.Info.Name = *name + sd.Signature.Info.Location = *location + sd.Signature.Info.Reason = *reason + sd.Signature.Info.ContactInfo = *contact + sd.Signature.CertType = uint32(*certType) + sd.Signature.Approval = *approval + sd.TSA.URL = *tsaUrl + sd.TSA.Username = *tsaUsername + sd.TSA.Password = *tsaPassword + return sd +} + +func simpleSign() { + signCommand := flag.NewFlagSet("sign", flag.ExitOnError) + configPath := signCommand.String("config", "", "Path to config file") + signCommand.Parse(os.Args[2:]) + if *configPath != "" { + config.Read(*configPath) + } + + inputPath := signCommand.String("in", "", "Input PDF file") + outputPath := signCommand.String("out", "", "Output PDF file") + crtPath := signCommand.String("crt", "", "Certificate") + keyPath := signCommand.String("key", "", "Private key") + crtChainPath := signCommand.String("chain", "", "Certificate chain") + help := signCommand.Bool("help", false, "Show this help") + signData := getSignDataFlags(signCommand) + signCommand.Parse(os.Args[2:]) + + usageText := `usage: pdfsign sign -in input.pdf -out output.pdf -crt certificate.crt -key private_key.key [-chain chain.crt]") +Description +` + if *help == true { + fmt.Println(usageText) + signCommand.PrintDefaults() + return + } + + if signCommand.Parsed() == false || *inputPath == "" || *outputPath == "" || *crtPath == "" || *keyPath == "" { + fmt.Println(usageText) + signCommand.PrintDefaults() + os.Exit(2) + } + + s, err := newSigner(*crtPath, *keyPath, *crtChainPath) + if err != nil { + log.Fatal(err) + } + + if err := s.sign(*inputPath, *outputPath, signData); err != nil { + log.Println(err) + } else { + log.Println("Signed PDF written to " + *outputPath) + } +} + +func p11sign() { + signCommand := flag.NewFlagSet("sign", flag.ExitOnError) + configPath := signCommand.String("config", "", "Path to config file") + signCommand.Parse(os.Args[2:]) + if *configPath != "" { + config.Read(*configPath) + } + + input := signCommand.String("in", "", "Input PDF file") + output := signCommand.String("out", "", "Output PDF file") + libPath := signCommand.String("lib", "", "Path to PKCS11 library") + pass := signCommand.String("pass", "", "PKCS11 password") + crtChainPath := signCommand.String("chain", "", "Certificate chain") + help := signCommand.Bool("help", false, "Show this help") + signData := getSignDataFlags(signCommand) + signCommand.Parse(os.Args[2:]) + usageText := `Usage: pdfsign sign -in input.pdf -out output.pdf -pass pkcs11-password [-chain chain.crt]") + +Description + +` + if *help == true { + fmt.Println(usageText) + signCommand.PrintDefaults() + return + } + + if signCommand.Parsed() == false || *input == "" || *output == "" || *pass == "" { + fmt.Println(usageText) + signCommand.PrintDefaults() + os.Exit(2) + } + + s, err := newP11Signer(*libPath, *pass, *crtChainPath) + if err != nil { + log.Fatal(err) + } + + if err := s.sign(*input, *output, signData); err != nil { + log.Println(err) + } else { + log.Println("Signed PDF written to " + *output) } } diff --git a/signer.go b/signer.go new file mode 100644 index 0000000..abc75b9 --- /dev/null +++ b/signer.go @@ -0,0 +1,154 @@ +package main + +import ( + "crypto" + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + "log" + "time" + + "bitbucket.org/digitorus/pdfsign/revocation" + "bitbucket.org/digitorus/pdfsign/sign" + "bitbucket.org/digitorus/pkcs11" +) + +type signer struct { + certificate *x509.Certificate + signer crypto.Signer + certificateChains [][]*x509.Certificate +} + +func newSigner(crtPath, keyPath, crtChainPath string) (*signer, error) { + var s signer + + // Set certificate + certificate_data, err := ioutil.ReadFile(crtPath) + if err != nil { + return &s, err + log.Fatal(err) + } + certificate_data_block, _ := pem.Decode(certificate_data) + if certificate_data_block == nil { + return &s, errors.New("failed to parse PEM block containing the certificate") + } + cert, err := x509.ParseCertificate(certificate_data_block.Bytes) + if err != nil { + return &s, err + } + s.certificate = cert + + // Set key + key_data, err := ioutil.ReadFile(keyPath) + if err != nil { + return &s, err + } + key_data_block, _ := pem.Decode(key_data) + if key_data_block == nil { + return &s, errors.New("failed to parse PEM block containing the private key") + } + pkey, err := x509.ParsePKCS1PrivateKey(key_data_block.Bytes) + if err != nil { + return &s, err + } + s.signer = pkey + + certificate_chains, err := getCertificateChains(crtChainPath, cert) + if err != nil { + return &s, err + } + s.certificateChains = certificate_chains + + return &s, nil +} + +func newP11Signer(libPath, pass, crtChainPath string) (*signer, error) { + var s signer + + // pkcs11 key + lib, err := pkcs11.FindLib(libPath) + if err != nil { + return &s, err + } + + // Load Library + ctx := pkcs11.New(lib) + if ctx == nil { + return &s, errors.New("Failed to load library") + } + err = ctx.Initialize() + if err != nil { + return &s, err + } + // login + session, err := pkcs11.CreateSession(ctx, 0, pass, false) + if err != nil { + return &s, err + } + // select the first certificate + cert, ckaId, err := pkcs11.GetCert(ctx, session, nil) + if err != nil { + return &s, err + } + s.certificate = cert + + // private key + pkey, err := pkcs11.InitPrivateKey(ctx, session, ckaId) + if err != nil { + return &s, err + } + s.signer = pkey + + certificate_chains, err := getCertificateChains(crtChainPath, cert) + if err != nil { + return &s, err + } + s.certificateChains = certificate_chains + + return &s, nil +} + +func getCertificateChains(crtChainPath string, cert *x509.Certificate) ([][]*x509.Certificate, error) { + certificate_chains := make([][]*x509.Certificate, 0) + if crtChainPath == "" { + return certificate_chains, nil + } + + chain_data, err := ioutil.ReadFile(crtChainPath) + if err != nil { + log.Fatal(err) + } + certificate_pool := x509.NewCertPool() + certificate_pool.AppendCertsFromPEM(chain_data) + certificate_chains, err = cert.Verify(x509.VerifyOptions{ + Intermediates: certificate_pool, + CurrentTime: cert.NotBefore, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + + return certificate_chains, err +} + +func (s *signer) sign(input, output string, d sign.SignData) error { + err := sign.SignFile(input, output, sign.SignData{ + Signature: sign.SignDataSignature{ + Info: sign.SignDataSignatureInfo{ + Name: d.Signature.Info.Name, + Location: d.Signature.Info.Location, + Reason: d.Signature.Info.Reason, + ContactInfo: d.Signature.Info.ContactInfo, + Date: time.Now().Local(), + }, + CertType: d.Signature.CertType, + Approval: d.Signature.Approval, + }, + Signer: s.signer, + Certificate: s.certificate, + CertificateChains: s.certificateChains, + TSA: d.TSA, + RevocationData: revocation.InfoArchival{}, + RevocationFunction: sign.DefaultEmbedRevocationStatusFunction, + }) + return err +} diff --git a/verify/verify.go b/verify/verify.go index 06b7e89..8e4fd12 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -12,11 +12,11 @@ import ( "bitbucket.org/digitorus/pdf" "bitbucket.org/digitorus/pdfsign/revocation" + "crypto" "github.com/digitorus/pkcs7" "github.com/digitorus/timestamp" - "log" "golang.org/x/crypto/ocsp" - "crypto" + "log" ) type Response struct {