diff --git a/Dockerfile b/Dockerfile index d95c4bf..aabad8e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,19 +18,23 @@ RUN apt update && apt clean && \ exit 1; \ fi -# Deploy Image -FROM alpine:3.17 +# Deploy Image using Alpine Linux +FROM alpine:3.19 -# Non-Root User -RUN adduser --home /home/baseca baseca --gecos "baseca" --disabled-password && \ - apk --no-cache add ca-certificates && \ +# Add a Non-Root User +RUN addgroup -S baseca && adduser -S baseca -G baseca && \ + mkdir -p /home/baseca/config && \ + chown -R baseca:baseca /home/baseca + +# Install Required Dependencies +RUN apk --no-cache add ca-certificates && \ rm -rf /var/cache/apk/* # Copy Binary and Configuration from Build Image COPY --from=builder /baseca/target/bin/linux/baseca /home/baseca/baseca COPY --from=builder /baseca/config /home/baseca/config -# Permissions for Non-Root User +# Set permissions for copied files RUN chown -R baseca:baseca /home/baseca # Switch to Non-Root User @@ -38,4 +42,4 @@ USER baseca WORKDIR /home/baseca # Execute coinbase/baseca -CMD ["/home/baseca/baseca"] +CMD ["/home/baseca/baseca"] \ No newline at end of file diff --git a/docs/ENDPOINTS.md b/docs/ENDPOINTS.md index 7fb008c..ca52ad0 100644 --- a/docs/ENDPOINTS.md +++ b/docs/ENDPOINTS.md @@ -59,7 +59,7 @@ Sign Certificate Signing Request (CSR) Service Account Authentication **Client Example** -[sign_csr.go](../examples/certificate/baseca.v1.Certificate/sign_csr.go) +[sign_csr.go](../examples/baseca.v1.Certificate/sign_csr.go) **Request** @@ -340,7 +340,7 @@ Manual Sign Certificate Signing Request (CSR) Provisioner Account Authentication **Client Example** -[operations_sign_csr.go](../examples/certificate/baseca.v1.Certificate/operations_sign_csr.go) +[operations_sign_csr.go](../examples/baseca.v1.Certificate/operations_sign_csr.go) **Request** diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 6389cf4..2cc260c 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -320,36 +320,29 @@ import ( "log" baseca "github.com/coinbase/baseca/pkg/client" + "github.com/coinbase/baseca/pkg/types" ) func main() { - configuration := baseca.Configuration{ - URL: "localhost:9090", - Environment: baseca.Env.Local, - } - - authentication := baseca.Authentication{ - ClientId: "CLIENT_ID", - ClientToken: "CLIENT_TOKEN", - } + client, err := baseca.NewClient("localhost:9090", baseca.Attestation.Local, + baseca.WithClientId("CLIENT_ID"), baseca.WithClientToken("CLIENT_TOKEN"), + baseca.WithInsecure()) // Insecure for Local Development - client, err := baseca.LoadDefaultConfiguration(configuration, baseca.Attestation.Local, authentication) if err != nil { - // Handle Error log.Fatal(err) } - metadata := baseca.CertificateRequest{ + metadata := types.CertificateRequest{ CommonName: "development.coinbase.com", SubjectAlternateNames: []string{"development.coinbase.com"}, SigningAlgorithm: x509.SHA512WithRSA, PublicKeyAlgorithm: x509.RSA, KeySize: 4096, - DistinguishedName: baseca.DistinguishedName{ + DistinguishedName: types.DistinguishedName{ Organization: []string{"Coinbase"}, // Additional Fields }, - Output: baseca.Output{ + Output: types.Output{ PrivateKey: "/tmp/private.key", // baseca Generate Private Key Output Location Certificate: "/tmp/certificate.crt", // baseca Signed Leaf Certificate Output Location IntermediateCertificateChain: "/tmp/intermediate_chain.crt", // baseca Signed Certificate Chain Up to Intermediate CA Output Location diff --git a/examples/baseca.v1.Certificate/code_sign.go b/examples/baseca.v1.Certificate/code_sign.go index 7454f73..79fea59 100644 --- a/examples/baseca.v1.Certificate/code_sign.go +++ b/examples/baseca.v1.Certificate/code_sign.go @@ -3,49 +3,47 @@ package examples import ( "crypto/x509" "log" - "os" baseca "github.com/coinbase/baseca/pkg/client" "github.com/coinbase/baseca/pkg/types" ) func CodeSign() { - configuration := baseca.Configuration{ - URL: "localhost:9090", - Environment: baseca.Env.Local, - } - - authentication := baseca.Authentication{ - ClientId: "CLIENT_ID", - ClientToken: "CLIENT_TOKEN", - } - - client, err := baseca.LoadDefaultConfiguration(configuration, baseca.Attestation.Local, authentication) + client, err := baseca.NewClient("localhost:9090", baseca.Attestation.Local, + baseca.WithClientId("CLIENT_ID"), baseca.WithClientToken("CLIENT_TOKEN"), + baseca.WithInsecure()) if err != nil { log.Fatal(err) } - metadata := baseca.CertificateRequest{ - CommonName: "example.coinbase.com", - SubjectAlternateNames: []string{"example.coinbase.com"}, - SigningAlgorithm: x509.ECDSAWithSHA384, - PublicKeyAlgorithm: x509.ECDSA, - KeySize: 256, - DistinguishedName: baseca.DistinguishedName{ - Organization: []string{"Coinbase"}, - // Additional Fields + metadata := types.Signature{ + CertificateRequest: types.CertificateRequest{ + CommonName: "example.coinbase.com", + SubjectAlternateNames: []string{"example.coinbase.com"}, + SigningAlgorithm: x509.ECDSAWithSHA512, + PublicKeyAlgorithm: x509.ECDSA, + KeySize: 256, + Output: types.Output{ + PrivateKey: "/tmp/private.key", + Certificate: "/tmp/certificate.crt", + IntermediateCertificateChain: "/tmp/intermediate_chain.crt", + RootCertificateChain: "/tmp/root_chain.crt", + CertificateSigningRequest: "/tmp/certificate_request.csr", + }, + DistinguishedName: types.DistinguishedName{ + Organization: []string{"Coinbase"}, + }, }, - Output: baseca.Output{ - PrivateKey: "/tmp/private.key", - Certificate: "/tmp/certificate.crt", - IntermediateCertificateChain: "/tmp/intermediate_chain.crt", - RootCertificateChain: "/tmp/root_chain.crt", - CertificateSigningRequest: "/tmp/certificate_request.csr", + SigningAlgorithm: x509.ECDSAWithSHA512, + Data: types.Data{ + Path: types.Path{ + File: "/path/to/artifact", + Buffer: 4096, + }, }, } - data, _ := os.ReadFile("/bin/chmod") - signature, chain, err := client.GenerateSignature(metadata, &data) + signature, chain, err := client.GenerateSignature(metadata) if err != nil { log.Fatal(err) } @@ -57,15 +55,15 @@ func CodeSign() { SigningAlgorithm: x509.ECDSAWithSHA512, Data: types.Data{ Path: types.Path{ - File: "/bin/chmod", + File: "/path/to/artifact", Buffer: 4096, }, }, } tc := types.TrustChain{ - CommonName: "sandbox.coinbase.com", - CertificateAuthorityFiles: []string{"/path/to/intermediate_ca.crt"}, + CommonName: "example.coinbase.com", + CertificateAuthorityFiles: []string{"/path/to/intermetidate.crt"}, } err = baseca.ValidateSignature(tc, manifest) diff --git a/examples/baseca.v1.Certificate/operations_sign_csr.go b/examples/baseca.v1.Certificate/operations_sign_csr.go index 341c361..6ee3a22 100644 --- a/examples/baseca.v1.Certificate/operations_sign_csr.go +++ b/examples/baseca.v1.Certificate/operations_sign_csr.go @@ -6,20 +6,13 @@ import ( apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" baseca "github.com/coinbase/baseca/pkg/client" + "github.com/coinbase/baseca/pkg/types" ) func OperationsSignCSR() { - configuration := baseca.Configuration{ - URL: "localhost:9090", - Environment: baseca.Env.Local, - } - - authentication := baseca.Authentication{ - ClientId: "CLIENT_ID", - ClientToken: "CLIENT_TOKEN", - } - - client, err := baseca.LoadDefaultConfiguration(configuration, baseca.Attestation.Local, authentication) + client, err := baseca.NewClient("localhost:9090", baseca.Attestation.Local, + baseca.WithClientId("CLIENT_ID"), baseca.WithClientToken("CLIENT_TOKEN"), + baseca.WithInsecure()) if err != nil { log.Fatal(err) } @@ -32,17 +25,17 @@ func OperationsSignCSR() { Validity: 30, } - certificateRequest := baseca.CertificateRequest{ + certificateRequest := types.CertificateRequest{ CommonName: "example.coinbase.com", SubjectAlternateNames: []string{"example.coinbase.com"}, SigningAlgorithm: x509.SHA384WithRSA, PublicKeyAlgorithm: x509.RSA, KeySize: 4096, - DistinguishedName: baseca.DistinguishedName{ + DistinguishedName: types.DistinguishedName{ Organization: []string{"Coinbase"}, // Additional Fields }, - Output: baseca.Output{ + Output: types.Output{ PrivateKey: "/tmp/sandbox.key", CertificateSigningRequest: "/tmp/sandbox.csr", Certificate: "/tmp/sandbox.crt", diff --git a/examples/baseca.v1.Certificate/sign_csr.go b/examples/baseca.v1.Certificate/sign_csr.go index a8d9459..409d963 100644 --- a/examples/baseca.v1.Certificate/sign_csr.go +++ b/examples/baseca.v1.Certificate/sign_csr.go @@ -5,35 +5,32 @@ import ( "log" baseca "github.com/coinbase/baseca/pkg/client" + "github.com/coinbase/baseca/pkg/types" ) func SignCSR() { - configuration := baseca.Configuration{ - URL: "localhost:9090", - Environment: baseca.Env.Local, - } - - authentication := baseca.Authentication{ - ClientId: "CLIENT_ID", - ClientToken: "CLIENT_TOKEN", - } - - client, err := baseca.LoadDefaultConfiguration(configuration, baseca.Attestation.Local, authentication) + client, err := baseca.NewClient("localhost:9090", baseca.Attestation.Local, + baseca.WithClientId("CLIENT_ID"), baseca.WithClientToken("CLIENT_TOKEN"), + baseca.WithInsecure()) if err != nil { log.Fatal(err) } - metadata := baseca.CertificateRequest{ + metadata := types.CertificateRequest{ CommonName: "example.coinbase.com", SubjectAlternateNames: []string{"example.coinbase.com"}, SigningAlgorithm: x509.ECDSAWithSHA384, PublicKeyAlgorithm: x509.ECDSA, KeySize: 256, - DistinguishedName: baseca.DistinguishedName{ - Organization: []string{"Coinbase"}, + DistinguishedName: types.DistinguishedName{ + Organization: []string{"Coinbase"}, + Locality: []string{"San Francisco"}, + Province: []string{"California"}, + Country: []string{"US"}, + OrganizationalUnit: []string{"Security"}, // Additional Fields }, - Output: baseca.Output{ + Output: types.Output{ PrivateKey: "/tmp/private.key", Certificate: "/tmp/certificate.crt", IntermediateCertificateChain: "/tmp/intermediate_chain.crt", diff --git a/go.mod b/go.mod index ec283b3..47df77d 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( go.uber.org/fx v1.20.0 go.uber.org/mock v0.3.0 go.uber.org/zap v1.25.0 - golang.org/x/crypto v0.14.0 + golang.org/x/crypto v0.19.0 golang.org/x/net v0.17.0 google.golang.org/grpc v1.57.1 google.golang.org/protobuf v1.31.0 @@ -75,8 +75,8 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9f28e3d..90a6abf 100644 --- a/go.sum +++ b/go.sum @@ -328,8 +328,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -457,8 +457,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -468,8 +468,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/v1/certificate/operations_test.go b/internal/v1/certificate/operations_test.go index 86153ce..d0c50bc 100644 --- a/internal/v1/certificate/operations_test.go +++ b/internal/v1/certificate/operations_test.go @@ -12,6 +12,7 @@ import ( db "github.com/coinbase/baseca/db/sqlc" apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" c "github.com/coinbase/baseca/pkg/client" + "github.com/coinbase/baseca/pkg/types" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) @@ -80,7 +81,7 @@ func TestOperationsSignCSR(t *testing.T) { { name: "OK_NO_CERTIFICATE_AUTHORITY_INPUT", req: func() *apiv1.OperationsSignRequest { - req := c.CertificateRequest{ + req := types.CertificateRequest{ CommonName: "development.example.com", SubjectAlternateNames: []string{"development.example.com"}, SigningAlgorithm: x509.SHA512WithRSA, @@ -138,7 +139,7 @@ func TestOperationsSignCSR(t *testing.T) { { name: "OK_CERTIFICATE_AUTHORITY_INPUT", req: func() *apiv1.OperationsSignRequest { - req := c.CertificateRequest{ + req := types.CertificateRequest{ CommonName: "development.example.com", SubjectAlternateNames: []string{"development.example.com"}, SigningAlgorithm: x509.SHA512WithRSA, diff --git a/internal/v1/certificate/sign.go b/internal/v1/certificate/sign.go index 723bffe..ecdfbd0 100644 --- a/internal/v1/certificate/sign.go +++ b/internal/v1/certificate/sign.go @@ -80,7 +80,7 @@ func (c *Certificate) SignCSR(ctx context.Context, req *apiv1.CertificateSigning func (c *Certificate) requestCertificate(ctx context.Context, authPayload *types.ServiceAccountPayload, certificateRequest *x509.CertificateRequest) (*types.CertificateResponseData, error) { var subordinate *types.CertificateAuthority - var parameters baseca.CertificateRequest + var parameters lib.CertificateRequest var csr *bytes.Buffer var err error @@ -108,13 +108,13 @@ func (c *Certificate) requestCertificate(ctx context.Context, authPayload *types return nil, fmt.Errorf("invalid signing algorithm: %s", c.ca.SigningAlgorithm) } - parameters = baseca.CertificateRequest{ + parameters = lib.CertificateRequest{ CommonName: intermediateCa, SubjectAlternateNames: []string{intermediateCa}, SigningAlgorithm: signingAlgorithm.Common, PublicKeyAlgorithm: lib.PublicKeyAlgorithmStrings[c.ca.KeyAlgorithm].Algorithm, KeySize: c.ca.KeySize, - DistinguishedName: baseca.DistinguishedName{ + DistinguishedName: lib.DistinguishedName{ Country: []string{c.ca.Country}, Province: []string{c.ca.Province}, Locality: []string{c.ca.Locality}, diff --git a/internal/v1/certificate/sign_test.go b/internal/v1/certificate/sign_test.go index e2dd171..579088e 100644 --- a/internal/v1/certificate/sign_test.go +++ b/internal/v1/certificate/sign_test.go @@ -9,6 +9,7 @@ import ( apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" "github.com/coinbase/baseca/internal/types" baseca "github.com/coinbase/baseca/pkg/client" + lib "github.com/coinbase/baseca/pkg/types" "github.com/google/uuid" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -24,7 +25,7 @@ func TestSignCSR(t *testing.T) { { name: "OK", req: func() *apiv1.CertificateSigningRequest { - req := baseca.CertificateRequest{ + req := lib.CertificateRequest{ CommonName: "example.com", SubjectAlternateNames: []string{"example.com"}, SigningAlgorithm: x509.SHA512WithRSA, diff --git a/pkg/client/certificate.go b/pkg/client/certificate.go index 5a630e7..70e885f 100644 --- a/pkg/client/certificate.go +++ b/pkg/client/certificate.go @@ -9,7 +9,7 @@ import ( "github.com/coinbase/baseca/pkg/types" ) -func (c *Client) IssueCertificate(certificateRequest CertificateRequest) (*apiv1.SignedCertificate, error) { +func (c *Client) IssueCertificate(certificateRequest types.CertificateRequest) (*apiv1.SignedCertificate, error) { signingRequest, err := GenerateCSR(certificateRequest) if err != nil { return nil, err diff --git a/pkg/client/client.go b/pkg/client/client.go index 0b77814..9b84cfa 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -29,10 +29,14 @@ type Client struct { Authentication Authentication Attestation string Certificate apiv1.CertificateClient + Account AccountClient Service apiv1.ServiceClient + Insecure bool *iidCache } +type ClientOptions func(*Client) error + type AccountClient interface { LoginUser(ctx context.Context, in *apiv1.LoginUserRequest, opts ...grpc.CallOption) (*apiv1.LoginUserResponse, error) DeleteUser(ctx context.Context, in *apiv1.UsernameRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) @@ -66,32 +70,44 @@ type ServiceClient interface { DeleteProvisionedServiceAccount(ctx context.Context, in *apiv1.AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) } -func LoadDefaultConfiguration(configuration Configuration, attestation string, authentication Authentication) (*Client, error) { +func NewClient(endpoint string, attestation string, opts ...ClientOptions) (*Client, error) { + var creds credentials.TransportCredentials + c := Client{ - Endpoint: configuration.URL, - Authentication: authentication, + Endpoint: endpoint, Attestation: attestation, + Authentication: Authentication{}, + Insecure: false, } - if configuration.Environment == Env.Local { - conn, err := grpc.Dial(configuration.URL, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(c.methodInterceptor())) + if len(opts) > 0 { + err := c.ApplyOptions(opts...) if err != nil { - return nil, fmt.Errorf("failed to initialize grpc client") + return nil, err } - c.Certificate = apiv1.NewCertificateClient(conn) - return &c, nil + } + + if c.Insecure { + creds = insecure.NewCredentials() } else { - conn, err := grpc.Dial(configuration.URL, grpc.WithTransportCredentials( - credentials.NewTLS(&tls.Config{ - MinVersion: tls.VersionTLS12, - }), - ), grpc.WithUnaryInterceptor(c.methodInterceptor())) - if err != nil { - return nil, fmt.Errorf("failed to initialize grpc client") - } - c.Certificate = apiv1.NewCertificateClient(conn) - return &c, nil + creds = credentials.NewTLS(&tls.Config{ + MinVersion: tls.VersionTLS12, + }) } + + conn, err := grpc.Dial( + endpoint, grpc.WithTransportCredentials(creds), + grpc.WithUnaryInterceptor(c.methodInterceptor()), + ) + if err != nil { + return nil, fmt.Errorf("failed to initialize grpc client %w", err) + } + + c.Certificate = apiv1.NewCertificateClient(conn) + c.Service = apiv1.NewServiceClient(conn) + c.Account = apiv1.NewAccountClient(conn) + + return &c, nil } func (c *Client) methodInterceptor() grpc.UnaryClientInterceptor { @@ -108,7 +124,6 @@ func (c *Client) methodInterceptor() grpc.UnaryClientInterceptor { // Account Interface "/baseca.v1.Account/LoginUser": c.accountAuthUnaryInterceptor, - // TODO: Add Additional RPC Methods } return mapMethodInterceptor(methodOptions) } @@ -145,7 +160,7 @@ func (c *Client) clientAuthUnaryInterceptor(ctx context.Context, method string, ctx = metadata.AppendToOutgoingContext(ctx, _client_token_header, c.Authentication.ClientToken) if c.Attestation == Attestation.AWS { - instance_metadata, err := c.iidCache.Get() + instance_metadata, err := globalIIDCache.Get() if err != nil { return fmt.Errorf("error generating aws_iid node attestation: %w", err) } @@ -163,6 +178,11 @@ func (c *Client) accountAuthUnaryInterceptor(ctx context.Context, method string, return err } +// We cache this globally in case multiple clients are instantiated: they +// share the cache. They will all have the same IID based on the EC2 instance +// they're running on. +var globalIIDCache = &iidCache{} + func (cache *iidCache) Get() (string, error) { cache.lock.Lock() defer cache.lock.Unlock() @@ -186,3 +206,33 @@ func (cache *iidCache) Get() (string, error) { cache.expiration = time.Now().Add(iidCacheExpiration) return cache.value, nil } + +func (c *Client) ApplyOptions(options ...ClientOptions) error { + for _, option := range options { + if err := option(c); err != nil { + return err + } + } + return nil +} + +func WithClientId(clientId string) ClientOptions { + return func(c *Client) error { + c.Authentication.ClientId = clientId + return nil + } +} + +func WithClientToken(clientToken string) ClientOptions { + return func(c *Client) error { + c.Authentication.ClientToken = clientToken + return nil + } +} + +func WithInsecure() ClientOptions { + return func(c *Client) error { + c.Insecure = true + return nil + } +} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go new file mode 100644 index 0000000..e575cbc --- /dev/null +++ b/pkg/client/client_test.go @@ -0,0 +1,35 @@ +package baseca + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClientConfiguration(t *testing.T) { + tests := []struct { + name string + endpoint string + attestation string + options []ClientOptions + check func(t *testing.T, c *Client, err error) + }{ + { + name: "OK_Local_Configuration_Client_Id_Client_Token", + endpoint: "localhost:9090", + attestation: Attestation.Local, + options: []ClientOptions{WithClientId("client_id"), WithClientToken("client_token")}, + check: func(t *testing.T, c *Client, err error) { + assert.NoError(t, err) + assert.Equal(t, "client_id", c.Authentication.ClientId) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + c, err := NewClient(tc.endpoint, tc.attestation, tc.options...) + tc.check(t, c, err) + }) + } +} diff --git a/pkg/client/csr.go b/pkg/client/csr.go index 48e5b0a..8c504e8 100644 --- a/pkg/client/csr.go +++ b/pkg/client/csr.go @@ -13,7 +13,7 @@ import ( "github.com/coinbase/baseca/pkg/types" ) -func GenerateCSR(csr CertificateRequest) (*types.SigningRequest, error) { +func GenerateCSR(csr types.CertificateRequest) (*types.SigningRequest, error) { var generator crypto.CSRGenerator switch csr.PublicKeyAlgorithm { diff --git a/pkg/client/csr_test.go b/pkg/client/csr_test.go index 34bfdf6..b09d595 100644 --- a/pkg/client/csr_test.go +++ b/pkg/client/csr_test.go @@ -10,17 +10,17 @@ import ( ) func TestGenerateCSR(t *testing.T) { - csr := CertificateRequest{ + csr := types.CertificateRequest{ PublicKeyAlgorithm: x509.RSA, KeySize: 2048, SigningAlgorithm: x509.SHA256WithRSA, CommonName: "example.com", - DistinguishedName: DistinguishedName{ + DistinguishedName: types.DistinguishedName{ Country: []string{"US"}, Province: []string{"CA"}, }, SubjectAlternateNames: []string{"www.example.com", "subordinate.example.com"}, - Output: Output{ + Output: types.Output{ CertificateSigningRequest: "/tmp/csr.pem", PrivateKey: "/tmp/pk.pem", }, @@ -35,17 +35,17 @@ func TestGenerateCSR(t *testing.T) { assert.Contains(t, string(pem.EncodeToMemory(rsaSigningRequest.PrivateKey)), types.RSA_PRIVATE_KEY.String()) // Create an ECDSA CertificateRequest - ecdsaCsr := CertificateRequest{ + ecdsaCsr := types.CertificateRequest{ PublicKeyAlgorithm: x509.ECDSA, KeySize: 256, SigningAlgorithm: x509.ECDSAWithSHA512, CommonName: "example.com", - DistinguishedName: DistinguishedName{ + DistinguishedName: types.DistinguishedName{ Country: []string{"US"}, Province: []string{"CA"}, }, SubjectAlternateNames: []string{"www.example.com", "subordinate.example.com"}, - Output: Output{ + Output: types.Output{ CertificateSigningRequest: "/tmp/csr.pem", PrivateKey: "/tmp/pk.pem", }, diff --git a/pkg/client/provisioner.go b/pkg/client/provisioner.go index dc12940..e3efe3c 100644 --- a/pkg/client/provisioner.go +++ b/pkg/client/provisioner.go @@ -8,7 +8,7 @@ import ( "github.com/coinbase/baseca/pkg/util" ) -func (c *Client) ProvisionIssueCertificate(certificateRequest CertificateRequest, ca *apiv1.CertificateAuthorityParameter, service, environment, extendedKey string) (*apiv1.SignedCertificate, error) { +func (c *Client) ProvisionIssueCertificate(certificateRequest types.CertificateRequest, ca *apiv1.CertificateAuthorityParameter, service, environment, extendedKey string) (*apiv1.SignedCertificate, error) { signingRequest, err := GenerateCSR(certificateRequest) if err != nil { return nil, err diff --git a/pkg/client/sign.go b/pkg/client/sign.go index 2e873bc..462c3df 100644 --- a/pkg/client/sign.go +++ b/pkg/client/sign.go @@ -8,6 +8,7 @@ import ( "encoding/pem" "errors" "fmt" + "io" "os" "path/filepath" @@ -22,11 +23,12 @@ type Signer interface { Sign(data []byte) ([]byte, error) } -func (c *Client) GenerateSignature(csr CertificateRequest, data *[]byte) (*[]byte, []*x509.Certificate, error) { +func (c *Client) GenerateSignature(s types.Signature) (*[]byte, []*x509.Certificate, error) { var certificatePem []*pem.Block var certificateChain []*x509.Certificate + var artifactHash []byte - signingRequest, err := GenerateCSR(csr) + signingRequest, err := GenerateCSR(s.CertificateRequest) if err != nil { return nil, nil, err } @@ -41,25 +43,46 @@ func (c *Client) GenerateSignature(csr CertificateRequest, data *[]byte) (*[]byt } err = ParseCertificateFormat(signedCertificate, types.SignedCertificate{ - CertificatePath: csr.Output.Certificate, - IntermediateCertificateChainPath: csr.Output.IntermediateCertificateChain, - RootCertificateChainPath: csr.Output.RootCertificateChain, + CertificatePath: s.CertificateRequest.Output.Certificate, + IntermediateCertificateChainPath: s.CertificateRequest.Output.IntermediateCertificateChain, + RootCertificateChainPath: s.CertificateRequest.Output.RootCertificateChain, }) if err != nil { return nil, nil, err } - signer, err := parsePrivateKey(signingRequest.EncodedPKCS8, csr.SigningAlgorithm) + signer, err := parsePrivateKey(signingRequest.EncodedPKCS8, s.SigningAlgorithm) if err != nil { return nil, nil, err } - signature, err := signer.Sign(*data) + // Sign Artifact + switch { + case s.Data.Path != types.Path{}: + artifactHash, err = streamSignature(s) + if err != nil { + return nil, nil, fmt.Errorf("[data.path] %s", err) + } + case s.Data.Reader != types.Reader{}: + artifactHash, err = readerSignature(s) + if err != nil { + return nil, nil, fmt.Errorf("[data.reader] %s", err) + } + case s.Data.Raw != nil: + artifactHash, err = signer.Sign(*s.Data.Raw) + if err != nil { + return nil, nil, fmt.Errorf("[data.raw] %s", err) + } + default: + return nil, nil, errors.New("data not present within manifest") + } + + signature, err := signer.Sign(artifactHash) if err != nil { return nil, nil, fmt.Errorf("error signing data: %w", err) } - fullChain, err := os.ReadFile(filepath.Clean(csr.Output.RootCertificateChain)) + fullChain, err := os.ReadFile(filepath.Clean(s.CertificateRequest.Output.RootCertificateChain)) if err != nil { return nil, nil, fmt.Errorf("error retrieving full chain certificate: %s", err) } @@ -118,3 +141,62 @@ func parsePrivateKey(pk []byte, signatureAlgorithm x509.SignatureAlgorithm) (Sig return nil, errors.New("unsupported private key type") } } + +// Stream Large Files and Sign the Hash by Passing in Filepath +func streamSignature(s types.Signature) ([]byte, error) { + algorithm, exists := types.SignatureAlgorithm[s.SigningAlgorithm] + if !exists { + return nil, fmt.Errorf("invalid signing algorithm: %s", s.SigningAlgorithm) + } + hashedAlgorithm, _ := algorithm() + + file, err := os.Open(s.Data.Path.File) + if err != nil { + return nil, fmt.Errorf("error opening file: %s", err) + } + defer file.Close() + + if s.Data.Reader.Buffer > 0 { + _buffer = s.Data.Reader.Buffer + } + + buffer := make([]byte, _buffer) + for { + n, err := file.Read(buffer) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("error reading file: %s", err) + } + if n == 0 { + break + } + hashedAlgorithm.Write(buffer[:n]) + } + + return hashedAlgorithm.Sum(nil), nil +} + +func readerSignature(s types.Signature) ([]byte, error) { + algorithm, exist := types.SignatureAlgorithm[s.SigningAlgorithm] + if !exist { + return nil, fmt.Errorf("invalid signing algorithm: %s", s.SigningAlgorithm) + } + hashedAlgorithm, _ := algorithm() + + if s.Data.Reader.Buffer > 0 { + _buffer = s.Data.Reader.Buffer + } + + buffer := make([]byte, _buffer) + for { + n, err := s.Data.Reader.Interface.Read(buffer) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("error reading file: %s", err) + } + if n == 0 { + break + } + hashedAlgorithm.Write(buffer[:n]) + } + + return hashedAlgorithm.Sum(nil), nil +} diff --git a/pkg/client/sign_test.go b/pkg/client/sign_test.go index 8b5b968..4ac56d7 100644 --- a/pkg/client/sign_test.go +++ b/pkg/client/sign_test.go @@ -5,6 +5,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "encoding/pem" "log" @@ -74,7 +75,9 @@ func TestSign(t *testing.T) { if err != nil { return nil, err } - return signer.Sign([]byte("_value")) + hasher := sha256.New() + hasher.Write([]byte("_value")) + return signer.Sign(hasher.Sum(nil)) }, check: func(t *testing.T, err error) { require.NoError(t, err) @@ -87,7 +90,9 @@ func TestSign(t *testing.T) { if err != nil { return nil, err } - return signer.Sign([]byte("_value")) + hasher := sha256.New() + hasher.Write([]byte("_value")) + return signer.Sign(hasher.Sum(nil)) }, check: func(t *testing.T, err error) { require.NoError(t, err) diff --git a/pkg/client/types.go b/pkg/client/types.go index 17e699b..a8342c4 100644 --- a/pkg/client/types.go +++ b/pkg/client/types.go @@ -1,7 +1,6 @@ package baseca import ( - "crypto/x509" "sync" "time" ) @@ -11,31 +10,8 @@ var Attestation Provider = Provider{ AWS: "AWS", } -var Env = Environment{ - Local: "Local", - Sandbox: "Sandbox", - Development: "Development", - Staging: "Staging", - PreProduction: "PreProduction", - Production: "Production", -} - var iidCacheExpiration = 10 * time.Minute -type Environment struct { - Local string - Sandbox string - Development string - Staging string - PreProduction string - Production string -} - -type Configuration struct { - URL string - Environment string -} - type Provider struct { Local string AWS string @@ -47,35 +23,6 @@ type Authentication struct { AuthToken string } -type CertificateRequest struct { - CommonName string - SubjectAlternateNames []string - DistinguishedName DistinguishedName - SigningAlgorithm x509.SignatureAlgorithm - PublicKeyAlgorithm x509.PublicKeyAlgorithm - KeySize int - Output Output -} - -type DistinguishedName struct { - Country []string - Province []string - Locality []string - Organization []string - OrganizationalUnit []string - StreetAddress []string - PostalCode []string - SerialNumber string -} - -type Output struct { - CertificateSigningRequest string - Certificate string - IntermediateCertificateChain string - RootCertificateChain string - PrivateKey string -} - type iidCache struct { expiration time.Time lock sync.Mutex diff --git a/pkg/client/validate.go b/pkg/client/validate.go index f9eccfa..89a9229 100644 --- a/pkg/client/validate.go +++ b/pkg/client/validate.go @@ -205,7 +205,7 @@ func generateCertificatePool(tc types.TrustChain) (*x509.CertPool, error) { return nil, fmt.Errorf("invalid certificate authority directory %s", dir) } - for _, certFile := range files { // #nosec G304 User Only Has Predefined Environment Parameters + for _, certFile := range files { data, err := os.ReadFile(filepath.Join(dir, certFile.Name())) if err != nil { return nil, fmt.Errorf("invalid certificate file %s", filepath.Join(dir, certFile.Name())) diff --git a/pkg/client/validate_test.go b/pkg/client/validate_test.go index ba41835..cc41a4c 100644 --- a/pkg/client/validate_test.go +++ b/pkg/client/validate_test.go @@ -3,6 +3,7 @@ package baseca import ( "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/pem" @@ -28,7 +29,10 @@ func TestValidateSignature(t *testing.T) { pk, certificate, path := generateSelfSignedCertificateAuthority() signer, _ := parsePrivateKey(pk, x509.SHA256WithRSA) - signature, err := signer.Sign(data) + hasher := sha256.New() + hasher.Write([]byte(data)) + + signature, err := signer.Sign(hasher.Sum(nil)) if err != nil { return fmt.Errorf("error signing data: %s", err) } diff --git a/pkg/crypto/pk.go b/pkg/crypto/pk.go index 45ab23d..3f3a96e 100644 --- a/pkg/crypto/pk.go +++ b/pkg/crypto/pk.go @@ -25,25 +25,20 @@ type ECDSASigner struct { Hash func() (hash.Hash, crypto.Hash) } -func (r *RSASigner) Sign(data []byte) ([]byte, error) { - hash, cryptoHash := r.Hash() - hash.Write(data) - hashedValue := hash.Sum(nil) +func (r *RSASigner) Sign(hashedData []byte) ([]byte, error) { + _, cryptoHash := r.Hash() switch r.SignatureAlgorithm { case x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS: - return rsa.SignPSS(rand.Reader, r.PrivateKey, cryptoHash, hashedValue, &rsa.PSSOptions{ + return rsa.SignPSS(rand.Reader, r.PrivateKey, cryptoHash, hashedData, &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthAuto, }) default: - return rsa.SignPKCS1v15(rand.Reader, r.PrivateKey, cryptoHash, hashedValue) + return rsa.SignPKCS1v15(rand.Reader, r.PrivateKey, cryptoHash, hashedData) } } -func (e *ECDSASigner) Sign(data []byte) ([]byte, error) { - hash, _ := e.Hash() - hash.Write(data) - hashedValue := hash.Sum(nil) - return ecdsa.SignASN1(rand.Reader, e.PrivateKey, hashedValue) +func (e *ECDSASigner) Sign(hashedData []byte) ([]byte, error) { + return ecdsa.SignASN1(rand.Reader, e.PrivateKey, hashedData) } func EncodeToPKCS8(pkBlock *pem.Block) (*pem.Block, error) { diff --git a/pkg/types/sign.go b/pkg/types/sign.go index 2d0d82b..e4d2eb0 100644 --- a/pkg/types/sign.go +++ b/pkg/types/sign.go @@ -9,6 +9,12 @@ import ( "io" ) +type Signature struct { + CertificateRequest CertificateRequest + SigningAlgorithm x509.SignatureAlgorithm + Data +} + type TrustChain struct { CommonName string CertificateAuthorityDirectory []string @@ -39,6 +45,35 @@ type Manifest struct { Data Data } +type CertificateRequest struct { + CommonName string + SubjectAlternateNames []string + DistinguishedName DistinguishedName + SigningAlgorithm x509.SignatureAlgorithm + PublicKeyAlgorithm x509.PublicKeyAlgorithm + KeySize int + Output Output +} + +type DistinguishedName struct { + Country []string + Province []string + Locality []string + Organization []string + OrganizationalUnit []string + StreetAddress []string + PostalCode []string + SerialNumber string +} + +type Output struct { + CertificateSigningRequest string + Certificate string + IntermediateCertificateChain string + RootCertificateChain string + PrivateKey string +} + var SignatureAlgorithm = map[x509.SignatureAlgorithm]func() (hash.Hash, crypto.Hash){ x509.ECDSAWithSHA256: func() (hash.Hash, crypto.Hash) { return sha256.New(), crypto.SHA256