diff --git a/README.md b/README.md index 2923af6..3b5e400 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This is an implementation of the standard Golang crypto interfaces that uses [PKCS#11](http://docs.oasis-open.org/pkcs11/pkcs11-base/v2.40/errata01/os/pkcs11-base-v2.40-errata01-os-complete.html) as a backend. The supported features are: * Generation and retrieval of RSA, DSA and ECDSA keys. +* Importing and retrieval of x509 certificates * PKCS#1 v1.5 signing. * PKCS#1 PSS signing. * PKCS#1 v1.5 decryption diff --git a/certificates.go b/certificates.go new file mode 100644 index 0000000..e703847 --- /dev/null +++ b/certificates.go @@ -0,0 +1,172 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package crypto11 + +import ( + "crypto/x509" + "encoding/asn1" + "math/big" + + "github.com/miekg/pkcs11" + "github.com/pkg/errors" +) + +// FindCertificate retrieves a previously imported certificate. Any combination of id, label +// and serial can be provided. An error is return if all are nil. +func (c *Context) FindCertificate(id []byte, label []byte, serial *big.Int) (*x509.Certificate, error) { + + if c.closed.Get() { + return nil, errClosed + } + + var cert *x509.Certificate + err := c.withSession(func(session *pkcs11Session) (err error) { + if id == nil && label == nil && serial == nil { + return errors.New("id, label and serial cannot all be nil") + } + + var template []*pkcs11.Attribute + + if id != nil { + template = append(template, pkcs11.NewAttribute(pkcs11.CKA_ID, id)) + } + if label != nil { + template = append(template, pkcs11.NewAttribute(pkcs11.CKA_LABEL, label)) + } + if serial != nil { + derSerial, err := asn1.Marshal(serial) + if err != nil { + return errors.WithMessage(err, "failed to encode serial") + } + + template = append(template, pkcs11.NewAttribute(pkcs11.CKA_SERIAL_NUMBER, derSerial)) + } + + template = append(template, pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE)) + + if err = session.ctx.FindObjectsInit(session.handle, template); err != nil { + return err + } + defer func() { + finalErr := session.ctx.FindObjectsFinal(session.handle) + if err == nil { + err = finalErr + } + }() + + handles, _, err := session.ctx.FindObjects(session.handle, 1) + if err != nil { + return err + } + if len(handles) == 0 { + return nil + } + + attributes := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_VALUE, 0), + } + + if attributes, err = session.ctx.GetAttributeValue(session.handle, handles[0], attributes); err != nil { + return err + } + + cert, err = x509.ParseCertificate(attributes[0].Value) + return err + }) + + return cert, err +} + +// ImportCertificate imports a certificate onto the token. The id parameter is used to +// set CKA_ID and must be non-nil. +func (c *Context) ImportCertificate(id []byte, certificate *x509.Certificate) error { + if c.closed.Get() { + return errClosed + } + + if err := notNilBytes(id, "id"); err != nil { + return err + } + + template, err := NewAttributeSetWithID(id) + if err != nil { + return err + } + return c.ImportCertificateWithAttributes(template, certificate) +} + +// ImportCertificateWithLabel imports a certificate onto the token. The id and label parameters are used to +// set CKA_ID and CKA_LABEL respectively and must be non-nil. +func (c *Context) ImportCertificateWithLabel(id []byte, label []byte, certificate *x509.Certificate) error { + if c.closed.Get() { + return errClosed + } + + if err := notNilBytes(id, "id"); err != nil { + return err + } + if err := notNilBytes(label, "label"); err != nil { + return err + } + + template, err := NewAttributeSetWithIDAndLabel(id, label) + if err != nil { + return err + } + return c.ImportCertificateWithAttributes(template, certificate) +} + +// ImportCertificateWithAttributes imports a certificate onto the token. After this function returns, template +// will contain the attributes applied to the certificate. If required attributes are missing, they will be set to a +// default value. +func (c *Context) ImportCertificateWithAttributes(template AttributeSet, certificate *x509.Certificate) error { + if c.closed.Get() { + return errClosed + } + + if certificate == nil { + return errors.New("certificate cannot be nil") + } + + serial, err := asn1.Marshal(certificate.SerialNumber) + if err != nil { + return err + } + + template.AddIfNotPresent([]*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), + pkcs11.NewAttribute(pkcs11.CKA_CERTIFICATE_TYPE, pkcs11.CKC_X_509), + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), + pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, false), + pkcs11.NewAttribute(pkcs11.CKA_SUBJECT, certificate.RawSubject), + pkcs11.NewAttribute(pkcs11.CKA_ISSUER, certificate.RawIssuer), + pkcs11.NewAttribute(pkcs11.CKA_SERIAL_NUMBER, serial), + pkcs11.NewAttribute(pkcs11.CKA_VALUE, certificate.Raw), + }) + + err = c.withSession(func(session *pkcs11Session) error { + _, err = session.ctx.CreateObject(session.handle, template.ToSlice()) + return err + }) + + return err +} diff --git a/certificates_test.go b/certificates_test.go new file mode 100644 index 0000000..49840de --- /dev/null +++ b/certificates_test.go @@ -0,0 +1,154 @@ +// Copyright 2018 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package crypto11 + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCertificate(t *testing.T) { + ctx, err := ConfigureFromFile("config") + require.NoError(t, err) + + defer func() { + require.NoError(t, ctx.Close()) + }() + + id := randomBytes() + label := randomBytes() + + cert := generateRandomCert(t) + + err = ctx.ImportCertificateWithLabel(id, label, cert) + require.NoError(t, err) + + cert2, err := ctx.FindCertificate(nil, label, nil) + require.NoError(t, err) + require.NotNil(t, cert2) + + assert.Equal(t, cert.Signature, cert2.Signature) + + cert2, err = ctx.FindCertificate(nil, []byte("test2"), nil) + require.NoError(t, err) + assert.Nil(t, cert2) + + cert2, err = ctx.FindCertificate(nil, nil, cert.SerialNumber) + require.NoError(t, err) + require.NotNil(t, cert2) + + assert.Equal(t, cert.Signature, cert2.Signature) +} + +// Test that provided attributes override default values +func TestCertificateAttributes(t *testing.T) { + ctx, err := ConfigureFromFile("config") + require.NoError(t, err) + + defer func() { + require.NoError(t, ctx.Close()) + }() + + cert := generateRandomCert(t) + + // We import this with a different serial number, to test this is obeyed + ourSerial := new(big.Int) + ourSerial.Add(cert.SerialNumber, big.NewInt(1)) + + derSerial, err := asn1.Marshal(ourSerial) + require.NoError(t, err) + + template := NewAttributeSet() + err = template.Set(CkaSerialNumber, derSerial) + require.NoError(t, err) + + err = ctx.ImportCertificateWithAttributes(template, cert) + require.NoError(t, err) + + // Try to find with old serial + c, err := ctx.FindCertificate(nil, nil, cert.SerialNumber) + assert.Nil(t, c) + + // Find with new serial + c, err = ctx.FindCertificate(nil, nil, ourSerial) + assert.NotNil(t, c) +} + +func TestCertificateRequiredArgs(t *testing.T) { + ctx, err := ConfigureFromFile("config") + require.NoError(t, err) + + defer func() { + require.NoError(t, ctx.Close()) + }() + + cert := generateRandomCert(t) + + val := randomBytes() + + err = ctx.ImportCertificateWithLabel(nil, val, cert) + require.Error(t, err) + + err = ctx.ImportCertificateWithLabel(val, nil, cert) + require.Error(t, err) + + err = ctx.ImportCertificateWithLabel(val, val, nil) + require.Error(t, err) +} + +func generateRandomCert(t *testing.T) *x509.Certificate { + serial, err := rand.Int(rand.Reader, big.NewInt(20000)) + require.NoError(t, err) + + ca := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "Foo", + }, + SerialNumber: serial, + NotAfter: time.Now().Add(365 * 24 * time.Hour), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + key, err := rsa.GenerateKey(rand.Reader, 4096) + require.NoError(t, err) + + csr := &key.PublicKey + certBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, csr, key) + require.NoError(t, err) + + cert, err := x509.ParseCertificate(certBytes) + require.NoError(t, err) + + return cert +} diff --git a/close_test.go b/close_test.go index e5d88c5..afdb477 100644 --- a/close_test.go +++ b/close_test.go @@ -29,6 +29,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) @@ -79,35 +81,46 @@ func TestErrorAfterClosed(t *testing.T) { bytes := randomBytes() _, err = ctx.FindKey(bytes, nil) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.FindKeyPair(bytes, nil) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.GenerateSecretKey(bytes, 256, CipherAES) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.GenerateSecretKeyWithLabel(bytes, bytes, 256, CipherAES) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.GenerateRSAKeyPair(bytes, 2048) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.GenerateRSAKeyPairWithLabel(bytes, bytes, 2048) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.GenerateDSAKeyPair(bytes, dsaSizes[dsa.L1024N160]) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.GenerateDSAKeyPairWithLabel(bytes, bytes, dsaSizes[dsa.L1024N160]) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.GenerateECDSAKeyPair(bytes, elliptic.P224()) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.GenerateECDSAKeyPairWithLabel(bytes, bytes, elliptic.P224()) - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) _, err = ctx.NewRandomReader() - require.Equal(t, errClosed, err) + assert.Equal(t, errClosed, err) + + cert := generateRandomCert(t) + + err = ctx.ImportCertificate(bytes, cert) + assert.Equal(t, errClosed, err) + + err = ctx.ImportCertificateWithLabel(bytes, bytes, cert) + assert.Equal(t, errClosed, err) + + err = ctx.ImportCertificateWithAttributes(NewAttributeSet(), cert) + assert.Equal(t, errClosed, err) } diff --git a/sessions.go b/sessions.go index 26e5a53..ed803f2 100644 --- a/sessions.go +++ b/sessions.go @@ -23,6 +23,7 @@ package crypto11 import ( "context" + "errors" "github.com/miekg/pkcs11" "github.com/vitessio/vitess/go/pools" @@ -65,8 +66,10 @@ func (c *Context) getSession() (*pkcs11Session, error) { resource, err := c.pool.Get(ctx) if err == pools.ErrClosed { - // Our Context must have been closed, return a nicer error - return nil, errClosed + // Our Context must have been closed, return a nicer error. + // We don't use errClosed to ensure our tests identify functions that aren't checking for closure + // correctly. + return nil, errors.New("context is closed") } if err != nil { return nil, err