diff --git a/.gitignore b/.gitignore index 5a813f1..7b6ff97 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ tmp/ .DS_Store # Hugo build -docs/public \ No newline at end of file +docs/public +fixtures \ No newline at end of file diff --git a/cmd/trisa/main.go b/cmd/trisa/main.go index c67d4ca..92a3a1e 100644 --- a/cmd/trisa/main.go +++ b/cmd/trisa/main.go @@ -1,7 +1,1386 @@ +/* + The TRISA CLI client allows you to create and execute TRISA requests from the command + line for development or testing purposes. For more information on how to use the CLI, + run `trisa --help` or see the documenation at https://trisa.dev. +*/ package main -import "fmt" +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" + "github.com/joho/godotenv" + "github.com/trisacrypto/trisa/pkg" + "github.com/trisacrypto/trisa/pkg/ivms101" + api "github.com/trisacrypto/trisa/pkg/trisa/api/v1beta1" + generic "github.com/trisacrypto/trisa/pkg/trisa/data/generic/v1beta1" + env "github.com/trisacrypto/trisa/pkg/trisa/envelope" + gds "github.com/trisacrypto/trisa/pkg/trisa/gds/api/v1beta1" + models "github.com/trisacrypto/trisa/pkg/trisa/gds/models/v1beta1" + "github.com/trisacrypto/trisa/pkg/trisa/mtls" + "github.com/trisacrypto/trisa/pkg/trust" + cli "github.com/urfave/cli/v2" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" +) + +// Clients to connect to various services and make RPC requests. +var ( + peer api.TRISANetworkClient + ping api.TRISAHealthClient + directory gds.TRISADirectoryClient +) + +// Directory service endpoints +const ( + testnet = "api.trisatest.net:443" + mainnet = "api.vaspdirectory.net:443" +) + +// Aliases that map directory names to endpoints +var directoryAliases = map[string]string{ + "testnet": testnet, + "trisatest": testnet, + "trisatest.net": testnet, + "mainnet": mainnet, + "vaspdirectory": mainnet, + "vaspdirectory.net": mainnet, +} func main() { - fmt.Println("coming soon!") + godotenv.Load() + + app := cli.NewApp() + app.Name = "trisa" + app.Usage = "create and execute TRISA requests" + app.Version = pkg.Version() + app.Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "endpoint", + Aliases: []string{"e"}, + Usage: "the endpoint of the TRISA peer to connect to", + EnvVars: []string{"TRISA_ENDPOINT"}, + }, + &cli.StringFlag{ + Name: "directory", + Aliases: []string{"d"}, + Usage: "the endpoint or name of the directory service to use", + EnvVars: []string{"TRISA_DIRECTORY", "TRISA_DIRECTORY_URL", "GDS_DIRECTORY_URL"}, + Value: "testnet", + }, + &cli.StringFlag{ + Name: "certs", + Aliases: []string{"c"}, + Usage: "specify the path to your mTLS TRISA identity certificates", + EnvVars: []string{"TRISA_CERTS"}, + }, + &cli.StringFlag{ + Name: "chain", + Aliases: []string{"t"}, + Usage: "the path to the trust chain without private keys if separate from the certs", + EnvVars: []string{"TRISA_TRUST_CHAIN"}, + }, + &cli.StringFlag{ + Name: "pkcs12password", + Aliases: []string{"P"}, + Usage: "the pkcs12 password of the certs if they are encrypted", + EnvVars: []string{"TRISA_CERTS_PASSWORD"}, + }, + } + app.Commands = []*cli.Command{ + { + Name: "make", + Aliases: []string{"envelope"}, + Usage: "create a secure envelope or payload template from a payload", + Action: envelope, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "identity", + Aliases: []string{"i"}, + Usage: "path to identity payload JSON to load", + }, + &cli.StringFlag{ + Name: "transaction", + Aliases: []string{"t"}, + Usage: "path to transaction payload JSON to load", + }, + &cli.StringFlag{ + Name: "out", + Aliases: []string{"o"}, + Usage: "path to save sealed secure envelope to disk", + DefaultText: "stdout", + }, + &cli.StringFlag{ + Name: "sealing-key", + Aliases: []string{"s", "seal"}, + Usage: "path to recipient's public key to seal outgoing envelope with (optional)", + }, + &cli.StringFlag{ + Name: "envelope-id", + Aliases: []string{"id", "I"}, + Usage: "specify the envelope ID for the outgoing secure envelope (optional)", + }, + &cli.StringFlag{ + Name: "sent-at", + Aliases: []string{"sent", "S"}, + Usage: "specify a sent at timestamp for the payload in RFC3339 format", + DefaultText: "now", + }, + &cli.StringFlag{ + Name: "received-at", + Aliases: []string{"received", "R"}, + Usage: "specify a received at timestamp for the payload in RFC3339 format or the keyword \"now\" (optional)", + }, + &cli.StringFlag{ + Name: "error-code", + Aliases: []string{"C"}, + Usage: "add an error with the specified code to the outgoing envelope", + Value: "REJECTED", + }, + &cli.StringFlag{ + Name: "error-message", + Aliases: []string{"error", "E"}, + Usage: "add an error message to the outgoing envelope", + }, + }, + }, + { + Name: "seal", + Usage: "seal a secure envelope from a payload template", + Action: seal, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "in", + Aliases: []string{"i"}, + Usage: "path to secure envelope or payload template to load", + }, + &cli.StringFlag{ + Name: "out", + Aliases: []string{"o"}, + Usage: "path to save sealed secure envelope to disk", + DefaultText: "stdout", + }, + &cli.StringFlag{ + Name: "sealing-key", + Aliases: []string{"s", "seal"}, + Usage: "path to recipient's public key to seal outgoing envelope with", + }, + &cli.StringFlag{ + Name: "envelope-id", + Aliases: []string{"id", "I"}, + Usage: "specify the envelope ID for the outgoing secure envelope", + DefaultText: "original or random UUID", + }, + &cli.StringFlag{ + Name: "sent-at", + Aliases: []string{"sent", "S"}, + Usage: "specify a sent at timestamp for the payload in RFC3339 format", + DefaultText: "original or now", + }, + &cli.StringFlag{ + Name: "received-at", + Aliases: []string{"received", "R"}, + Usage: "specify a received at timestamp for the payload in RFC3339 format or the keyword \"now\"", + DefaultText: "original or empty", + }, + &cli.StringFlag{ + Name: "error-code", + Aliases: []string{"C"}, + Usage: "add an error with the specified code to the outgoing envelope", + Value: "REJECTED", + }, + &cli.StringFlag{ + Name: "error-message", + Aliases: []string{"error", "E"}, + Usage: "add an error message to the outgoing envelope", + }, + }, + }, + { + Name: "open", + Usage: "unseal a secure envelope from an envelope saved to disk", + Action: open, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "in", + Aliases: []string{"i"}, + Usage: "path to secure envelope to open and unseal or parse", + Required: true, + }, + &cli.StringFlag{ + Name: "out", + Aliases: []string{"o"}, + Usage: "path to save unsealed envelope to disk", + DefaultText: "stdout", + }, + &cli.BoolFlag{ + Name: "payload", + Aliases: []string{"p"}, + Usage: "extract payload when saving to disk (if -out is specified)", + }, + &cli.BoolFlag{ + Name: "error", + Aliases: []string{"E"}, + Usage: "extract error from secure envelope", + }, + &cli.StringFlag{ + Name: "unsealing-key", + Aliases: []string{"key", "k"}, + Usage: "path to private key to unseal the secure envelope", + }, + }, + }, + { + Name: "transfer", + Usage: "execute a TRISA transfer with a TRISA peer", + UsageText: "trisa transfer -i sealed_envelope.json\ntrisa transfer -i payload.json -s public.pem\ntrisa transfer -I [envelope-id] -E [error message]", + Before: initClient, + Action: transfer, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "in", + Aliases: []string{"i"}, + Usage: "path to secure envelope or payload template to load", + }, + &cli.StringFlag{ + Name: "out", + Aliases: []string{"o"}, + Usage: "path to save response envelope to disk", + DefaultText: "stdout", + }, + &cli.StringFlag{ + Name: "unsealing-key", + Aliases: []string{"key", "k"}, + Usage: "path to private key to unseal the incoming secure envelope", + }, + &cli.BoolFlag{ + Name: "payload", + Aliases: []string{"p"}, + Usage: "extract payload when saving to disk (if -out and -key are specified)", + }, + &cli.StringFlag{ + Name: "sealing-key", + Aliases: []string{"s", "seal"}, + Usage: "path to recipient's public key to seal outgoing envelope with", + }, + &cli.StringFlag{ + Name: "envelope-id", + Aliases: []string{"id", "I"}, + Usage: "specify the envelope ID for the outgoing secure envelope", + DefaultText: "original or random UUID", + }, + &cli.StringFlag{ + Name: "sent-at", + Aliases: []string{"sent", "S"}, + Usage: "specify a sent at timestamp for the payload in RFC3339 format", + DefaultText: "original or now", + }, + &cli.StringFlag{ + Name: "received-at", + Aliases: []string{"recv", "R"}, + Usage: "specify a received at timestamp for the payload in RFC3339 format or the keyword \"now\"", + DefaultText: "original", + }, + &cli.StringFlag{ + Name: "error-code", + Aliases: []string{"C"}, + Usage: "add an error with the specified code to the outgoing envelope", + Value: "REJECTED", + }, + &cli.StringFlag{ + Name: "error-message", + Aliases: []string{"error", "E"}, + Usage: "add an error message to the outgoing envelope", + }, + }, + }, + { + Name: "exchange", + Aliases: []string{"key-exchange"}, + Usage: "exchange public sealing keys with a TRISA peer", + Before: initClient, + Action: exchange, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "in", + Aliases: []string{"i"}, + Usage: "path to PEM encoded public key to send to remote", + DefaultText: "TRISA certs", + }, + &cli.StringFlag{ + Name: "out", + Aliases: []string{"o"}, + Usage: "path to write PEM encoded keys sent from remote", + }, + }, + }, + { + Name: "confirm", + Aliases: []string{"confirm-address"}, + Usage: "execute an address confirmation request with a TRISA peer", + Before: initClient, + Action: confirm, + Flags: []cli.Flag{}, + }, + { + Name: "status", + Aliases: []string{"health-check"}, + Usage: "execute a health check against a TRISA peer and directory service", + Before: initHealthCheckClient, + Action: health, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "insecure", + Aliases: []string{"S"}, + Usage: "do not connect to the peer health check endpoint with mTLS", + }, + }, + }, + { + Name: "lookup", + Usage: "lookup a TRISA record on the directory service", + UsageText: "trisa lookup [-dir value] -[in]\nlookup a VASP by ID or common name\nspecify registered directory for alternative network issuers", + Before: initDirectoryClient, + Action: lookup, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "vasp-id", + Aliases: []string{"id", "i"}, + Usage: "the UUID of the VASP to lookup", + }, + &cli.StringFlag{ + Name: "registered-directory", + Aliases: []string{"dir", "d"}, + Usage: "the registered directory of the VASP record", + }, + &cli.StringFlag{ + Name: "common-name", + Aliases: []string{"cn", "n"}, + Usage: "the common name of the VASP to lookup", + }, + }, + }, + { + Name: "search", + Usage: "search for a VASP on the directory service", + Before: initDirectoryClient, + Action: search, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "one or more names of VASPs to search for", + }, + &cli.StringSliceFlag{ + Name: "website", + Aliases: []string{"w"}, + Usage: "one or more urls of VASP websites to search for", + }, + &cli.StringSliceFlag{ + Name: "country", + Aliases: []string{"c"}, + Usage: "one or more countries to filter requests on", + }, + &cli.StringSliceFlag{ + Name: "category", + Aliases: []string{"C"}, + Usage: "one or more categories to filter requests on", + }, + }, + }, + } + app.Run(os.Args) +} + +//==================================================================================== +// Envelope Handling Commands +//==================================================================================== + +func envelope(c *cli.Context) (err error) { + // Is this an error only envelope? + if c.String("error-message") != "" { + if c.String("envelope-id") == "" { + return cli.Exit("an envelope id is required to create an error only secure envelope", 1) + } + + if c.String("identity") != "" || c.String("transaction") != "" { + return cli.Exit("this command does not create secure envelopes with both an error and a payload, specify either -error or -transaction and -identity", 1) + } + + var msg *api.SecureEnvelope + if msg, err = errorMessage(c.String("envelope-id"), c.String("error-message"), c.String("error-code")); err != nil { + return cli.Exit(err, 1) + } + + if out := c.String("out"); out != "" { + if err = dumpProto(msg, out); err != nil { + return cli.Exit(err, 1) + } + return nil + } + return printJSON(msg) + } + + // Creating an envelope with a payload, identity and transaction are required + if c.String("identity") == "" || c.String("transaction") == "" { + return cli.Exit("a path to both the identity and transaction JSON is required", 1) + } + + var sealingKey interface{} + if path := c.String("sealing-key"); path != "" { + if sealingKey, err = loadSealingKey(path); err != nil { + return cli.Exit(err, 1) + } + } + + // Create the payload + payload := &api.Payload{} + ts := c.String("sent-at") + if strings.ToLower(ts) == "now" || ts == "" { + ts = time.Now().Format(time.RFC3339) + } + payload.SentAt = ts + + if ts := c.String("received-at"); ts != "" { + if strings.ToLower(ts) == "now" { + ts = time.Now().Format(time.RFC3339) + } + payload.ReceivedAt = ts + } + + // Load the payloads from JSON + if payload.Identity, err = loadIdentity(c.String("identity")); err != nil { + return cli.Exit(err, 1) + } + + if payload.Transaction, err = loadTransaction(c.String("transaction")); err != nil { + return cli.Exit(err, 1) + } + + envelopeID := c.String("envelope-id") + if envelopeID == "" { + envelopeID = uuid.NewString() + } + + // Create the envelope + var handler *env.Envelope + if handler, err = env.New(payload, env.WithEnvelopeID(envelopeID)); err != nil { + return cli.Exit(err, 1) + } + + // Create the unsealed envelope + if handler, _, err = handler.Encrypt(); err != nil { + return cli.Exit(err, 1) + } + + // Create the unsealed envelope if necessary + if sealingKey != nil { + if handler, _, err = handler.Seal(env.WithSealingKey(sealingKey)); err != nil { + return cli.Exit(err, 1) + } + } + + // Save to disk + if out := c.String("out"); out != "" { + if err = dumpProto(handler.Proto(), out); err != nil { + return cli.Exit(err, 1) + } + return nil + } + return printJSON(handler.Proto()) +} + +func seal(c *cli.Context) (err error) { + var msg *api.SecureEnvelope + if in := c.String("in"); in != "" { + // Load the envelope or payload template + if msg, err = loadEnvelope(in); err != nil { + return cli.Exit(err, 1) + } + + // If the sealing key is provided, use it to seal the envelope + var sealKey interface{} + if path := c.String("sealing-key"); path != "" { + if sealKey, err = loadSealingKey(path); err != nil { + return cli.Exit(err, 1) + } + } else { + return cli.Exit("a path to the public sealing keys is required to seal envelope", 1) + } + + if msg, err = updateEnvelope(msg, c); err != nil { + return cli.Exit(err, 1) + } + + if msg, err = sealEnvelope(msg, sealKey); err != nil { + return cli.Exit(err, 1) + } + } else { + // Attempt to create an error-only envelope + if c.String("error-message") == "" || c.String("envelope-id") == "" { + return cli.Exit("specify an envelope to load or an error message and id", 1) + } + + if msg, err = errorMessage(c.String("envelope-id"), c.String("error-message"), c.String("error-code")); err != nil { + return cli.Exit(err, 1) + } + } + + // Always use the current timestamp for ordering purposes + msg.Timestamp = time.Now().Format(time.RFC3339Nano) + + // Did we manage to load a valid secure envelope? + if err = env.Validate(msg); err != nil { + return cli.Exit(fmt.Errorf("could not load envelope to send: %s", err), 1) + } + + if out := c.String("out"); out != "" { + if err = dumpProto(msg, out); err != nil { + return cli.Exit(err, 1) + } + return nil + } + return printJSON(msg) +} + +func open(c *cli.Context) (err error) { + var msg *api.SecureEnvelope + if msg, err = loadEnvelope(c.String("in")); err != nil { + return cli.Exit(err, 1) + } + + // Extract the error if requested - no cryptography required + if c.Bool("error") { + if msg.Error == nil || msg.Error.IsZero() { + return cli.Exit("there is no error on the secure envelope", 1) + } + + if out := c.String("out"); out != "" { + if err = dumpProto(msg.Error, out); err != nil { + return cli.Exit(err, 1) + } + } + return printJSON(msg.Error) + } + + var handler *env.Envelope + if handler, err = env.Wrap(msg); err != nil { + return cli.Exit(err, 1) + } + + // Has the unsealing key been provided? + var unsealingKey interface{} + if path := c.String("unsealing-key"); path != "" { + if unsealingKey, err = loadPrivateKey(path); err != nil { + return cli.Exit(err, 1) + } + } + + var ( + payload *api.Payload + unsealedEnvelope *api.SecureEnvelope + ) + + // Figure out if we can unseal the envelope + switch handler.State() { + case env.Sealed, env.SealedError: + if unsealingKey == nil { + return cli.Exit("must specify unsealing key to open sealed envelope", 1) + } + + if handler, _, err = handler.Unseal(env.WithUnsealingKey(unsealingKey)); err != nil { + return cli.Exit(err, 1) + } + + unsealedEnvelope = handler.Proto() + if handler, _, err = handler.Decrypt(); err != nil { + return cli.Exit(err, 1) + } + + payload, _ = handler.Payload() + + case env.Unsealed, env.UnsealedError: + unsealedEnvelope = handler.Proto() + if handler, _, err = handler.Decrypt(); err != nil { + return cli.Exit(err, 1) + } + payload, _ = handler.Payload() + + case env.Clear, env.ClearError: + payload, _ = handler.Payload() + default: + return cli.Exit(fmt.Errorf("envelope in unhandled state %s", handler.State()), 1) + } + + if out := c.String("out"); out != "" { + if c.Bool("payload") { + if err = dumpProto(payload, out); err != nil { + return cli.Exit(err, 1) + } + return nil + } + + if err = dumpProto(unsealedEnvelope, out); err != nil { + return cli.Exit(err, 1) + } + return nil + } + + if c.Bool("payload") { + return printJSON(payload) + } + return printJSON(unsealedEnvelope) +} + +//==================================================================================== +// TRISA RPC Commands +//==================================================================================== + +func transfer(c *cli.Context) (err error) { + // There are three cases for loading a secure envelope to send: + // 1. a sealed secure envelope is unmarshaled from disk + // 2. an error and envelope ID is specified on the command line + // 3. a payload template is unmarshaled from disk with a sealing key + var req *api.SecureEnvelope + if in := c.String("in"); in != "" { + // Load the envelope or payload template + if req, err = loadEnvelope(in); err != nil { + return cli.Exit(err, 1) + } + + // If the sealing key is provided, use it to seal the envelope + if seal := c.String("sealing-key"); seal != "" { + var sealKey interface{} + if sealKey, err = loadSealingKey(seal); err != nil { + return cli.Exit(err, 1) + } + + if req, err = updateEnvelope(req, c); err != nil { + return cli.Exit(err, 1) + } + + if req, err = sealEnvelope(req, sealKey); err != nil { + return cli.Exit(err, 1) + } + } + } else { + // Attempt to create an error-only envelope + if c.String("error-message") == "" || c.String("envelope-id") == "" { + return cli.Exit("specify an envelope to load or an error message and id", 1) + } + + if req, err = errorMessage(c.String("envelope-id"), c.String("error-message"), c.String("error-code")); err != nil { + return cli.Exit(err, 1) + } + } + + // Always use the current timestamp for ordering purposes + req.Timestamp = time.Now().Format(time.RFC3339Nano) + + // Did we manage to load a valid secure envelope? + if err = env.Validate(req); err != nil { + return cli.Exit(fmt.Errorf("could not load envelope to send: %s", err), 1) + } + + // Is the envelope sealed? + if !req.Sealed && (req.Error == nil || req.Error.IsZero()) { + return cli.Exit("envelope has not been sealed", 1) + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + var rep *api.SecureEnvelope + if rep, err = peer.Transfer(ctx, req); err != nil { + return rpcerr(err) + } + + // If a private key is provided, unseal the envelope + if unseal := c.String("unsealing-key"); unseal != "" { + var unsealKey interface{} + if unsealKey, err = loadPrivateKey(unseal); err != nil { + return cli.Exit(err, 1) + } + + var handler *env.Envelope + if handler, err = env.Wrap(rep, env.WithUnsealingKey(unsealKey)); err != nil { + return cli.Exit(err, 1) + } + + if handler, _, err = handler.Unseal(); err != nil { + return cli.Exit(err, 1) + } + + // Are we saving to disk or printing JSON? + if out := c.String("out"); out != "" { + if extractPayload := c.Bool("payload"); extractPayload { + if handler, _, err = handler.Decrypt(); err != nil { + return cli.Exit(err, 1) + } + payload, _ := handler.Payload() + if err = dumpProto(payload, out); err != nil { + return cli.Exit(err, 1) + } + return nil + } + + if err = dumpProto(handler.Proto(), out); err != nil { + return cli.Exit(err, 1) + } + return nil + } + + // Print the decrypted envelope + if handler, _, err = handler.Decrypt(); err != nil { + return cli.Exit(err, 1) + } + payload, _ := handler.Payload() + return printJSON(payload) + } + + if out := c.String("out"); out != "" { + if err = dumpProto(rep, out); err != nil { + return cli.Exit(err, 1) + } + return nil + } + return printJSON(rep) +} + +func exchange(c *cli.Context) (err error) { + var req *api.SigningKey + if in := c.String("in"); in != "" { + if req, err = loadPublicKeys(in); err != nil { + return cli.Exit(err, 1) + } + } else { + // By default use the TRISA identity certificates in the key exchange + var provider *trust.Provider + if provider, _, err = loadCerts(c); err != nil { + return err + } + + var certs *x509.Certificate + if certs, err = provider.GetLeafCertificate(); err != nil { + return cli.Exit(err, 1) + } + + req = &api.SigningKey{} + req.Version = int64(certs.Version) + req.Signature = certs.Signature + req.SignatureAlgorithm = certs.SignatureAlgorithm.String() + req.PublicKeyAlgorithm = certs.PublicKeyAlgorithm.String() + req.NotBefore = certs.NotBefore.Format(time.RFC3339) + req.NotAfter = certs.NotAfter.Format(time.RFC3339) + + if req.Data, err = x509.MarshalPKIXPublicKey(certs.PublicKey); err != nil { + return cli.Exit("could not create public sealing key from certs", 1) + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + var rep *api.SigningKey + if rep, err = peer.KeyExchange(ctx, req); err != nil { + return rpcerr(err) + } + + if out := c.String("out"); out != "" { + if err = dumpKeys(rep, out); err != nil { + return cli.Exit(err, 1) + } + return nil + } + return printJSON(rep) +} + +func confirm(c *cli.Context) (err error) { + return cli.Exit("unimplemented: the address confirmation protocol has not been fully specified by the TRISA working group", 9) +} + +func health(c *cli.Context) (err error) { + // Performs a status check with an empty health check request. + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + var rep *api.ServiceState + if rep, err = ping.Status(ctx, &api.HealthCheck{}); err != nil { + return rpcerr(err) + } + return printJSON(rep) +} + +//==================================================================================== +// Directory RPC Commands +//==================================================================================== + +func lookup(c *cli.Context) (err error) { + req := &gds.LookupRequest{ + Id: c.String("vasp-id"), + RegisteredDirectory: c.String("registered-directory"), + CommonName: c.String("common-name"), + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + var rep *gds.LookupReply + if rep, err = directory.Lookup(ctx, req); err != nil { + return rpcerr(err) + } + + return printJSON(rep) +} + +func search(c *cli.Context) (err error) { + req := &gds.SearchRequest{ + Name: c.StringSlice("name"), + Website: c.StringSlice("website"), + Country: c.StringSlice("country"), + BusinessCategory: make([]models.BusinessCategory, 0, len(c.StringSlice("category"))), + VaspCategory: make([]string, 0, len(c.StringSlice("category"))), + } + + for _, cat := range c.StringSlice("category") { + if enum, ok := models.BusinessCategory_value[cat]; ok { + req.BusinessCategory = append(req.BusinessCategory, models.BusinessCategory(enum)) + } else { + req.VaspCategory = append(req.VaspCategory, cat) + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + var rep *gds.SearchReply + if rep, err = directory.Search(ctx, req); err != nil { + return rpcerr(err) + } + + return printJSON(rep) +} + +//==================================================================================== +// Helper Commands - Clients +//==================================================================================== + +func initClient(c *cli.Context) (err error) { + var endpoint string + if endpoint = c.String("endpoint"); endpoint == "" { + return cli.Exit("specify endpoint of TRISA peer to connect to", 1) + } + + var creds grpc.DialOption + if creds, err = loadCreds(endpoint, c); err != nil { + return err + } + + var cc *grpc.ClientConn + if cc, err = grpc.Dial(endpoint, creds); err != nil { + return cli.Exit(err, 1) + } + + peer = api.NewTRISANetworkClient(cc) + return nil +} + +func initHealthCheckClient(c *cli.Context) (err error) { + var endpoint string + if endpoint = c.String("endpoint"); endpoint == "" { + return cli.Exit("specify endpoint of TRISA peer to connect to", 1) + } + + var opts []grpc.DialOption + if c.Bool("insecure") { + fmt.Println("warning: connecting in insecure mode is not supported by all TRISA peers") + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + var creds grpc.DialOption + if creds, err = loadCreds(endpoint, c); err != nil { + return err + } + opts = append(opts, creds) + } + + var cc *grpc.ClientConn + if cc, err = grpc.Dial(endpoint, opts...); err != nil { + return cli.Exit(err, 1) + } + + ping = api.NewTRISAHealthClient(cc) + return nil +} + +func initDirectoryClient(c *cli.Context) (err error) { + endpoint := c.String("directory") + if _, ok := directoryAliases[endpoint]; ok { + endpoint = directoryAliases[endpoint] + } + + if endpoint == "" { + return cli.Exit("specify endpoint or name of directory service to connect to", 1) + } + + var cc *grpc.ClientConn + if cc, err = grpc.Dial(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))); err != nil { + return cli.Exit(err, 1) + } + + directory = gds.NewTRISADirectoryClient(cc) + return nil +} + +//==================================================================================== +// Helper Commands - Serialization and Deserialization +//==================================================================================== + +func loadCreds(endpoint string, c *cli.Context) (creds grpc.DialOption, err error) { + var ( + certs *trust.Provider + pool trust.ProviderPool + ) + + if certs, pool, err = loadCerts(c); err != nil { + return nil, cli.Exit(err, 1) + } + + if creds, err = mtls.ClientCreds(endpoint, certs, pool); err != nil { + return nil, cli.Exit(err, 1) + } + return creds, nil +} + +func loadCerts(c *cli.Context) (certs *trust.Provider, pool trust.ProviderPool, err error) { + // Get configuration for certificates + certPath := c.String("certs") + if certPath == "" { + return nil, nil, cli.Exit("path to identity certificates required for this command", 1) + } + + var sz *trust.Serializer + if passwd := c.String("pkcs12password"); passwd != "" { + if sz, err = trust.NewSerializer(true, passwd); err != nil { + return nil, nil, cli.Exit(err, 1) + } + } else { + if sz, err = trust.NewSerializer(false); err != nil { + return nil, nil, cli.Exit(err, 1) + } + } + + if certs, err = sz.ReadFile(certPath); err != nil { + return nil, nil, cli.Exit(err, 1) + } + + if chainPath := c.String("chain"); chainPath != "" { + if pool, err = sz.ReadPoolFile(chainPath); err != nil { + return nil, nil, cli.Exit(err, 1) + } + } else { + if pool, err = sz.ReadPoolFile(certPath); err != nil { + return nil, nil, cli.Exit(err, 1) + } + } + + return certs, pool, nil +} + +func loadPublicKeys(path string) (key *api.SigningKey, err error) { + key = new(api.SigningKey) + + var data []byte + if data, err = ioutil.ReadFile(path); err != nil { + return nil, fmt.Errorf("could not read key file: %s", err) + } + + switch filepath.Ext(path) { + case ".json": + opts := protojson.UnmarshalOptions{ + AllowPartial: true, + DiscardUnknown: true, + } + if err = opts.Unmarshal(data, key); err != nil { + return nil, fmt.Errorf("could not unmarshal json keys: %s", err) + } + case ".pb": + if err = proto.Unmarshal(data, key); err != nil { + return nil, fmt.Errorf("could not unmarshal pb keys: %s", err) + } + case ".pem", ".crt": + var block *pem.Block + pemblocks: + for { + block, data = pem.Decode(data) + if block == nil { + return nil, fmt.Errorf("could not find public key in key file %s", path) + } + + switch block.Type { + case trust.BlockPublicKey, trust.BlockRSAPublicKey: + key.Data = block.Bytes + break pemblocks + case trust.BlockCertificate: + // assumes that the first certificate in a trust chain is the "leaf" + var certs *x509.Certificate + if certs, err = x509.ParseCertificate(block.Bytes); err != nil { + return nil, fmt.Errorf("could not parse certificate: %s", err) + } + key.Version = int64(certs.Version) + key.Signature = certs.Signature + key.SignatureAlgorithm = certs.SignatureAlgorithm.String() + key.PublicKeyAlgorithm = certs.PublicKeyAlgorithm.String() + key.NotBefore = certs.NotBefore.Format(time.RFC3339) + key.NotAfter = certs.NotAfter.Format(time.RFC3339) + + if key.Data, err = x509.MarshalPKIXPublicKey(certs.PublicKey); err != nil { + return nil, fmt.Errorf("could not parse certificate: %s", err) + } + break pemblocks + } + } + default: + return nil, fmt.Errorf("unhandled extension %q use .json or .pem", filepath.Ext(path)) + } + + return key, nil +} + +func loadSealingKey(path string) (key interface{}, err error) { + var sealingKey *api.SigningKey + if sealingKey, err = loadPublicKeys(path); err != nil { + return nil, err + } + + if key, err = x509.ParsePKIXPublicKey(sealingKey.Data); err != nil { + return nil, err + } + return key, nil +} + +func loadPrivateKey(path string) (key interface{}, err error) { + var data []byte + if data, err = ioutil.ReadFile(path); err != nil { + return nil, fmt.Errorf("could not read key file: %s", err) + } + + var block *pem.Block + for { + block, data = pem.Decode(data) + if block == nil { + break + } + + switch block.Type { + case trust.BlockPrivateKey, trust.BlockRSAPrivateKey, trust.BlockECPrivateKey: + return trust.ParsePrivateKey(block) + } + } + return nil, fmt.Errorf("could not find private key in key file %s", path) +} + +func dumpKeys(key *api.SigningKey, path string) (err error) { + var data []byte + switch filepath.Ext(path) { + case ".json": + opts := protojson.MarshalOptions{ + Multiline: true, + Indent: " ", + AllowPartial: true, + UseProtoNames: true, + UseEnumNumbers: false, + EmitUnpopulated: true, + } + + if data, err = opts.Marshal(key); err != nil { + return cli.Exit(err, 1) + } + case ".pb": + if data, err = proto.Marshal(key); err != nil { + return cli.Exit(err, 1) + } + case ".pem": + var out interface{} + if out, err = x509.ParsePKIXPublicKey(key.Data); err != nil { + return fmt.Errorf("invalid PKIX public key received from remote") + } + if data, err = trust.PEMEncodePublicKey(out); err != nil { + return err + } + default: + return fmt.Errorf("unknown extension %q use .json or .pem", filepath.Ext(path)) + } + + if err = ioutil.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("could not write keys to disk: %s", err) + } + fmt.Printf("saved keys to %s\n", path) + return nil +} + +func loadIdentity(path string) (_ *anypb.Any, err error) { + opts := protojson.UnmarshalOptions{ + AllowPartial: true, + DiscardUnknown: false, + } + + var data []byte + if data, err = ioutil.ReadFile(path); err != nil { + return nil, fmt.Errorf("could not read identity from %s", err) + } + + // Attempt to unmarshal the data as IVMS 101 + identity := &ivms101.IdentityPayload{} + if err = opts.Unmarshal(data, identity); err == nil { + return anypb.New(identity) + } + + // Attempt to unmarshal the data as a serialized any + msg := &anypb.Any{} + if err = opts.Unmarshal(data, msg); err == nil { + return msg, nil + } + return nil, fmt.Errorf("could not unmarshal identity: unknown type or format") +} + +func loadTransaction(path string) (_ *anypb.Any, err error) { + opts := protojson.UnmarshalOptions{ + AllowPartial: true, + DiscardUnknown: false, + } + + var data []byte + if data, err = ioutil.ReadFile(path); err != nil { + return nil, fmt.Errorf("could not read transaction from %s", err) + } + + // Attempt to unmarshal the data as a generic Transaction + transaction := &generic.Transaction{} + if err = opts.Unmarshal(data, transaction); err == nil { + return anypb.New(transaction) + } + + // Attempt to unmarshal the data as a generic Pending + pending := &generic.Pending{} + if err = opts.Unmarshal(data, pending); err == nil { + return anypb.New(pending) + } + + // Attempt to unmarshal the data as a serialized any + msg := &anypb.Any{} + if err = opts.Unmarshal(data, msg); err == nil { + return msg, nil + } + + return nil, fmt.Errorf("could not unmarshal transaction: unknown type or format") +} + +func loadEnvelope(path string) (msg *api.SecureEnvelope, err error) { + var data []byte + if data, err = ioutil.ReadFile(path); err != nil { + return nil, fmt.Errorf("could not read envelope file: %s", err) + } + + msg = &api.SecureEnvelope{} + switch filepath.Ext(path) { + case ".json": + opts := protojson.UnmarshalOptions{ + AllowPartial: true, + DiscardUnknown: true, + } + if err = opts.Unmarshal(data, msg); err != nil { + return nil, fmt.Errorf("could not unmarshal json envelope: %s", err) + } + case ".pb": + if err = proto.Unmarshal(data, msg); err != nil { + return nil, fmt.Errorf("could not unmarshal pb envelope: %s", err) + } + + default: + return nil, fmt.Errorf("unhandled extension %q use .json or .pb", filepath.Ext(path)) + } + return msg, nil +} + +func dumpProto(msg proto.Message, path string) (err error) { + var data []byte + switch filepath.Ext(path) { + case ".json": + opts := protojson.MarshalOptions{ + Multiline: true, + Indent: " ", + AllowPartial: true, + UseProtoNames: true, + UseEnumNumbers: false, + EmitUnpopulated: true, + } + + if data, err = opts.Marshal(msg); err != nil { + return cli.Exit(err, 1) + } + case ".pb": + if data, err = proto.Marshal(msg); err != nil { + return cli.Exit(err, 1) + } + default: + return fmt.Errorf("unknown extension %q use .json or .pb", filepath.Ext(path)) + } + if err = ioutil.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("could not write message to disk: %s", err) + } + fmt.Printf("message saved to %s\n", path) + return nil +} + +//==================================================================================== +// Helper Commands - Managing Secure Envelopes +//==================================================================================== + +func errorMessage(id, message, code string) (_ *api.SecureEnvelope, err error) { + code = strings.ToUpper(code) + val, ok := api.Error_Code_value[code] + if !ok { + return nil, fmt.Errorf("%q is not a valid error code", code) + } + + return &api.SecureEnvelope{ + Id: id, + Error: &api.Error{ + Code: api.Error_Code(val), + Message: message, + }, + }, nil +} + +func updateEnvelope(in *api.SecureEnvelope, c *cli.Context) (out *api.SecureEnvelope, err error) { + // Validation: cannot update error message or update the payload if the envelope is sealed + if emsg := c.String("error-message"); emsg != "" { + return nil, errors.New("error message is ignored when -in is supplied") + } + + if in.Sealed && (c.String("sent-at") != "" || c.String("received-at") != "") { + return nil, errors.New("cannot update payload on a sealed envelope") + } + + // Set the envelope id if it is supplied on the command line; otherwise if there + // is no envelope id then set it to a new random id. + if eid := c.String("envelope-id"); eid != "" { + in.Id = eid + } + + if in.Id == "" { + in.Id = uuid.NewString() + } + + // If the envelope is sealed, return here. + if in.Sealed { + return in, nil + } + + // Otherwise continue to update the payload + var handler *env.Envelope + if handler, err = env.Wrap(in); err != nil { + return nil, err + } + + // Decrypt and parse the secure envelope + if handler, _, err = handler.Decrypt(); err != nil { + return nil, err + } + + // Fetch the payload to update it + var payload *api.Payload + if payload, err = handler.Payload(); err != nil { + return nil, err + } + + if ts := c.String("sent-at"); ts != "" { + payload.SentAt = ts + } + + if payload.SentAt == "" { + payload.SentAt = time.Now().Format(time.RFC3339) + } + + if ts := c.String("received-at"); ts != "" { + if strings.ToLower(ts) == "now" { + ts = time.Now().Format(time.RFC3339) + } + payload.ReceivedAt = ts + } + + if handler, err = handler.Update(payload); err != nil { + return nil, err + } + + if handler, _, err = handler.Encrypt(); err != nil { + return nil, err + } + + return handler.Proto(), nil +} + +func sealEnvelope(in *api.SecureEnvelope, key interface{}) (out *api.SecureEnvelope, err error) { + var handler *env.Envelope + if handler, err = env.Wrap(in, env.WithSealingKey(key)); err != nil { + return nil, err + } + + if handler, _, err = handler.Seal(); err != nil { + return nil, err + } + return handler.Proto(), nil +} + +//==================================================================================== +// Helper Commands - CLI output +//==================================================================================== + +func printJSON(msg interface{}) (err error) { + var data []byte + switch m := msg.(type) { + case proto.Message: + opts := protojson.MarshalOptions{ + Multiline: true, + Indent: " ", + AllowPartial: true, + UseProtoNames: true, + UseEnumNumbers: false, + EmitUnpopulated: true, + } + + if data, err = opts.Marshal(m); err != nil { + return cli.Exit(err, 1) + } + default: + if data, err = json.MarshalIndent(msg, "", " "); err != nil { + return cli.Exit(err, 1) + } + } + + fmt.Println(string(data)) + return nil +} + +func rpcerr(err error) error { + if serr, ok := status.FromError(err); ok { + return cli.Exit(fmt.Errorf("%s: %s", serr.Code().String(), serr.Message()), int(serr.Code())) + } + return cli.Exit(err, 2) } diff --git a/docs/content/gds/_index.en.md b/docs/content/gds/_index.en.md index d6cd330..668fdc9 100644 --- a/docs/content/gds/_index.en.md +++ b/docs/content/gds/_index.en.md @@ -1,7 +1,7 @@ --- title: "Directory Service" date: 2021-06-14T11:21:42-05:00 -lastmod: 2021-06-14T11:21:42-05:00 +lastmod: 2022-04-10T09:32:16-05:00 description: "The Global TRISA Directory Service (GDS)" weight: 50 --- @@ -13,3 +13,20 @@ The Global TRISA Directory Service (GDS) facilitates peer-to-peer exchanges betw - By providing certificate and KYC information for verification Interactions with a Directory Service are specified by the TRISA protocol. Currently, the TRISA organization hosts the GDS on behalf of the TRISA network. This documentation describes the TRISA implementation of the directory service and TRISA-specific interactions with it. + +## Networks + +TRISA currently operates two directory services: a TestNet (trisatest.net) and the MainNet (vaspdirectory.net). The [TestNet]({{< ref "/testnet" >}}) is intended to facilitate development and integration and should not be used for actual compliance exchanges. The MainNet is separated from the TestNet with a completely different certificate authority, and certificates issued to TestNet nodes cannot be used to connect to MainNet nodes and vice-versa. + +Connect to the GDS and register for certificates with the following endpoints/urls: + +| Directory | Network | Website | gRPC Endpoint | +|-------------------|---------|---------------------------|-----------------------------| +| trisatest.net | TestNet | https://trisatest.net | `api.trisatest.net:443` | +| vaspdirectory.net | MainNet | https://vaspdirectory.net | `api.vaspdirectory.net:443` | + +## Registered Directories + +TRISA supports the idea of different directory services that can interoperate by exchanging VASP records with each other. A directory service by definition is a system that has an intermediate certificate authority under one of the TRISA root authority networks (e.g. TestNet or MainNet) and can issue leaf certificates via the intermediate authority. Directory services exchange records with each other to faciliate lookups. + +Currently the only registered directories are the TRISA hosted directory services. \ No newline at end of file diff --git a/docs/content/testnet/_index.en.md b/docs/content/testnet/_index.en.md index f6ff3b1..3e309a8 100644 --- a/docs/content/testnet/_index.en.md +++ b/docs/content/testnet/_index.en.md @@ -1,7 +1,7 @@ --- title: TestNet date: 2021-06-14T11:20:10-05:00 -lastmod: 2021-06-14T11:20:10-05:00 +lastmod: 2022-04-10T09:32:16-05:00 description: "The TRISA TestNet" weight: 30 --- @@ -10,9 +10,9 @@ The TRISA TestNet has been established to provide a demonstration of the TRISA p {{% figure src="/img/testnet_architecture.png" %}} -The TRISA TestNEt is comprised of the following services: +The TRISA TestNet is comprised of the following services: -- [TRISA Directory Service](https://vaspdirectory.net) - a user interface to explore the TRISA Global Directory Service and register to become a TRISA member +- [TRISA Directory Service](https://trisatest.net) - a user interface to explore the TRISA Global Directory Service and register to become a TRISA member - [TestNet Demo](https://vaspbot.net) - a demo site to show TRISA interactions between “robot” VASPs that run in the TestNet The TestNet also hosts three robot VASPs or rVASPs that have been implemented as a convenience for TRISA members to integrate their TRISA services. The primary rVASP is Alice, a secondary for demo purposes is Bob, and to test interactions with non-verified TRISA members, there is also an "evil" rVASP. \ No newline at end of file diff --git a/docs/content/testnet/trisa-cli.en.md b/docs/content/testnet/trisa-cli.en.md new file mode 100644 index 0000000..d1efc24 --- /dev/null +++ b/docs/content/testnet/trisa-cli.en.md @@ -0,0 +1,366 @@ +--- +title: TRISA CLI +date: 2022-04-02T12:09:09-05:00 +lastmod: 2022-04-10T09:32:16-05:00 +description: "Using the TRISA command line interface for development" +weight: 15 +--- + +The TRISA command line client is a utility that assists TRISA integrators and developers testing their TRISA service. Advanced users may also use the TRISA client to execute TRISA requests for compliance purposes, although this is not recommended for extensive use. To install the latest version of the TRISA CLI, you must have [Go installed](https://go.dev/doc/install) on your computer. The following command will install the latest version of the CLI: + +``` +$ go install github.com/trisacrypto/trisa/cmd/trisa@main +``` + +{{% notice note %}} +We are currently working on a release mechanism that will automatically build the CLI for a variety of platforms so that in the future you will not need to have Go installed. +Stay tuned! +{{% /notice %}} + +## Configuration + +Before you can start using the TRISA CLI, you must first configure your environment to ensure that you can successfully connect to a remote peer or the directory service. + +**Prerequisites**: + +1. The `trisa` command installed and on your `$PATH` +2. Your [testnet certificates]({{< ref "/gds/registration" >}}) that include both the trust chain and private key. + +The TRISA CLI command is configured via flags specified for each command or by setting environment variables in your shell with the configuration. The CLI also supports the use of [.env](https://platform.sh/blog/2021/we-need-to-talk-about-the-env/) files in the current working directory for configuration. To see what CLI flags should be specified use `trisa --help`. An example `.env` configuration file is as follows: + +```ini +# The endpoint to the TRISA node that you'd like to connect to. The endpoint can be +# found using the directory service lookup command. +TRISA_ENDPOINT=example.com:443 + +# Directory service you'd like to connect to. You can specify a short name such as +# "testnet" or "mainnet" or the endpoint of the directory service to connect to. The +# configured directory is trisatest.net by default. +TRISA_DIRECTORY=testnet + +# Path to your TRISA identity certificates that include the private key. This can be the +# original .zip file sent by Sectigo or the unzipped .p12 file; in which case the +# PKCS12 password must also be supplied. If you've decrypted it manually it should be in +# PEM encoded format with the .pem or .crt extension. +TRISA_CERTS=path/to/certs.pem + +# If you've split your certs into the public trust chain without private keys and a +# private key file, then specify the path to the trust chain (optional). +TRISA_TRUST_CHAIN=path/to/chain.pem + +# If the certs are PKCS12 encrypted then specify the password for decryption (optional). +TRISA_CERTS_PASSWORD=supersecret +``` + +The simplest way to get started with TRISA is to copy and paste the above snippet into a `.env` file in your current directory, then modifying the values as necessary. + +## Creating Secure Envelopes + +The first step when using the TRISA CLI is to create some payload data that can be sealed inside of secure envelopes for TRISA envelopes. At a minimum, there are two JSON files that you need to create or provide for the payload: + +1. An _identity_ payload containing [IVMS 101](https://github.com/trisacrypto/trisa/blob/347f88d55df4d4e0167ad4e005721b638991ecef/proto/ivms101/identity.proto#L46-L53) data. +2. A _transaction_ payload containing a [Transaction](https://github.com/trisacrypto/trisa/blob/347f88d55df4d4e0167ad4e005721b638991ecef/proto/trisa/data/generic/v1beta1/transaction.proto#L11) or [Pending](https://github.com/trisacrypto/trisa/blob/347f88d55df4d4e0167ad4e005721b638991ecef/proto/trisa/data/generic/v1beta1/transaction.proto#L30) message. + +The identity payload is the compliance information required for the transfer and the transaction payload is used to identify the transaction on the chain and associate it with the identity information. For ease of data entry, these files may be specified as JSON files and the protocol buffer payload created using the `trisa make` command. + +``` +$ trisa make -i identity.json -t transaction.json -o envelope.json +``` + +With no other arguments, this command creates an unsealed envelope that has a random envelope ID, the current time as the sent at timestamp, and no received at timestamp in the payload. The documentation refers to this kind of secure envelope as a "payload template" in the rest of the documentation because it can be loaded by the `trisa seal` or `trisa transfer` commands to update the envelope ID, timestamps, before sealing the envelope with the public keys of the recipient. + +To create a complete envelope or a fully sealed envelope, simply specify the public sealing key with the `-seal` flag as well as any additional metadata you'd like to supply on the envelope such as the envelope ID (see `trisa make --help` for more details). + +## Sealing + +To seal an envelope you must have the public keys of the recipient, see [the key exchanges section]({{< relref "#key-exchanges" >}}) for more detail on how to retrieve the public sealing key of a remote peer. Once you've exchanged keys and saved them to disk, you can seal an unsealed envelope with the following command: + +``` +$ trisa seal -in unsealed_envelope.json -out sealed_evelope.json -seal public.pem +``` + +Once the envelope has been sealed, only the recipient with the private key counterpart to the public key used to seal the envelope can open the secure envelope. + +While sealing the envelope you also have the opportunity to update the envelope, e.g. to mark the received at timestamp or set a different envelope ID to create a new transfer: + +``` +$ trisa seal -in envelope.json -out sealed.json -seal public.pem -received-at now +``` + +Another common workflow is to generate an error envelope with the same ID as an incoming envelope. Error envelopes do not require any cryptography, so the public key is not required: + +``` +$ trisa seal -in envelope.json -error-code COMPLIANCE_CHECK_FAIL -error-message "sanctioned entity" +``` + +## Opening + +By default the `trisa open` command is used to unseal an envelope and save it as an unsealed envelope for further processing. This command can also be used to extract the payload or check if an incoming envelope has an error on it. To extract an unsealed envelope and save it to disk: + +``` +$ trisa open -in envelope.json -out unsealed_envelope.json -key private.pem +``` + +If you add the `-payload` flag, then the payload will be decrypted and saved to disk; adding the `-error` flag will extract an error and save it to disk. If the `envelope.json` is an unsealed envelope, then the `-key` flag can be omitted. If the `-out` flag is ommitted, the contents will be printed to disk. For example to simply view the payload of a sealed envelope: + +``` +$ trisa open -in envelope.json -key private.pem -payload +``` + +Or to view an error on the envelope: + +``` +$ trisa open -in envelope.json -error +``` + +Note that no private key is required for errors since errors are not encrypted. + +{{% notice tip %}} +By default a key exchange will use your TRISA identity certs as the sealing key, however the `trisa open` command won't automatically use your TRISA identity certs for unsealing the envelope. If you used the default key exchange then you can take advantage of the environment configuration to pass the path to your identity certs that contain your private key as follows: + +``` +$ trisa open -in envelope.json -key $TRISA_CERTS +``` +{{% /notice %}} + + +## Interacting with TRISA Peers + +The primary use of the `trisa` CLI is to execute TRISA RPC requests to a TRISA node. A general workflow is as follows: + +1. Identify the peer endpoint using the Directory Service lookup or search functionality. +2. Create a secure envelope or payload template to prepare to send to the remote peer. +3. Perform a key exchange with the remote peer and save the sealing keys. +4. Seal the secure envelope or payload template with the remote peer's sealing keys. +5. Execute a transfer and save the response envelope. + +This workflow generally mirrors the workflow of live TRISA compliance operations, though many of the steps are manual to facilitate integration and development. + +### Transfers + +Send a secure envelope to the remote TRISA peer and receive a secure envelope in exchange. Transfers are the central compliance exchange mechanism in the TRISA protocol. If you have already created and sealed an envelope, saving it to `outgoing.json` you can transfer it as follows: + +``` +$ trisa transfer -i outgoing.json -o response.json +``` + +This will execute the TRISA transfer and save the response, including TRISA error envelopes, to disk at the specified path. If the extension of the output path is `.json` then the envelope is marshaled to `.json` format, if it is the `.pb` extension it will be saved as a raw protocol buffer. If the `-o` flag is not supplied, then the JSON response will be printed to the command line. If you would like the decrypted payload printed, then you must provide the private sealing key: + +``` +$ trisa transfer -i outgoing.json -k private.pem +``` + +If both an output path and the private key are provided then a JSON file is produced with the unsealed envelope that can be read using the `open` command or resent using the `seal` command. + +You can also use a secure envelope payload template to seal and transfer an envelope in one step instead of using the intermediate `seal` command: + +``` +$ trisa transfer -i outgoing.json -s public_sealing_key.pem +``` + +See [sealing secure envelopes]({{< relref "#sealing" >}}) for more information on the command line arguments that can be used to adapt secure envelopes before sending them. + +If you would like to send an error-only secure envelope to the recipient, then you must supply the envelope ID, error code, and error message as follows: + +``` +$ trisa transfer -I envelope-id-foo -C COMPLIANCE_CHECK_FAIL -E "something went wrong" +``` + +Note that sending an error-only secure envelope is usually a response to an incoming message. This mechanism is used primarily to test a server's handling of an asynchronous transfer workflow. + +{{% notice note %}} +The TRISA CLI command currently does not implement the `TRISANetwork/TransferStream` birdirectional streaming RPC and does not have plans to implement this in the CLI. If you would like an implementation of streaming from the command line, please open an issue on our [GitHub repository](https://github.com/trisacrypto/trisa/issues). +{{% /notice %}} + +### Key Exchanges + +Send a key exchange request to get the public sealing key of the node. Key management is a somewhat complex topic, and the TRISA CLI attempts to do the simplest possible thing to enable testing and development. A key exchange requires you to send your public sealing keys to the remote node, and they will return keys to you. Prior to a transfer, a key exchange must be completed so that you have the sealing keys to create a secure envelope and so that the remote has your public keys to send a response. + +By default, the TRISA CLI will simply use your TRISA mTLS identity certificates as the keys for a key exchange. The simplest exchange is therefore: + +``` +$ trisa exchange -o peer_sealing_keys.pem +``` + +The `-o` flag saves the keys to disk at the specified path, so that you can use the keys later on to make secure envelopes or conduct transfers. If the `-o` flag is ommitted, the JSON data of the response, an `SigningKey` protocol buffer message will be printed to `stdout`. There are several formats that the keys can be saved in: a path with a `.json` or `.pb` extension will save the protocol buffer message to disk in the specified format; a path with a `.pem` or `.crt` extension will save the keys as PEM encoded public key. + +To send alternative keys to the remote peer in a key exchange, you may use the `-i` flag to specify the path of keys to send. If the input path ends in `.json` or `.pb` it is parsed as a `SigningKey` protocol buffer message in JSON format or raw protobuf format respectively. If the input path ends in `.pem` or `.crt` it is parsed as a PEM encoded public key or x.509 certificate. Note that the PEM encoded format, the first `PUBLIC KEY` or `CERTIFICATE` block that is found is used for parsing. + +To generate your own RSA keys to send to the remote server for key exchange, use the following commands: + +``` +$ openssl genrsa -out private.pem 4096 +$ openssl rsa -in private.pem -pubout -out public.pem +``` + +This will create two files, `private.pem` that contains your private keys and `public.pem` which contains your public keys. Send the public key to the remote TRISA peer as follows: + +``` +$ trisa exchange -i public.pem -o peer_sealing_keys.pem +``` + +Ensure that you keep the `private.pem` file so that you can decrypt transfers that follow; it is likely that the remote TRISA node will use the key you just exchanged in sending outgoing transfers and preparing responses to your transfers. The only way to decrypt that data is with the private key! + +### Address Confirmation + +{{% notice warning %}} +Address confirmation has not yet been fully defined by the TRISA working group. The TRISA technical subcommittee is currently working on the Address Confirmation protocol, so stay tuned for more information! +{{% /notice %}} + +### Health Checks + +Send a health check request to check the status of the TRISA node. + +The health check RPC is primariliy for the directory service to monitor if the TRISA network is online, however it is also useful for debugging or diagnosing connection issues (e.g. is the remote peer offline or are my certificates invalid). A simple health check request is as follows: + +``` +$ trisa status +``` + +Use the `--insecure` flag to connect without mTLS credentials, though not all TRISA peers will support an insecure status check. + +## Interacting with the GDS + +The TRISA CLI supports some basic interactions with the TRISA Global Directory Service (GDS) based on your initial configuration. + +### Lookup + +Lookup a TRISA VASP by common name or VASP ID. The following requests will lookup Alice on the TestNet by both common name and ID: + +``` +$ trisa lookup -n api.alice.vaspbot.net +``` + +``` +$ trisa lookup -i 7a96ca2c-2818-4106-932e-1bcfd743b04c +``` + +Lookups also support the registered directory argument if looking up a VASP that is a member of the network but was issued certificates from a different directory service. If omitted, by default the directory service will lookup the VASP record as though it was the registered directory. + +### Search + +Search for a TRISA VASP by name or by website. You can specify multiple names and websites to expand your search. E.g. to search for "Alice" and "Bob" on the TestNet: + +``` +$ trisa search -n alice -n bob +``` + +If this returns too many results you may specify either category or country filters for the results. Country filters are inclusive and should be ISO Alpha-2 country codes: + +``` +$ trisa search -n alice -n bob -c US -c SG +``` + +{{% notice tip %}} +Categories are case sensitive and websites must be full URLs for the search to work. If you're not getting any results for a website search, try adding the `http://` prefix or removing any paths from the URL. If you're not getting any results for the name, try using a prefix of the name that is greater than 3 characters. +{{% /notice %}} + +Categories that may be helpful in filtering: + +| VASP Categories | Business Categories | +|-----------------|-----------------------| +| Exchange | PRIVATE_ORGANIZATION | +| DEX | GOVERNMENT_ENTITY | +| P2P | BUSINESS_ENTITY | +| Kiosk | NON_COMMERCIAL_ENTITY | +| Custodian | | +| OTC | | +| Fund | | +| Project | | +| Gambling | | +| Miner | | +| Mixer | | +| Individual | | +| Other | | + +## Guided Walkthrough + +This section contains a guided walkthrough of an interaction with the [Alice rVASP]({{< relref "rvasps.md" >}}) using the CLI. To complete this walkthrough you will need TRISA TestNet certificates issued by the TRISA Global Directory Service, the `trisa` CLI application installed and configured with those certs as discussed at the top of this guide. Ensure that the `$TRISA_DIRECTORY` environment variable is set to `testnet`. + +First, perform a TRISA Global Directory search for the Alice VASP: + +``` +$ trisa search -n alice +{ + "error": null, + "results": [ + { + "id": "7a96ca2c-2818-4106-932e-1bcfd743b04c", + "registered_directory": "trisatest.net", + "common_name": "api.alice.vaspbot.net", + "endpoint": "api.alice.vaspbot.net:443" + } + ] +} +``` + +To get more information about Alice VASP, lookup the record in the GDS: + +``` +$ trisa lookup -cn api.alice.vaspbot.net +{ + "name": "AliceCoin", + "country": "US", + [...] +} +``` + +The rest of the interactions will be with the Alice rVASP, so ensure that the `$TRISA_ENDPOINT` environment variable is set to `api.alice.vaspbot.net:443` (or whatever endpoint was returned by the directory service). + +{{% notice tip %}} +Managing the environment variables for configuring the `trisa` CLI can be done with a `.env` file in your current working directory. +{{% /notice %}} + +Download a copy of the following data for the payload: + +- [`identity.json`](https://gist.github.com/bbengfort/be1f255756834268cc006c31d088eb3b) +- [`transaction.json`](https://gist.github.com/bbengfort/b1b2a883ca61da95d84019310cbfd091) + +This contains payload information as though we are sending Alice a compliance information transfer from a VASP named "MyVASP". + +Build the payload template: + +``` +$ trisa make -i identity.json -t transaction.json -o unsealed_envelope.json +``` + +This should create an unsealed envelope with the payload data, the sent at timestamp set to now and a random envelope ID. To view the payload in the unsealed envelope: + +``` +$ trisa open -i unsealed_envelope.json -payload +``` + +Conduct a key exchange with Alice to get Alice's public keys to seal the envelope: + +``` +$ trisa exchange -o alice.pem +``` + +You can then seal the envelope so that only Alice can open it: + +``` +$ trisa seal -i unsealed_envelope.json -s alice.pem -o outgoing.json +``` + +You can now make the transfer to Alice: + +``` +$ trisa transfer -i outgoing.json -o incoming.json +``` + +View the payload from Alice using your private key to decrypt the message. Note that you'll need to source the `.env` file for the following command to work if you're using the `.env` file for configuration: + +``` +$ source .env +$ trisa open -i incoming.json -k $TRISA_CERTS -payload +``` + +{{% notice warning %}} +If you receive the following error when running the above command: + +``` +envelope in unhandled state corrupted +``` + +It means that the rVASP is running an older TRISA version. Please contact the TRISA admins if this is the rVASP running in TestNet or use the latest docker image if you're running an rVASP in your local environment. +{{% /notice %}} diff --git a/go.mod b/go.mod index 9bd8dc5..9608895 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,27 @@ module github.com/trisacrypto/trisa -go 1.16 +go 1.17 require ( + github.com/google/uuid v1.3.0 + github.com/joho/godotenv v1.4.0 + github.com/stretchr/testify v1.7.1 + github.com/urfave/cli/v2 v2.4.0 + google.golang.org/grpc v1.45.0 + google.golang.org/protobuf v1.28.0 + software.sslmate.com/src/go-pkcs12 v0.1.0 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/uuid v1.2.0 - github.com/stretchr/testify v1.7.0 - google.golang.org/grpc v1.37.0 - google.golang.org/protobuf v1.26.0 - software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 // indirect + golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 // indirect + golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 6c44890..47c8e99 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,33 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -20,6 +35,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -31,57 +47,95 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I= +github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac h1:qSNTkEN+L2mvWcLgJOR+8bdHX9rN/IdU3A1Ghpfb1Rg= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.37.0 h1:uSZWeQJX5j11bIQ4AJoj+McDBo29cY1MCoC1wO3ts+c= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -92,14 +146,18 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4= -software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78/go.mod h1:B7Wf0Ya4DHF9Yw+qfZuJijQYkWicqDa+79Ytmmq3Kjg= +software.sslmate.com/src/go-pkcs12 v0.1.0 h1:u9nw7H7/HLcDrmntgFcIeO+KS/dqfLbcTRacXln3PQs= +software.sslmate.com/src/go-pkcs12 v0.1.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= diff --git a/pkg/trisa/envelope/envelope.go b/pkg/trisa/envelope/envelope.go index cf491b8..59b6832 100644 --- a/pkg/trisa/envelope/envelope.go +++ b/pkg/trisa/envelope/envelope.go @@ -220,6 +220,16 @@ func Wrap(msg *api.SecureEnvelope, opts ...Option) (env *Envelope, err error) { return env, nil } +// Validate is a one-liner for Wrap(msg).ValidateMessage() and can be used to ensure +// that a secure envelope has been correctly initialized and can be processed. +func Validate(msg *api.SecureEnvelope) (err error) { + var env *Envelope + if env, err = Wrap(msg); err != nil { + return err + } + return env.ValidateMessage() +} + //=========================================================================== // Envelope State Transitions //=========================================================================== @@ -252,6 +262,47 @@ func (e *Envelope) Reject(reject *api.Error, opts ...Option) (env *Envelope, err return env, nil } +// Update the envelope with a new payload maintaining the original crypto method. This +// is useful to prepare a response to the user, for example updating the ReceivedAt +// timestamp in the payload then re-encrypting the secure envelope to send back to the +// originator. Most often, this method is also used with the WithSealingKey option so +// that the envelope workflow for sealing an envelope can be applied completely. +// The original envelope is not modified, the secure envelope is cloned. +func (e *Envelope) Update(payload *api.Payload, opts ...Option) (env *Envelope, err error) { + state := e.State() + if state != Clear && state != ClearError { + return nil, fmt.Errorf("cannot update envelope from %q state", state) + } + + // Clone the envelope + env = &Envelope{ + msg: &api.SecureEnvelope{ + Id: e.msg.Id, + Payload: nil, + EncryptionKey: e.msg.EncryptionKey, + EncryptionAlgorithm: e.msg.EncryptionAlgorithm, + Hmac: nil, + HmacSecret: e.msg.HmacSecret, + HmacAlgorithm: e.msg.HmacAlgorithm, + Error: e.msg.Error, + Timestamp: time.Now().Format(time.RFC3339Nano), + Sealed: false, + PublicKeySignature: "", + }, + crypto: e.crypto, + seal: e.seal, + payload: payload, + } + + // Apply the options + for _, opt := range opts { + if err = opt(env); err != nil { + return nil, err + } + } + return env, nil +} + // Encrypt the envelope by marshaling the payload, encrypting, and digitally signing it. // If the original envelope does not have a crypto method (either by the user supplying // one via options or from an incoming secure envelope) then a new AESGCM crypto is @@ -323,7 +374,7 @@ func (e *Envelope) encrypt(payload *api.Payload) (_ *api.Error, err error) { } var cleartext []byte - if cleartext, err = proto.Marshal(e.payload); err != nil { + if cleartext, err = proto.Marshal(payload); err != nil { return nil, fmt.Errorf("could not marshal payload: %s", err) } diff --git a/pkg/trust/errors.go b/pkg/trust/errors.go index e692a64..86ee990 100644 --- a/pkg/trust/errors.go +++ b/pkg/trust/errors.go @@ -5,6 +5,7 @@ import "errors" // Standard errors for error type checking var ( ErrDecodePrivateKey = errors.New("could not decode PEM private key") + ErrDecodePublicKey = errors.New("could not decode PEM public key") ErrDecodeCertificate = errors.New("could not decode PEM certificate") ErrDecodeCSR = errors.New("could not decode PEM certificate request") ErrNoCertificates = errors.New("provider does not contain any certificates") diff --git a/pkg/trust/pem.go b/pkg/trust/pem.go index 0df66fa..fc5cce4 100644 --- a/pkg/trust/pem.go +++ b/pkg/trust/pem.go @@ -6,6 +6,17 @@ import ( "encoding/pem" ) +// PEM Block types +const ( + BlockPublicKey = "PUBLIC KEY" + BlockPrivateKey = "PRIVATE KEY" + BlockRSAPublicKey = "RSA PUBLIC KEY" + BlockRSAPrivateKey = "RSA PRIVATE KEY" + BlockECPrivateKey = "EC PRIVATE KEY" + BlockCertificate = "CERTIFICATE" + BlockCertificateRequest = "CERTIFICATE REQUEST" +) + // PEMEncodePrivateKey as a PKCS8 ASN.1 DER key and write a PEM block with type "PRIVATE KEY" func PEMEncodePrivateKey(key interface{}) ([]byte, error) { pkcs8, err := x509.MarshalPKCS8PrivateKey(key) @@ -26,16 +37,17 @@ func PEMEncodePrivateKey(key interface{}) ([]byte, error) { // is "RSA PRIVATE KEY" then it is decoded as a PKCS 1, ASN.1 DER form. If the block // type is "PRIVATE KEY", the block is decoded as a PKCS 8 ASN.1 DER key, if that fails, // then the PKCS 1 and EC parsers are tried in that order, before returning an error. -func PEMDecodePrivateKey(in []byte) (key interface{}, err error) { +func PEMDecodePrivateKey(in []byte) (interface{}, error) { block, _ := pem.Decode(in) if block == nil { return nil, ErrDecodePrivateKey } - - return parsePrivateKey(block) + return ParsePrivateKey(block) } -func parsePrivateKey(block *pem.Block) (key interface{}, err error) { +// ParsePrivateKey from PEM block. May return an *ecdsa.PrivateKey, *rsa.PrivateKey, or +// ed25519.PrivateKey depending on the block type and the x509 parsing method. +func ParsePrivateKey(block *pem.Block) (interface{}, error) { // EC PRIVATE KEY specific handling if block.Type == BlockECPrivateKey { return x509.ParseECPrivateKey(block.Bytes) @@ -52,15 +64,15 @@ func parsePrivateKey(block *pem.Block) (key interface{}, err error) { } // Try parsing private key using PKCS8, PKCS1, then EC - if key, err = x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { + if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { return key, nil } - if key, err = x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { + if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { return key, nil } - if key, err = x509.ParseECPrivateKey(block.Bytes); err == nil { + if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil { return key, nil } @@ -68,13 +80,46 @@ func parsePrivateKey(block *pem.Block) (key interface{}, err error) { return nil, ErrDecodePrivateKey } +// PEMEncodePublicKey as a PKIX ASN1.1 DER key and write a PEM block with type "PUBLIC KEY" +func PEMEncodePublicKey(key interface{}) ([]byte, error) { + pkix, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + return nil, err + } + + var b bytes.Buffer + if err := pem.Encode(&b, &pem.Block{Type: BlockPublicKey, Bytes: pkix}); err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +// PEMDecodePublicKey from a PEM encoded block. If the block type is "RSA PUBLIC KEY", +// then it is deocded as a PKCS 1, ASN.1 DER form. If the block is "PUBLIC KEY", then it +// is decoded from PKIX ASN1.1 DER form. +func PEMDecodePublicKey(in []byte) (interface{}, error) { + block, _ := pem.Decode(in) + if block == nil { + return nil, ErrDecodePublicKey + } + + if block.Type == BlockRSAPublicKey { + return x509.ParsePKCS1PublicKey(block.Bytes) + } + + if block.Type != BlockPublicKey { + return nil, ErrDecodePublicKey + } + return x509.ParsePKIXPublicKey(block.Bytes) +} + // PEMEncodeCertificate and write a PEM block with type "CERTIFICATE" func PEMEncodeCertificate(c *x509.Certificate) ([]byte, error) { var b bytes.Buffer if err := pem.Encode(&b, &pem.Block{Type: BlockCertificate, Bytes: c.Raw}); err != nil { return nil, err } - return b.Bytes(), nil } diff --git a/pkg/trust/trust.go b/pkg/trust/trust.go index 4a020e5..1067e25 100644 --- a/pkg/trust/trust.go +++ b/pkg/trust/trust.go @@ -19,17 +19,6 @@ import ( "software.sslmate.com/src/go-pkcs12" ) -// PEM Block types -const ( - BlockPublickKey = "PUBLIC KEY" - BlockPrivateKey = "PRIVATE KEY" - BlockRSAPublicKey = "RSA PUBLIC KEY" - BlockRSAPrivateKey = "RSA PRIVATE KEY" - BlockECPrivateKey = "EC PRIVATE KEY" - BlockCertificate = "CERTIFICATE" - BlockCertificateRequest = "CERTIFICATE REQUEST" -) - // Provider wraps a PEM-encoded certificate chain, which can optionally include private // keys. Providers with keys (private providers) are used to instantiate mTLS servers, // while public Providers are used in ProviderPools to facilitate mTLS clients. @@ -120,7 +109,7 @@ func (p *Provider) Decode(in []byte) (err error) { case BlockCertificate: p.chain.Certificate = append(p.chain.Certificate, block.Bytes) case BlockPrivateKey, BlockECPrivateKey, BlockRSAPrivateKey: - if p.key, err = parsePrivateKey(block); err != nil { + if p.key, err = ParsePrivateKey(block); err != nil { return err } default: diff --git a/pkg/trust/zip.go b/pkg/trust/zip.go index a93fec9..0b5513c 100644 --- a/pkg/trust/zip.go +++ b/pkg/trust/zip.go @@ -29,6 +29,12 @@ var validFormats = map[string]struct{}{ CompressionAuto: {}, } +var extensionAliases = map[string]string{ + ".crt": CompressionNone, + ".p12": CompressionNone, + ".gzip": CompressionGZIP, +} + // Serializer maintains options for compression, encoding, and pkcs12 encryption when // serializing and deserializing Provider and ProviderPool objects to and from disk. type Serializer struct { @@ -418,6 +424,11 @@ func (s *Serializer) getFormat() (string, error) { ext = filepath.Ext(s.path) } + // Handle extension aliases + if alias, ok := extensionAliases[ext]; ok { + ext = alias + } + if s.Format == CompressionAuto { if ext != "" { if _, ok := validFormats[ext]; !ok { diff --git a/pkg/version.go b/pkg/version.go index d344a5d..ca3a911 100644 --- a/pkg/version.go +++ b/pkg/version.go @@ -9,9 +9,9 @@ import "fmt" const ( VersionMajor = 0 VersionMinor = 3 - VersionPatch = 1 + VersionPatch = 2 VersionReleaseLevel = "v1beta1" - VersionReleaseNumber = 5 + VersionReleaseNumber = 6 ) // Version returns the semantic version for the current build. @@ -25,9 +25,9 @@ func Version() string { if VersionReleaseLevel != "" { if VersionReleaseNumber > 0 { - return fmt.Sprintf("%s-%s.%d", versionCore, VersionReleaseLevel, VersionReleaseNumber) + return fmt.Sprintf("%s (%s revision %d)", versionCore, VersionReleaseLevel, VersionReleaseNumber) } - return fmt.Sprintf("%s-%s", versionCore, VersionReleaseLevel) + return fmt.Sprintf("%s (%s)", versionCore, VersionReleaseLevel) } return versionCore }