Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83d42dc489 | |||
| 984baa6bb4 | |||
| 34982c122f | |||
| fb11c0c27c | |||
| 27cc67d2cf | |||
| 0fe13439b6 | |||
| fb9caec663 | |||
| e3e83630b5 | |||
| c1f06604e3 | |||
| 9091cc7682 | |||
| 74ce7bc58a | |||
| 8b2d10209e | |||
| 770100b688 | |||
| 29630c55a9 | |||
| 3c2ec896f8 | |||
| 7828726ba4 | |||
| 458f3ceaed | |||
| 2c45ae7b4e | |||
| c1b8b72cf1 | |||
| bfc7fedbf9 | |||
| 965312f48e | |||
| 237aa46ddd | |||
| f8c64d3be5 | |||
| d66cfe1145 | |||
| ad03c5f991 | |||
|
|
0dd4e1c6ca | ||
| 078230217d | |||
| 90318f861b | |||
| 3bb1362c0e | |||
| 30ffbbdbc5 | |||
| b893e99864 | |||
| c7c51568d8 | |||
| 7793021260 | |||
| 692562818c | |||
| 9e19346fc0 | |||
| cb827169dc | |||
| 027d0173bc | |||
| 6f19b69bbd | |||
| 7e118bfdb0 | |||
|
|
e0868841bf | ||
|
|
c558405d11 | ||
|
|
a1eb035af7 | ||
| 5eedcff042 | |||
|
|
6ac8eb04b4 | ||
|
|
4a4e4cd3fd | ||
|
|
1207093a56 | ||
| 2b6ae03d1a | |||
| ef0f14a512 | |||
| 6ae393ebf2 | |||
| 76d88c220d | |||
|
|
40e015373f | ||
| 50c226b726 | |||
| 070ffb9dff | |||
| 5ac05bd298 | |||
| cf1edf2d31 | |||
|
|
03e8958dd7 | ||
|
|
6cef585071 | ||
| 06678499d4 | |||
| fad17065fe | |||
| 63e0cbeacb | |||
| 231b98dd68 | |||
|
|
160a42ec26 | ||
|
|
b6b33e00c8 | ||
|
|
9e1aed257b | ||
|
|
411907c0ad | ||
|
|
06c7f8f42f | ||
|
|
8b638065d1 | ||
|
|
9ac378eaa5 | ||
|
|
eaaaabe439 | ||
|
|
4122f01644 | ||
|
|
263a5d3973 | ||
|
|
afef3eea62 | ||
| d6c5360a06 | |||
| 0ab21e12f3 | |||
| 832475db56 | |||
| cb16cfa183 | |||
| d083a39a7d | |||
| fc77225740 | |||
| 41df73d7a8 | |||
| 0dc478746a | |||
| f44bbc9eca | |||
|
|
1df0350fc7 | ||
|
|
d42c1fa1c5 | ||
|
|
4fa6e4ab0e | ||
|
|
a3ead16faf | ||
| c8f839de73 | |||
| 0c56a477bc | |||
|
|
763dbec310 | ||
|
|
0e6b60a2c4 | ||
|
|
be34ad263d | ||
|
|
48b03c908d | ||
|
|
70d7ff505b | ||
|
|
68e5822176 | ||
|
|
54dd461733 | ||
|
|
eba03a2f4a |
59
.circleci/config.yml
Normal file
59
.circleci/config.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Use the latest 2.1 version of CircleCI pipeline process engine.
|
||||||
|
# See: https://circleci.com/docs/2.0/configuration-reference
|
||||||
|
version: 2.1
|
||||||
|
|
||||||
|
commands:
|
||||||
|
setup-bazel:
|
||||||
|
description: |
|
||||||
|
Setup the Bazel build system used for building the repo
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Add Bazel Apt repository
|
||||||
|
command: |
|
||||||
|
sudo apt install curl gnupg
|
||||||
|
curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor > bazel.gpg
|
||||||
|
sudo mv bazel.gpg /etc/apt/trusted.gpg.d/
|
||||||
|
echo "deb [arch=amd64] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
|
||||||
|
- run:
|
||||||
|
name: Install Bazel from Apt
|
||||||
|
command: sudo apt update && sudo apt install bazel
|
||||||
|
|
||||||
|
# Define a job to be invoked later in a workflow.
|
||||||
|
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
|
||||||
|
jobs:
|
||||||
|
testbuild:
|
||||||
|
working_directory: ~/repo
|
||||||
|
# Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub.
|
||||||
|
# See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.15.8
|
||||||
|
# Add steps to the job
|
||||||
|
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- setup-bazel
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
- go-mod-v4-{{ checksum "go.sum" }}
|
||||||
|
- run:
|
||||||
|
name: Install Dependencies
|
||||||
|
command: go mod download
|
||||||
|
- save_cache:
|
||||||
|
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||||
|
paths:
|
||||||
|
- "/go/pkg/mod"
|
||||||
|
- run:
|
||||||
|
name: Run tests
|
||||||
|
command: bazel test //...
|
||||||
|
- run:
|
||||||
|
name: Run build
|
||||||
|
command: bazel build //...
|
||||||
|
- store_test_results:
|
||||||
|
path: /tmp/test-reports
|
||||||
|
|
||||||
|
# Invoke jobs via workflows
|
||||||
|
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
|
||||||
|
workflows:
|
||||||
|
testbuild:
|
||||||
|
jobs:
|
||||||
|
- testbuild
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
bazel-bin
|
||||||
|
bazel-goutils
|
||||||
|
bazel-out
|
||||||
|
bazel-testlogs
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
9
.idea/goutils.iml
generated
Normal file
9
.idea/goutils.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/goutils.iml" filepath="$PROJECT_DIR$/.idea/goutils.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
26
.travis.yml
Normal file
26
.travis.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- ppc64le
|
||||||
|
sudo: false
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
- 1.9
|
||||||
|
jobs:
|
||||||
|
exclude:
|
||||||
|
- go: 1.9
|
||||||
|
arch: amd64
|
||||||
|
- go: 1.9
|
||||||
|
arch: ppc64le
|
||||||
|
script:
|
||||||
|
- go get golang.org/x/lint/golint
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
- go get github.com/kisom/goutils/...
|
||||||
|
- go test -cover github.com/kisom/goutils/...
|
||||||
|
- golint github.com/kisom/goutils/...
|
||||||
|
notifications:
|
||||||
|
email:
|
||||||
|
recipients:
|
||||||
|
- coder@kyleisom.net
|
||||||
|
on_success: change
|
||||||
|
on_failure: change
|
||||||
22
BUILD.bazel
Normal file
22
BUILD.bazel
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
load("@bazel_gazelle//:def.bzl", "gazelle")
|
||||||
|
|
||||||
|
# gazelle:prefix git.wntrmute.dev/kyle/goutils
|
||||||
|
gazelle(name = "gazelle")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "goutils",
|
||||||
|
srcs = ["doc.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
gazelle(
|
||||||
|
name = "gazelle-update-repos",
|
||||||
|
args = [
|
||||||
|
"-from_file=go.mod",
|
||||||
|
"-to_macro=deps.bzl%go_dependencies",
|
||||||
|
"-prune",
|
||||||
|
],
|
||||||
|
command = "update-repos",
|
||||||
|
)
|
||||||
27
CHANGELOG
Normal file
27
CHANGELOG
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Release 1.2.1 - 2018-09-15
|
||||||
|
|
||||||
|
+ Add missing format argument to Errorf call in kgz.
|
||||||
|
|
||||||
|
Release 1.2.0 - 2018-09-15
|
||||||
|
|
||||||
|
+ Adds the kgz command line utility.
|
||||||
|
|
||||||
|
Release 1.1.0 - 2017-11-16
|
||||||
|
|
||||||
|
+ A number of new command line utilities were added
|
||||||
|
|
||||||
|
+ atping
|
||||||
|
+ cruntar
|
||||||
|
+ renfnv
|
||||||
|
+
|
||||||
|
+ ski
|
||||||
|
+ subjhash
|
||||||
|
+ yamll
|
||||||
|
|
||||||
|
+ new package: ahash
|
||||||
|
+ package for loading hashes from an algorithm string
|
||||||
|
|
||||||
|
+ new certificate loading functions in the lib package
|
||||||
|
|
||||||
|
+ new package: tee
|
||||||
|
+ emulates tee(1)
|
||||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2015 Kyle Isom <kyle@tyrfingr.is>
|
Copyright (c) 2015-2023 Kyle Isom <kyle@tyrfingr.is>
|
||||||
|
|
||||||
Permission to use, copy, modify, and distribute this software for any
|
Permission to use, copy, modify, and distribute this software for any
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -2,12 +2,18 @@ GOUTILS
|
|||||||
|
|
||||||
This is a collection of small utility code I've written in Go; the `cmd/`
|
This is a collection of small utility code I've written in Go; the `cmd/`
|
||||||
directory has a number of command-line utilities. Rather than keep all
|
directory has a number of command-line utilities. Rather than keep all
|
||||||
of these in superfluous repositories of their own, I'm putting them here.
|
of these in superfluous repositories of their own, or rewriting them
|
||||||
|
for each project, I'm putting them here.
|
||||||
|
|
||||||
|
The project can be built with the standard Go tooling, or it can be built
|
||||||
|
with Bazel.
|
||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
|
|
||||||
|
ahash/ Provides hashes from string algorithm specifiers.
|
||||||
assert/ Error handling, assertion-style.
|
assert/ Error handling, assertion-style.
|
||||||
cmd/
|
cmd/
|
||||||
|
atping/ Automated TCP ping, meant for putting in cronjobs.
|
||||||
certchain/ Display the certificate chain from a
|
certchain/ Display the certificate chain from a
|
||||||
TLS connection.
|
TLS connection.
|
||||||
certdump/ Dump certificate information.
|
certdump/ Dump certificate information.
|
||||||
@@ -18,32 +24,55 @@ Contents:
|
|||||||
the time to expiry and checking for revocations.
|
the time to expiry and checking for revocations.
|
||||||
clustersh/ Run commands or transfer files across multiple
|
clustersh/ Run commands or transfer files across multiple
|
||||||
servers via SSH.
|
servers via SSH.
|
||||||
|
cruntar/ Untar an archive with hard links, copying instead of
|
||||||
|
linking.
|
||||||
csrpubdump/ Dump the public key from an X.509 certificate request.
|
csrpubdump/ Dump the public key from an X.509 certificate request.
|
||||||
|
diskimg/ Write a disk image to a device.
|
||||||
|
eig/ EEPROM image generator.
|
||||||
fragment/ Print a fragment of a file.
|
fragment/ Print a fragment of a file.
|
||||||
jlp/ JSON linter/prettifier.
|
jlp/ JSON linter/prettifier.
|
||||||
|
kgz/ Custom gzip compressor / decompressor that handles 99%
|
||||||
|
of my use cases.
|
||||||
|
parts/ Simple parts database management for my collection of
|
||||||
|
electronic components.
|
||||||
pem2bin/ Dump the binary body of a PEM-encoded block.
|
pem2bin/ Dump the binary body of a PEM-encoded block.
|
||||||
pembody/ Print the body of a PEM certificate.
|
pembody/ Print the body of a PEM certificate.
|
||||||
pemit/ Dump data to a PEM file.
|
pemit/ Dump data to a PEM file.
|
||||||
showimp/ List the external (e.g. non-stdlib and outside the
|
|
||||||
current working directory) imports for a Go file.
|
|
||||||
readchain/ Print the common name for the certificates
|
readchain/ Print the common name for the certificates
|
||||||
in a bundle.
|
in a bundle.
|
||||||
showimp Display the external imports in a package.
|
renfnv/ Rename a file to base32-encoded 64-bit FNV-1a hash.
|
||||||
|
rhash/ Compute the digest of remote files.
|
||||||
|
showimp/ List the external (e.g. non-stdlib and outside the
|
||||||
|
current working directory) imports for a Go file.
|
||||||
|
ski Display the SKI for PEM-encoded TLS material.
|
||||||
|
sprox/ Simple TCP proxy.
|
||||||
stealchain/ Dump the verified chain from a TLS
|
stealchain/ Dump the verified chain from a TLS
|
||||||
connection.
|
connection to a server.
|
||||||
|
stealchain- Dump the verified chain from a TLS
|
||||||
|
server/ connection from a client.
|
||||||
|
subjhash/ Print or match subject info from a certificate.
|
||||||
tlskeypair/ Check whether a TLS certificate and key file match.
|
tlskeypair/ Check whether a TLS certificate and key file match.
|
||||||
utc/ Convert times to UTC.
|
utc/ Convert times to UTC.
|
||||||
|
yamll/ A small YAML linter.
|
||||||
|
config/ A simple global configuration system where configuration
|
||||||
|
data is pulled from a file or an environment variable
|
||||||
|
transparently.
|
||||||
|
dbg/ A debug printer.
|
||||||
die/ Death of a program.
|
die/ Death of a program.
|
||||||
fileutil/ Common file functions.
|
fileutil/ Common file functions.
|
||||||
lib/ Commonly-useful functions for writing Go programs.
|
lib/ Commonly-useful functions for writing Go programs.
|
||||||
logging/ A logging library.
|
logging/ A logging library.
|
||||||
mwc/ MultiwriteCloser implementation.
|
mwc/ MultiwriteCloser implementation.
|
||||||
|
rand/ Utilities for working with math/rand.
|
||||||
sbuf/ A byte buffer that can be wiped.
|
sbuf/ A byte buffer that can be wiped.
|
||||||
testio/ Various I/O utilities useful during testing.
|
seekbuf/ A read-seekable byte buffer.
|
||||||
testutil/ Various utility functions useful during testing.
|
syslog/ Syslog-type logging.
|
||||||
|
tee/ Emulate tee(1)'s functionality in io.Writers.
|
||||||
|
testio/ Various I/O utilities useful during testing.
|
||||||
|
testutil/ Various utility functions useful during testing.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Each program should have a small README in the directory with more
|
Each program should have a small README in the directory with more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
All code here is licensed under the MIT license.
|
All code here is licensed under the ISC license.
|
||||||
|
|||||||
32
WORKSPACE
Normal file
32
WORKSPACE
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||||
|
|
||||||
|
|
||||||
|
### Go tooling, including Gazelle to generate and maintain BUILD files.
|
||||||
|
http_archive(
|
||||||
|
name = "io_bazel_rules_go",
|
||||||
|
sha256 = "6b65cb7917b4d1709f9410ffe00ecf3e160edf674b78c54a894471320862184f",
|
||||||
|
urls = [
|
||||||
|
"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.39.0/rules_go-v0.39.0.zip",
|
||||||
|
"https://github.com/bazelbuild/rules_go/releases/download/v0.39.0/rules_go-v0.39.0.zip",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
http_archive(
|
||||||
|
name = "bazel_gazelle",
|
||||||
|
sha256 = "ecba0f04f96b4960a5b250c8e8eeec42281035970aa8852dda73098274d14a1d",
|
||||||
|
urls = [
|
||||||
|
"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
|
||||||
|
"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
|
||||||
|
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
|
||||||
|
load("//:deps.bzl", "go_dependencies")
|
||||||
|
|
||||||
|
# gazelle:repository_macro deps.bzl%go_dependencies
|
||||||
|
go_dependencies()
|
||||||
|
go_rules_dependencies()
|
||||||
|
go_register_toolchains(version = "1.20.4")
|
||||||
|
gazelle_dependencies()
|
||||||
|
|
||||||
24
ahash/BUILD.bazel
Normal file
24
ahash/BUILD.bazel
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "ahash",
|
||||||
|
srcs = ["ahash.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/ahash",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//assert",
|
||||||
|
"@org_golang_x_crypto//blake2b",
|
||||||
|
"@org_golang_x_crypto//blake2s",
|
||||||
|
"@org_golang_x_crypto//md4",
|
||||||
|
"@org_golang_x_crypto//ripemd160",
|
||||||
|
"@org_golang_x_crypto//sha3",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "ahash_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["ahash_test.go"],
|
||||||
|
embed = [":ahash"],
|
||||||
|
deps = ["//assert"],
|
||||||
|
)
|
||||||
263
ahash/ahash.go
Normal file
263
ahash/ahash.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
// Package ahash provides support for hashing data with a selectable
|
||||||
|
//
|
||||||
|
// hash function.
|
||||||
|
package ahash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"errors"
|
||||||
|
"hash"
|
||||||
|
"hash/adler32"
|
||||||
|
"hash/crc32"
|
||||||
|
"hash/crc64"
|
||||||
|
"hash/fnv"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/assert"
|
||||||
|
"golang.org/x/crypto/blake2b"
|
||||||
|
"golang.org/x/crypto/blake2s"
|
||||||
|
"golang.org/x/crypto/md4"
|
||||||
|
"golang.org/x/crypto/ripemd160"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sha224Slicer(bs []byte) []byte {
|
||||||
|
sum := sha256.Sum224(bs)
|
||||||
|
return sum[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256Slicer(bs []byte) []byte {
|
||||||
|
sum := sha256.Sum256(bs)
|
||||||
|
return sum[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha384Slicer(bs []byte) []byte {
|
||||||
|
sum := sha512.Sum384(bs)
|
||||||
|
return sum[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha512Slicer(bs []byte) []byte {
|
||||||
|
sum := sha512.Sum512(bs)
|
||||||
|
return sum[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash represents a generic hash function that may or may not be secure. It
|
||||||
|
// satisfies the hash.Hash interface.
|
||||||
|
type Hash struct {
|
||||||
|
hash.Hash
|
||||||
|
secure bool
|
||||||
|
algo string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashAlgo returns the name of the underlying hash algorithm.
|
||||||
|
func (h *Hash) HashAlgo() string {
|
||||||
|
return h.algo
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSecure returns true if the Hash is a cryptographic hash.
|
||||||
|
func (h *Hash) IsSecure() bool {
|
||||||
|
return h.secure
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum32 returns true if the underlying hash is a 32-bit hash; if is, the
|
||||||
|
// uint32 parameter will contain the hash.
|
||||||
|
func (h *Hash) Sum32() (uint32, bool) {
|
||||||
|
h32, ok := h.Hash.(hash.Hash32)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return h32.Sum32(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHash32 returns true if the underlying hash is a 32-bit hash function.
|
||||||
|
func (h *Hash) IsHash32() bool {
|
||||||
|
_, ok := h.Hash.(hash.Hash32)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum64 returns true if the underlying hash is a 64-bit hash; if is, the
|
||||||
|
// uint64 parameter will contain the hash.
|
||||||
|
func (h *Hash) Sum64() (uint64, bool) {
|
||||||
|
h64, ok := h.Hash.(hash.Hash64)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return h64.Sum64(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHash64 returns true if the underlying hash is a 64-bit hash function.
|
||||||
|
func (h *Hash) IsHash64() bool {
|
||||||
|
_, ok := h.Hash.(hash.Hash64)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func blakeFunc(bf func(key []byte) (hash.Hash, error)) func() hash.Hash {
|
||||||
|
return func() hash.Hash {
|
||||||
|
h, err := bf(nil)
|
||||||
|
assert.NoError(err, "while constructing a BLAKE2 hash function")
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var secureHashes = map[string]func() hash.Hash{
|
||||||
|
"ripemd160": ripemd160.New,
|
||||||
|
"sha224": sha256.New224,
|
||||||
|
"sha256": sha256.New,
|
||||||
|
"sha384": sha512.New384,
|
||||||
|
"sha512": sha512.New,
|
||||||
|
"sha3-224": sha3.New224,
|
||||||
|
"sha3-256": sha3.New256,
|
||||||
|
"sha3-384": sha3.New384,
|
||||||
|
"sha3-512": sha3.New512,
|
||||||
|
"blake2s-256": blakeFunc(blake2s.New256),
|
||||||
|
"blake2b-256": blakeFunc(blake2b.New256),
|
||||||
|
"blake2b-384": blakeFunc(blake2b.New384),
|
||||||
|
"blake2b-512": blakeFunc(blake2b.New512),
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHash32(f func() hash.Hash32) func() hash.Hash {
|
||||||
|
return func() hash.Hash {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHash64(f func() hash.Hash64) func() hash.Hash {
|
||||||
|
return func() hash.Hash {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCRC64(tab uint64) func() hash.Hash {
|
||||||
|
return newHash64(
|
||||||
|
func() hash.Hash64 {
|
||||||
|
return crc64.New(crc64.MakeTable(tab))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var insecureHashes = map[string]func() hash.Hash{
|
||||||
|
"md4": md4.New,
|
||||||
|
"md5": md5.New,
|
||||||
|
"sha1": sha1.New,
|
||||||
|
"adler32": newHash32(adler32.New),
|
||||||
|
"crc32-ieee": newHash32(crc32.NewIEEE),
|
||||||
|
"crc64": newCRC64(crc64.ISO),
|
||||||
|
"crc64-ecma": newCRC64(crc64.ECMA),
|
||||||
|
"fnv1-32a": newHash32(fnv.New32a),
|
||||||
|
"fnv1-32": newHash32(fnv.New32),
|
||||||
|
"fnv1-64a": newHash64(fnv.New64a),
|
||||||
|
"fnv1-64": newHash64(fnv.New64),
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Hash for the specified algorithm.
|
||||||
|
func New(algo string) (*Hash, error) {
|
||||||
|
h := &Hash{algo: algo}
|
||||||
|
|
||||||
|
hf, ok := secureHashes[algo]
|
||||||
|
if ok {
|
||||||
|
h.Hash = hf()
|
||||||
|
h.secure = true
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hf, ok = insecureHashes[algo]
|
||||||
|
if ok {
|
||||||
|
h.Hash = hf()
|
||||||
|
h.secure = false
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("chash: unsupport hash algorithm " + algo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum returns the digest (not the hex digest) of the data using the given
|
||||||
|
// algorithm.
|
||||||
|
func Sum(algo string, data []byte) ([]byte, error) {
|
||||||
|
h, err := New(algo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SumReader reads all the data from the given io.Reader and returns the
|
||||||
|
// digest (not the hex digest) from the specified algorithm.
|
||||||
|
func SumReader(algo string, r io.Reader) ([]byte, error) {
|
||||||
|
h, err := New(algo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(h, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SumLimitedReader reads n bytes of data from the io.reader and returns the
|
||||||
|
// digest (not the hex digest) from the specified algorithm.
|
||||||
|
func SumLimitedReader(algo string, r io.Reader, n int64) ([]byte, error) {
|
||||||
|
limit := &io.LimitedReader{
|
||||||
|
R: r,
|
||||||
|
N: n,
|
||||||
|
}
|
||||||
|
|
||||||
|
return SumReader(algo, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
var insecureHashList, secureHashList, hashList []string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
shl := len(secureHashes) // secure hash list length
|
||||||
|
ihl := len(insecureHashes) // insecure hash list length
|
||||||
|
ahl := shl + ihl // all hash list length
|
||||||
|
|
||||||
|
insecureHashList = make([]string, 0, ihl)
|
||||||
|
secureHashList = make([]string, 0, shl)
|
||||||
|
hashList = make([]string, 0, ahl)
|
||||||
|
|
||||||
|
for algo := range insecureHashes {
|
||||||
|
insecureHashList = append(insecureHashList, algo)
|
||||||
|
}
|
||||||
|
sort.Strings(insecureHashList)
|
||||||
|
|
||||||
|
for algo := range secureHashes {
|
||||||
|
secureHashList = append(secureHashList, algo)
|
||||||
|
}
|
||||||
|
sort.Strings(secureHashList)
|
||||||
|
|
||||||
|
hashList = append(hashList, insecureHashList...)
|
||||||
|
hashList = append(hashList, secureHashList...)
|
||||||
|
sort.Strings(hashList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashList returns a sorted list of all the hash algorithms supported by the
|
||||||
|
// package.
|
||||||
|
func HashList() []string {
|
||||||
|
return hashList[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureHashList returns a sorted list of all the secure (cryptographic) hash
|
||||||
|
// algorithms supported by the package.
|
||||||
|
func SecureHashList() []string {
|
||||||
|
return secureHashList[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsecureHashList returns a sorted list of all the insecure hash algorithms
|
||||||
|
// supported by the package.
|
||||||
|
func InsecureHashList() []string {
|
||||||
|
return insecureHashList[:]
|
||||||
|
}
|
||||||
157
ahash/ahash_test.go
Normal file
157
ahash/ahash_test.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package ahash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecureHash(t *testing.T) {
|
||||||
|
algo := "sha256"
|
||||||
|
h, err := New(algo)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, h.IsSecure(), algo+" should be a secure hash")
|
||||||
|
assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo")
|
||||||
|
assert.BoolT(t, !h.IsHash32(), algo+" isn't actually a 32-bit hash")
|
||||||
|
assert.BoolT(t, !h.IsHash64(), algo+" isn't actually a 64-bit hash")
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
sum, err := Sum(algo, data)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum))
|
||||||
|
|
||||||
|
data = []byte("hello, world")
|
||||||
|
buf := bytes.NewBuffer(data)
|
||||||
|
expected = "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b"
|
||||||
|
sum, err = SumReader(algo, buf)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum))
|
||||||
|
|
||||||
|
data = []byte("hello world")
|
||||||
|
_, err = h.Write(data)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
unExpected := "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b"
|
||||||
|
sum = h.Sum(nil)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) != unExpected, fmt.Sprintf("hash shouldn't have returned %x", unExpected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsecureHash(t *testing.T) {
|
||||||
|
algo := "md5"
|
||||||
|
h, err := New(algo)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, !h.IsSecure(), algo+" shouldn't be a secure hash")
|
||||||
|
assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo")
|
||||||
|
assert.BoolT(t, !h.IsHash32(), algo+" isn't actually a 32-bit hash")
|
||||||
|
assert.BoolT(t, !h.IsHash64(), algo+" isn't actually a 64-bit hash")
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var expected = "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
|
sum, err := Sum(algo, data)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum))
|
||||||
|
|
||||||
|
data = []byte("hello, world")
|
||||||
|
buf := bytes.NewBuffer(data)
|
||||||
|
expected = "e4d7f1b4ed2e42d15898f4b27b019da4"
|
||||||
|
sum, err = SumReader(algo, buf)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum))
|
||||||
|
|
||||||
|
data = []byte("hello world")
|
||||||
|
_, err = h.Write(data)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
unExpected := "e4d7f1b4ed2e42d15898f4b27b019da4"
|
||||||
|
sum = h.Sum(nil)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) != unExpected, fmt.Sprintf("hash shouldn't have returned %x", unExpected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHash32(t *testing.T) {
|
||||||
|
algo := "crc32-ieee"
|
||||||
|
h, err := New(algo)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, !h.IsSecure(), algo+" shouldn't be a secure hash")
|
||||||
|
assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo")
|
||||||
|
assert.BoolT(t, h.IsHash32(), algo+" is actually a 32-bit hash")
|
||||||
|
assert.BoolT(t, !h.IsHash64(), algo+" isn't actually a 64-bit hash")
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var expected uint32
|
||||||
|
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok := h.Sum32()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum32")
|
||||||
|
assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected))
|
||||||
|
|
||||||
|
data = []byte("hello, world")
|
||||||
|
expected = 0xffab723a
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok = h.Sum32()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum32")
|
||||||
|
assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected))
|
||||||
|
|
||||||
|
h.Reset()
|
||||||
|
data = []byte("hello world")
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok = h.Sum32()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum32")
|
||||||
|
assert.BoolT(t, expected != sum, fmt.Sprintf("%s returned %d but shouldn't have", algo, sum))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHash64(t *testing.T) {
|
||||||
|
algo := "crc64"
|
||||||
|
h, err := New(algo)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, !h.IsSecure(), algo+" shouldn't be a secure hash")
|
||||||
|
assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo")
|
||||||
|
assert.BoolT(t, h.IsHash64(), algo+" is actually a 64-bit hash")
|
||||||
|
assert.BoolT(t, !h.IsHash32(), algo+" isn't actually a 32-bit hash")
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var expected uint64
|
||||||
|
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok := h.Sum64()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum64")
|
||||||
|
assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected))
|
||||||
|
|
||||||
|
data = []byte("hello, world")
|
||||||
|
expected = 0x16c45c0eb1d9c2ec
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok = h.Sum64()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum64")
|
||||||
|
assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected))
|
||||||
|
|
||||||
|
h.Reset()
|
||||||
|
data = []byte("hello world")
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok = h.Sum64()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum64")
|
||||||
|
assert.BoolT(t, expected != sum, fmt.Sprintf("%s returned %d but shouldn't have", algo, sum))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListLengthSanity(t *testing.T) {
|
||||||
|
all := HashList()
|
||||||
|
secure := SecureHashList()
|
||||||
|
insecure := InsecureHashList()
|
||||||
|
|
||||||
|
assert.BoolT(t, len(all) == len(secure)+len(insecure))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSumLimitedReader(t *testing.T) {
|
||||||
|
data := bytes.NewBufferString("hello, world")
|
||||||
|
dataLen := data.Len()
|
||||||
|
extendedData := bytes.NewBufferString("hello, world! this is an extended message")
|
||||||
|
expected := "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b"
|
||||||
|
|
||||||
|
hash, err := SumReader("sha256", data)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", hash) == expected, fmt.Sprintf("have hash %x, want %s", hash, expected))
|
||||||
|
|
||||||
|
extendedHash, err := SumLimitedReader("sha256", extendedData, int64(dataLen))
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
|
||||||
|
assert.BoolT(t, bytes.Equal(hash, extendedHash), fmt.Sprintf("have hash %x, want %x", extendedHash, hash))
|
||||||
|
}
|
||||||
8
assert/BUILD.bazel
Normal file
8
assert/BUILD.bazel
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "assert",
|
||||||
|
srcs = ["assert.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/assert",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NoDebug, if set to true, will cause all asserts to be ignored.
|
// NoDebug can be set to true to cause all asserts to be ignored.
|
||||||
var NoDebug bool
|
var NoDebug bool
|
||||||
|
|
||||||
func die(what string, a ...string) {
|
func die(what string, a ...string) {
|
||||||
@@ -94,7 +94,7 @@ func NoError(err error, s ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if nil != err {
|
if nil != err {
|
||||||
die(err.Error())
|
die(err.Error(), s...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,5 +170,5 @@ func ErrorEqT(t *testing.T, expected, actual error) {
|
|||||||
should = fmt.Sprintf("have '%s'", actual)
|
should = fmt.Sprintf("have '%s'", actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
die(fmt.Sprintf("assert.Error2: expected '%s', but %s", expected, should))
|
t.Fatalf("assert.Error2: expected '%s', but %s", expected, should)
|
||||||
}
|
}
|
||||||
|
|||||||
30
certlib/BUILD.bazel
Normal file
30
certlib/BUILD.bazel
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "certlib",
|
||||||
|
srcs = [
|
||||||
|
"certlib.go",
|
||||||
|
"der_helpers.go",
|
||||||
|
"ed25519.go",
|
||||||
|
"helpers.go",
|
||||||
|
],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/certlib",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//certlib/certerr",
|
||||||
|
"//certlib/pkcs7",
|
||||||
|
"@com_github_google_certificate_transparency_go//:certificate-transparency-go",
|
||||||
|
"@com_github_google_certificate_transparency_go//tls",
|
||||||
|
"@com_github_google_certificate_transparency_go//x509",
|
||||||
|
"@org_golang_x_crypto//ocsp",
|
||||||
|
"@org_golang_x_crypto//pkcs12",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "certlib_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["certlib_test.go"],
|
||||||
|
embed = [":certlib"],
|
||||||
|
deps = ["//assert"],
|
||||||
|
)
|
||||||
8
certlib/certerr/BUILD.bazel
Normal file
8
certlib/certerr/BUILD.bazel
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "certerr",
|
||||||
|
srcs = ["errors.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/certlib/certerr",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
79
certlib/certerr/errors.go
Normal file
79
certlib/certerr/errors.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package certerr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrEmptyCertificate indicates that a certificate could not be processed
|
||||||
|
// because there was no data to process.
|
||||||
|
var ErrEmptyCertificate = errors.New("certlib: empty certificate")
|
||||||
|
|
||||||
|
type ErrorSourceType uint8
|
||||||
|
|
||||||
|
func (t ErrorSourceType) String() string {
|
||||||
|
switch t {
|
||||||
|
case ErrorSourceCertificate:
|
||||||
|
return "certificate"
|
||||||
|
case ErrorSourcePrivateKey:
|
||||||
|
return "private key"
|
||||||
|
case ErrorSourceCSR:
|
||||||
|
return "CSR"
|
||||||
|
case ErrorSourceSCTList:
|
||||||
|
return "SCT list"
|
||||||
|
case ErrorSourceKeypair:
|
||||||
|
return "TLS keypair"
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown error source %d", t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrorSourceCertificate ErrorSourceType = 1
|
||||||
|
ErrorSourcePrivateKey ErrorSourceType = 2
|
||||||
|
ErrorSourceCSR ErrorSourceType = 3
|
||||||
|
ErrorSourceSCTList ErrorSourceType = 4
|
||||||
|
ErrorSourceKeypair ErrorSourceType = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// InvalidPEMType is used to indicate that we were expecting one type of PEM
|
||||||
|
// file, but saw another.
|
||||||
|
type InvalidPEMType struct {
|
||||||
|
have string
|
||||||
|
want []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *InvalidPEMType) Error() string {
|
||||||
|
if len(err.want) == 1 {
|
||||||
|
return fmt.Sprintf("invalid PEM type: have %s, expected %s", err.have, err.want[0])
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("invalid PEM type: have %s, expected one of %s", err.have, strings.Join(err.want, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidPEMType returns a new InvalidPEMType error.
|
||||||
|
func ErrInvalidPEMType(have string, want ...string) error {
|
||||||
|
return &InvalidPEMType{
|
||||||
|
have: have,
|
||||||
|
want: want,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadingError(t ErrorSourceType, err error) error {
|
||||||
|
return fmt.Errorf("failed to load %s from disk: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParsingError(t ErrorSourceType, err error) error {
|
||||||
|
return fmt.Errorf("failed to parse %s: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeError(t ErrorSourceType, err error) error {
|
||||||
|
return fmt.Errorf("failed to decode %s: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyError(t ErrorSourceType, err error) error {
|
||||||
|
return fmt.Errorf("failed to verify %s: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrEncryptedPrivateKey = errors.New("private key is encrypted")
|
||||||
85
certlib/certlib.go
Normal file
85
certlib/certlib.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package certlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib/certerr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadCertificate reads a DER or PEM-encoded certificate from the
|
||||||
|
// byte slice.
|
||||||
|
func ReadCertificate(in []byte) (cert *x509.Certificate, rest []byte, err error) {
|
||||||
|
if len(in) == 0 {
|
||||||
|
err = certerr.ErrEmptyCertificate
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if in[0] == '-' {
|
||||||
|
p, remaining := pem.Decode(in)
|
||||||
|
if p == nil {
|
||||||
|
err = errors.New("certlib: invalid PEM file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rest = remaining
|
||||||
|
if p.Type != "CERTIFICATE" {
|
||||||
|
err = certerr.ErrInvalidPEMType(p.Type, "CERTIFICATE")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
in = p.Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err = x509.ParseCertificate(in)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadCertificates tries to read all the certificates in a
|
||||||
|
// PEM-encoded collection.
|
||||||
|
func ReadCertificates(in []byte) (certs []*x509.Certificate, err error) {
|
||||||
|
var cert *x509.Certificate
|
||||||
|
for {
|
||||||
|
cert, in, err = ReadCertificate(in)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if cert == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
certs = append(certs, cert)
|
||||||
|
if len(in) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCertificate tries to read a single certificate from disk. If
|
||||||
|
// the file contains multiple certificates (e.g. a chain), only the
|
||||||
|
// first certificate is returned.
|
||||||
|
func LoadCertificate(path string) (*x509.Certificate, error) {
|
||||||
|
in, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, _, err := ReadCertificate(in)
|
||||||
|
return cert, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCertificates tries to read all the certificates in a file,
|
||||||
|
// returning them in the order that it found them in the file.
|
||||||
|
func LoadCertificates(path string) ([]*x509.Certificate, error) {
|
||||||
|
in, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadCertificates(in)
|
||||||
|
}
|
||||||
139
certlib/certlib_test.go
Normal file
139
certlib/certlib_test.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package certlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// some CA certs I found on my computerbox.
|
||||||
|
var testCerts = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE
|
||||||
|
AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw
|
||||||
|
CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ
|
||||||
|
BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND
|
||||||
|
VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb
|
||||||
|
qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY
|
||||||
|
HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo
|
||||||
|
G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA
|
||||||
|
lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr
|
||||||
|
IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/
|
||||||
|
0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH
|
||||||
|
k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47
|
||||||
|
4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO
|
||||||
|
m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa
|
||||||
|
cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl
|
||||||
|
uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI
|
||||||
|
KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls
|
||||||
|
ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG
|
||||||
|
AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2
|
||||||
|
VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT
|
||||||
|
VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG
|
||||||
|
CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA
|
||||||
|
cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA
|
||||||
|
QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA
|
||||||
|
7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA
|
||||||
|
cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA
|
||||||
|
QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA
|
||||||
|
czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu
|
||||||
|
aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt
|
||||||
|
aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud
|
||||||
|
DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF
|
||||||
|
BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp
|
||||||
|
D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU
|
||||||
|
JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m
|
||||||
|
AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD
|
||||||
|
vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms
|
||||||
|
tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH
|
||||||
|
7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h
|
||||||
|
I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA
|
||||||
|
h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF
|
||||||
|
d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H
|
||||||
|
pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE
|
||||||
|
AwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x
|
||||||
|
CzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW
|
||||||
|
MBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF
|
||||||
|
RElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
|
||||||
|
AgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7
|
||||||
|
09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7
|
||||||
|
XBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P
|
||||||
|
Grjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK
|
||||||
|
t0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb
|
||||||
|
X79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28
|
||||||
|
MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU
|
||||||
|
fecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI
|
||||||
|
2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH
|
||||||
|
K9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae
|
||||||
|
ZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP
|
||||||
|
BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ
|
||||||
|
MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw
|
||||||
|
RAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv
|
||||||
|
bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm
|
||||||
|
fQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3
|
||||||
|
gvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe
|
||||||
|
I6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i
|
||||||
|
5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi
|
||||||
|
ipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn
|
||||||
|
MCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ
|
||||||
|
o5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6
|
||||||
|
zqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN
|
||||||
|
GHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt
|
||||||
|
r0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK
|
||||||
|
Z05phkOTOPu220+DkdRgfks+KzgHVZhepA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE
|
||||||
|
BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w
|
||||||
|
MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290
|
||||||
|
IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC
|
||||||
|
SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1
|
||||||
|
ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB
|
||||||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv
|
||||||
|
UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX
|
||||||
|
4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9
|
||||||
|
KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/
|
||||||
|
gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb
|
||||||
|
rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ
|
||||||
|
51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F
|
||||||
|
be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe
|
||||||
|
KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F
|
||||||
|
v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn
|
||||||
|
fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7
|
||||||
|
jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz
|
||||||
|
ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt
|
||||||
|
ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL
|
||||||
|
e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70
|
||||||
|
jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz
|
||||||
|
WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V
|
||||||
|
SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j
|
||||||
|
pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX
|
||||||
|
X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok
|
||||||
|
fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R
|
||||||
|
K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU
|
||||||
|
ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU
|
||||||
|
LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT
|
||||||
|
LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestReadCertificate(t *testing.T) {
|
||||||
|
cert, remaining, err := ReadCertificate([]byte(testCerts))
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
|
||||||
|
assert.BoolT(t, len(remaining) > 0, "lib: expected extra data from ReadCertificate")
|
||||||
|
assert.BoolT(t, cert != nil, "lib: expected an actual certificate to have been returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadCertificates(t *testing.T) {
|
||||||
|
certs, err := ReadCertificates([]byte(testCerts))
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
|
||||||
|
assert.BoolT(t, len(certs) == 3, fmt.Sprintf("lib: expected three certificates, have %d", len(certs)))
|
||||||
|
for _, cert := range certs {
|
||||||
|
assert.BoolT(t, cert != nil, "lib: expected an actual certificate to have been returned")
|
||||||
|
}
|
||||||
|
}
|
||||||
75
certlib/der_helpers.go
Normal file
75
certlib/der_helpers.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package certlib
|
||||||
|
|
||||||
|
// Originally from CFSSL, mostly written by me originally, and licensed under:
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014 CloudFlare Inc.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// I've modified it for use in my own code e.g. by removing the CFSSL errors
|
||||||
|
// and replacing them with sane ones.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib/certerr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParsePrivateKeyDER parses a PKCS #1, PKCS #8, ECDSA, or Ed25519 DER-encoded
|
||||||
|
// private key. The key must not be in PEM format. If an error is returned, it
|
||||||
|
// may contain information about the private key, so care should be taken when
|
||||||
|
// displaying it directly.
|
||||||
|
func ParsePrivateKeyDER(keyDER []byte) (key crypto.Signer, err error) {
|
||||||
|
generalKey, err := x509.ParsePKCS8PrivateKey(keyDER)
|
||||||
|
if err != nil {
|
||||||
|
generalKey, err = x509.ParsePKCS1PrivateKey(keyDER)
|
||||||
|
if err != nil {
|
||||||
|
generalKey, err = x509.ParseECPrivateKey(keyDER)
|
||||||
|
if err != nil {
|
||||||
|
generalKey, err = ParseEd25519PrivateKey(keyDER)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourcePrivateKey, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch generalKey := generalKey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return generalKey, nil
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return generalKey, nil
|
||||||
|
case ed25519.PrivateKey:
|
||||||
|
return generalKey, nil
|
||||||
|
default:
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourcePrivateKey, fmt.Errorf("unknown key type %t", generalKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
164
certlib/ed25519.go
Normal file
164
certlib/ed25519.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package certlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Originally from CFSSL, mostly written by me originally, and licensed under:
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014 CloudFlare Inc.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// I've modified it for use in my own code e.g. by removing the CFSSL errors
|
||||||
|
// and replacing them with sane ones.
|
||||||
|
|
||||||
|
var errEd25519WrongID = errors.New("incorrect object identifier")
|
||||||
|
var errEd25519WrongKeyType = errors.New("incorrect key type")
|
||||||
|
|
||||||
|
// ed25519OID is the OID for the Ed25519 signature scheme: see
|
||||||
|
// https://datatracker.ietf.org/doc/draft-ietf-curdle-pkix-04.
|
||||||
|
var ed25519OID = asn1.ObjectIdentifier{1, 3, 101, 112}
|
||||||
|
|
||||||
|
// subjectPublicKeyInfo reflects the ASN.1 object defined in the X.509 standard.
|
||||||
|
//
|
||||||
|
// This is defined in crypto/x509 as "publicKeyInfo".
|
||||||
|
type subjectPublicKeyInfo struct {
|
||||||
|
Algorithm pkix.AlgorithmIdentifier
|
||||||
|
PublicKey asn1.BitString
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalEd25519PublicKey creates a DER-encoded SubjectPublicKeyInfo for an
|
||||||
|
// ed25519 public key, as defined in
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04. This is analogous to
|
||||||
|
// MarshalPKIXPublicKey in crypto/x509, which doesn't currently support Ed25519.
|
||||||
|
func MarshalEd25519PublicKey(pk crypto.PublicKey) ([]byte, error) {
|
||||||
|
pub, ok := pk.(ed25519.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errEd25519WrongKeyType
|
||||||
|
}
|
||||||
|
|
||||||
|
spki := subjectPublicKeyInfo{
|
||||||
|
Algorithm: pkix.AlgorithmIdentifier{
|
||||||
|
Algorithm: ed25519OID,
|
||||||
|
},
|
||||||
|
PublicKey: asn1.BitString{
|
||||||
|
BitLength: len(pub) * 8,
|
||||||
|
Bytes: pub,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return asn1.Marshal(spki)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseEd25519PublicKey returns the Ed25519 public key encoded by the input.
|
||||||
|
func ParseEd25519PublicKey(der []byte) (crypto.PublicKey, error) {
|
||||||
|
var spki subjectPublicKeyInfo
|
||||||
|
if rest, err := asn1.Unmarshal(der, &spki); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(rest) > 0 {
|
||||||
|
return nil, errors.New("SubjectPublicKeyInfo too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !spki.Algorithm.Algorithm.Equal(ed25519OID) {
|
||||||
|
return nil, errEd25519WrongID
|
||||||
|
}
|
||||||
|
|
||||||
|
if spki.PublicKey.BitLength != ed25519.PublicKeySize*8 {
|
||||||
|
return nil, errors.New("SubjectPublicKeyInfo PublicKey length mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ed25519.PublicKey(spki.PublicKey.Bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// oneAsymmetricKey reflects the ASN.1 structure for storing private keys in
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04, excluding the optional
|
||||||
|
// fields, which we don't use here.
|
||||||
|
//
|
||||||
|
// This is identical to pkcs8 in crypto/x509.
|
||||||
|
type oneAsymmetricKey struct {
|
||||||
|
Version int
|
||||||
|
Algorithm pkix.AlgorithmIdentifier
|
||||||
|
PrivateKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// curvePrivateKey is the innter type of the PrivateKey field of
|
||||||
|
// oneAsymmetricKey.
|
||||||
|
type curvePrivateKey []byte
|
||||||
|
|
||||||
|
// MarshalEd25519PrivateKey returns a DER encoding of the input private key as
|
||||||
|
// specified in https://tools.ietf.org/html/draft-ietf-curdle-pkix-04.
|
||||||
|
func MarshalEd25519PrivateKey(sk crypto.PrivateKey) ([]byte, error) {
|
||||||
|
priv, ok := sk.(ed25519.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errEd25519WrongKeyType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the innter CurvePrivateKey.
|
||||||
|
curvePrivateKey, err := asn1.Marshal(priv.Seed())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the OneAsymmetricKey.
|
||||||
|
asym := oneAsymmetricKey{
|
||||||
|
Version: 0,
|
||||||
|
Algorithm: pkix.AlgorithmIdentifier{
|
||||||
|
Algorithm: ed25519OID,
|
||||||
|
},
|
||||||
|
PrivateKey: curvePrivateKey,
|
||||||
|
}
|
||||||
|
return asn1.Marshal(asym)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseEd25519PrivateKey returns the Ed25519 private key encoded by the input.
|
||||||
|
func ParseEd25519PrivateKey(der []byte) (crypto.PrivateKey, error) {
|
||||||
|
asym := new(oneAsymmetricKey)
|
||||||
|
if rest, err := asn1.Unmarshal(der, asym); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(rest) > 0 {
|
||||||
|
return nil, errors.New("OneAsymmetricKey too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the key type is correct.
|
||||||
|
if !asym.Algorithm.Algorithm.Equal(ed25519OID) {
|
||||||
|
return nil, errEd25519WrongID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal the inner CurvePrivateKey.
|
||||||
|
seed := new(curvePrivateKey)
|
||||||
|
if rest, err := asn1.Unmarshal(asym.PrivateKey, seed); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(rest) > 0 {
|
||||||
|
return nil, errors.New("CurvePrivateKey too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ed25519.NewKeyFromSeed(*seed), nil
|
||||||
|
}
|
||||||
630
certlib/helpers.go
Normal file
630
certlib/helpers.go
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
package certlib
|
||||||
|
|
||||||
|
// Originally from CFSSL, mostly written by me originally, and licensed under:
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014 CloudFlare Inc.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// I've modified it for use in my own code e.g. by removing the CFSSL errors
|
||||||
|
// and replacing them with sane ones.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib/certerr"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib/pkcs7"
|
||||||
|
|
||||||
|
ct "github.com/google/certificate-transparency-go"
|
||||||
|
cttls "github.com/google/certificate-transparency-go/tls"
|
||||||
|
ctx509 "github.com/google/certificate-transparency-go/x509"
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
|
"golang.org/x/crypto/pkcs12"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OneYear is a time.Duration representing a year's worth of seconds.
|
||||||
|
const OneYear = 8760 * time.Hour
|
||||||
|
|
||||||
|
// OneDay is a time.Duration representing a day's worth of seconds.
|
||||||
|
const OneDay = 24 * time.Hour
|
||||||
|
|
||||||
|
// DelegationUsage is the OID for the DelegationUseage extensions
|
||||||
|
var DelegationUsage = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44363, 44}
|
||||||
|
|
||||||
|
// DelegationExtension
|
||||||
|
var DelegationExtension = pkix.Extension{
|
||||||
|
Id: DelegationUsage,
|
||||||
|
Critical: false,
|
||||||
|
Value: []byte{0x05, 0x00}, // ASN.1 NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// InclusiveDate returns the time.Time representation of a date - 1
|
||||||
|
// nanosecond. This allows time.After to be used inclusively.
|
||||||
|
func InclusiveDate(year int, month time.Month, day int) time.Time {
|
||||||
|
return time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Add(-1 * time.Nanosecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jul2012 is the July 2012 CAB Forum deadline for when CAs must stop
|
||||||
|
// issuing certificates valid for more than 5 years.
|
||||||
|
var Jul2012 = InclusiveDate(2012, time.July, 01)
|
||||||
|
|
||||||
|
// Apr2015 is the April 2015 CAB Forum deadline for when CAs must stop
|
||||||
|
// issuing certificates valid for more than 39 months.
|
||||||
|
var Apr2015 = InclusiveDate(2015, time.April, 01)
|
||||||
|
|
||||||
|
// KeyLength returns the bit size of ECDSA or RSA PublicKey
|
||||||
|
func KeyLength(key interface{}) int {
|
||||||
|
if key == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if ecdsaKey, ok := key.(*ecdsa.PublicKey); ok {
|
||||||
|
return ecdsaKey.Curve.Params().BitSize
|
||||||
|
} else if rsaKey, ok := key.(*rsa.PublicKey); ok {
|
||||||
|
return rsaKey.N.BitLen()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiryTime returns the time when the certificate chain is expired.
|
||||||
|
func ExpiryTime(chain []*x509.Certificate) (notAfter time.Time) {
|
||||||
|
if len(chain) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notAfter = chain[0].NotAfter
|
||||||
|
for _, cert := range chain {
|
||||||
|
if notAfter.After(cert.NotAfter) {
|
||||||
|
notAfter = cert.NotAfter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonthsValid returns the number of months for which a certificate is valid.
|
||||||
|
func MonthsValid(c *x509.Certificate) int {
|
||||||
|
issued := c.NotBefore
|
||||||
|
expiry := c.NotAfter
|
||||||
|
years := (expiry.Year() - issued.Year())
|
||||||
|
months := years*12 + int(expiry.Month()) - int(issued.Month())
|
||||||
|
|
||||||
|
// Round up if valid for less than a full month
|
||||||
|
if expiry.Day() > issued.Day() {
|
||||||
|
months++
|
||||||
|
}
|
||||||
|
return months
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidExpiry determines if a certificate is valid for an acceptable
|
||||||
|
// length of time per the CA/Browser Forum baseline requirements.
|
||||||
|
// See https://cabforum.org/wp-content/uploads/CAB-Forum-BR-1.3.0.pdf
|
||||||
|
func ValidExpiry(c *x509.Certificate) bool {
|
||||||
|
issued := c.NotBefore
|
||||||
|
|
||||||
|
var maxMonths int
|
||||||
|
switch {
|
||||||
|
case issued.After(Apr2015):
|
||||||
|
maxMonths = 39
|
||||||
|
case issued.After(Jul2012):
|
||||||
|
maxMonths = 60
|
||||||
|
case issued.Before(Jul2012):
|
||||||
|
maxMonths = 120
|
||||||
|
}
|
||||||
|
|
||||||
|
if MonthsValid(c) > maxMonths {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureString returns the TLS signature string corresponding to
|
||||||
|
// an X509 signature algorithm.
|
||||||
|
func SignatureString(alg x509.SignatureAlgorithm) string {
|
||||||
|
switch alg {
|
||||||
|
case x509.MD2WithRSA:
|
||||||
|
return "MD2WithRSA"
|
||||||
|
case x509.MD5WithRSA:
|
||||||
|
return "MD5WithRSA"
|
||||||
|
case x509.SHA1WithRSA:
|
||||||
|
return "SHA1WithRSA"
|
||||||
|
case x509.SHA256WithRSA:
|
||||||
|
return "SHA256WithRSA"
|
||||||
|
case x509.SHA384WithRSA:
|
||||||
|
return "SHA384WithRSA"
|
||||||
|
case x509.SHA512WithRSA:
|
||||||
|
return "SHA512WithRSA"
|
||||||
|
case x509.DSAWithSHA1:
|
||||||
|
return "DSAWithSHA1"
|
||||||
|
case x509.DSAWithSHA256:
|
||||||
|
return "DSAWithSHA256"
|
||||||
|
case x509.ECDSAWithSHA1:
|
||||||
|
return "ECDSAWithSHA1"
|
||||||
|
case x509.ECDSAWithSHA256:
|
||||||
|
return "ECDSAWithSHA256"
|
||||||
|
case x509.ECDSAWithSHA384:
|
||||||
|
return "ECDSAWithSHA384"
|
||||||
|
case x509.ECDSAWithSHA512:
|
||||||
|
return "ECDSAWithSHA512"
|
||||||
|
default:
|
||||||
|
return "Unknown Signature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashAlgoString returns the hash algorithm name contains in the signature
|
||||||
|
// method.
|
||||||
|
func HashAlgoString(alg x509.SignatureAlgorithm) string {
|
||||||
|
switch alg {
|
||||||
|
case x509.MD2WithRSA:
|
||||||
|
return "MD2"
|
||||||
|
case x509.MD5WithRSA:
|
||||||
|
return "MD5"
|
||||||
|
case x509.SHA1WithRSA:
|
||||||
|
return "SHA1"
|
||||||
|
case x509.SHA256WithRSA:
|
||||||
|
return "SHA256"
|
||||||
|
case x509.SHA384WithRSA:
|
||||||
|
return "SHA384"
|
||||||
|
case x509.SHA512WithRSA:
|
||||||
|
return "SHA512"
|
||||||
|
case x509.DSAWithSHA1:
|
||||||
|
return "SHA1"
|
||||||
|
case x509.DSAWithSHA256:
|
||||||
|
return "SHA256"
|
||||||
|
case x509.ECDSAWithSHA1:
|
||||||
|
return "SHA1"
|
||||||
|
case x509.ECDSAWithSHA256:
|
||||||
|
return "SHA256"
|
||||||
|
case x509.ECDSAWithSHA384:
|
||||||
|
return "SHA384"
|
||||||
|
case x509.ECDSAWithSHA512:
|
||||||
|
return "SHA512"
|
||||||
|
default:
|
||||||
|
return "Unknown Hash Algorithm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringTLSVersion returns underlying enum values from human names for TLS
|
||||||
|
// versions, defaults to current golang default of TLS 1.0
|
||||||
|
func StringTLSVersion(version string) uint16 {
|
||||||
|
switch version {
|
||||||
|
case "1.2":
|
||||||
|
return tls.VersionTLS12
|
||||||
|
case "1.1":
|
||||||
|
return tls.VersionTLS11
|
||||||
|
default:
|
||||||
|
return tls.VersionTLS10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeCertificatesPEM encodes a number of x509 certificates to PEM
|
||||||
|
func EncodeCertificatesPEM(certs []*x509.Certificate) []byte {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for _, cert := range certs {
|
||||||
|
pem.Encode(&buffer, &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: cert.Raw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeCertificatePEM encodes a single x509 certificates to PEM
|
||||||
|
func EncodeCertificatePEM(cert *x509.Certificate) []byte {
|
||||||
|
return EncodeCertificatesPEM([]*x509.Certificate{cert})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCertificatesPEM parses a sequence of PEM-encoded certificate and returns them,
|
||||||
|
// can handle PEM encoded PKCS #7 structures.
|
||||||
|
func ParseCertificatesPEM(certsPEM []byte) ([]*x509.Certificate, error) {
|
||||||
|
var certs []*x509.Certificate
|
||||||
|
var err error
|
||||||
|
certsPEM = bytes.TrimSpace(certsPEM)
|
||||||
|
for len(certsPEM) > 0 {
|
||||||
|
var cert []*x509.Certificate
|
||||||
|
cert, certsPEM, err = ParseOneCertificateFromPEM(certsPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
|
||||||
|
} else if cert == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
certs = append(certs, cert...)
|
||||||
|
}
|
||||||
|
if len(certsPEM) > 0 {
|
||||||
|
return nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("trailing data at end of certificate"))
|
||||||
|
}
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCertificatesDER parses a DER encoding of a certificate object and possibly private key,
|
||||||
|
// either PKCS #7, PKCS #12, or raw x509.
|
||||||
|
func ParseCertificatesDER(certsDER []byte, password string) (certs []*x509.Certificate, key crypto.Signer, err error) {
|
||||||
|
certsDER = bytes.TrimSpace(certsDER)
|
||||||
|
pkcs7data, err := pkcs7.ParsePKCS7(certsDER)
|
||||||
|
if err != nil {
|
||||||
|
var pkcs12data interface{}
|
||||||
|
certs = make([]*x509.Certificate, 1)
|
||||||
|
pkcs12data, certs[0], err = pkcs12.Decode(certsDER, password)
|
||||||
|
if err != nil {
|
||||||
|
certs, err = x509.ParseCertificates(certsDER)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
key = pkcs12data.(crypto.Signer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pkcs7data.ContentInfo != "SignedData" {
|
||||||
|
return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("can only extract certificates from signed data content info"))
|
||||||
|
}
|
||||||
|
certs = pkcs7data.Content.SignedData.Certificates
|
||||||
|
}
|
||||||
|
if certs == nil {
|
||||||
|
return nil, key, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificates decoded"))
|
||||||
|
}
|
||||||
|
return certs, key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseSelfSignedCertificatePEM parses a PEM-encoded certificate and check if it is self-signed.
|
||||||
|
func ParseSelfSignedCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
|
||||||
|
cert, err := ParseCertificatePEM(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil {
|
||||||
|
return nil, certerr.VerifyError(certerr.ErrorSourceCertificate, err)
|
||||||
|
}
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCertificatePEM parses and returns a PEM-encoded certificate,
|
||||||
|
// can handle PEM encoded PKCS #7 structures.
|
||||||
|
func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
|
||||||
|
certPEM = bytes.TrimSpace(certPEM)
|
||||||
|
cert, rest, err := ParseOneCertificateFromPEM(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
|
||||||
|
} else if cert == nil {
|
||||||
|
return nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificate decoded"))
|
||||||
|
} else if len(rest) > 0 {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("the PEM file should contain only one object"))
|
||||||
|
} else if len(cert) > 1 {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("the PKCS7 object in the PEM file should contain only one certificate"))
|
||||||
|
}
|
||||||
|
return cert[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseOneCertificateFromPEM attempts to parse one PEM encoded certificate object,
|
||||||
|
// either a raw x509 certificate or a PKCS #7 structure possibly containing
|
||||||
|
// multiple certificates, from the top of certsPEM, which itself may
|
||||||
|
// contain multiple PEM encoded certificate objects.
|
||||||
|
func ParseOneCertificateFromPEM(certsPEM []byte) ([]*x509.Certificate, []byte, error) {
|
||||||
|
|
||||||
|
block, rest := pem.Decode(certsPEM)
|
||||||
|
if block == nil {
|
||||||
|
return nil, rest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
pkcs7data, err := pkcs7.ParsePKCS7(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, rest, err
|
||||||
|
}
|
||||||
|
if pkcs7data.ContentInfo != "SignedData" {
|
||||||
|
return nil, rest, errors.New("only PKCS #7 Signed Data Content Info supported for certificate parsing")
|
||||||
|
}
|
||||||
|
certs := pkcs7data.Content.SignedData.Certificates
|
||||||
|
if certs == nil {
|
||||||
|
return nil, rest, errors.New("PKCS #7 structure contains no certificates")
|
||||||
|
}
|
||||||
|
return certs, rest, nil
|
||||||
|
}
|
||||||
|
var certs = []*x509.Certificate{cert}
|
||||||
|
return certs, rest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPEMCertPool loads a pool of PEM certificates from file.
|
||||||
|
func LoadPEMCertPool(certsFile string) (*x509.CertPool, error) {
|
||||||
|
if certsFile == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
pemCerts, err := os.ReadFile(certsFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return PEMToCertPool(pemCerts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PEMToCertPool concerts PEM certificates to a CertPool.
|
||||||
|
func PEMToCertPool(pemCerts []byte) (*x509.CertPool, error) {
|
||||||
|
if len(pemCerts) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
certPool := x509.NewCertPool()
|
||||||
|
if !certPool.AppendCertsFromPEM(pemCerts) {
|
||||||
|
return nil, errors.New("failed to load cert pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
return certPool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePrivateKeyPEM parses and returns a PEM-encoded private
|
||||||
|
// key. The private key may be either an unencrypted PKCS#8, PKCS#1,
|
||||||
|
// or elliptic private key.
|
||||||
|
func ParsePrivateKeyPEM(keyPEM []byte) (key crypto.Signer, err error) {
|
||||||
|
return ParsePrivateKeyPEMWithPassword(keyPEM, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePrivateKeyPEMWithPassword parses and returns a PEM-encoded private
|
||||||
|
// key. The private key may be a potentially encrypted PKCS#8, PKCS#1,
|
||||||
|
// or elliptic private key.
|
||||||
|
func ParsePrivateKeyPEMWithPassword(keyPEM []byte, password []byte) (key crypto.Signer, err error) {
|
||||||
|
keyDER, err := GetKeyDERFromPEM(keyPEM, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParsePrivateKeyDER(keyDER)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyDERFromPEM parses a PEM-encoded private key and returns DER-format key bytes.
|
||||||
|
func GetKeyDERFromPEM(in []byte, password []byte) ([]byte, error) {
|
||||||
|
// Ignore any EC PARAMETERS blocks when looking for a key (openssl includes
|
||||||
|
// them by default).
|
||||||
|
var keyDER *pem.Block
|
||||||
|
for {
|
||||||
|
keyDER, in = pem.Decode(in)
|
||||||
|
if keyDER == nil || keyDER.Type != "EC PARAMETERS" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if keyDER != nil {
|
||||||
|
if procType, ok := keyDER.Headers["Proc-Type"]; ok {
|
||||||
|
if strings.Contains(procType, "ENCRYPTED") {
|
||||||
|
if password != nil {
|
||||||
|
return x509.DecryptPEMBlock(keyDER, password)
|
||||||
|
}
|
||||||
|
return nil, certerr.DecodeError(certerr.ErrorSourcePrivateKey, certerr.ErrEncryptedPrivateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keyDER.Bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, certerr.DecodeError(certerr.ErrorSourcePrivateKey, errors.New("failed to decode private key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCSR parses a PEM- or DER-encoded PKCS #10 certificate signing request.
|
||||||
|
func ParseCSR(in []byte) (csr *x509.CertificateRequest, rest []byte, err error) {
|
||||||
|
in = bytes.TrimSpace(in)
|
||||||
|
p, rest := pem.Decode(in)
|
||||||
|
if p != nil {
|
||||||
|
if p.Type != "NEW CERTIFICATE REQUEST" && p.Type != "CERTIFICATE REQUEST" {
|
||||||
|
return nil, rest, certerr.ParsingError(certerr.ErrorSourceCSR, certerr.ErrInvalidPEMType(p.Type, "NEW CERTIFICATE REQUEST", "CERTIFICATE REQUEST"))
|
||||||
|
}
|
||||||
|
|
||||||
|
csr, err = x509.ParseCertificateRequest(p.Bytes)
|
||||||
|
} else {
|
||||||
|
csr, err = x509.ParseCertificateRequest(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, rest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = csr.CheckSignature()
|
||||||
|
if err != nil {
|
||||||
|
return nil, rest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return csr, rest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCSRPEM parses a PEM-encoded certificate signing request.
|
||||||
|
// It does not check the signature. This is useful for dumping data from a CSR
|
||||||
|
// locally.
|
||||||
|
func ParseCSRPEM(csrPEM []byte) (*x509.CertificateRequest, error) {
|
||||||
|
block, _ := pem.Decode([]byte(csrPEM))
|
||||||
|
if block == nil {
|
||||||
|
return nil, certerr.DecodeError(certerr.ErrorSourceCSR, errors.New("PEM block is empty"))
|
||||||
|
}
|
||||||
|
csrObject, err := x509.ParseCertificateRequest(block.Bytes)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return csrObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignerAlgo returns an X.509 signature algorithm from a crypto.Signer.
|
||||||
|
func SignerAlgo(priv crypto.Signer) x509.SignatureAlgorithm {
|
||||||
|
switch pub := priv.Public().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
bitLength := pub.N.BitLen()
|
||||||
|
switch {
|
||||||
|
case bitLength >= 4096:
|
||||||
|
return x509.SHA512WithRSA
|
||||||
|
case bitLength >= 3072:
|
||||||
|
return x509.SHA384WithRSA
|
||||||
|
case bitLength >= 2048:
|
||||||
|
return x509.SHA256WithRSA
|
||||||
|
default:
|
||||||
|
return x509.SHA1WithRSA
|
||||||
|
}
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
switch pub.Curve {
|
||||||
|
case elliptic.P521():
|
||||||
|
return x509.ECDSAWithSHA512
|
||||||
|
case elliptic.P384():
|
||||||
|
return x509.ECDSAWithSHA384
|
||||||
|
case elliptic.P256():
|
||||||
|
return x509.ECDSAWithSHA256
|
||||||
|
default:
|
||||||
|
return x509.ECDSAWithSHA1
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return x509.UnknownSignatureAlgorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadClientCertificate load key/certificate from pem files
|
||||||
|
func LoadClientCertificate(certFile string, keyFile string) (*tls.Certificate, error) {
|
||||||
|
if certFile != "" && keyFile != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.LoadingError(certerr.ErrorSourceKeypair, err)
|
||||||
|
}
|
||||||
|
return &cert, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTLSConfig creates a tls.Config object from certs and roots
|
||||||
|
func CreateTLSConfig(remoteCAs *x509.CertPool, cert *tls.Certificate) *tls.Config {
|
||||||
|
var certs []tls.Certificate
|
||||||
|
if cert != nil {
|
||||||
|
certs = []tls.Certificate{*cert}
|
||||||
|
}
|
||||||
|
return &tls.Config{
|
||||||
|
Certificates: certs,
|
||||||
|
RootCAs: remoteCAs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializeSCTList serializes a list of SCTs.
|
||||||
|
func SerializeSCTList(sctList []ct.SignedCertificateTimestamp) ([]byte, error) {
|
||||||
|
list := ctx509.SignedCertificateTimestampList{}
|
||||||
|
for _, sct := range sctList {
|
||||||
|
sctBytes, err := cttls.Marshal(sct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list.SCTList = append(list.SCTList, ctx509.SerializedSCT{Val: sctBytes})
|
||||||
|
}
|
||||||
|
return cttls.Marshal(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeserializeSCTList deserializes a list of SCTs.
|
||||||
|
func DeserializeSCTList(serializedSCTList []byte) ([]ct.SignedCertificateTimestamp, error) {
|
||||||
|
var sctList ctx509.SignedCertificateTimestampList
|
||||||
|
rest, err := cttls.Unmarshal(serializedSCTList, &sctList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rest) != 0 {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceSCTList, errors.New("serialized SCT list contained trailing garbage"))
|
||||||
|
}
|
||||||
|
|
||||||
|
list := make([]ct.SignedCertificateTimestamp, len(sctList.SCTList))
|
||||||
|
for i, serializedSCT := range sctList.SCTList {
|
||||||
|
var sct ct.SignedCertificateTimestamp
|
||||||
|
rest, err := cttls.Unmarshal(serializedSCT.Val, &sct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rest) != 0 {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceSCTList, errors.New("serialized SCT list contained trailing garbage"))
|
||||||
|
}
|
||||||
|
list[i] = sct
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SCTListFromOCSPResponse extracts the SCTList from an ocsp.Response,
|
||||||
|
// returning an empty list if the SCT extension was not found or could not be
|
||||||
|
// unmarshalled.
|
||||||
|
func SCTListFromOCSPResponse(response *ocsp.Response) ([]ct.SignedCertificateTimestamp, error) {
|
||||||
|
// This loop finds the SCTListExtension in the OCSP response.
|
||||||
|
var SCTListExtension, ext pkix.Extension
|
||||||
|
for _, ext = range response.Extensions {
|
||||||
|
// sctExtOid is the ObjectIdentifier of a Signed Certificate Timestamp.
|
||||||
|
sctExtOid := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 5}
|
||||||
|
if ext.Id.Equal(sctExtOid) {
|
||||||
|
SCTListExtension = ext
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This code block extracts the sctList from the SCT extension.
|
||||||
|
var sctList []ct.SignedCertificateTimestamp
|
||||||
|
var err error
|
||||||
|
if numBytes := len(SCTListExtension.Value); numBytes != 0 {
|
||||||
|
var serializedSCTList []byte
|
||||||
|
rest := make([]byte, numBytes)
|
||||||
|
copy(rest, SCTListExtension.Value)
|
||||||
|
for len(rest) != 0 {
|
||||||
|
rest, err = asn1.Unmarshal(rest, &serializedSCTList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceSCTList, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sctList, err = DeserializeSCTList(serializedSCTList)
|
||||||
|
}
|
||||||
|
return sctList, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBytes reads a []byte either from a file or an environment variable.
|
||||||
|
// If valFile has a prefix of 'env:', the []byte is read from the environment
|
||||||
|
// using the subsequent name. If the prefix is 'file:' the []byte is read from
|
||||||
|
// the subsequent file. If no prefix is provided, valFile is assumed to be a
|
||||||
|
// file path.
|
||||||
|
func ReadBytes(valFile string) ([]byte, error) {
|
||||||
|
switch splitVal := strings.SplitN(valFile, ":", 2); len(splitVal) {
|
||||||
|
case 1:
|
||||||
|
return os.ReadFile(valFile)
|
||||||
|
case 2:
|
||||||
|
switch splitVal[0] {
|
||||||
|
case "env":
|
||||||
|
return []byte(os.Getenv(splitVal[1])), nil
|
||||||
|
case "file":
|
||||||
|
return os.ReadFile(splitVal[1])
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown prefix: %s", splitVal[0])
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("multiple prefixes: %s",
|
||||||
|
strings.Join(splitVal[:len(splitVal)-1], ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
9
certlib/pkcs7/BUILD.bazel
Normal file
9
certlib/pkcs7/BUILD.bazel
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "pkcs7",
|
||||||
|
srcs = ["pkcs7.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/certlib/pkcs7",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = ["//certlib/certerr"],
|
||||||
|
)
|
||||||
220
certlib/pkcs7/pkcs7.go
Normal file
220
certlib/pkcs7/pkcs7.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package pkcs7
|
||||||
|
|
||||||
|
// Originally from CFSSL, and licensed under:
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014 CloudFlare Inc.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// I've modified it for use in my own code e.g. by removing the CFSSL errors
|
||||||
|
// and replacing them with sane ones.
|
||||||
|
|
||||||
|
// Package pkcs7 implements the subset of the CMS PKCS #7 datatype that is typically
|
||||||
|
// used to package certificates and CRLs. Using openssl, every certificate converted
|
||||||
|
// to PKCS #7 format from another encoding such as PEM conforms to this implementation.
|
||||||
|
// reference: https://www.openssl.org/docs/man1.1.0/apps/crl2pkcs7.html
|
||||||
|
//
|
||||||
|
// PKCS #7 Data type, reference: https://tools.ietf.org/html/rfc2315
|
||||||
|
//
|
||||||
|
// The full pkcs#7 cryptographic message syntax allows for cryptographic enhancements,
|
||||||
|
// for example data can be encrypted and signed and then packaged through pkcs#7 to be
|
||||||
|
// sent over a network and then verified and decrypted. It is asn1, and the type of
|
||||||
|
// PKCS #7 ContentInfo, which comprises the PKCS #7 structure, is:
|
||||||
|
//
|
||||||
|
// ContentInfo ::= SEQUENCE {
|
||||||
|
// contentType ContentType,
|
||||||
|
// content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// There are 6 possible ContentTypes, data, signedData, envelopedData,
|
||||||
|
// signedAndEnvelopedData, digestedData, and encryptedData. Here signedData, Data, and encrypted
|
||||||
|
// Data are implemented, as the degenerate case of signedData without a signature is the typical
|
||||||
|
// format for transferring certificates and CRLS, and Data and encryptedData are used in PKCS #12
|
||||||
|
// formats.
|
||||||
|
// The ContentType signedData has the form:
|
||||||
|
//
|
||||||
|
// signedData ::= SEQUENCE {
|
||||||
|
// version Version,
|
||||||
|
// digestAlgorithms DigestAlgorithmIdentifiers,
|
||||||
|
// contentInfo ContentInfo,
|
||||||
|
// certificates [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL
|
||||||
|
// crls [1] IMPLICIT CertificateRevocationLists OPTIONAL,
|
||||||
|
// signerInfos SignerInfos
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// As of yet signerInfos and digestAlgorithms are not parsed, as they are not relevant to
|
||||||
|
// this system's use of PKCS #7 data. Version is an integer type, note that PKCS #7 is
|
||||||
|
// recursive, this second layer of ContentInfo is similar ignored for our degenerate
|
||||||
|
// usage. The ExtendedCertificatesAndCertificates type consists of a sequence of choices
|
||||||
|
// between PKCS #6 extended certificates and x509 certificates. Any sequence consisting
|
||||||
|
// of any number of extended certificates is not yet supported in this implementation.
|
||||||
|
//
|
||||||
|
// The ContentType Data is simply a raw octet string and is parsed directly into a Go []byte slice.
|
||||||
|
//
|
||||||
|
// The ContentType encryptedData is the most complicated and its form can be gathered by
|
||||||
|
// the go type below. It essentially contains a raw octet string of encrypted data and an
|
||||||
|
// algorithm identifier for use in decrypting this data.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib/certerr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Types used for asn1 Unmarshaling.
|
||||||
|
|
||||||
|
type signedData struct {
|
||||||
|
Version int
|
||||||
|
DigestAlgorithms asn1.RawValue
|
||||||
|
ContentInfo asn1.RawValue
|
||||||
|
Certificates asn1.RawValue `asn1:"optional" asn1:"tag:0"`
|
||||||
|
Crls asn1.RawValue `asn1:"optional"`
|
||||||
|
SignerInfos asn1.RawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
type initPKCS7 struct {
|
||||||
|
Raw asn1.RawContent
|
||||||
|
ContentType asn1.ObjectIdentifier
|
||||||
|
Content asn1.RawValue `asn1:"tag:0,explicit,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object identifier strings of the three implemented PKCS7 types.
|
||||||
|
const (
|
||||||
|
ObjIDData = "1.2.840.113549.1.7.1"
|
||||||
|
ObjIDSignedData = "1.2.840.113549.1.7.2"
|
||||||
|
ObjIDEncryptedData = "1.2.840.113549.1.7.6"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PKCS7 represents the ASN1 PKCS #7 Content type. It contains one of three
|
||||||
|
// possible types of Content objects, as denoted by the object identifier in
|
||||||
|
// the ContentInfo field, the other two being nil. SignedData
|
||||||
|
// is the degenerate SignedData Content info without signature used
|
||||||
|
// to hold certificates and crls. Data is raw bytes, and EncryptedData
|
||||||
|
// is as defined in PKCS #7 standard.
|
||||||
|
type PKCS7 struct {
|
||||||
|
Raw asn1.RawContent
|
||||||
|
ContentInfo string
|
||||||
|
Content Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content implements three of the six possible PKCS7 data types. Only one is non-nil.
|
||||||
|
type Content struct {
|
||||||
|
Data []byte
|
||||||
|
SignedData SignedData
|
||||||
|
EncryptedData EncryptedData
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignedData defines the typical carrier of certificates and crls.
|
||||||
|
type SignedData struct {
|
||||||
|
Raw asn1.RawContent
|
||||||
|
Version int
|
||||||
|
Certificates []*x509.Certificate
|
||||||
|
Crl *x509.RevocationList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data contains raw bytes. Used as a subtype in PKCS12.
|
||||||
|
type Data struct {
|
||||||
|
Bytes []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptedData contains encrypted data. Used as a subtype in PKCS12.
|
||||||
|
type EncryptedData struct {
|
||||||
|
Raw asn1.RawContent
|
||||||
|
Version int
|
||||||
|
EncryptedContentInfo EncryptedContentInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptedContentInfo is a subtype of PKCS7EncryptedData.
|
||||||
|
type EncryptedContentInfo struct {
|
||||||
|
Raw asn1.RawContent
|
||||||
|
ContentType asn1.ObjectIdentifier
|
||||||
|
ContentEncryptionAlgorithm pkix.AlgorithmIdentifier
|
||||||
|
EncryptedContent []byte `asn1:"tag:0,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePKCS7 attempts to parse the DER encoded bytes of a
|
||||||
|
// PKCS7 structure.
|
||||||
|
func ParsePKCS7(raw []byte) (msg *PKCS7, err error) {
|
||||||
|
|
||||||
|
var pkcs7 initPKCS7
|
||||||
|
_, err = asn1.Unmarshal(raw, &pkcs7)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = new(PKCS7)
|
||||||
|
msg.Raw = pkcs7.Raw
|
||||||
|
msg.ContentInfo = pkcs7.ContentType.String()
|
||||||
|
switch {
|
||||||
|
case msg.ContentInfo == ObjIDData:
|
||||||
|
msg.ContentInfo = "Data"
|
||||||
|
_, err = asn1.Unmarshal(pkcs7.Content.Bytes, &msg.Content.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
|
||||||
|
}
|
||||||
|
case msg.ContentInfo == ObjIDSignedData:
|
||||||
|
msg.ContentInfo = "SignedData"
|
||||||
|
var signedData signedData
|
||||||
|
_, err = asn1.Unmarshal(pkcs7.Content.Bytes, &signedData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
|
||||||
|
}
|
||||||
|
if len(signedData.Certificates.Bytes) != 0 {
|
||||||
|
msg.Content.SignedData.Certificates, err = x509.ParseCertificates(signedData.Certificates.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(signedData.Crls.Bytes) != 0 {
|
||||||
|
msg.Content.SignedData.Crl, err = x509.ParseRevocationList(signedData.Crls.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg.Content.SignedData.Version = signedData.Version
|
||||||
|
msg.Content.SignedData.Raw = pkcs7.Content.Bytes
|
||||||
|
case msg.ContentInfo == ObjIDEncryptedData:
|
||||||
|
msg.ContentInfo = "EncryptedData"
|
||||||
|
var encryptedData EncryptedData
|
||||||
|
_, err = asn1.Unmarshal(pkcs7.Content.Bytes, &encryptedData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
|
||||||
|
}
|
||||||
|
if encryptedData.Version != 0 {
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("only PKCS #7 encryptedData version 0 is supported"))
|
||||||
|
}
|
||||||
|
msg.Content.EncryptedData = encryptedData
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("only PKCS# 7 content of type data, signed data or encrypted data can be parsed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg, nil
|
||||||
|
|
||||||
|
}
|
||||||
19
certlib/revoke/BUILD.bazel
Normal file
19
certlib/revoke/BUILD.bazel
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "revoke",
|
||||||
|
srcs = ["revoke.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/certlib/revoke",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//certlib",
|
||||||
|
"//log",
|
||||||
|
"@org_golang_x_crypto//ocsp",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "revoke_test",
|
||||||
|
srcs = ["revoke_test.go"],
|
||||||
|
embed = [":revoke"],
|
||||||
|
)
|
||||||
363
certlib/revoke/revoke.go
Normal file
363
certlib/revoke/revoke.go
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
// Package revoke provides functionality for checking the validity of
|
||||||
|
// a cert. Specifically, the temporal validity of the certificate is
|
||||||
|
// checked first, then any CRL and OCSP url in the cert is checked.
|
||||||
|
package revoke
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
neturl "net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/log"
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Originally from CFSSL, mostly written by me originally, and licensed under:
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014 CloudFlare Inc.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// I've modified it for use in my own code e.g. by removing the CFSSL errors
|
||||||
|
// and replacing them with sane ones.
|
||||||
|
|
||||||
|
// HTTPClient is an instance of http.Client that will be used for all HTTP requests.
|
||||||
|
var HTTPClient = http.DefaultClient
|
||||||
|
|
||||||
|
// HardFail determines whether the failure to check the revocation
|
||||||
|
// status of a certificate (i.e. due to network failure) causes
|
||||||
|
// verification to fail (a hard failure).
|
||||||
|
var HardFail = false
|
||||||
|
|
||||||
|
// CRLSet associates a PKIX certificate list with the URL the CRL is
|
||||||
|
// fetched from.
|
||||||
|
var CRLSet = map[string]*x509.RevocationList{}
|
||||||
|
var crlLock = new(sync.Mutex)
|
||||||
|
|
||||||
|
// We can't handle LDAP certificates, so this checks to see if the
|
||||||
|
// URL string points to an LDAP resource so that we can ignore it.
|
||||||
|
func ldapURL(url string) bool {
|
||||||
|
u, err := neturl.Parse(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("error parsing url %s: %v", url, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if u.Scheme == "ldap" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// revCheck should check the certificate for any revocations. It
|
||||||
|
// returns a pair of booleans: the first indicates whether the certificate
|
||||||
|
// is revoked, the second indicates whether the revocations were
|
||||||
|
// successfully checked.. This leads to the following combinations:
|
||||||
|
//
|
||||||
|
// - false, false: an error was encountered while checking revocations.
|
||||||
|
// - false, true: the certificate was checked successfully, and it is not revoked.
|
||||||
|
// - true, true: the certificate was checked successfully, and it is revoked.
|
||||||
|
// - true, false: failure to check revocation status causes verification to fail
|
||||||
|
func revCheck(cert *x509.Certificate) (revoked, ok bool, err error) {
|
||||||
|
for _, url := range cert.CRLDistributionPoints {
|
||||||
|
if ldapURL(url) {
|
||||||
|
log.Infof("skipping LDAP CRL: %s", url)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if revoked, ok, err := certIsRevokedCRL(cert, url); !ok {
|
||||||
|
log.Warning("error checking revocation via CRL")
|
||||||
|
if HardFail {
|
||||||
|
return true, false, err
|
||||||
|
}
|
||||||
|
return false, false, err
|
||||||
|
} else if revoked {
|
||||||
|
log.Info("certificate is revoked via CRL")
|
||||||
|
return true, true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if revoked, ok, err := certIsRevokedOCSP(cert, HardFail); !ok {
|
||||||
|
log.Warning("error checking revocation via OCSP")
|
||||||
|
if HardFail {
|
||||||
|
return true, false, err
|
||||||
|
}
|
||||||
|
return false, false, err
|
||||||
|
} else if revoked {
|
||||||
|
log.Info("certificate is revoked via OCSP")
|
||||||
|
return true, true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchCRL fetches and parses a CRL.
|
||||||
|
func fetchCRL(url string) (*x509.RevocationList, error) {
|
||||||
|
resp, err := HTTPClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return nil, errors.New("failed to retrieve CRL")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := crlRead(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x509.ParseRevocationList(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIssuer(cert *x509.Certificate) *x509.Certificate {
|
||||||
|
var issuer *x509.Certificate
|
||||||
|
var err error
|
||||||
|
for _, issuingCert := range cert.IssuingCertificateURL {
|
||||||
|
issuer, err = fetchRemote(issuingCert)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return issuer
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// check a cert against a specific CRL. Returns the same bool pair
|
||||||
|
// as revCheck, plus an error if one occurred.
|
||||||
|
func certIsRevokedCRL(cert *x509.Certificate, url string) (revoked, ok bool, err error) {
|
||||||
|
crlLock.Lock()
|
||||||
|
crl, ok := CRLSet[url]
|
||||||
|
if ok && crl == nil {
|
||||||
|
ok = false
|
||||||
|
delete(CRLSet, url)
|
||||||
|
}
|
||||||
|
crlLock.Unlock()
|
||||||
|
|
||||||
|
var shouldFetchCRL = true
|
||||||
|
if ok {
|
||||||
|
if time.Now().After(crl.ThisUpdate) {
|
||||||
|
shouldFetchCRL = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer := getIssuer(cert)
|
||||||
|
|
||||||
|
if shouldFetchCRL {
|
||||||
|
var err error
|
||||||
|
crl, err = fetchCRL(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("failed to fetch CRL: %v", err)
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check CRL signature
|
||||||
|
if issuer != nil {
|
||||||
|
err = crl.CheckSignatureFrom(issuer)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("failed to verify CRL: %v", err)
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crlLock.Lock()
|
||||||
|
CRLSet[url] = crl
|
||||||
|
crlLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, revoked := range crl.RevokedCertificates {
|
||||||
|
if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 {
|
||||||
|
log.Info("Serial number match: intermediate is revoked.")
|
||||||
|
return true, true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCertificate ensures that the certificate passed in hasn't
|
||||||
|
// expired and checks the CRL for the server.
|
||||||
|
func VerifyCertificate(cert *x509.Certificate) (revoked, ok bool) {
|
||||||
|
revoked, ok, _ = VerifyCertificateError(cert)
|
||||||
|
return revoked, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCertificateError ensures that the certificate passed in hasn't
|
||||||
|
// expired and checks the CRL for the server.
|
||||||
|
func VerifyCertificateError(cert *x509.Certificate) (revoked, ok bool, err error) {
|
||||||
|
if !time.Now().Before(cert.NotAfter) {
|
||||||
|
msg := fmt.Sprintf("Certificate expired %s\n", cert.NotAfter)
|
||||||
|
log.Info(msg)
|
||||||
|
return true, true, fmt.Errorf(msg)
|
||||||
|
} else if !time.Now().After(cert.NotBefore) {
|
||||||
|
msg := fmt.Sprintf("Certificate isn't valid until %s\n", cert.NotBefore)
|
||||||
|
log.Info(msg)
|
||||||
|
return true, true, fmt.Errorf(msg)
|
||||||
|
}
|
||||||
|
return revCheck(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchRemote(url string) (*x509.Certificate, error) {
|
||||||
|
resp, err := HTTPClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
in, err := remoteRead(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p, _ := pem.Decode(in)
|
||||||
|
if p != nil {
|
||||||
|
return certlib.ParseCertificatePEM(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
return x509.ParseCertificate(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ocspOpts = ocsp.RequestOptions{
|
||||||
|
Hash: crypto.SHA1,
|
||||||
|
}
|
||||||
|
|
||||||
|
func certIsRevokedOCSP(leaf *x509.Certificate, strict bool) (revoked, ok bool, e error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ocspURLs := leaf.OCSPServer
|
||||||
|
if len(ocspURLs) == 0 {
|
||||||
|
// OCSP not enabled for this certificate.
|
||||||
|
return false, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer := getIssuer(leaf)
|
||||||
|
|
||||||
|
if issuer == nil {
|
||||||
|
return false, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ocspRequest, err := ocsp.CreateRequest(leaf, issuer, &ocspOpts)
|
||||||
|
if err != nil {
|
||||||
|
return revoked, ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, server := range ocspURLs {
|
||||||
|
resp, err := sendOCSPRequest(server, ocspRequest, leaf, issuer)
|
||||||
|
if err != nil {
|
||||||
|
if strict {
|
||||||
|
return revoked, ok, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// There wasn't an error fetching the OCSP status.
|
||||||
|
ok = true
|
||||||
|
|
||||||
|
if resp.Status != ocsp.Good {
|
||||||
|
// The certificate was revoked.
|
||||||
|
revoked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return revoked, ok, err
|
||||||
|
}
|
||||||
|
return revoked, ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendOCSPRequest attempts to request an OCSP response from the
|
||||||
|
// server. The error only indicates a failure to *fetch* the
|
||||||
|
// certificate, and *does not* mean the certificate is valid.
|
||||||
|
func sendOCSPRequest(server string, req []byte, leaf, issuer *x509.Certificate) (*ocsp.Response, error) {
|
||||||
|
var resp *http.Response
|
||||||
|
var err error
|
||||||
|
if len(req) > 256 {
|
||||||
|
buf := bytes.NewBuffer(req)
|
||||||
|
resp, err = HTTPClient.Post(server, "application/ocsp-request", buf)
|
||||||
|
} else {
|
||||||
|
reqURL := server + "/" + neturl.QueryEscape(base64.StdEncoding.EncodeToString(req))
|
||||||
|
resp, err = HTTPClient.Get(reqURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, errors.New("failed to retrieve OSCP")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ocspRead(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(body, ocsp.UnauthorizedErrorResponse):
|
||||||
|
return nil, errors.New("OSCP unauthorized")
|
||||||
|
case bytes.Equal(body, ocsp.MalformedRequestErrorResponse):
|
||||||
|
return nil, errors.New("OSCP malformed")
|
||||||
|
case bytes.Equal(body, ocsp.InternalErrorErrorResponse):
|
||||||
|
return nil, errors.New("OSCP internal error")
|
||||||
|
case bytes.Equal(body, ocsp.TryLaterErrorResponse):
|
||||||
|
return nil, errors.New("OSCP try later")
|
||||||
|
case bytes.Equal(body, ocsp.SigRequredErrorResponse):
|
||||||
|
return nil, errors.New("OSCP signature required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ocsp.ParseResponseForCert(body, leaf, issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
var crlRead = io.ReadAll
|
||||||
|
|
||||||
|
// SetCRLFetcher sets the function to use to read from the http response body
|
||||||
|
func SetCRLFetcher(fn func(io.Reader) ([]byte, error)) {
|
||||||
|
crlRead = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
var remoteRead = io.ReadAll
|
||||||
|
|
||||||
|
// SetRemoteFetcher sets the function to use to read from the http response body
|
||||||
|
func SetRemoteFetcher(fn func(io.Reader) ([]byte, error)) {
|
||||||
|
remoteRead = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
var ocspRead = io.ReadAll
|
||||||
|
|
||||||
|
// SetOCSPFetcher sets the function to use to read from the http response body
|
||||||
|
func SetOCSPFetcher(fn func(io.Reader) ([]byte, error)) {
|
||||||
|
ocspRead = fn
|
||||||
|
}
|
||||||
262
certlib/revoke/revoke_test.go
Normal file
262
certlib/revoke/revoke_test.go
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
package revoke
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Originally from CFSSL, mostly written by me originally, and licensed under:
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) 2014 CloudFlare Inc.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// I've modified it for use in my own code e.g. by removing the CFSSL errors
|
||||||
|
// and replacing them with sane ones.
|
||||||
|
|
||||||
|
// The first three test cases represent known revoked, expired, and good
|
||||||
|
// certificates that were checked on the date listed in the log. The
|
||||||
|
// good certificate will eventually need to be replaced in year 2029.
|
||||||
|
|
||||||
|
// If there is a soft-fail, the test will pass to mimic the default
|
||||||
|
// behaviour used in this software. However, it will print a warning
|
||||||
|
// to indicate that this is the case.
|
||||||
|
|
||||||
|
// 2014/05/22 14:18:17 Certificate expired 2014-04-04 14:14:20 +0000 UTC
|
||||||
|
// 2014/05/22 14:18:17 Revoked certificate: misc/intermediate_ca/ActalisServerAuthenticationCA.crt
|
||||||
|
var expiredCert = mustParse(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEXTCCA8agAwIBAgIEBycURTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJV
|
||||||
|
UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU
|
||||||
|
cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds
|
||||||
|
b2JhbCBSb290MB4XDTA3MDQwNDE0MTUxNFoXDTE0MDQwNDE0MTQyMFowejELMAkG
|
||||||
|
A1UEBhMCSVQxFzAVBgNVBAoTDkFjdGFsaXMgUy5wLkEuMScwJQYDVQQLEx5DZXJ0
|
||||||
|
aWZpY2F0aW9uIFNlcnZpY2UgUHJvdmlkZXIxKTAnBgNVBAMTIEFjdGFsaXMgU2Vy
|
||||||
|
dmVyIEF1dGhlbnRpY2F0aW9uIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||||
|
CgKCAQEAv6P0bhXbUQkVW8ox0HJ+sP5+j6pTwS7yg/wGEUektB/G1duQiT1v21fo
|
||||||
|
LANr6F353jILQDCpHIfal3MhbSsHEMKU7XaqsyLWV93bcIKbIloS/eXDfkog6KB3
|
||||||
|
u0JHgrtNz584Jg/OLm9feffNbCJ38TiLo0/UWkAQ6PQWaOwZEgyKjVI5F3swoTB3
|
||||||
|
g0LZAzegvkU00Kfp13cSg+cJeU4SajwtfQ+g6s6dlaekaHy/0ef46PfiHHRuhEhE
|
||||||
|
JWIpDtUN2ywTT33MSSUe5glDIiXYfcamJQrebzGsHEwyqI195Yaxb+FLNND4n3HM
|
||||||
|
e7EI2OrLyT+r/WMvQbl+xNihwtv+HwIDAQABo4IBbzCCAWswEgYDVR0TAQH/BAgw
|
||||||
|
BgEB/wIBADBTBgNVHSAETDBKMEgGCSsGAQQBsT4BADA7MDkGCCsGAQUFBwIBFi1o
|
||||||
|
dHRwOi8vd3d3LnB1YmxpYy10cnVzdC5jb20vQ1BTL09tbmlSb290Lmh0bWwwDgYD
|
||||||
|
VR0PAQH/BAQDAgEGMIGJBgNVHSMEgYEwf6F5pHcwdTELMAkGA1UEBhMCVVMxGDAW
|
||||||
|
BgNVBAoTD0dURSBDb3Jwb3JhdGlvbjEnMCUGA1UECxMeR1RFIEN5YmVyVHJ1c3Qg
|
||||||
|
U29sdXRpb25zLCBJbmMuMSMwIQYDVQQDExpHVEUgQ3liZXJUcnVzdCBHbG9iYWwg
|
||||||
|
Um9vdIICAaUwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL3d3dy5wdWJsaWMtdHJ1
|
||||||
|
c3QuY29tL2NnaS1iaW4vQ1JMLzIwMTgvY2RwLmNybDAdBgNVHQ4EFgQUpi6OuXYt
|
||||||
|
oxHC3cTezVLuraWpAFEwDQYJKoZIhvcNAQEFBQADgYEAAtjJBwjsvw7DBs+v7BQz
|
||||||
|
gSGeg6nbYUuPL7+1driT5XsUKJ7WZjiwW2zW/WHZ+zGo1Ev8Dc574RpSrg/EIlfH
|
||||||
|
TpBiBuFgiKtJksKdoxPZGSI8FitwcgeW+y8wotmm0CtDzWN27g2kfSqHb5eHfZY5
|
||||||
|
sESPRwHkcMUNdAp37FLweUw=
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
|
||||||
|
// 2014/05/22 14:18:31 Serial number match: intermediate is revoked.
|
||||||
|
// 2014/05/22 14:18:31 certificate is revoked via CRL
|
||||||
|
// 2014/05/22 14:18:31 Revoked certificate: misc/intermediate_ca/MobileArmorEnterpriseCA.crt
|
||||||
|
var revokedCert = mustParse(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEEzCCAvugAwIBAgILBAAAAAABGMGjftYwDQYJKoZIhvcNAQEFBQAwcTEoMCYG
|
||||||
|
A1UEAxMfR2xvYmFsU2lnbiBSb290U2lnbiBQYXJ0bmVycyBDQTEdMBsGA1UECxMU
|
||||||
|
Um9vdFNpZ24gUGFydG5lcnMgQ0ExGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex
|
||||||
|
CzAJBgNVBAYTAkJFMB4XDTA4MDMxODEyMDAwMFoXDTE4MDMxODEyMDAwMFowJTEj
|
||||||
|
MCEGA1UEAxMaTW9iaWxlIEFybW9yIEVudGVycHJpc2UgQ0EwggEiMA0GCSqGSIb3
|
||||||
|
DQEBAQUAA4IBDwAwggEKAoIBAQCaEjeDR73jSZVlacRn5bc5VIPdyouHvGIBUxyS
|
||||||
|
C6483HgoDlWrWlkEndUYFjRPiQqJFthdJxfglykXD+btHixMIYbz/6eb7hRTdT9w
|
||||||
|
HKsfH+wTBIdb5AZiNjkg3QcCET5HfanJhpREjZWP513jM/GSrG3VwD6X5yttCIH1
|
||||||
|
NFTDAr7aqpW/UPw4gcPfkwS92HPdIkb2DYnsqRrnKyNValVItkxJiotQ1HOO3YfX
|
||||||
|
ivGrHIbJdWYg0rZnkPOgYF0d+aIA4ZfwvdW48+r/cxvLevieuKj5CTBZZ8XrFt8r
|
||||||
|
JTZhZljbZvnvq/t6ZIzlwOj082f+lTssr1fJ3JsIPnG2lmgTAgMBAAGjgfcwgfQw
|
||||||
|
DgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFIZw
|
||||||
|
ns4uzXdLX6xDRXUzFgZxWM7oME0GA1UdIARGMEQwQgYJKwYBBAGgMgE8MDUwMwYI
|
||||||
|
KwYBBQUHAgIwJxolaHR0cDovL3d3dy5nbG9iYWxzaWduLmNvbS9yZXBvc2l0b3J5
|
||||||
|
LzA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmdsb2JhbHNpZ24ubmV0L1Jv
|
||||||
|
b3RTaWduUGFydG5lcnMuY3JsMB8GA1UdIwQYMBaAFFaE7LVxpedj2NtRBNb65vBI
|
||||||
|
UknOMA0GCSqGSIb3DQEBBQUAA4IBAQBZvf+2xUJE0ekxuNk30kPDj+5u9oI3jZyM
|
||||||
|
wvhKcs7AuRAbcxPtSOnVGNYl8By7DPvPun+U3Yci8540y143RgD+kz3jxIBaoW/o
|
||||||
|
c4+X61v6DBUtcBPEt+KkV6HIsZ61SZmc/Y1I2eoeEt6JYoLjEZMDLLvc1cK/+wpg
|
||||||
|
dUZSK4O9kjvIXqvsqIOlkmh/6puSugTNao2A7EIQr8ut0ZmzKzMyZ0BuQhJDnAPd
|
||||||
|
Kz5vh+5tmytUPKA8hUgmLWe94lMb7Uqq2wgZKsqun5DAWleKu81w7wEcOrjiiB+x
|
||||||
|
jeBHq7OnpWm+ccTOPCE6H4ZN4wWVS7biEBUdop/8HgXBPQHWAdjL
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
|
||||||
|
// A Comodo intermediate CA certificate with issuer url, CRL url and OCSP url
|
||||||
|
var goodComodoCA = (`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
|
||||||
|
hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||||
|
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
|
||||||
|
BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy
|
||||||
|
MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
|
||||||
|
EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
|
||||||
|
Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh
|
||||||
|
bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||||
|
ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh
|
||||||
|
bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0
|
||||||
|
Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6
|
||||||
|
ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51
|
||||||
|
UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n
|
||||||
|
c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY
|
||||||
|
MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz
|
||||||
|
30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV
|
||||||
|
HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG
|
||||||
|
BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv
|
||||||
|
bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB
|
||||||
|
AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E
|
||||||
|
T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v
|
||||||
|
ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p
|
||||||
|
mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/
|
||||||
|
e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps
|
||||||
|
P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY
|
||||||
|
dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc
|
||||||
|
2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG
|
||||||
|
V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4
|
||||||
|
HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX
|
||||||
|
j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII
|
||||||
|
0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap
|
||||||
|
lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf
|
||||||
|
+AZxAeKCINT+b72x
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
|
||||||
|
var goodCert = mustParse(goodComodoCA)
|
||||||
|
|
||||||
|
func mustParse(pemData string) *x509.Certificate {
|
||||||
|
block, _ := pem.Decode([]byte(pemData))
|
||||||
|
if block == nil {
|
||||||
|
panic("Invalid PEM data.")
|
||||||
|
} else if block.Type != "CERTIFICATE" {
|
||||||
|
panic("Invalid PEM type.")
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate([]byte(block.Bytes))
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevoked(t *testing.T) {
|
||||||
|
if revoked, ok := VerifyCertificate(revokedCert); !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: soft fail checking revocation")
|
||||||
|
} else if !revoked {
|
||||||
|
t.Fatalf("revoked certificate should have been marked as revoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpired(t *testing.T) {
|
||||||
|
if revoked, ok := VerifyCertificate(expiredCert); !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: soft fail checking revocation")
|
||||||
|
} else if !revoked {
|
||||||
|
t.Fatalf("expired certificate should have been marked as revoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGood(t *testing.T) {
|
||||||
|
if revoked, ok := VerifyCertificate(goodCert); !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: soft fail checking revocation")
|
||||||
|
} else if revoked {
|
||||||
|
t.Fatalf("good certificate should not have been marked as revoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLdap(t *testing.T) {
|
||||||
|
ldapCert := mustParse(goodComodoCA)
|
||||||
|
ldapCert.CRLDistributionPoints = append(ldapCert.CRLDistributionPoints, "ldap://myldap.example.com")
|
||||||
|
if revoked, ok := VerifyCertificate(ldapCert); revoked || !ok {
|
||||||
|
t.Fatalf("ldap certificate should have been recognized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLdapURLErr(t *testing.T) {
|
||||||
|
if ldapURL(":") {
|
||||||
|
t.Fatalf("bad url does not cause error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertNotYetValid(t *testing.T) {
|
||||||
|
notReadyCert := expiredCert
|
||||||
|
notReadyCert.NotBefore = time.Date(3000, time.January, 1, 1, 1, 1, 1, time.Local)
|
||||||
|
notReadyCert.NotAfter = time.Date(3005, time.January, 1, 1, 1, 1, 1, time.Local)
|
||||||
|
if revoked, _ := VerifyCertificate(expiredCert); !revoked {
|
||||||
|
t.Fatalf("not yet verified certificate should have been marked as revoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRLFetchError(t *testing.T) {
|
||||||
|
ldapCert := mustParse(goodComodoCA)
|
||||||
|
ldapCert.CRLDistributionPoints[0] = ""
|
||||||
|
if revoked, ok := VerifyCertificate(ldapCert); ok || revoked {
|
||||||
|
t.Fatalf("Fetching error not encountered")
|
||||||
|
}
|
||||||
|
HardFail = true
|
||||||
|
if revoked, ok := VerifyCertificate(ldapCert); ok || !revoked {
|
||||||
|
t.Fatalf("Fetching error not encountered, hardfail not registered")
|
||||||
|
}
|
||||||
|
HardFail = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadCRLSet(t *testing.T) {
|
||||||
|
ldapCert := mustParse(goodComodoCA)
|
||||||
|
ldapCert.CRLDistributionPoints[0] = ""
|
||||||
|
CRLSet[""] = nil
|
||||||
|
certIsRevokedCRL(ldapCert, "")
|
||||||
|
if _, ok := CRLSet[""]; ok {
|
||||||
|
t.Fatalf("key emptystring should be deleted from CRLSet")
|
||||||
|
}
|
||||||
|
delete(CRLSet, "")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCachedCRLSet(t *testing.T) {
|
||||||
|
VerifyCertificate(goodCert)
|
||||||
|
if revoked, ok := VerifyCertificate(goodCert); !ok || revoked {
|
||||||
|
t.Fatalf("Previously fetched CRL's should be read smoothly and unrevoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteFetchError(t *testing.T) {
|
||||||
|
|
||||||
|
badurl := ":"
|
||||||
|
|
||||||
|
if _, err := fetchRemote(badurl); err == nil {
|
||||||
|
t.Fatalf("fetching bad url should result in non-nil error")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoOCSPServers(t *testing.T) {
|
||||||
|
badIssuer := goodCert
|
||||||
|
badIssuer.IssuingCertificateURL = []string{" "}
|
||||||
|
certIsRevokedOCSP(badIssuer, true)
|
||||||
|
noOCSPCert := goodCert
|
||||||
|
noOCSPCert.OCSPServer = make([]string, 0)
|
||||||
|
if revoked, ok, _ := certIsRevokedOCSP(noOCSPCert, true); revoked || !ok {
|
||||||
|
t.Fatalf("OCSP falsely registered as enabled for this certificate")
|
||||||
|
}
|
||||||
|
}
|
||||||
14
cmd/atping/BUILD.bazel
Normal file
14
cmd/atping/BUILD.bazel
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "atping_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/atping",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "atping",
|
||||||
|
embed = [":atping_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
15
cmd/certchain/BUILD.bazel
Normal file
15
cmd/certchain/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "certchain_lib",
|
||||||
|
srcs = ["certchain.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certchain",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//die"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "certchain",
|
||||||
|
embed = [":certchain_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
var hasPort = regexp.MustCompile(`:\d+$`)
|
var hasPort = regexp.MustCompile(`:\d+$`)
|
||||||
|
|||||||
22
cmd/certdump/BUILD.bazel
Normal file
22
cmd/certdump/BUILD.bazel
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "certdump_lib",
|
||||||
|
srcs = [
|
||||||
|
"certdump.go",
|
||||||
|
"util.go",
|
||||||
|
],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certdump",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//certlib",
|
||||||
|
"//lib",
|
||||||
|
"@com_github_kr_text//:text",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "certdump",
|
||||||
|
embed = [":certdump_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -6,16 +6,19 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cloudflare/cfssl/helpers"
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func certPublic(cert *x509.Certificate) string {
|
func certPublic(cert *x509.Certificate) string {
|
||||||
@@ -82,6 +85,7 @@ func keyUsages(ku x509.KeyUsage) string {
|
|||||||
uses = append(uses, s)
|
uses = append(uses, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sort.Strings(uses)
|
||||||
|
|
||||||
return strings.Join(uses, ", ")
|
return strings.Join(uses, ", ")
|
||||||
}
|
}
|
||||||
@@ -91,6 +95,7 @@ func extUsage(ext []x509.ExtKeyUsage) string {
|
|||||||
for i := range ext {
|
for i := range ext {
|
||||||
ns = append(ns, extKeyUsages[ext[i]])
|
ns = append(ns, extKeyUsages[ext[i]])
|
||||||
}
|
}
|
||||||
|
sort.Strings(ns)
|
||||||
|
|
||||||
return strings.Join(ns, ", ")
|
return strings.Join(ns, ", ")
|
||||||
}
|
}
|
||||||
@@ -116,7 +121,10 @@ func showBasicConstraints(cert *x509.Certificate) {
|
|||||||
|
|
||||||
const oneTrueDateFormat = "2006-01-02T15:04:05-0700"
|
const oneTrueDateFormat = "2006-01-02T15:04:05-0700"
|
||||||
|
|
||||||
var dateFormat string
|
var (
|
||||||
|
dateFormat string
|
||||||
|
showHash bool // if true, print a SHA256 hash of the certificate's Raw field
|
||||||
|
)
|
||||||
|
|
||||||
func wrapPrint(text string, indent int) {
|
func wrapPrint(text string, indent int) {
|
||||||
tabs := ""
|
tabs := ""
|
||||||
@@ -129,6 +137,9 @@ func wrapPrint(text string, indent int) {
|
|||||||
|
|
||||||
func displayCert(cert *x509.Certificate) {
|
func displayCert(cert *x509.Certificate) {
|
||||||
fmt.Println("CERTIFICATE")
|
fmt.Println("CERTIFICATE")
|
||||||
|
if showHash {
|
||||||
|
fmt.Println(wrap(fmt.Sprintf("SHA256: %x", sha256.Sum256(cert.Raw)), 0))
|
||||||
|
}
|
||||||
fmt.Println(wrap("Subject: "+displayName(cert.Subject), 0))
|
fmt.Println(wrap("Subject: "+displayName(cert.Subject), 0))
|
||||||
fmt.Println(wrap("Issuer: "+displayName(cert.Issuer), 0))
|
fmt.Println(wrap("Issuer: "+displayName(cert.Issuer), 0))
|
||||||
fmt.Printf("\tSignature algorithm: %s / %s\n", sigAlgoPK(cert.SignatureAlgorithm),
|
fmt.Printf("\tSignature algorithm: %s / %s\n", sigAlgoPK(cert.SignatureAlgorithm),
|
||||||
@@ -198,17 +209,17 @@ func displayCert(cert *x509.Certificate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func displayAllCerts(in []byte, leafOnly bool) {
|
func displayAllCerts(in []byte, leafOnly bool) {
|
||||||
certs, err := helpers.ParseCertificatesPEM(in)
|
certs, err := certlib.ParseCertificatesPEM(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
certs, _, err = helpers.ParseCertificatesDER(in, "")
|
certs, _, err = certlib.ParseCertificatesDER(in, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warn(TranslateCFSSLError(err), "failed to parse certificates")
|
lib.Warn(err, "failed to parse certificates")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(certs) == 0 {
|
if len(certs) == 0 {
|
||||||
Warnx("no certificates found")
|
lib.Warnx("no certificates found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +237,7 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
|||||||
ci := getConnInfo(uri)
|
ci := getConnInfo(uri)
|
||||||
conn, err := tls.Dial("tcp", ci.Addr, permissiveConfig())
|
conn, err := tls.Dial("tcp", ci.Addr, permissiveConfig())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warn(err, "couldn't connect to %s", ci.Addr)
|
lib.Warn(err, "couldn't connect to %s", ci.Addr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
@@ -242,11 +253,11 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
|||||||
}
|
}
|
||||||
conn.Close()
|
conn.Close()
|
||||||
} else {
|
} else {
|
||||||
Warn(err, "TLS verification error with server name %s", ci.Host)
|
lib.Warn(err, "TLS verification error with server name %s", ci.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(state.PeerCertificates) == 0 {
|
if len(state.PeerCertificates) == 0 {
|
||||||
Warnx("no certificates found")
|
lib.Warnx("no certificates found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +267,7 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(state.VerifiedChains) == 0 {
|
if len(state.VerifiedChains) == 0 {
|
||||||
Warnx("no verified chains found; using peer chain")
|
lib.Warnx("no verified chains found; using peer chain")
|
||||||
for i := range state.PeerCertificates {
|
for i := range state.PeerCertificates {
|
||||||
displayCert(state.PeerCertificates[i])
|
displayCert(state.PeerCertificates[i])
|
||||||
}
|
}
|
||||||
@@ -273,14 +284,15 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var leafOnly bool
|
var leafOnly bool
|
||||||
|
flag.BoolVar(&showHash, "d", false, "show hashes of raw DER contents")
|
||||||
flag.StringVar(&dateFormat, "s", oneTrueDateFormat, "date `format` in Go time format")
|
flag.StringVar(&dateFormat, "s", oneTrueDateFormat, "date `format` in Go time format")
|
||||||
flag.BoolVar(&leafOnly, "l", false, "only show the leaf certificate")
|
flag.BoolVar(&leafOnly, "l", false, "only show the leaf certificate")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if flag.NArg() == 0 || (flag.NArg() == 1 && flag.Arg(0) == "-") {
|
if flag.NArg() == 0 || (flag.NArg() == 1 && flag.Arg(0) == "-") {
|
||||||
certs, err := ioutil.ReadAll(os.Stdin)
|
certs, err := io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warn(err, "couldn't read certificates from standard input")
|
lib.Warn(err, "couldn't read certificates from standard input")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,9 +307,9 @@ func main() {
|
|||||||
if strings.HasPrefix(filename, "https://") {
|
if strings.HasPrefix(filename, "https://") {
|
||||||
displayAllCertsWeb(filename, leafOnly)
|
displayAllCertsWeb(filename, leafOnly)
|
||||||
} else {
|
} else {
|
||||||
in, err := ioutil.ReadFile(filename)
|
in, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warn(err, "couldn't read certificate")
|
lib.Warn(err, "couldn't read certificate")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
cferr "github.com/cloudflare/cfssl/errors"
|
|
||||||
"github.com/kr/text"
|
"github.com/kr/text"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,34 +86,6 @@ func sigAlgoHash(a x509.SignatureAlgorithm) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslateCFSSLError turns a CFSSL error into a more readable string.
|
|
||||||
func TranslateCFSSLError(err error) error {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// printing errors as json is terrible
|
|
||||||
if cfsslError, ok := err.(*cferr.Error); ok {
|
|
||||||
err = errors.New(cfsslError.Message)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warnx displays a formatted error message to standard error, à la
|
|
||||||
// warnx(3).
|
|
||||||
func Warnx(format string, a ...interface{}) (int, error) {
|
|
||||||
format += "\n"
|
|
||||||
return fmt.Fprintf(os.Stderr, format, a...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn displays a formatted error message to standard output,
|
|
||||||
// appending the error string, à la warn(3).
|
|
||||||
func Warn(err error, format string, a ...interface{}) (int, error) {
|
|
||||||
format += ": %v\n"
|
|
||||||
a = append(a, err)
|
|
||||||
return fmt.Fprintf(os.Stderr, format, a...)
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxLine = 78
|
const maxLine = 78
|
||||||
|
|
||||||
func makeIndent(n int) string {
|
func makeIndent(n int) string {
|
||||||
|
|||||||
19
cmd/certexpiry/BUILD.bazel
Normal file
19
cmd/certexpiry/BUILD.bazel
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "certexpiry_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certexpiry",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//certlib",
|
||||||
|
"//die",
|
||||||
|
"//lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "certexpiry",
|
||||||
|
embed = [":certexpiry_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudflare/cfssl/helpers"
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
"github.com/kisom/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
"github.com/kisom/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
var warnOnly bool
|
var warnOnly bool
|
||||||
@@ -87,7 +87,7 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
certs, err := helpers.ParseCertificatesPEM(in)
|
certs, err := certlib.ParseCertificatesPEM(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lib.Warn(err, "while parsing certificates")
|
lib.Warn(err, "while parsing certificates")
|
||||||
continue
|
continue
|
||||||
|
|||||||
20
cmd/certverify/BUILD.bazel
Normal file
20
cmd/certverify/BUILD.bazel
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "certverify_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certverify",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//certlib",
|
||||||
|
"//certlib/revoke",
|
||||||
|
"//die",
|
||||||
|
"//lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "certverify",
|
||||||
|
embed = [":certverify_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -8,14 +8,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudflare/cfssl/helpers"
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
"github.com/cloudflare/cfssl/revoke"
|
"git.wntrmute.dev/kyle/goutils/certlib/revoke"
|
||||||
"github.com/kisom/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
"github.com/kisom/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printRevocation(cert *x509.Certificate) {
|
func printRevocation(cert *x509.Certificate) {
|
||||||
remaining := cert.NotAfter.Sub(time.Now())
|
remaining := time.Until(cert.NotAfter)
|
||||||
fmt.Printf("certificate expires in %s.\n", lib.Duration(remaining))
|
fmt.Printf("certificate expires in %s.\n", lib.Duration(remaining))
|
||||||
|
|
||||||
revoked, ok := revoke.VerifyCertificate(cert)
|
revoked, ok := revoke.VerifyCertificate(cert)
|
||||||
@@ -47,7 +47,7 @@ func main() {
|
|||||||
if verbose {
|
if verbose {
|
||||||
fmt.Println("[+] loading root certificates from", caFile)
|
fmt.Println("[+] loading root certificates from", caFile)
|
||||||
}
|
}
|
||||||
roots, err = helpers.LoadPEMCertPool(caFile)
|
roots, err = certlib.LoadPEMCertPool(caFile)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ func main() {
|
|||||||
if verbose {
|
if verbose {
|
||||||
fmt.Println("[+] loading intermediate certificates from", intFile)
|
fmt.Println("[+] loading intermediate certificates from", intFile)
|
||||||
}
|
}
|
||||||
ints, err = helpers.LoadPEMCertPool(caFile)
|
ints, err = certlib.LoadPEMCertPool(caFile)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
} else {
|
} else {
|
||||||
ints = x509.NewCertPool()
|
ints = x509.NewCertPool()
|
||||||
@@ -71,7 +71,7 @@ func main() {
|
|||||||
fileData, err := ioutil.ReadFile(flag.Arg(0))
|
fileData, err := ioutil.ReadFile(flag.Arg(0))
|
||||||
die.If(err)
|
die.If(err)
|
||||||
|
|
||||||
chain, err := helpers.ParseCertificatesPEM(fileData)
|
chain, err := certlib.ParseCertificatesPEM(fileData)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
if verbose {
|
if verbose {
|
||||||
fmt.Printf("[+] %s has %d certificates\n", flag.Arg(0), len(chain))
|
fmt.Printf("[+] %s has %d certificates\n", flag.Arg(0), len(chain))
|
||||||
|
|||||||
20
cmd/clustersh/BUILD.bazel
Normal file
20
cmd/clustersh/BUILD.bazel
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "clustersh_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/clustersh",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//lib",
|
||||||
|
"@com_github_pkg_sftp//:sftp",
|
||||||
|
"@org_golang_x_crypto//ssh",
|
||||||
|
"@org_golang_x_crypto//ssh/agent",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "clustersh",
|
||||||
|
embed = [":clustersh_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/kisom/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/agent"
|
"golang.org/x/crypto/ssh/agent"
|
||||||
|
|||||||
18
cmd/cruntar/BUILD.bazel
Normal file
18
cmd/cruntar/BUILD.bazel
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "cruntar_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/cruntar",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//die",
|
||||||
|
"//fileutil",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "cruntar",
|
||||||
|
embed = [":cruntar_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
20
cmd/cruntar/README
Normal file
20
cmd/cruntar/README
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
ChromeOS untar
|
||||||
|
|
||||||
|
This is a tool that is intended to support untarring on SquashFS file
|
||||||
|
systems. In particular, every time it encounters a hard link, it
|
||||||
|
will just create a copy of the file.
|
||||||
|
|
||||||
|
Usage: cruntar [-jmvpz] archive [dest]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-a Shortcut for -m -p: preserve owners and file mode.
|
||||||
|
-j The archive is compressed with bzip2.
|
||||||
|
-m Preserve file modes.
|
||||||
|
-p Preserve ownership.
|
||||||
|
-v Print the name of each file as it is being processed.
|
||||||
|
-z The archive is compressed with gzip.
|
||||||
|
|
||||||
|
I wrote this after running into problems with untarring the
|
||||||
|
gcc-arm-eabi-none toolchain. The shared storage in Termux under
|
||||||
|
ChromeOS doesn't support hard links, so I opted to just make a copy
|
||||||
|
rather than dealing with links and whatnot.
|
||||||
277
cmd/cruntar/main.go
Normal file
277
cmd/cruntar/main.go
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/bzip2"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/fileutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
preserveOwners bool
|
||||||
|
preserveMode bool
|
||||||
|
verbose bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupFile(hdr *tar.Header, file *os.File) error {
|
||||||
|
if preserveMode {
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("\tchmod %0#o\n", hdr.Mode)
|
||||||
|
}
|
||||||
|
err := file.Chmod(os.FileMode(hdr.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if preserveOwners {
|
||||||
|
fmt.Printf("\tchown %d:%d\n", hdr.Uid, hdr.Gid)
|
||||||
|
err := file.Chown(hdr.Uid, hdr.Gid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkTarget(target, top string) string {
|
||||||
|
if filepath.IsAbs(target) {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Clean(filepath.Join(target, top))
|
||||||
|
}
|
||||||
|
|
||||||
|
func processFile(tfr *tar.Reader, hdr *tar.Header, top string) error {
|
||||||
|
if verbose {
|
||||||
|
fmt.Println(hdr.Name)
|
||||||
|
}
|
||||||
|
filePath := filepath.Clean(filepath.Join(top, hdr.Name))
|
||||||
|
switch hdr.Typeflag {
|
||||||
|
case tar.TypeReg:
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(file, tfr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setupFile(hdr, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case tar.TypeLink:
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
source, err := os.Open(hdr.Linkname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(file, source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setupFile(hdr, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case tar.TypeSymlink:
|
||||||
|
if !fileutil.ValidateSymlink(hdr.Linkname, top) {
|
||||||
|
return fmt.Errorf("symlink %s is outside the top-level %s",
|
||||||
|
hdr.Linkname, top)
|
||||||
|
}
|
||||||
|
path := linkTarget(hdr.Linkname, top)
|
||||||
|
if ok, err := filepath.Match(top+"/*", filepath.Clean(path)); !ok {
|
||||||
|
return fmt.Errorf("symlink %s isn't in %s", hdr.Linkname, top)
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.Symlink(linkTarget(hdr.Linkname, top), filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case tar.TypeDir:
|
||||||
|
err := os.MkdirAll(filePath, os.FileMode(hdr.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var compression = map[string]bool{
|
||||||
|
"gzip": false,
|
||||||
|
"bzip2": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
type bzipCloser struct {
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (brc *bzipCloser) Read(p []byte) (int, error) {
|
||||||
|
return brc.r.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (brc *bzipCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBzipCloser(r io.ReadCloser) (io.ReadCloser, error) {
|
||||||
|
br := bzip2.NewReader(r)
|
||||||
|
return &bzipCloser{r: br}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var compressFuncs = map[string]func(io.ReadCloser) (io.ReadCloser, error){
|
||||||
|
"gzip": func(r io.ReadCloser) (io.ReadCloser, error) { return gzip.NewReader(r) },
|
||||||
|
"bzip2": newBzipCloser,
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyCompression() bool {
|
||||||
|
var compressed bool
|
||||||
|
for _, v := range compression {
|
||||||
|
if compressed && v {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
compressed = compressed || v
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReader(r io.ReadCloser) (io.ReadCloser, error) {
|
||||||
|
for c, v := range compression {
|
||||||
|
if v {
|
||||||
|
return compressFuncs[c](r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openArchive(path string) (io.ReadCloser, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := getReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var compressFlags struct {
|
||||||
|
z bool
|
||||||
|
j bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCompressFlags() error {
|
||||||
|
if compressFlags.z {
|
||||||
|
compression["gzip"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if compressFlags.j {
|
||||||
|
compression["bzip2"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verifyCompression() {
|
||||||
|
return errors.New("multiple compression formats specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `ChromeOS untar
|
||||||
|
|
||||||
|
This is a tool that is intended to support untarring on SquashFS file
|
||||||
|
systems. In particular, every time it encounters a hard link, it
|
||||||
|
will just create a copy of the file.
|
||||||
|
|
||||||
|
Usage: cruntar [-jmvpz] archive [dest]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-a Shortcut for -m -p: preserve owners and file mode.
|
||||||
|
-j The archive is compressed with bzip2.
|
||||||
|
-m Preserve file modes.
|
||||||
|
-p Preserve ownership.
|
||||||
|
-v Print the name of each file as it is being processed.
|
||||||
|
-z The archive is compressed with gzip.
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stderr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var archive, help bool
|
||||||
|
flag.BoolVar(&archive, "a", false, "Shortcut for -m -p: preserve owners and file mode.")
|
||||||
|
flag.BoolVar(&help, "h", false, "print a help message")
|
||||||
|
flag.BoolVar(&compressFlags.j, "j", false, "bzip2 compression")
|
||||||
|
flag.BoolVar(&preserveMode, "m", false, "preserve file modes")
|
||||||
|
flag.BoolVar(&preserveOwners, "p", false, "preserve ownership")
|
||||||
|
flag.BoolVar(&verbose, "v", false, "verbose mode")
|
||||||
|
flag.BoolVar(&compressFlags.z, "z", false, "gzip compression")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if help {
|
||||||
|
usage(os.Stdout)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if archive {
|
||||||
|
preserveMode = true
|
||||||
|
preserveOwners = true
|
||||||
|
}
|
||||||
|
|
||||||
|
err := parseCompressFlags()
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
if flag.NArg() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
top := "./"
|
||||||
|
if flag.NArg() > 1 {
|
||||||
|
top = flag.Arg(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := openArchive(flag.Arg(0))
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
tfr := tar.NewReader(r)
|
||||||
|
for {
|
||||||
|
hdr, err := tfr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
err = processFile(tfr, hdr, top)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
15
cmd/csrpubdump/BUILD.bazel
Normal file
15
cmd/csrpubdump/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "csrpubdump_lib",
|
||||||
|
srcs = ["pubdump.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/csrpubdump",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//die"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "csrpubdump",
|
||||||
|
embed = [":csrpubdump_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
19
cmd/diskimg/BUILD.bazel
Normal file
19
cmd/diskimg/BUILD.bazel
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "diskimg_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/diskimg",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//ahash",
|
||||||
|
"//dbg",
|
||||||
|
"//die",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "diskimg",
|
||||||
|
embed = [":diskimg_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
34
cmd/diskimg/README
Normal file
34
cmd/diskimg/README
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
diskimg: write disk images
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
diskimg [-a algo] [-v] image device
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-a algo Select the hashing algorithm to use. The default
|
||||||
|
is 'sha256'. Specifying an algorithm of 'list'
|
||||||
|
will print the supported algorithms to standard
|
||||||
|
output and exit with error code 2.
|
||||||
|
-v Enable verbose (debug) output.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
Copying images/server.img to /dev/sda:
|
||||||
|
|
||||||
|
$ sudo diskimg images/server.img /dev/sda
|
||||||
|
|
||||||
|
Write a bladerunner node image to /dev/sda:
|
||||||
|
|
||||||
|
$ sudo diskimg -v ~/code/bladerunner/packer/build/cm4-cnode-ubuntu-22.04.2.img /dev/sda
|
||||||
|
opening image /home/kyle/code/bladerunner/packer/build/cm4-cnode-ubuntu-22.04.2.img for read
|
||||||
|
/home/kyle/code/bladerunner/packer/build/cm4-cnode-ubuntu-22.04.2.img 416d4c8f890904167419e3d488d097e9c847273376b650546fdb1f6f9809c184
|
||||||
|
opening device /dev/sda for rw
|
||||||
|
writing /home/kyle/code/bladerunner/packer/build/cm4-cnode-ubuntu-22.04.2.img -> /dev/sda
|
||||||
|
wrote 4151312384 bytes to /dev/sda
|
||||||
|
syncing /dev/sda
|
||||||
|
verifying the image was written successfully
|
||||||
|
OK
|
||||||
|
|
||||||
|
Motivation:
|
||||||
|
|
||||||
|
I wanted to write something like balena's Etcher, but commandline only.
|
||||||
|
|
||||||
116
cmd/diskimg/main.go
Normal file
116
cmd/diskimg/main.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/ahash"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/dbg"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultHashAlgorithm = "sha256"
|
||||||
|
|
||||||
|
var (
|
||||||
|
hAlgo string
|
||||||
|
debug = dbg.New()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func openImage(imageFile string) (image *os.File, hash []byte, err error) {
|
||||||
|
image, err = os.Open(imageFile)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err = ahash.SumReader(hAlgo, image)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = image.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Printf("%s %x\n", imageFile, hash)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func openDevice(devicePath string) (device *os.File, err error) {
|
||||||
|
fi, err := os.Stat(devicePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err = os.OpenFile(devicePath, os.O_RDWR|os.O_SYNC, fi.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&hAlgo, "a", defaultHashAlgorithm, "default hash algorithm")
|
||||||
|
flag.BoolVar(&debug.Enabled, "v", false, "enable debug logging")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if hAlgo == "list" {
|
||||||
|
fmt.Println("Supported hashing algorithms:")
|
||||||
|
for _, algo := range ahash.SecureHashList() {
|
||||||
|
fmt.Printf("\t- %s\n", algo)
|
||||||
|
}
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if flag.NArg() != 2 {
|
||||||
|
die.With("usage: diskimg image device")
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFile := flag.Arg(0)
|
||||||
|
devicePath := flag.Arg(1)
|
||||||
|
|
||||||
|
debug.Printf("opening image %s for read\n", imageFile)
|
||||||
|
image, hash, err := openImage(imageFile)
|
||||||
|
if image != nil {
|
||||||
|
defer image.Close()
|
||||||
|
}
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
debug.Printf("opening device %s for rw\n", devicePath)
|
||||||
|
device, err := openDevice(devicePath)
|
||||||
|
if device != nil {
|
||||||
|
defer device.Close()
|
||||||
|
}
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
debug.Printf("writing %s -> %s\n", imageFile, devicePath)
|
||||||
|
n, err := io.Copy(device, image)
|
||||||
|
die.If(err)
|
||||||
|
debug.Printf("wrote %d bytes to %s\n", n, devicePath)
|
||||||
|
|
||||||
|
debug.Printf("syncing %s\n", devicePath)
|
||||||
|
err = device.Sync()
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
debug.Println("verifying the image was written successfully")
|
||||||
|
_, err = device.Seek(0, 0)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
deviceHash, err := ahash.SumLimitedReader(hAlgo, device, n)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
if !bytes.Equal(deviceHash, hash) {
|
||||||
|
fmt.Fprintln(os.Stderr, "Hash mismatch:")
|
||||||
|
fmt.Fprintf(os.Stderr, "\t%s: %s\n", imageFile, hash)
|
||||||
|
fmt.Fprintf(os.Stderr, "\t%s: %s\n", devicePath, deviceHash)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Println("OK")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
15
cmd/eig/BUILD.bazel
Normal file
15
cmd/eig/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "eig_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/eig",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//die"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "eig",
|
||||||
|
embed = [":eig_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
52
cmd/eig/main.go
Normal file
52
cmd/eig/main.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
// size of a kilobit in bytes
|
||||||
|
const kilobit = 128
|
||||||
|
const pageSize = 4096
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
size := flag.Int("s", 256*kilobit, "size of EEPROM image in kilobits")
|
||||||
|
fill := flag.Uint("f", 0, "byte to fill image with")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *fill > 256 {
|
||||||
|
die.With("`fill` argument must be a byte value")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := "eeprom.img"
|
||||||
|
|
||||||
|
if flag.NArg() > 0 {
|
||||||
|
path = flag.Arg(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fillByte := uint8(*fill)
|
||||||
|
|
||||||
|
buf := make([]byte, pageSize)
|
||||||
|
for i := 0; i < pageSize; i++ {
|
||||||
|
buf[i] = fillByte
|
||||||
|
}
|
||||||
|
|
||||||
|
pages := *size / pageSize
|
||||||
|
last := *size % pageSize
|
||||||
|
|
||||||
|
file, err := os.Create(path)
|
||||||
|
die.If(err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
for i := 0; i < pages; i++ {
|
||||||
|
_, err = file.Write(buf)
|
||||||
|
die.If(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if last != 0 {
|
||||||
|
_, err = file.Write(buf[:last])
|
||||||
|
die.If(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
cmd/fragment/BUILD.bazel
Normal file
15
cmd/fragment/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "fragment_lib",
|
||||||
|
srcs = ["fragment.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/fragment",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//die"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "fragment",
|
||||||
|
embed = [":fragment_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -4,16 +4,21 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
func usage() {
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stdout); os.Exit(1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
progname := filepath.Base(os.Args[0])
|
progname := filepath.Base(os.Args[0])
|
||||||
fmt.Printf(`Usage: %s [-nl] file start [end]
|
fmt.Fprintf(w, `Usage: %s [-nl] file start [end]
|
||||||
|
|
||||||
Print a fragment of a file starting a line 'start' and ending
|
Print a fragment of a file starting a line 'start' and ending
|
||||||
at line 'end', or EOF if no end is specified.
|
at line 'end', or EOF if no end is specified.
|
||||||
@@ -27,7 +32,7 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if flag.NArg() < 2 || flag.NArg() > 3 {
|
if flag.NArg() < 2 || flag.NArg() > 3 {
|
||||||
usage()
|
usage(os.Stderr)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
cmd/jlp/BUILD.bazel
Normal file
15
cmd/jlp/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "jlp_lib",
|
||||||
|
srcs = ["jlp.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/jlp",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//lib"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "jlp",
|
||||||
|
embed = [":jlp_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func prettify(file string, validateOnly bool) error {
|
func prettify(file string, validateOnly bool) error {
|
||||||
|
|||||||
15
cmd/kgz/BUILD.bazel
Normal file
15
cmd/kgz/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "kgz_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/kgz",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["@com_github_pkg_errors//:errors"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "kgz",
|
||||||
|
embed = [":kgz_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
23
cmd/kgz/README
Normal file
23
cmd/kgz/README
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
kgz
|
||||||
|
|
||||||
|
kgz is like gzip, but supports compressing and decompressing to a different
|
||||||
|
directory than the source file is in.
|
||||||
|
|
||||||
|
Usage: kgz [-l] source [target]
|
||||||
|
|
||||||
|
If target is a directory, the basename of the sourcefile will be used
|
||||||
|
as the target filename. Compression and decompression is selected
|
||||||
|
based on whether the source filename ends in ".gz".
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-l level Compression level (0-9). Only meaninful when
|
||||||
|
compressing a file.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
182
cmd/kgz/main.go
Normal file
182
cmd/kgz/main.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/flate"
|
||||||
|
"compress/gzip"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const gzipExt = ".gz"
|
||||||
|
|
||||||
|
func compress(path, target string, level int) error {
|
||||||
|
sourceFile, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "opening file for read")
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(target)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "opening file for write")
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
gzipCompressor, err := gzip.NewWriterLevel(destFile, level)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid compression level")
|
||||||
|
}
|
||||||
|
defer gzipCompressor.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(gzipCompressor, sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "compressing file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "stat(2)ing destination file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uncompress(path, target string) error {
|
||||||
|
sourceFile, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "opening file for read")
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
gzipUncompressor, err := gzip.NewReader(sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "reading gzip headers")
|
||||||
|
}
|
||||||
|
defer gzipUncompressor.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(target)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "opening file for write")
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(destFile, gzipUncompressor)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "uncompressing file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `Usage: %s [-l] source [target]
|
||||||
|
|
||||||
|
kgz is like gzip, but supports compressing and decompressing to a different
|
||||||
|
directory than the source file is in.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-l level Compression level (0-9). Only meaninful when
|
||||||
|
compressing a file.
|
||||||
|
`, os.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stderr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDir(path string) bool {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
stat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.IsDir() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathForUncompressing(source, dest string) (string, error) {
|
||||||
|
if !isDir(dest) {
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
source = filepath.Base(source)
|
||||||
|
if !strings.HasSuffix(source, gzipExt) {
|
||||||
|
return "", errors.Errorf("%s is a not gzip-compressed file", source)
|
||||||
|
}
|
||||||
|
outFile := source[:len(source)-len(gzipExt)]
|
||||||
|
outFile = filepath.Join(dest, outFile)
|
||||||
|
return outFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathForCompressing(source, dest string) (string, error) {
|
||||||
|
if !isDir(dest) {
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
source = filepath.Base(source)
|
||||||
|
if strings.HasSuffix(source, gzipExt) {
|
||||||
|
return "", errors.Errorf("%s is a gzip-compressed file", source)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest = filepath.Join(dest, source+gzipExt)
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var level int
|
||||||
|
var path string
|
||||||
|
var target = "."
|
||||||
|
|
||||||
|
flag.IntVar(&level, "l", flate.DefaultCompression, "compression level")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() < 1 || flag.NArg() > 2 {
|
||||||
|
usage(os.Stderr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
path = flag.Arg(0)
|
||||||
|
if flag.NArg() == 2 {
|
||||||
|
target = flag.Arg(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(path, gzipExt) {
|
||||||
|
target, err := pathForUncompressing(path, target)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = uncompress(path, target)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(target)
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target, err := pathForCompressing(path, target)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = compress(path, target, level)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(target)
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
cmd/parts/BUILD.bazel
Normal file
15
cmd/parts/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "parts_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/parts",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//die"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "parts",
|
||||||
|
embed = [":parts_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
9
cmd/parts/README
Normal file
9
cmd/parts/README
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
parts: simple parts database for electronic components
|
||||||
|
|
||||||
|
Usage: parts [id] -- query the database for a part
|
||||||
|
parts [-c class] [id] [description] -- store a part in the database
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-f path Path to parts database (default is
|
||||||
|
/home/kyle/.parts.json).
|
||||||
|
|
||||||
142
cmd/parts/main.go
Normal file
142
cmd/parts/main.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dbVersion = "1"
|
||||||
|
|
||||||
|
var dbFile = filepath.Join(os.Getenv("HOME"), ".parts.json")
|
||||||
|
var partsDB = &database{Version: dbVersion}
|
||||||
|
|
||||||
|
type part struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Class string `json:"class,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p part) String() string {
|
||||||
|
return fmt.Sprintf("%s: %s", p.Name, p.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
LastUpdate int64 `json:"json"`
|
||||||
|
Parts map[string]part `json:"parts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func help(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `Usage: parts [id] -- query the database for a part
|
||||||
|
parts [-c class] [id] [description] -- store a part in the database
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-f path Path to parts database (default is
|
||||||
|
%s).
|
||||||
|
|
||||||
|
`, dbFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDatabase() {
|
||||||
|
data, err := ioutil.ReadFile(dbFile)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
partsDB = &database{
|
||||||
|
Version: dbVersion,
|
||||||
|
Parts: map[string]part{},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, partsDB)
|
||||||
|
die.If(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPart(partName string) {
|
||||||
|
partName = strings.ToLower(partName)
|
||||||
|
for name, part := range partsDB.Parts {
|
||||||
|
if strings.Contains(strings.ToLower(name), partName) {
|
||||||
|
fmt.Println(part.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDB() {
|
||||||
|
data, err := json.Marshal(partsDB)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(dbFile, data, 0644)
|
||||||
|
die.If(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func storePart(name, class, description string) {
|
||||||
|
p, exists := partsDB.Parts[name]
|
||||||
|
if exists {
|
||||||
|
fmt.Printf("warning: replacing part %s\n", name)
|
||||||
|
fmt.Printf("\t%s\n", p.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
partsDB.Parts[name] = part{
|
||||||
|
Name: name,
|
||||||
|
Class: class,
|
||||||
|
Description: description,
|
||||||
|
}
|
||||||
|
|
||||||
|
writeDB()
|
||||||
|
}
|
||||||
|
|
||||||
|
func listParts() {
|
||||||
|
parts := make([]string, 0, len(partsDB.Parts))
|
||||||
|
for partName := range partsDB.Parts {
|
||||||
|
parts = append(parts, partName)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(parts)
|
||||||
|
for _, partName := range parts {
|
||||||
|
fmt.Println(partsDB.Parts[partName].String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var class string
|
||||||
|
var helpFlag bool
|
||||||
|
|
||||||
|
flag.StringVar(&class, "c", "", "device class")
|
||||||
|
flag.StringVar(&dbFile, "f", dbFile, "`path` to database")
|
||||||
|
flag.BoolVar(&helpFlag, "h", false, "Print a help message.")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if helpFlag {
|
||||||
|
help(os.Stdout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDatabase()
|
||||||
|
|
||||||
|
switch flag.NArg() {
|
||||||
|
case 0:
|
||||||
|
help(os.Stdout)
|
||||||
|
return
|
||||||
|
case 1:
|
||||||
|
partName := flag.Arg(0)
|
||||||
|
if partName == "list" {
|
||||||
|
listParts()
|
||||||
|
} else {
|
||||||
|
findPart(flag.Arg(0))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
description := strings.Join(flag.Args()[1:], " ")
|
||||||
|
storePart(flag.Arg(0), class, description)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
14
cmd/pem2bin/BUILD.bazel
Normal file
14
cmd/pem2bin/BUILD.bazel
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "pem2bin_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/pem2bin",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "pem2bin",
|
||||||
|
embed = [":pem2bin_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
15
cmd/pembody/BUILD.bazel
Normal file
15
cmd/pembody/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "pembody_lib",
|
||||||
|
srcs = ["pembody.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/pembody",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//lib"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "pembody",
|
||||||
|
embed = [":pembody_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
19
cmd/pemit/BUILD.bazel
Normal file
19
cmd/pemit/BUILD.bazel
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "pemit_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/pemit",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//assert",
|
||||||
|
"//die",
|
||||||
|
"//lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "pemit",
|
||||||
|
embed = [":pemit_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/assert"
|
"git.wntrmute.dev/kyle/goutils/assert"
|
||||||
"github.com/kisom/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
"github.com/kisom/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func usage(w io.Writer) {
|
func usage(w io.Writer) {
|
||||||
|
|||||||
14
cmd/readchain/BUILD.bazel
Normal file
14
cmd/readchain/BUILD.bazel
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "readchain_lib",
|
||||||
|
srcs = ["chain.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/readchain",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "readchain",
|
||||||
|
embed = [":readchain_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
18
cmd/renfnv/BUILD.bazel
Normal file
18
cmd/renfnv/BUILD.bazel
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "renfnv_lib",
|
||||||
|
srcs = ["renfnv.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/renfnv",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//fileutil",
|
||||||
|
"//lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "renfnv",
|
||||||
|
embed = [":renfnv_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kisom/goutils/fileutil"
|
"git.wntrmute.dev/kyle/goutils/fileutil"
|
||||||
"github.com/kisom/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func hashName(path, encodedHash string) string {
|
func hashName(path, encodedHash string) string {
|
||||||
@@ -46,7 +46,6 @@ func newName(path string) (string, error) {
|
|||||||
func move(dst, src string, force bool) (err error) {
|
func move(dst, src string, force bool) (err error) {
|
||||||
if fileutil.FileDoesExist(dst) && !force {
|
if fileutil.FileDoesExist(dst) && !force {
|
||||||
return fmt.Errorf("%s exists (pass the -f flag to overwrite)", dst)
|
return fmt.Errorf("%s exists (pass the -f flag to overwrite)", dst)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
dstFile, err := os.Create(dst)
|
dstFile, err := os.Create(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,7 +91,7 @@ Options:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.Usage = func () { usage(os.Stdout) }
|
flag.Usage = func() { usage(os.Stdout) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
19
cmd/rhash/BUILD.bazel
Normal file
19
cmd/rhash/BUILD.bazel
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "rhash_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/rhash",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//ahash",
|
||||||
|
"//die",
|
||||||
|
"//lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "rhash",
|
||||||
|
embed = [":rhash_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
22
cmd/rhash/README
Normal file
22
cmd/rhash/README
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
rhash: remote hashing tool
|
||||||
|
|
||||||
|
Usage: rhash [-a algo] [-h] [-l set] urls...
|
||||||
|
Compute the hash over each URL.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-a algo Specify the hash algorithm to use; the default is sha256.
|
||||||
|
-h Print this help message.
|
||||||
|
-l set List the hash functions under set. Set can be one of all,
|
||||||
|
secure to list only cryptographic hash functions, or
|
||||||
|
insecure to list only non-cryptographic hash functions.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Compute the SHA256 digest of the LICENSE in this repository:
|
||||||
|
|
||||||
|
$ rhash https://raw.githubusercontent.com/kisom/goutils/7391da8567952f69990194ead2842d21df217c89/LICENSE
|
||||||
|
LICENSE: sha256=620bfadeb698df6c6db73908689a29371a9d4cff32b08c48a5c4307946093980
|
||||||
|
|
||||||
|
Compute the SHA-1 digest of the LICENSE in this repository:
|
||||||
|
|
||||||
|
$ rhash -a sha1 https://raw.githubusercontent.com/kisom/goutils/7391da8567952f69990194ead2842d21df217c89/LICENSE
|
||||||
|
LICENSE: sha1=83c6e2e410715058ed6e7c1572176122c024e367
|
||||||
97
cmd/rhash/main.go
Normal file
97
cmd/rhash/main.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/ahash"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `Usage: %s [-a algo] [-h] [-l set] urls...
|
||||||
|
Compute the hash over each URL.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-a algo Specify the hash algorithm to use; the default is sha256.
|
||||||
|
-h Print this help message.
|
||||||
|
-l set List the hash functions under set. Set can be one of all,
|
||||||
|
secure to list only cryptographic hash functions, or
|
||||||
|
insecure to list only non-cryptographic hash functions.
|
||||||
|
|
||||||
|
`, lib.ProgName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stderr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var algo, list string
|
||||||
|
var help bool
|
||||||
|
flag.StringVar(&algo, "a", "sha256", "hash algorithm to use")
|
||||||
|
flag.BoolVar(&help, "h", false, "print a help message")
|
||||||
|
flag.StringVar(&list, "l", "", "list known hash algorithms (one of all, secure, insecure)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if help {
|
||||||
|
usage(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if list != "" {
|
||||||
|
var hashes []string
|
||||||
|
switch list {
|
||||||
|
case "all":
|
||||||
|
hashes = ahash.HashList()
|
||||||
|
case "secure":
|
||||||
|
hashes = ahash.SecureHashList()
|
||||||
|
case "insecure":
|
||||||
|
hashes = ahash.InsecureHashList()
|
||||||
|
default:
|
||||||
|
die.With("list option must be one of all, secure, or insecure.")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, algo := range hashes {
|
||||||
|
fmt.Printf("- %s\n", algo)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, remote := range flag.Args() {
|
||||||
|
u, err := url.Parse(remote)
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "parsing %s", remote)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := filepath.Base(u.Path)
|
||||||
|
if name == "" {
|
||||||
|
lib.Warnx("source URL doesn't appear to name a file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(remote)
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "fetching %s", remote)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "fetching %s", remote)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sum, err := ahash.SumReader(algo, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
lib.Err(lib.ExitFailure, err, "while hashing data")
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %s=%x\n", name, algo, sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
cmd/showimp/BUILD.bazel
Normal file
18
cmd/showimp/BUILD.bazel
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "showimp_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/showimp",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//dbg",
|
||||||
|
"//die",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "showimp",
|
||||||
|
embed = [":showimp_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -12,36 +12,23 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/dbg"
|
||||||
"github.com/kisom/goutils/logging"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gopath string
|
gopath string
|
||||||
project string
|
project string
|
||||||
debug bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
stdLibRegexp = regexp.MustCompile(`^\w+(/\w+)*$`)
|
debug = dbg.New()
|
||||||
sourceRegexp = regexp.MustCompile(`^[^.].*\.go$`)
|
|
||||||
log = logging.NewConsole()
|
|
||||||
imports = map[string]bool{}
|
|
||||||
fset = &token.FileSet{}
|
fset = &token.FileSet{}
|
||||||
|
imports = map[string]bool{}
|
||||||
|
sourceRegexp = regexp.MustCompile(`^[^.].*\.go$`)
|
||||||
|
stdLibRegexp = regexp.MustCompile(`^\w+(/\w+)*$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func debugf(format string, args ...interface{}) {
|
|
||||||
if debug {
|
|
||||||
fmt.Printf(format, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func debugln(args ...interface{}) {
|
|
||||||
if debug {
|
|
||||||
fmt.Println(args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gopath = os.Getenv("GOPATH")
|
gopath = os.Getenv("GOPATH")
|
||||||
if gopath == "" {
|
if gopath == "" {
|
||||||
@@ -67,11 +54,15 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func walkFile(path string, info os.FileInfo, err error) error {
|
func walkFile(path string, info os.FileInfo, err error) error {
|
||||||
|
if ignores[path] {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
if !sourceRegexp.MatchString(path) {
|
if !sourceRegexp.MatchString(path) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
debugln(path)
|
debug.Println(path)
|
||||||
|
|
||||||
f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
|
f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -81,26 +72,40 @@ func walkFile(path string, info os.FileInfo, err error) error {
|
|||||||
for _, importSpec := range f.Imports {
|
for _, importSpec := range f.Imports {
|
||||||
importPath := strings.Trim(importSpec.Path.Value, `"`)
|
importPath := strings.Trim(importSpec.Path.Value, `"`)
|
||||||
if stdLibRegexp.MatchString(importPath) {
|
if stdLibRegexp.MatchString(importPath) {
|
||||||
debugln("standard lib:", importPath)
|
debug.Println("standard lib:", importPath)
|
||||||
continue
|
continue
|
||||||
} else if strings.HasPrefix(importPath, project) {
|
} else if strings.HasPrefix(importPath, project) {
|
||||||
debugln("internal import:", importPath)
|
debug.Println("internal import:", importPath)
|
||||||
continue
|
continue
|
||||||
} else if strings.HasPrefix(importPath, "golang.org/") {
|
} else if strings.HasPrefix(importPath, "golang.org/") {
|
||||||
debugln("extended lib:", importPath)
|
debug.Println("extended lib:", importPath)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
debugln("import:", importPath)
|
debug.Println("import:", importPath)
|
||||||
imports[importPath] = true
|
imports[importPath] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ignores = map[string]bool{}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.BoolVar(&debug, "v", false, "log debugging information")
|
var ignoreLine string
|
||||||
|
var noVendor bool
|
||||||
|
flag.StringVar(&ignoreLine, "i", "", "comma-separated list of directories to ignore")
|
||||||
|
flag.BoolVar(&noVendor, "nv", false, "ignore the vendor directory")
|
||||||
|
flag.BoolVar(&debug.Enabled, "v", false, "log debugging information")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if noVendor {
|
||||||
|
ignores["vendor"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, word := range strings.Split(ignoreLine, ",") {
|
||||||
|
ignores[strings.TrimSpace(word)] = true
|
||||||
|
}
|
||||||
|
|
||||||
err := filepath.Walk(".", walkFile)
|
err := filepath.Walk(".", walkFile)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
|
|
||||||
|
|||||||
18
cmd/ski/BUILD.bazel
Normal file
18
cmd/ski/BUILD.bazel
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "ski_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/ski",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//die",
|
||||||
|
"//lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "ski",
|
||||||
|
embed = [":ski_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
30
cmd/ski/README
Normal file
30
cmd/ski/README
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
ski: print subject public key info
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ski [-hm] files...
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h Print a help message and exit.
|
||||||
|
-m All SKIs should match.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
Printing the SKI of a private key and certificate:
|
||||||
|
|
||||||
|
$ ski *
|
||||||
|
server.key 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA private key)
|
||||||
|
[ski] trailing data in PEM file
|
||||||
|
server.pem 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA certificate)
|
||||||
|
|
||||||
|
Making sure the SKIs match:
|
||||||
|
|
||||||
|
$ ski -m *
|
||||||
|
tyrfingr.key 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA private key)
|
||||||
|
[ski] trailing data in PEM file
|
||||||
|
tyrfingr.pem 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA certificate)
|
||||||
|
|
||||||
|
Making sure the SKIs match with a bad certificate:
|
||||||
|
$ ski -m server.key bad.pem
|
||||||
|
server.key 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA private key)
|
||||||
|
[ski] bad.pem: SKI mismatch (3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E != 90:AF:6A:3A:94:5A:0B:D8:90:EA:12:56:73:DF:43:B4:3A:28:DA:E7)
|
||||||
|
bad.pem 90:AF:6A:3A:94:5A:0B:D8:90:EA:12:56:73:DF:43:B4:3A:28:DA:E7 (RSA certificate)
|
||||||
191
cmd/ski/main.go
Normal file
191
cmd/ski/main.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"encoding/pem"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `ski: print subject key info for PEM-encoded files
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ski [-hm] files...
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h Print this help message.
|
||||||
|
-m All SKIs should match; as soon as an SKI mismatch is found,
|
||||||
|
it is reported.
|
||||||
|
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stderr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(path string) (public []byte, kt, ft string) {
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
data = bytes.TrimSpace(data)
|
||||||
|
p, rest := pem.Decode(data)
|
||||||
|
if len(rest) > 0 {
|
||||||
|
lib.Warnx("trailing data in PEM file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p == nil {
|
||||||
|
die.With("no PEM data found")
|
||||||
|
}
|
||||||
|
|
||||||
|
data = p.Bytes
|
||||||
|
|
||||||
|
switch p.Type {
|
||||||
|
case "PRIVATE KEY", "RSA PRIVATE KEY", "EC PRIVATE KEY":
|
||||||
|
public, kt = parseKey(data)
|
||||||
|
ft = "private key"
|
||||||
|
case "CERTIFICATE":
|
||||||
|
public, kt = parseCertificate(data)
|
||||||
|
ft = "certificate"
|
||||||
|
case "CERTIFICATE REQUEST":
|
||||||
|
public, kt = parseCSR(data)
|
||||||
|
ft = "certificate request"
|
||||||
|
default:
|
||||||
|
die.With("unknown PEM type %s", p.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKey(data []byte) (public []byte, kt string) {
|
||||||
|
privInterface, err := x509.ParsePKCS8PrivateKey(data)
|
||||||
|
if err != nil {
|
||||||
|
privInterface, err = x509.ParsePKCS1PrivateKey(data)
|
||||||
|
if err != nil {
|
||||||
|
privInterface, err = x509.ParseECPrivateKey(data)
|
||||||
|
if err != nil {
|
||||||
|
die.With("couldn't parse private key.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var priv crypto.Signer
|
||||||
|
switch privInterface.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
priv = privInterface.(*rsa.PrivateKey)
|
||||||
|
kt = "RSA"
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
priv = privInterface.(*ecdsa.PrivateKey)
|
||||||
|
kt = "ECDSA"
|
||||||
|
default:
|
||||||
|
die.With("unknown private key type %T", privInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
public, err = x509.MarshalPKIXPublicKey(priv.Public())
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCertificate(data []byte) (public []byte, kt string) {
|
||||||
|
cert, err := x509.ParseCertificate(data)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
pub := cert.PublicKey
|
||||||
|
switch pub.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
kt = "RSA"
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
kt = "ECDSA"
|
||||||
|
default:
|
||||||
|
die.With("unknown public key type %T", pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
public, err = x509.MarshalPKIXPublicKey(pub)
|
||||||
|
die.If(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCSR(data []byte) (public []byte, kt string) {
|
||||||
|
csr, err := x509.ParseCertificateRequest(data)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
pub := csr.PublicKey
|
||||||
|
switch pub.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
kt = "RSA"
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
kt = "ECDSA"
|
||||||
|
default:
|
||||||
|
die.With("unknown public key type %T", pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
public, err = x509.MarshalPKIXPublicKey(pub)
|
||||||
|
die.If(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpHex(in []byte) string {
|
||||||
|
var s string
|
||||||
|
for i := range in {
|
||||||
|
s += fmt.Sprintf("%02X:", in[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(s, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
type subjectPublicKeyInfo struct {
|
||||||
|
Algorithm pkix.AlgorithmIdentifier
|
||||||
|
SubjectPublicKey asn1.BitString
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var help, shouldMatch bool
|
||||||
|
flag.BoolVar(&help, "h", false, "print a help message and exit")
|
||||||
|
flag.BoolVar(&shouldMatch, "m", false, "all SKIs should match")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if help {
|
||||||
|
usage(os.Stdout)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ski string
|
||||||
|
for _, path := range flag.Args() {
|
||||||
|
public, kt, ft := parse(path)
|
||||||
|
|
||||||
|
var subPKI subjectPublicKeyInfo
|
||||||
|
_, err := asn1.Unmarshal(public, &subPKI)
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "failed to get subject PKI")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pubHash := sha1.Sum(subPKI.SubjectPublicKey.Bytes)
|
||||||
|
pubHashString := dumpHex(pubHash[:])
|
||||||
|
if ski == "" {
|
||||||
|
ski = pubHashString
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldMatch && ski != pubHashString {
|
||||||
|
lib.Warnx("%s: SKI mismatch (%s != %s)",
|
||||||
|
path, ski, pubHashString)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s %s (%s %s)\n", path, pubHashString, kt, ft)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
cmd/sprox/BUILD.bazel
Normal file
15
cmd/sprox/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "sprox_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/sprox",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//die"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "sprox",
|
||||||
|
embed = [":sprox_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
46
cmd/sprox/main.go
Normal file
46
cmd/sprox/main.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
func proxy(conn net.Conn, inside string) error {
|
||||||
|
proxyConn, err := net.Dial("tcp", inside)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer proxyConn.Close()
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
io.Copy(conn, proxyConn)
|
||||||
|
}()
|
||||||
|
_, err = io.Copy(proxyConn, conn)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var outside, inside string
|
||||||
|
flag.StringVar(&outside, "f", "8080", "outside port")
|
||||||
|
flag.StringVar(&inside, "p", "4000", "inside port")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", "0.0.0.0:"+outside)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go proxy(conn, "127.0.0.1:"+inside)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
cmd/stealchain-server/BUILD.bazel
Normal file
15
cmd/stealchain-server/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "stealchain-server_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/stealchain-server",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//die"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "stealchain-server",
|
||||||
|
embed = [":stealchain-server_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
17
cmd/stealchain-server/README
Normal file
17
cmd/stealchain-server/README
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
stealchain-server
|
||||||
|
|
||||||
|
This is a utility to extract the verified X.509 chain from a TLS
|
||||||
|
connection initiated by another client. It listens on a port, and
|
||||||
|
for each connection, it will dump the certificates that the peer
|
||||||
|
actually sent (and not the verified chain that is built from this).
|
||||||
|
|
||||||
|
It was written to assist in debugging issues with certificate chains.
|
||||||
|
|
||||||
|
There are a few knobs:
|
||||||
|
|
||||||
|
-listen specifies the address to listen on.
|
||||||
|
|
||||||
|
-ca allows the trusted CA roots to be specified via a PEM bundle of
|
||||||
|
root certificates.
|
||||||
|
|
||||||
|
-verify requires that the client present a valid certificate chain.
|
||||||
106
cmd/stealchain-server/main.go
Normal file
106
cmd/stealchain-server/main.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &tls.Config{}
|
||||||
|
|
||||||
|
var sysRoot, listenAddr, certFile, keyFile string
|
||||||
|
var verify bool
|
||||||
|
flag.StringVar(&sysRoot, "ca", "", "provide an alternate CA bundle")
|
||||||
|
flag.StringVar(&listenAddr, "listen", ":443", "address to listen on")
|
||||||
|
flag.StringVar(&certFile, "cert", "", "server certificate to present to clients")
|
||||||
|
flag.StringVar(&keyFile, "key", "", "key for server certificate")
|
||||||
|
flag.BoolVar(&verify, "verify", false, "verify client certificates")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if verify {
|
||||||
|
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
} else {
|
||||||
|
cfg.ClientAuth = tls.RequestClientCert
|
||||||
|
}
|
||||||
|
if certFile == "" {
|
||||||
|
fmt.Println("[!] missing required flag -cert")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if keyFile == "" {
|
||||||
|
fmt.Println("[!] missing required flag -key")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[!] could not load server key pair: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cfg.Certificates = append(cfg.Certificates, cert)
|
||||||
|
if sysRoot != "" {
|
||||||
|
pemList, err := ioutil.ReadFile(sysRoot)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
if !roots.AppendCertsFromPEM(pemList) {
|
||||||
|
fmt.Printf("[!] no valid roots found")
|
||||||
|
roots = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.RootCAs = roots
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", listenAddr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
raddr := conn.RemoteAddr()
|
||||||
|
tconn := tls.Server(conn, cfg)
|
||||||
|
err = tconn.Handshake()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[+] %v: failed to complete handshake: %v\n", raddr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cs := tconn.ConnectionState()
|
||||||
|
if len(cs.PeerCertificates) == 0 {
|
||||||
|
fmt.Printf("[+] %v: no chain presented\n", raddr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var chain []byte
|
||||||
|
for _, cert := range cs.PeerCertificates {
|
||||||
|
p := &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: cert.Raw,
|
||||||
|
}
|
||||||
|
chain = append(chain, pem.EncodeToMemory(p)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonce [16]byte
|
||||||
|
_, err = rand.Read(nonce[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fname := fmt.Sprintf("%v-%v.pem", raddr, hex.EncodeToString(nonce[:]))
|
||||||
|
err = ioutil.WriteFile(fname, chain, 0644)
|
||||||
|
die.If(err)
|
||||||
|
fmt.Printf("%v: [+] wrote %v.\n", raddr, fname)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
cmd/stealchain/BUILD.bazel
Normal file
15
cmd/stealchain/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "stealchain_lib",
|
||||||
|
srcs = ["thief.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/stealchain",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//die"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "stealchain",
|
||||||
|
embed = [":stealchain_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
19
cmd/subjhash/BUILD.bazel
Normal file
19
cmd/subjhash/BUILD.bazel
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "subjhash_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/subjhash",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//certlib",
|
||||||
|
"//die",
|
||||||
|
"//lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "subjhash",
|
||||||
|
embed = [":subjhash_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
20
cmd/subjhash/README
Normal file
20
cmd/subjhash/README
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
subjhash
|
||||||
|
|
||||||
|
This tool prints the SHA-256 hash of an X.509 certificate's subject
|
||||||
|
info or issuer fields. It can also verify that the hashes of the
|
||||||
|
subject are the same between two certificates.
|
||||||
|
|
||||||
|
Usage: subjhash [-im] certs...
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-i Print hash of issuer field.
|
||||||
|
-m Matching mode. This expects arguments to be in the form of
|
||||||
|
pairs of certificates (e.g. previous, new) whose subjects
|
||||||
|
will be compared. For example,
|
||||||
|
|
||||||
|
subjhash -m ca1.pem ca1-renewed.pem \
|
||||||
|
ca2.pem ca2-renewed.pem
|
||||||
|
|
||||||
|
will exit with a non-zero status if the subject in the
|
||||||
|
ca1-renewed.pem certificate doesn't match the subject in the
|
||||||
|
ca.pem certificate; similarly for ca2.
|
||||||
113
cmd/subjhash/main.go
Normal file
113
cmd/subjhash/main.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stdout); os.Exit(1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `Print hash of subject or issuer fields in certificates.
|
||||||
|
|
||||||
|
Usage: subjhash [-im] certs...
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-i Print hash of issuer field.
|
||||||
|
-m Matching mode. This expects arguments to be in the form of
|
||||||
|
pairs of certificates (e.g. previous, new) whose subjects
|
||||||
|
will be compared. For example,
|
||||||
|
|
||||||
|
subjhash -m ca1.pem ca1-renewed.pem \
|
||||||
|
ca2.pem ca2-renewed.pem
|
||||||
|
|
||||||
|
will exit with a non-zero status if the subject in the
|
||||||
|
ca1-renewed.pem certificate doesn't match the subject in the
|
||||||
|
ca.pem certificate; similarly for ca2.
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: the Issuer field is *also* a subject field. Also, the returned
|
||||||
|
// hash is *not* hex encoded.
|
||||||
|
func getSubjectInfoHash(cert *x509.Certificate, issuer bool) []byte {
|
||||||
|
if cert == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var subject []byte
|
||||||
|
if issuer {
|
||||||
|
subject = cert.RawIssuer
|
||||||
|
} else {
|
||||||
|
subject = cert.RawSubject
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := sha256.Sum256(subject)
|
||||||
|
return digest[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDigests(paths []string, issuer bool) {
|
||||||
|
for _, path := range paths {
|
||||||
|
cert, err := certlib.LoadCertificate(path)
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "failed to load certificate from %s", path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := getSubjectInfoHash(cert, issuer)
|
||||||
|
fmt.Printf("%x %s\n", digest, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchDigests(paths []string, issuer bool) {
|
||||||
|
if (len(paths) % 2) != 0 {
|
||||||
|
lib.Errx(lib.ExitFailure, "not all certificates are paired")
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalid int
|
||||||
|
for {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fst := paths[0]
|
||||||
|
snd := paths[1]
|
||||||
|
paths = paths[2:]
|
||||||
|
|
||||||
|
fstCert, err := certlib.LoadCertificate(fst)
|
||||||
|
die.If(err)
|
||||||
|
sndCert, err := certlib.LoadCertificate(snd)
|
||||||
|
die.If(err)
|
||||||
|
if !bytes.Equal(getSubjectInfoHash(fstCert, issuer), getSubjectInfoHash(sndCert, issuer)) {
|
||||||
|
lib.Warnx("certificates don't match: %s and %s", fst, snd)
|
||||||
|
invalid++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if invalid > 0 {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var issuer, match bool
|
||||||
|
flag.BoolVar(&issuer, "i", false, "print the issuer")
|
||||||
|
flag.BoolVar(&match, "m", false, "match mode")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
paths := flag.Args()
|
||||||
|
if match {
|
||||||
|
matchDigests(paths, issuer)
|
||||||
|
} else {
|
||||||
|
printDigests(paths, issuer)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
cmd/tlskeypair/BUILD.bazel
Normal file
15
cmd/tlskeypair/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "tlskeypair_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/tlskeypair",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["//die"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "tlskeypair",
|
||||||
|
embed = [":tlskeypair_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validPEMs = map[string]bool{
|
var validPEMs = map[string]bool{
|
||||||
|
|||||||
14
cmd/utc/BUILD.bazel
Normal file
14
cmd/utc/BUILD.bazel
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "utc_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/utc",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "utc",
|
||||||
|
embed = [":utc_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
@@ -11,12 +11,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
format = "2006-01-02 15:04" // Format that will be used for times.
|
format = "2006-01-02 15:04" // Format that will be used for times.
|
||||||
outFormat = format + " MST" // Output format.
|
outFormat = format + " MST" // Output format.
|
||||||
tz = "Local" // String descriptor for timezone.
|
tz = "Local" // String descriptor for timezone.
|
||||||
fromLoc *time.Location = time.Local // Go time.Location for the named timezone.
|
fromLoc = time.Local // Go time.Location for the named timezone.
|
||||||
fromUnix bool // Input times are Unix timestamps.
|
fromUnix bool // Input times are Unix timestamps.
|
||||||
toLoc *time.Location = time.UTC // Go time.Location for output timezone.
|
toLoc = time.UTC // Go time.Location for output timezone.
|
||||||
)
|
)
|
||||||
|
|
||||||
func usage(w io.Writer) {
|
func usage(w io.Writer) {
|
||||||
@@ -72,7 +72,7 @@ Flags:
|
|||||||
|
|
||||||
func usageExamples() {
|
func usageExamples() {
|
||||||
usage(os.Stdout)
|
usage(os.Stdout)
|
||||||
fmt.Println(`
|
fmt.Printf(`
|
||||||
Examples (note that the examples are done in the America/Los_Angeles /
|
Examples (note that the examples are done in the America/Los_Angeles /
|
||||||
PST8PDT time zone):
|
PST8PDT time zone):
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ PST8PDT time zone):
|
|||||||
2016-06-14 21:30 PDT = 2016-06-15 04:30 UTC
|
2016-06-14 21:30 PDT = 2016-06-15 04:30 UTC
|
||||||
+ Converting a local EST timestamp to UTC (on a machine set to
|
+ Converting a local EST timestamp to UTC (on a machine set to
|
||||||
PST8PDT):
|
PST8PDT):
|
||||||
$ utc -z EST '2016-06-14 21:30'
|
$ utc -z EST '2016-06-14 21:30'
|
||||||
2016-06-14 21:30 EST = 2016-06-15 02:30 UTC
|
2016-06-14 21:30 EST = 2016-06-15 02:30 UTC
|
||||||
+ Converting timestamps in the form '14-06-2016 3:04PM':
|
+ Converting timestamps in the form '14-06-2016 3:04PM':
|
||||||
$ utc -f '02-01-2006 3:04PM' '14-06-2016 9:30PM'
|
$ utc -f '02-01-2006 3:04PM' '14-06-2016 9:30PM'
|
||||||
@@ -101,7 +101,7 @@ PST8PDT time zone):
|
|||||||
$ utc -u -z EST '2016-06-14 21:30'
|
$ utc -u -z EST '2016-06-14 21:30'
|
||||||
2016-06-14 21:30 UTC = 2016-06-14 16:30 EST
|
2016-06-14 21:30 UTC = 2016-06-14 16:30 EST
|
||||||
+ Using a different output format:
|
+ Using a different output format:
|
||||||
$ utc -o '2006-01-02T15:03:04-0700' '2016-06-14 21:30'
|
$ utc -o '2006-01-02T15:03:04-0700' '2016-06-14 21:30'
|
||||||
2016-06-14T21:09:30-0700 = 2016-06-15T04:04:30+0000
|
2016-06-14T21:09:30-0700 = 2016-06-15T04:04:30+0000
|
||||||
+ Converting a Unix timestamp to a UTC time:
|
+ Converting a Unix timestamp to a UTC time:
|
||||||
$ utc -t 1466052938
|
$ utc -t 1466052938
|
||||||
@@ -134,6 +134,7 @@ PST8PDT time zone):
|
|||||||
(Converting from GMT (offset +0000) to UTC (offset +0000).)
|
(Converting from GMT (offset +0000) to UTC (offset +0000).)
|
||||||
==================================================================
|
==================================================================
|
||||||
2016-06-14 23:46 = 2016-06-14 23:46
|
2016-06-14 23:46 = 2016-06-14 23:46
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
cmd/yamll/BUILD.bazel
Normal file
15
cmd/yamll/BUILD.bazel
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "yamll_lib",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/yamll",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = ["@in_gopkg_yaml_v2//:yaml_v2"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "yamll",
|
||||||
|
embed = [":yamll_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
24
config/BUILD.bazel
Normal file
24
config/BUILD.bazel
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "config",
|
||||||
|
srcs = [
|
||||||
|
"config.go",
|
||||||
|
"path.go",
|
||||||
|
"path_linux.go",
|
||||||
|
],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/config",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = ["//config/iniconf"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "config_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = [
|
||||||
|
"config_test.go",
|
||||||
|
"path_test.go",
|
||||||
|
],
|
||||||
|
data = glob(["testdata/**"]),
|
||||||
|
embed = [":config"],
|
||||||
|
)
|
||||||
153
config/config.go
Normal file
153
config/config.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// Package config implements a simple global configuration system that
|
||||||
|
// supports a file with key=value pairs and environment variables. Note
|
||||||
|
// that the config system is global.
|
||||||
|
//
|
||||||
|
// This package is intended to be used for small daemons: some configuration
|
||||||
|
// file is optionally populated at program start, then this is used to
|
||||||
|
// transparently look up configuration values from either that file or the
|
||||||
|
// environment.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/config/iniconf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NB: Rather than define a singleton type, everything is defined at
|
||||||
|
// the top-level
|
||||||
|
|
||||||
|
var (
|
||||||
|
vars = map[string]string{}
|
||||||
|
prefix = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetEnvPrefix sets the prefix for all environment variables; it's
|
||||||
|
// assumed to not be needed for files.
|
||||||
|
func SetEnvPrefix(pfx string) {
|
||||||
|
prefix = pfx
|
||||||
|
}
|
||||||
|
|
||||||
|
func addLine(line string) {
|
||||||
|
if strings.HasPrefix(line, "#") || line == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lineParts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(lineParts) != 2 {
|
||||||
|
log.Print("skipping line: ", line)
|
||||||
|
return // silently ignore empty keys
|
||||||
|
}
|
||||||
|
|
||||||
|
lineParts[0] = strings.TrimSpace(lineParts[0])
|
||||||
|
lineParts[1] = strings.TrimSpace(lineParts[1])
|
||||||
|
vars[lineParts[0]] = lineParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile scans the file at path for key=value pairs and adds them
|
||||||
|
// to the configuration.
|
||||||
|
func LoadFile(path string) error {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
addLine(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFileFor scans the ini file at path, loading the default section
|
||||||
|
// and overriding any keys found under section. If strict is true, the
|
||||||
|
// named section must exist (i.e. to catch typos in the section name).
|
||||||
|
func LoadFileFor(path, section string, strict bool) error {
|
||||||
|
cmap, err := iniconf.ParseFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range cmap[iniconf.DefaultSection] {
|
||||||
|
vars[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
smap, ok := cmap[section]
|
||||||
|
if !ok {
|
||||||
|
if strict {
|
||||||
|
return fmt.Errorf("config: section '%s' wasn't found in the config file", section)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range smap {
|
||||||
|
vars[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a value from either a configuration file or the
|
||||||
|
// environment. Note that values from a file will override environment
|
||||||
|
// variables.
|
||||||
|
func Get(key string) string {
|
||||||
|
if v, ok := vars[key]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return os.Getenv(prefix + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefault retrieves a value from either a configuration file or
|
||||||
|
// the environment. Note that value from a file will override
|
||||||
|
// environment variables. If a value isn't found (e.g. Get returns an
|
||||||
|
// empty string), the default value will be used.
|
||||||
|
func GetDefault(key, def string) string {
|
||||||
|
if v := Get(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require retrieves a value from either a configuration file or the
|
||||||
|
// environment. If the key isn't present, it will call log.Fatal, printing
|
||||||
|
// the missing key.
|
||||||
|
func Require(key string) string {
|
||||||
|
if v, ok := vars[key]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := os.LookupEnv(prefix + key)
|
||||||
|
if !ok {
|
||||||
|
var envMessage string
|
||||||
|
if prefix != "" {
|
||||||
|
envMessage = " (note: looked for the key " + prefix + key
|
||||||
|
envMessage += " in the local env)"
|
||||||
|
}
|
||||||
|
log.Fatalf("missing required configuration value %s%s", key, envMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListKeys returns a slice of the currently known keys.
|
||||||
|
func ListKeys() []string {
|
||||||
|
keyList := []string{}
|
||||||
|
for k := range vars {
|
||||||
|
keyList = append(keyList, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keyList)
|
||||||
|
return keyList
|
||||||
|
}
|
||||||
66
config/config_test.go
Normal file
66
config/config_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testFilePath = "testdata/test.env"
|
||||||
|
|
||||||
|
// Keys
|
||||||
|
kOrder = "ORDER"
|
||||||
|
kSpecies = "SPECIES"
|
||||||
|
kName = "COMMON_NAME"
|
||||||
|
|
||||||
|
// Env
|
||||||
|
eOrder = "corvus"
|
||||||
|
eSpecies = "corvus corax"
|
||||||
|
eName = "northern raven"
|
||||||
|
|
||||||
|
// File
|
||||||
|
fOrder = "stringiformes"
|
||||||
|
fSpecies = "strix aluco"
|
||||||
|
// Name isn't set in the file to test fall through.
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
os.Setenv(kOrder, eOrder)
|
||||||
|
os.Setenv(kSpecies, eSpecies)
|
||||||
|
os.Setenv(kName, eName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadEnvOnly(t *testing.T) {
|
||||||
|
order := Get(kOrder)
|
||||||
|
species := Get(kSpecies)
|
||||||
|
if order != eOrder {
|
||||||
|
t.Errorf("want %s, have %s", eOrder, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
if species != eSpecies {
|
||||||
|
t.Errorf("want %s, have %s", eSpecies, species)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFile(t *testing.T) {
|
||||||
|
err := LoadFile(testFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
order := Get(kOrder)
|
||||||
|
species := Get(kSpecies)
|
||||||
|
name := Get(kName)
|
||||||
|
|
||||||
|
if order != fOrder {
|
||||||
|
t.Errorf("want %s, have %s", fOrder, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
if species != fSpecies {
|
||||||
|
t.Errorf("want %s, have %s", fSpecies, species)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name != eName {
|
||||||
|
t.Errorf("want %s, have %s", eName, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
config/iniconf/BUILD.bazel
Normal file
16
config/iniconf/BUILD.bazel
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "iniconf",
|
||||||
|
srcs = ["iniconf.go"],
|
||||||
|
importpath = "git.wntrmute.dev/kyle/goutils/config/iniconf",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "iniconf_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["iniconf_test.go"],
|
||||||
|
data = glob(["testdata/**"]),
|
||||||
|
embed = [":iniconf"],
|
||||||
|
)
|
||||||
223
config/iniconf/iniconf.go
Normal file
223
config/iniconf/iniconf.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package iniconf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigMap is shorthand for the type used as a config struct.
|
||||||
|
type ConfigMap map[string]map[string]string
|
||||||
|
|
||||||
|
var (
|
||||||
|
configSection = regexp.MustCompile(`^\s*\[\s*(\w+)\s*\]\s*$`)
|
||||||
|
quotedConfigLine = regexp.MustCompile(`^\s*(\w+)\s*=\s*["'](.*)["']\s*$`)
|
||||||
|
configLine = regexp.MustCompile(`^\s*(\w+)\s*=\s*(.*)\s*$`)
|
||||||
|
commentLine = regexp.MustCompile(`^#.*$`)
|
||||||
|
blankLine = regexp.MustCompile(`^\s*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultSection is the label for the default ini file section.
|
||||||
|
var DefaultSection = "default"
|
||||||
|
|
||||||
|
// ParseFile attempts to load the named config file.
|
||||||
|
func ParseFile(fileName string) (cfg ConfigMap, err error) {
|
||||||
|
var file *os.File
|
||||||
|
file, err = os.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
return ParseReader(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseReader reads a configuration from an io.Reader.
|
||||||
|
func ParseReader(r io.Reader) (cfg ConfigMap, err error) {
|
||||||
|
cfg = ConfigMap{}
|
||||||
|
buf := bufio.NewReader(r)
|
||||||
|
|
||||||
|
var (
|
||||||
|
line string
|
||||||
|
longLine bool
|
||||||
|
currentSection string
|
||||||
|
lineBytes []byte
|
||||||
|
isPrefix bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
err = nil
|
||||||
|
lineBytes, isPrefix, err = buf.ReadLine()
|
||||||
|
if io.EOF == err {
|
||||||
|
err = nil
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
break
|
||||||
|
} else if isPrefix {
|
||||||
|
line += string(lineBytes)
|
||||||
|
|
||||||
|
longLine = true
|
||||||
|
continue
|
||||||
|
} else if longLine {
|
||||||
|
line += string(lineBytes)
|
||||||
|
longLine = false
|
||||||
|
} else {
|
||||||
|
line = string(lineBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if commentLine.MatchString(line) {
|
||||||
|
continue
|
||||||
|
} else if blankLine.MatchString(line) {
|
||||||
|
continue
|
||||||
|
} else if configSection.MatchString(line) {
|
||||||
|
section := configSection.ReplaceAllString(line,
|
||||||
|
"$1")
|
||||||
|
if section == "" {
|
||||||
|
err = fmt.Errorf("invalid structure in file")
|
||||||
|
break
|
||||||
|
} else if !cfg.SectionInConfig(section) {
|
||||||
|
cfg[section] = make(map[string]string, 0)
|
||||||
|
}
|
||||||
|
currentSection = section
|
||||||
|
} else if configLine.MatchString(line) {
|
||||||
|
regex := configLine
|
||||||
|
if quotedConfigLine.MatchString(line) {
|
||||||
|
regex = quotedConfigLine
|
||||||
|
}
|
||||||
|
if currentSection == "" {
|
||||||
|
currentSection = DefaultSection
|
||||||
|
if !cfg.SectionInConfig(currentSection) {
|
||||||
|
cfg[currentSection] = map[string]string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key := regex.ReplaceAllString(line, "$1")
|
||||||
|
val := regex.ReplaceAllString(line, "$2")
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cfg[currentSection][key] = val
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("invalid config file")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SectionInConfig determines whether a section is in the configuration.
|
||||||
|
func (c ConfigMap) SectionInConfig(section string) bool {
|
||||||
|
_, ok := c[section]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSections returns the list of sections in the config map.
|
||||||
|
func (c ConfigMap) ListSections() (sections []string) {
|
||||||
|
for section := range c {
|
||||||
|
sections = append(sections, section)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile writes out the configuration to a file.
|
||||||
|
func (c ConfigMap) WriteFile(filename string) (err error) {
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
for _, section := range c.ListSections() {
|
||||||
|
sName := fmt.Sprintf("[ %s ]\n", section)
|
||||||
|
_, err = file.Write([]byte(sName))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range c[section] {
|
||||||
|
line := fmt.Sprintf("%s = %s\n", k, v)
|
||||||
|
_, err = file.Write([]byte(line))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = file.Write([]byte{0x0a})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSection creates a new section in the config map.
|
||||||
|
func (c ConfigMap) AddSection(section string) {
|
||||||
|
if nil != c[section] {
|
||||||
|
c[section] = map[string]string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddKeyVal adds a key value pair to a config map.
|
||||||
|
func (c ConfigMap) AddKeyVal(section, key, val string) {
|
||||||
|
if section == "" {
|
||||||
|
section = DefaultSection
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil == c[section] {
|
||||||
|
c.AddSection(section)
|
||||||
|
}
|
||||||
|
|
||||||
|
c[section][key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue retrieves the value from a key map.
|
||||||
|
func (c ConfigMap) GetValue(section, key string) (val string, present bool) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if section == "" {
|
||||||
|
section = DefaultSection
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := c[section]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val, present = c[section][key]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValueDefault retrieves the value from a key map if present,
|
||||||
|
// otherwise the default value.
|
||||||
|
func (c ConfigMap) GetValueDefault(section, key, value string) (val string) {
|
||||||
|
kval, ok := c.GetValue(section, key)
|
||||||
|
if !ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return kval
|
||||||
|
}
|
||||||
|
|
||||||
|
// SectionKeys returns the sections in the config map.
|
||||||
|
func (c ConfigMap) SectionKeys(section string) (keys []string, present bool) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if section == "" {
|
||||||
|
section = DefaultSection
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := c
|
||||||
|
s, ok := cm[section]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
keys = make([]string, 0, len(s))
|
||||||
|
for key := range s {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, true
|
||||||
|
}
|
||||||
142
config/iniconf/iniconf_test.go
Normal file
142
config/iniconf/iniconf_test.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package iniconf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FailWithError is a utility for dumping errors and failing the test.
|
||||||
|
func FailWithError(t *testing.T, err error) {
|
||||||
|
fmt.Println("failed")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[!] ", err.Error())
|
||||||
|
}
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlinkIfExists removes a file if it exists.
|
||||||
|
func UnlinkIfExists(file string) {
|
||||||
|
_, err := os.Stat(file)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
panic("failed to remove " + file)
|
||||||
|
}
|
||||||
|
os.Remove(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringSlicesEqual compares two string lists, checking that they
|
||||||
|
// contain the same elements.
|
||||||
|
func stringSlicesEqual(slice1, slice2 []string) bool {
|
||||||
|
if len(slice1) != len(slice2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range slice1 {
|
||||||
|
if slice1[i] != slice2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range slice2 {
|
||||||
|
if slice1[i] != slice2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoodConfig(t *testing.T) {
|
||||||
|
testFile := "testdata/test.conf"
|
||||||
|
fmt.Printf("[+] validating known-good config... ")
|
||||||
|
cmap, err := ParseFile(testFile)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
} else if len(cmap) != 2 {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoodConfig2(t *testing.T) {
|
||||||
|
testFile := "testdata/test2.conf"
|
||||||
|
fmt.Printf("[+] validating second known-good config... ")
|
||||||
|
cmap, err := ParseFile(testFile)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
} else if len(cmap) != 1 {
|
||||||
|
FailWithError(t, err)
|
||||||
|
} else if len(cmap["default"]) != 3 {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadConfig(t *testing.T) {
|
||||||
|
testFile := "testdata/bad.conf"
|
||||||
|
fmt.Printf("[+] ensure invalid config file fails... ")
|
||||||
|
_, err := ParseFile(testFile)
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("invalid config file should fail")
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteConfigFile(t *testing.T) {
|
||||||
|
fmt.Printf("[+] ensure config file is written properly... ")
|
||||||
|
const testFile = "testdata/test.conf"
|
||||||
|
const testOut = "testdata/test.out"
|
||||||
|
|
||||||
|
cmap, err := ParseFile(testFile)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer UnlinkIfExists(testOut)
|
||||||
|
err = cmap.WriteFile(testOut)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmap2, err := ParseFile(testOut)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionList1 := cmap.ListSections()
|
||||||
|
sectionList2 := cmap2.ListSections()
|
||||||
|
sort.Strings(sectionList1)
|
||||||
|
sort.Strings(sectionList2)
|
||||||
|
if !stringSlicesEqual(sectionList1, sectionList2) {
|
||||||
|
err = fmt.Errorf("section lists don't match")
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, section := range sectionList1 {
|
||||||
|
for _, k := range cmap[section] {
|
||||||
|
if cmap[section][k] != cmap2[section][k] {
|
||||||
|
err = fmt.Errorf("config key doesn't match")
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotedValue(t *testing.T) {
|
||||||
|
testFile := "testdata/test.conf"
|
||||||
|
fmt.Printf("[+] validating quoted value... ")
|
||||||
|
cmap, _ := ParseFile(testFile)
|
||||||
|
val := cmap["sectionName"]["key4"]
|
||||||
|
if val != " space at beginning and end " {
|
||||||
|
FailWithError(t, errors.New("Wrong value in double quotes ["+val+"]"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val = cmap["sectionName"]["key5"]
|
||||||
|
if val != " is quoted with single quotes " {
|
||||||
|
FailWithError(t, errors.New("Wrong value in single quotes ["+val+"]"))
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
5
config/iniconf/testdata/bad.conf
vendored
Normal file
5
config/iniconf/testdata/bad.conf
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[]
|
||||||
|
|
||||||
|
key
|
||||||
|
another key
|
||||||
|
key = val
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user