From ae11483a7b856d94c057dab00754320d00295aa9 Mon Sep 17 00:00:00 2001 From: Diogo Monica Date: Mon, 19 Oct 2015 16:02:50 -0700 Subject: [PATCH 01/85] initial work on pkcs11 support Signed-off-by: David Lawrence Signed-off-by: Diogo Monica (github: endophage) --- signer/api/ecdsa_hardware_crypto_service.go | 127 ++++++++++++++++++++ signer/api/rsa_hardware_crypto_service.go | 8 +- signer/keys/keys.go | 43 +++---- 3 files changed, 153 insertions(+), 25 deletions(-) create mode 100644 signer/api/ecdsa_hardware_crypto_service.go diff --git a/signer/api/ecdsa_hardware_crypto_service.go b/signer/api/ecdsa_hardware_crypto_service.go new file mode 100644 index 0000000000..d52295c8e8 --- /dev/null +++ b/signer/api/ecdsa_hardware_crypto_service.go @@ -0,0 +1,127 @@ +package api + +import ( + "crypto/rand" + "crypto/sha256" + "errors" + "log" + + "github.com/docker/notary/trustmanager" + "github.com/endophage/gotuf/data" + "github.com/miekg/pkcs11" +) + +// YubikeyECDSACryptoService is an implementation of SigningService +type YubikeyECDSACryptoService struct { + context *pkcs11.Ctx + session pkcs11.SessionHandle +} + +// Create creates a key and returns its public components +func (s *YubikeyECDSACryptoService) Create(role string, algo data.KeyAlgorithm) (data.PublicKey, error) { + if role != "root" { + return nil, errors.New("can only generate root keys inside of Yubikey") + } + // TODO(diogo): Generate ECDSA key + // TODO(diogo): Generate Certificate from ECDSA public key + // TODO(diogo): Import Private key and certificate into yubikey + // TODO(diogo): Return TUF Key + return trustmanager.GenerateECDSAKey(rand.Reader) +} + +// RemoveKey removes a key from the key database +func (s *YubikeyECDSACryptoService) RemoveKey(keyID string) error { + return errors.New("cannot delete root key from Yubikey") +} + +// GetKey returns the public components of a particular key +func (s *YubikeyECDSACryptoService) GetKey(keyID string) data.PublicKey { + // TODO(diogo): Get the certificate out of the Yubikey and return the ID from it + class = pkcs11.CKO_PRIVATE_KEY + privateKeyTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, class), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), + pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{2}), + } + + if err := s.context.FindObjectsInit(session, privateKeyTemplate); err != nil { + return nil + } + obj, b, err := s.context.FindObjects(session, 1) + if err != nil { + return nil + } + if err = s.context.FindObjectsFinal(session); e != nil { + return nil + } + if len(obj) != 1 { + return nil + } + + return data.NewPublicKey(data.ECDSAKey, []byte{0}) +} + +// Sign returns a signature for a given signature request +func (s *YubikeyECDSACryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signature, error) { + signatures := make([]data.Signature, 0, len(keyIDs)) + for _, keyid := range keyIDs { + key := s.GetKey(keyID) + if key == nil { + // We skip keys that aren't found + continue + } + + // Define the ECDSA Private key template + class = pkcs11.CKO_PRIVATE_KEY + privateKeyTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, class), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), + pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{2}), + } + + if err := s.context.FindObjectsInit(session, privateKeyTemplate); err != nil { + return nil, err + } + obj, b, err := s.context.FindObjects(session, 1) + if err != nil { + return nil, err + } + if err = s.context.FindObjectsFinal(session); e != nil { + return nil, err + } + if len(obj) != 1 { + return nil, errors.New("length of objects found not 1") + } + + var sig []byte + s.context.SignInit(s.session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_ECDSA, nil)}, obj[0]) + + // Get the SHA256 of the payload + digest := sha256.Sum256(payload) + + sig, err := s.context.Sign(s.session, digest) + if err != nil { + log.Printf("Error while signing: %s", err) + return nil, err + } + + if sig == nil { + return nil, errors.New("Failed to create signature") + } + + signatures = append(signatures, data.Signature{ + KeyID: keyid, + Method: data.ECDSASignature, + Signature: sig[:], + }) + } + return signatures, nil +} + +// NewYubikeyECDSACryptoService returns an instance of RSAHardwareCryptoService +func NewYubikeyECDSACryptoService(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) *YubikeyECDSACryptoService { + return &YubikeyECDSACryptoService{ + context: ctx, + session: session, + } +} diff --git a/signer/api/rsa_hardware_crypto_service.go b/signer/api/rsa_hardware_crypto_service.go index 8e673be341..5ffe8536d5 100644 --- a/signer/api/rsa_hardware_crypto_service.go +++ b/signer/api/rsa_hardware_crypto_service.go @@ -17,7 +17,7 @@ import ( // RSAHardwareCryptoService is an implementation of SigningService type RSAHardwareCryptoService struct { - keys map[string]*keys.HSMRSAKey + keys map[string]*keys.HSMKey context *pkcs11.Ctx session pkcs11.SessionHandle } @@ -68,7 +68,7 @@ func (s *RSAHardwareCryptoService) Create(role, algo string) (data.PublicKey, er pkcs11.NewAttribute(pkcs11.CKA_MODULUS, nil), } - // Retrieve the public-key material to be able to create a new HSMRSAKey + // Retrieve the public-key material to be able to create a new HSMKey attr, err := s.context.GetAttributeValue(s.session, pub, template) if err != nil { return nil, errors.New("Failed to get Attribute value.") @@ -97,7 +97,7 @@ func (s *RSAHardwareCryptoService) Create(role, algo string) (data.PublicKey, er } // (diogo): Ideally I would like to return base64 PEM encoded public keys to the client - k := keys.NewHSMRSAKey(pubBytes, priv) + k := keys.NewHSMKey(pubBytes, priv) keyID := k.ID() @@ -190,7 +190,7 @@ func (s *RSAHardwareCryptoService) Sign(keyIDs []string, payload []byte) ([]data // NewRSAHardwareCryptoService returns an instance of RSAHardwareCryptoService func NewRSAHardwareCryptoService(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) *RSAHardwareCryptoService { return &RSAHardwareCryptoService{ - keys: make(map[string]*keys.HSMRSAKey), + keys: make(map[string]*keys.HSMKey), context: ctx, session: session, } diff --git a/signer/keys/keys.go b/signer/keys/keys.go index 80396c6939..19cb83022e 100644 --- a/signer/keys/keys.go +++ b/signer/keys/keys.go @@ -16,48 +16,49 @@ var ( ErrFailedKeyGeneration = errors.New("notary-signer: failed to generate new key") ) -// HSMRSAKey represents the information for an HSMRSAKey with ObjectHandle for private portion -type HSMRSAKey struct { - id string - public []byte - private pkcs11.ObjectHandle +// HSMKey represents the information for an HSMKey with ObjectHandle for private portion +type HSMKey struct { + id string + algorithm data.KeyAlgorithm + public []byte + private pkcs11.ObjectHandle } -// NewHSMRSAKey returns a HSMRSAKey -func NewHSMRSAKey(public []byte, private pkcs11.ObjectHandle) *HSMRSAKey { - return &HSMRSAKey{ +// NewHSMKey returns a HSMKey +func NewHSMKey(public []byte, private pkcs11.ObjectHandle) *HSMKey { + return &HSMKey{ public: public, private: private, } } // Algorithm implements a method of the data.Key interface -func (rsa *HSMRSAKey) Algorithm() string { - return data.RSAKey +func (k *HSMKey) Algorithm() data.KeyAlgorithm { + return k.algorithm } // ID implements a method of the data.Key interface -func (rsa *HSMRSAKey) ID() string { - if rsa.id == "" { - pubK := data.NewPublicKey(rsa.Algorithm(), rsa.Public()) - rsa.id = pubK.ID() +func (k *HSMKey) ID() string { + if k.id == "" { + pubK := data.NewPublicKey(k.algorithm, k.public) + k.id = pubK.ID() } - return rsa.id + return k.id } // Public implements a method of the data.Key interface -func (rsa *HSMRSAKey) Public() []byte { - return rsa.public +func (k *HSMKey) Public() []byte { + return k.public } // Private implements a method of the data.PrivateKey interface -func (rsa *HSMRSAKey) Private() []byte { +func (k *HSMKey) Private() []byte { // Not possible to return private key bytes from a hardware device return nil } -// PKCS11ObjectHandle returns the PKCS11 object handle stored in the HSMRSAKey +// PKCS11ObjectHandle returns the PKCS11 object handle stored in the HSMKey // structure -func (rsa *HSMRSAKey) PKCS11ObjectHandle() pkcs11.ObjectHandle { - return rsa.private +func (k *HSMKey) PKCS11ObjectHandle() pkcs11.ObjectHandle { + return k.private } From 21138e6badd7639528a0ca20e95e9e11bfe480c1 Mon Sep 17 00:00:00 2001 From: Diogo Monica Date: Wed, 21 Oct 2015 12:21:03 -0700 Subject: [PATCH 02/85] Working version of Notary and Yubikey Signed-off-by: Diogo Monica Remove symlinks from notary-client repo creation Signed-off-by: Ying Li Signed-off-by: Diogo Monica WIP Signed-off-by: Diogo Monica working yubikey integration Signed-off-by: David Lawrence (github: endophage) Fixing small colon bug Signed-off-by: Diogo Monica Added things. Ship it. Signed-off-by: Diogo Monica Bringing ecdsahwcryptosigner to 2015 Signed-off-by: Diogo Monica Working version of notary and yubikey Signed-off-by: Diogo Monica --- client/client.go | 4 +- cmd/notary-signer/main.go | 3 +- cmd/notary/tuf.go | 21 +- cryptoservice/certificate.go | 11 +- cryptoservice/crypto_service.go | 14 +- signer/api/ecdsa_hardware_crypto_service.go | 431 +++++++++++++++----- signer/api/rsa_hardware_crypto_service.go | 8 +- signer/{ => keydbstore}/keydbstore.go | 2 +- signer/{ => keydbstore}/keydbstore_test.go | 2 +- signer/keys/keys.go | 43 +- tuf/data/keys.go | 17 + 11 files changed, 398 insertions(+), 158 deletions(-) rename signer/{ => keydbstore}/keydbstore.go (99%) rename signer/{ => keydbstore}/keydbstore_test.go (99%) diff --git a/client/client.go b/client/client.go index 03b689ca15..36a361d2a6 100644 --- a/client/client.go +++ b/client/client.go @@ -15,6 +15,7 @@ import ( "github.com/docker/notary/cryptoservice" "github.com/docker/notary/keystoremanager" "github.com/docker/notary/pkg/passphrase" + "github.com/docker/notary/signer/api" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf" tufclient "github.com/docker/notary/tuf/client" @@ -108,7 +109,8 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, return nil, err } - cryptoService := cryptoservice.NewCryptoService(gun, keyStoreManager.KeyStore) + yubiKeyStore := api.NewYubiKeyStore() + cryptoService := cryptoservice.NewCryptoService(gun, yubiKeyStore, keyStoreManager.KeyStore) nRepo := &NotaryRepository{ gun: gun, diff --git a/cmd/notary-signer/main.go b/cmd/notary-signer/main.go index 37339329ff..a904ba76ae 100644 --- a/cmd/notary-signer/main.go +++ b/cmd/notary-signer/main.go @@ -22,6 +22,7 @@ import ( "github.com/docker/notary/cryptoservice" "github.com/docker/notary/signer" "github.com/docker/notary/signer/api" + "github.com/docker/notary/signer/keydbstore" "github.com/docker/notary/tuf/data" "github.com/docker/notary/utils" "github.com/docker/notary/version" @@ -155,7 +156,7 @@ func main() { defaultAlias := mainViper.GetString(defaultAliasEnv) logrus.Debug("Default Alias: ", defaultAlias) - keyStore, err := signer.NewKeyDBStore(passphraseRetriever, defaultAlias, configDBType, dbSQL) + keyStore, err := keydbstore.NewKeyDBStore(passphraseRetriever, defaultAlias, configDBType, dbSQL) if err != nil { log.Fatalf("failed to create a new keydbstore: %v", err) } diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 2120675c46..6faf590002 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -20,6 +20,7 @@ import ( "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/pkg/term" notaryclient "github.com/docker/notary/client" + "github.com/docker/notary/tuf/data" "github.com/docker/notary/utils" "github.com/spf13/cobra" ) @@ -123,26 +124,20 @@ func tufInit(cmd *cobra.Command, args []string) { fatalf(err.Error()) } - keysMap := make(map[string]string) - for k, role := range nRepo.KeyStoreManager.KeyStore.ListKeys() { - if role == "root" { - keysMap[k] = role - } - } + rootKeyList := nRepo.CryptoService.ListKeys(data.CanonicalRootRole) var rootKeyID string - if len(keysMap) < 1 { + if len(rootKeyList) < 1 { fmt.Println("No root keys found. Generating a new root key...") - rootKeyID, err = nRepo.KeyStoreManager.GenRootKey("ECDSA") + rootPublicKey, err := nRepo.CryptoService.Create(data.CanonicalRootRole, data.ECDSAKey) + rootKeyID = rootPublicKey.ID() if err != nil { fatalf(err.Error()) } } else { - // TODO(diogo): ask which root key to use - for keyID := range keysMap { - rootKeyID = keyID - } - + // Choses the first root key available, which is initialization specific + // but should return the HW one first. + rootKeyID = rootKeyList[0] fmt.Printf("Root key found, using: %s\n", rootKeyID) } diff --git a/cryptoservice/certificate.go b/cryptoservice/certificate.go index 6e932ed288..a23bac83ec 100644 --- a/cryptoservice/certificate.go +++ b/cryptoservice/certificate.go @@ -11,12 +11,9 @@ import ( // GenerateCertificate generates an X509 Certificate from a template, given a GUN func GenerateCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) { - - switch rootKey.(type) { - case *data.RSAPrivateKey, *data.ECDSAPrivateKey: - // go doesn't fall through - default: - return nil, fmt.Errorf("only bare RSA or ECDSA keys (not x509 variants) are currently supported. Found: %s", rootKey.Algorithm()) + signer := rootKey.CryptoSigner() + if signer == nil { + return nil, fmt.Errorf("key type not supported for Certificate generation: %s\n", rootKey.Algorithm()) } template, err := trustmanager.NewCertificate(gun) @@ -24,7 +21,7 @@ func GenerateCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate return nil, fmt.Errorf("failed to create the certificate template for: %s (%v)", gun, err) } - derBytes, err := x509.CreateCertificate(rand.Reader, template, template, rootKey.CryptoSigner().Public(), rootKey.CryptoSigner()) + derBytes, err := x509.CreateCertificate(rand.Reader, template, template, signer.Public(), signer) if err != nil { return nil, fmt.Errorf("failed to create the certificate for: %s (%v)", gun, err) } diff --git a/cryptoservice/crypto_service.go b/cryptoservice/crypto_service.go index 4b0ec94aae..ec0971c34e 100644 --- a/cryptoservice/crypto_service.go +++ b/cryptoservice/crypto_service.go @@ -115,6 +115,7 @@ func (ccs *CryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signatur // key). If that fails, then we don't have the key. privKey, _, err := ccs.GetPrivateKey(keyName) if err != nil { + logrus.Debugf("error attempting to retrieve private key: %s, %v", keyid, err) keyName = filepath.Join(ccs.gun, keyid) privKey, _, err = ccs.GetPrivateKey(keyName) if err != nil { @@ -123,16 +124,7 @@ func (ccs *CryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signatur } } - var sigAlgorithm data.SigAlgorithm - - switch privKey.(type) { - case *data.RSAPrivateKey: - sigAlgorithm = data.RSAPSSSignature - case *data.ECDSAPrivateKey: - sigAlgorithm = data.ECDSASignature - case *data.ED25519PrivateKey: - sigAlgorithm = data.EDDSASignature - } + sigAlgo := privKey.SignatureAlgorithm() sig, err := privKey.Sign(rand.Reader, payload, nil) if err != nil { logrus.Debugf("ignoring error attempting to %s sign with keyID: %s, %v", privKey.Algorithm(), keyid, err) @@ -144,7 +136,7 @@ func (ccs *CryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signatur // Append signatures to result array signatures = append(signatures, data.Signature{ KeyID: keyid, - Method: sigAlgorithm, + Method: sigAlgo, Signature: sig[:], }) } diff --git a/signer/api/ecdsa_hardware_crypto_service.go b/signer/api/ecdsa_hardware_crypto_service.go index d52295c8e8..b025900d0f 100644 --- a/signer/api/ecdsa_hardware_crypto_service.go +++ b/signer/api/ecdsa_hardware_crypto_service.go @@ -1,127 +1,364 @@ +// +build pkcs11 package api import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/sha256" + "crypto/x509" "errors" - "log" + "fmt" + "io" + "math/big" + "github.com/Sirupsen/logrus" "github.com/docker/notary/trustmanager" - "github.com/endophage/gotuf/data" + "github.com/docker/notary/tuf/data" "github.com/miekg/pkcs11" ) -// YubikeyECDSACryptoService is an implementation of SigningService -type YubikeyECDSACryptoService struct { - context *pkcs11.Ctx - session pkcs11.SessionHandle +const pkcs11Lib = "/usr/local/lib/libykcs11.so" +const USER_PIN = "123456" + +// Hardcoded yubikey PKCS11 ID +var YUBIKEY_ROOT_KEY_ID = []byte{2} + +// YubiPrivateKey represents a private key inside of a yubikey +type YubiPrivateKey struct { + data.ECDSAPublicKey } -// Create creates a key and returns its public components -func (s *YubikeyECDSACryptoService) Create(role string, algo data.KeyAlgorithm) (data.PublicKey, error) { - if role != "root" { - return nil, errors.New("can only generate root keys inside of Yubikey") - } - // TODO(diogo): Generate ECDSA key - // TODO(diogo): Generate Certificate from ECDSA public key - // TODO(diogo): Import Private key and certificate into yubikey - // TODO(diogo): Return TUF Key - return trustmanager.GenerateECDSAKey(rand.Reader) +type YubikeySigner struct { + YubiPrivateKey } -// RemoveKey removes a key from the key database -func (s *YubikeyECDSACryptoService) RemoveKey(keyID string) error { - return errors.New("cannot delete root key from Yubikey") +func NewYubiPrivateKey(pubKey data.ECDSAPublicKey) *YubiPrivateKey { + return &YubiPrivateKey{ECDSAPublicKey: pubKey} } -// GetKey returns the public components of a particular key -func (s *YubikeyECDSACryptoService) GetKey(keyID string) data.PublicKey { - // TODO(diogo): Get the certificate out of the Yubikey and return the ID from it - class = pkcs11.CKO_PRIVATE_KEY - privateKeyTemplate := []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_CLASS, class), - pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), - pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{2}), - } - - if err := s.context.FindObjectsInit(session, privateKeyTemplate); err != nil { - return nil - } - obj, b, err := s.context.FindObjects(session, 1) +func (ys *YubikeySigner) Public() crypto.PublicKey { + publicKey, err := x509.ParsePKIXPublicKey(ys.YubiPrivateKey.Public()) if err != nil { return nil } - if err = s.context.FindObjectsFinal(session); e != nil { - return nil + + return publicKey +} + +// CryptoSigner returns a crypto.Signer tha wraps the YubiPrivateKey. Needed for +// Certificate generation only +func (y *YubiPrivateKey) CryptoSigner() crypto.Signer { + return &YubikeySigner{YubiPrivateKey: *y} +} + +// Private is not implemented in hardware keys +func (y *YubiPrivateKey) Private() []byte { + // We cannot return the private material from a Yubikey + // TODO(david): We probably want to return an error here + return nil +} + +func (y YubiPrivateKey) SignatureAlgorithm() data.SigAlgorithm { + return data.ECDSASignature +} + +func (y *YubiPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) { + ctx, session, err := SetupHSMEnv(pkcs11Lib) + if err != nil { + return nil, err } - if len(obj) != 1 { - return nil + defer cleanup(ctx, session) + + sig, err := sign(ctx, session, YUBIKEY_ROOT_KEY_ID, msg) + if err != nil { + return nil, fmt.Errorf("failed to sign using Yubikey: %v", err) } - return data.NewPublicKey(data.ECDSAKey, []byte{0}) + return sig, nil +} + +// addECDSAKey adds a key to the yubikey +func addECDSAKey(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, privKey data.PrivateKey, pkcs11KeyID []byte) error { + logrus.Debugf("Got into add key with key: %s\n", privKey.ID()) + + // TODO(diogo): Figure out CKU_SO with yubikey + err := ctx.Login(session, pkcs11.CKU_SO, "010203040506070801020304050607080102030405060708") + if err != nil { + return err + } + defer ctx.Logout(session) + + fmt.Println("Parsing EC private key") + // Create an ecdsa.PrivateKey out of the private key bytes + ecdsaPrivKey, err := x509.ParseECPrivateKey(privKey.Private()) + if err != nil { + return err + } + + ecdsaPrivKeyD := ecdsaPrivKey.D.Bytes() + logrus.Debugf("Getting D bytes: %v\n", ecdsaPrivKeyD) + + template, err := trustmanager.NewCertificate(data.CanonicalRootRole) + if err != nil { + return fmt.Errorf("failed to create the certificate template: %v", err) + } + + certBytes, err := x509.CreateCertificate(rand.Reader, template, template, ecdsaPrivKey.Public(), ecdsaPrivKey) + if err != nil { + return fmt.Errorf("failed to create the certificate: %v", err) + } + fmt.Println("Got a certificate!") + + certTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), + pkcs11.NewAttribute(pkcs11.CKA_VALUE, certBytes), + pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + } + + privateKeyTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), + pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}), + pkcs11.NewAttribute(pkcs11.CKA_VALUE, ecdsaPrivKeyD), + } + + fmt.Println("About to import the certificate") + + _, err = ctx.CreateObject(session, certTemplate) + if err != nil { + return fmt.Errorf("error importing: %v", err) + } + + fmt.Println("About to import the private key") + _, err = ctx.CreateObject(session, privateKeyTemplate) + if err != nil { + return fmt.Errorf("error importing: %v", err) + } + + return nil +} + +func getECDSAKey(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte) (*data.ECDSAPublicKey, string, error) { + findTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), + pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), + } + + attrTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, []byte{0}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{0}), + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{0}), + } + + if err := ctx.FindObjectsInit(session, findTemplate); err != nil { + logrus.Debugf("Failed to init: %s\n", err.Error()) + return nil, "", err + } + obj, b, err := ctx.FindObjects(session, 1) + if err != nil { + logrus.Debugf("Failed to find: %s %v\n", err.Error(), b) + return nil, "", err + } + if err := ctx.FindObjectsFinal(session); err != nil { + logrus.Debugf("Failed to finalize: %s\n", err.Error()) + return nil, "", err + } + if len(obj) != 1 { + logrus.Debugf("should have found one object") + return nil, "", errors.New("no matching keys found inside of yubikey") + } + + // Retrieve the public-key material to be able to create a new HSMRSAKey + attr, err := ctx.GetAttributeValue(session, obj[0], attrTemplate) + if err != nil { + logrus.Debugf("Failed to get Attribute for: %v\n", obj[0]) + return nil, "", err + } + + // Iterate through all the attributes of this key and saves CKA_PUBLIC_EXPONENT and CKA_MODULUS. Removes ordering specific issues. + var rawPubKey []byte + for _, a := range attr { + if a.Type == pkcs11.CKA_EC_POINT { + rawPubKey = a.Value + } + + } + + ecdsaPubKey := ecdsa.PublicKey{Curve: elliptic.P256(), X: new(big.Int).SetBytes(rawPubKey[3:35]), Y: new(big.Int).SetBytes(rawPubKey[35:])} + pubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPubKey) + if err != nil { + logrus.Debugf("Failed to Marshal public key") + return nil, "", err + } + + // TODO(diogo): Actually get the right alias from the certificate instead of + // alwars returning data.CanonicalRootRole + return data.NewECDSAPublicKey(pubBytes), data.CanonicalRootRole, nil } // Sign returns a signature for a given signature request -func (s *YubikeyECDSACryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signature, error) { - signatures := make([]data.Signature, 0, len(keyIDs)) - for _, keyid := range keyIDs { - key := s.GetKey(keyID) - if key == nil { - // We skip keys that aren't found - continue - } - - // Define the ECDSA Private key template - class = pkcs11.CKO_PRIVATE_KEY - privateKeyTemplate := []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_CLASS, class), - pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), - pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{2}), - } - - if err := s.context.FindObjectsInit(session, privateKeyTemplate); err != nil { - return nil, err - } - obj, b, err := s.context.FindObjects(session, 1) - if err != nil { - return nil, err - } - if err = s.context.FindObjectsFinal(session); e != nil { - return nil, err - } - if len(obj) != 1 { - return nil, errors.New("length of objects found not 1") - } - - var sig []byte - s.context.SignInit(s.session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_ECDSA, nil)}, obj[0]) - - // Get the SHA256 of the payload - digest := sha256.Sum256(payload) - - sig, err := s.context.Sign(s.session, digest) - if err != nil { - log.Printf("Error while signing: %s", err) - return nil, err - } - - if sig == nil { - return nil, errors.New("Failed to create signature") - } - - signatures = append(signatures, data.Signature{ - KeyID: keyid, - Method: data.ECDSASignature, - Signature: sig[:], - }) +func sign(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, pkcs11KeyID, payload []byte) ([]byte, error) { + err := ctx.Login(session, pkcs11.CKU_USER, USER_PIN) + if err != nil { + return nil, fmt.Errorf("error logging in: %v", err) } - return signatures, nil + defer ctx.Logout(session) + + // Define the ECDSA Private key template + class := pkcs11.CKO_PRIVATE_KEY + privateKeyTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, class), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA), + pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), + } + + if err := ctx.FindObjectsInit(session, privateKeyTemplate); err != nil { + return nil, err + } + obj, _, err := ctx.FindObjects(session, 1) + if err != nil { + return nil, err + } + if err = ctx.FindObjectsFinal(session); err != nil { + return nil, err + } + if len(obj) != 1 { + return nil, errors.New("length of objects found not 1") + } + + var sig []byte + ctx.SignInit(session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_ECDSA, nil)}, obj[0]) + + // Get the SHA256 of the payload + digest := sha256.Sum256(payload) + + sig, err = ctx.Sign(session, digest[:]) + if err != nil { + logrus.Debugf("Error while signing: %s", err) + return nil, err + } + + if sig == nil { + return nil, errors.New("Failed to create signature") + } + return sig[:], nil } -// NewYubikeyECDSACryptoService returns an instance of RSAHardwareCryptoService -func NewYubikeyECDSACryptoService(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) *YubikeyECDSACryptoService { - return &YubikeyECDSACryptoService{ - context: ctx, - session: session, - } +type YubiKeyStore struct{} + +func NewYubiKeyStore() *YubiKeyStore { + return &YubiKeyStore{} +} + +func (s *YubiKeyStore) ListKeys() map[string]string { + ctx, session, err := SetupHSMEnv(pkcs11Lib) + if err != nil { + return nil + } + defer cleanup(ctx, session) + ecdsaPubKey, alias, err := getECDSAKey(ctx, session, YUBIKEY_ROOT_KEY_ID) + if err != nil { + logrus.Debugf("error while listing keys from Yubikey: %s", err) + return nil + } + return map[string]string{ecdsaPubKey.ID(): alias} +} + +func (s *YubiKeyStore) AddKey(keyID, alias string, privKey data.PrivateKey) error { + // We only allow adding root keys for now + if alias != data.CanonicalRootRole { + return fmt.Errorf("yubikey only supports storing root keys, got %s for key: %s\n", alias, keyID) + } + + ctx, session, err := SetupHSMEnv(pkcs11Lib) + if err != nil { + return err + } + defer cleanup(ctx, session) + + return addECDSAKey(ctx, session, privKey, YUBIKEY_ROOT_KEY_ID) +} + +func (s *YubiKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) { + ctx, session, err := SetupHSMEnv(pkcs11Lib) + if err != nil { + return nil, "", err + } + defer cleanup(ctx, session) + + pubKey, alias, err := getECDSAKey(ctx, session, YUBIKEY_ROOT_KEY_ID) + if err != nil { + return nil, "", err + } + // Check to see if we're returning the intended keyID + if pubKey.ID() != keyID { + return nil, "", fmt.Errorf("expected root key: %s, but found: %s\n", keyID, pubKey.ID()) + } + privKey := NewYubiPrivateKey(*pubKey) + if privKey == nil { + return nil, "", errors.New("could not initialize new YubiPrivateKey") + } + + return privKey, alias, err +} + +func (s *YubiKeyStore) RemoveKey(keyID string) error { + // TODO(diogo): actually implement this + logrus.Debugf("Attempting to remove: %s key inside of YubiKeyStore", keyID) + return nil +} + +func (s *YubiKeyStore) ExportKey(keyID string) ([]byte, error) { + // TODO(diogo): actually implement this + logrus.Debugf("Attempting to export: %s key inside of YubiKeyStore", keyID) + return nil, nil +} + +func (s *YubiKeyStore) ImportKey(pemBytes []byte, keyID string) error { + // TODO(diogo): actually implement this + logrus.Debugf("Attempting to import: %s key inside of YubiKeyStore", keyID) + return nil +} + +func cleanup(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) { + fmt.Println("Tearing down yubikey session") + ctx.CloseSession(session) + ctx.Finalize() + ctx.Destroy() +} + +// SetupHSMEnv is a method that depends on the existences +func SetupHSMEnv(libraryPath string) (*pkcs11.Ctx, pkcs11.SessionHandle, error) { + fmt.Println("Setting up Yubikey") + p := pkcs11.New(libraryPath) + + if p == nil { + return nil, 0, errors.New("Failed to init library") + } + + if err := p.Initialize(); err != nil { + return nil, 0, fmt.Errorf("Initialize error %s\n", err.Error()) + } + + slots, err := p.GetSlotList(true) + if err != nil { + return nil, 0, fmt.Errorf("Failed to list HSM slots %s", err) + } + // Check to see if we got any slots from the HSM. + if len(slots) < 1 { + return nil, 0, fmt.Errorf("No HSM Slots found") + } + + // CKF_SERIAL_SESSION: TRUE if cryptographic functions are performed in serial with the application; FALSE if the functions may be performed in parallel with the application. + // CKF_RW_SESSION: TRUE if the session is read/write; FALSE if the session is read-only + session, err := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) + if err != nil { + return nil, 0, fmt.Errorf("Failed to Start Session with HSM %s", err) + } + + return p, session, nil } diff --git a/signer/api/rsa_hardware_crypto_service.go b/signer/api/rsa_hardware_crypto_service.go index 5ffe8536d5..8e673be341 100644 --- a/signer/api/rsa_hardware_crypto_service.go +++ b/signer/api/rsa_hardware_crypto_service.go @@ -17,7 +17,7 @@ import ( // RSAHardwareCryptoService is an implementation of SigningService type RSAHardwareCryptoService struct { - keys map[string]*keys.HSMKey + keys map[string]*keys.HSMRSAKey context *pkcs11.Ctx session pkcs11.SessionHandle } @@ -68,7 +68,7 @@ func (s *RSAHardwareCryptoService) Create(role, algo string) (data.PublicKey, er pkcs11.NewAttribute(pkcs11.CKA_MODULUS, nil), } - // Retrieve the public-key material to be able to create a new HSMKey + // Retrieve the public-key material to be able to create a new HSMRSAKey attr, err := s.context.GetAttributeValue(s.session, pub, template) if err != nil { return nil, errors.New("Failed to get Attribute value.") @@ -97,7 +97,7 @@ func (s *RSAHardwareCryptoService) Create(role, algo string) (data.PublicKey, er } // (diogo): Ideally I would like to return base64 PEM encoded public keys to the client - k := keys.NewHSMKey(pubBytes, priv) + k := keys.NewHSMRSAKey(pubBytes, priv) keyID := k.ID() @@ -190,7 +190,7 @@ func (s *RSAHardwareCryptoService) Sign(keyIDs []string, payload []byte) ([]data // NewRSAHardwareCryptoService returns an instance of RSAHardwareCryptoService func NewRSAHardwareCryptoService(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) *RSAHardwareCryptoService { return &RSAHardwareCryptoService{ - keys: make(map[string]*keys.HSMKey), + keys: make(map[string]*keys.HSMRSAKey), context: ctx, session: session, } diff --git a/signer/keydbstore.go b/signer/keydbstore/keydbstore.go similarity index 99% rename from signer/keydbstore.go rename to signer/keydbstore/keydbstore.go index 06576b209a..f99d3e31c3 100644 --- a/signer/keydbstore.go +++ b/signer/keydbstore/keydbstore.go @@ -1,4 +1,4 @@ -package signer +package keydbstore import ( "database/sql" diff --git a/signer/keydbstore_test.go b/signer/keydbstore/keydbstore_test.go similarity index 99% rename from signer/keydbstore_test.go rename to signer/keydbstore/keydbstore_test.go index 8e40a8be60..d22e5a590f 100644 --- a/signer/keydbstore_test.go +++ b/signer/keydbstore/keydbstore_test.go @@ -1,4 +1,4 @@ -package signer +package keydbstore import ( "crypto/rand" diff --git a/signer/keys/keys.go b/signer/keys/keys.go index 19cb83022e..80396c6939 100644 --- a/signer/keys/keys.go +++ b/signer/keys/keys.go @@ -16,49 +16,48 @@ var ( ErrFailedKeyGeneration = errors.New("notary-signer: failed to generate new key") ) -// HSMKey represents the information for an HSMKey with ObjectHandle for private portion -type HSMKey struct { - id string - algorithm data.KeyAlgorithm - public []byte - private pkcs11.ObjectHandle +// HSMRSAKey represents the information for an HSMRSAKey with ObjectHandle for private portion +type HSMRSAKey struct { + id string + public []byte + private pkcs11.ObjectHandle } -// NewHSMKey returns a HSMKey -func NewHSMKey(public []byte, private pkcs11.ObjectHandle) *HSMKey { - return &HSMKey{ +// NewHSMRSAKey returns a HSMRSAKey +func NewHSMRSAKey(public []byte, private pkcs11.ObjectHandle) *HSMRSAKey { + return &HSMRSAKey{ public: public, private: private, } } // Algorithm implements a method of the data.Key interface -func (k *HSMKey) Algorithm() data.KeyAlgorithm { - return k.algorithm +func (rsa *HSMRSAKey) Algorithm() string { + return data.RSAKey } // ID implements a method of the data.Key interface -func (k *HSMKey) ID() string { - if k.id == "" { - pubK := data.NewPublicKey(k.algorithm, k.public) - k.id = pubK.ID() +func (rsa *HSMRSAKey) ID() string { + if rsa.id == "" { + pubK := data.NewPublicKey(rsa.Algorithm(), rsa.Public()) + rsa.id = pubK.ID() } - return k.id + return rsa.id } // Public implements a method of the data.Key interface -func (k *HSMKey) Public() []byte { - return k.public +func (rsa *HSMRSAKey) Public() []byte { + return rsa.public } // Private implements a method of the data.PrivateKey interface -func (k *HSMKey) Private() []byte { +func (rsa *HSMRSAKey) Private() []byte { // Not possible to return private key bytes from a hardware device return nil } -// PKCS11ObjectHandle returns the PKCS11 object handle stored in the HSMKey +// PKCS11ObjectHandle returns the PKCS11 object handle stored in the HSMRSAKey // structure -func (k *HSMKey) PKCS11ObjectHandle() pkcs11.ObjectHandle { - return k.private +func (rsa *HSMRSAKey) PKCS11ObjectHandle() pkcs11.ObjectHandle { + return rsa.private } diff --git a/tuf/data/keys.go b/tuf/data/keys.go index be5e297f42..22a1fb786c 100644 --- a/tuf/data/keys.go +++ b/tuf/data/keys.go @@ -30,6 +30,7 @@ type PrivateKey interface { Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) Private() []byte CryptoSigner() crypto.Signer + SignatureAlgorithm() SigAlgorithm } // KeyPair holds the public and private key bytes @@ -485,6 +486,22 @@ func (k UnknownPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOp return nil, errors.New("Unknown key type, cannot sign.") } +func (k ECDSAPrivateKey) SignatureAlgorithm() SigAlgorithm { + return ECDSASignature +} + +func (k RSAPrivateKey) SignatureAlgorithm() SigAlgorithm { + return RSAPSSSignature +} + +func (k ED25519PrivateKey) SignatureAlgorithm() SigAlgorithm { + return EDDSASignature +} + +func (k UnknownPrivateKey) SignatureAlgorithm() SigAlgorithm { + return "" +} + // PublicKeyFromPrivate returns a new tufKey based on a private key, with // the private key bytes guaranteed to be nil. func PublicKeyFromPrivate(pk PrivateKey) PublicKey { From 4648666b7cfd50f419e1acc40313e19a9e540652 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 30 Oct 2015 15:07:39 -0700 Subject: [PATCH 03/85] add pkcs11 build tags Signed-off-by: Jessica Frazelle Signed-off-by: David Lawrence Signed-off-by: Jessica Frazelle (github: endophage) --- client/client.go | 40 ---------------- client/repo.go | 50 ++++++++++++++++++++ client/repo_pkcs11.go | 52 +++++++++++++++++++++ signer/api/ecdsa_hardware_crypto_service.go | 1 + 4 files changed, 103 insertions(+), 40 deletions(-) create mode 100644 client/repo.go create mode 100644 client/repo_pkcs11.go diff --git a/client/client.go b/client/client.go index 36a361d2a6..6bd95d7f82 100644 --- a/client/client.go +++ b/client/client.go @@ -14,8 +14,6 @@ import ( "github.com/docker/notary/client/changelist" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/keystoremanager" - "github.com/docker/notary/pkg/passphrase" - "github.com/docker/notary/signer/api" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf" tufclient "github.com/docker/notary/tuf/client" @@ -98,44 +96,6 @@ func NewTarget(targetName string, targetPath string) (*Target, error) { return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length}, nil } -// NewNotaryRepository is a helper method that returns a new notary repository. -// It takes the base directory under where all the trust files will be stored -// (usually ~/.docker/trust/). -func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, - passphraseRetriever passphrase.Retriever) (*NotaryRepository, error) { - - keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, passphraseRetriever) - if err != nil { - return nil, err - } - - yubiKeyStore := api.NewYubiKeyStore() - cryptoService := cryptoservice.NewCryptoService(gun, yubiKeyStore, keyStoreManager.KeyStore) - - nRepo := &NotaryRepository{ - gun: gun, - baseDir: baseDir, - baseURL: baseURL, - tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), - CryptoService: cryptoService, - roundTrip: rt, - KeyStoreManager: keyStoreManager, - } - - fileStore, err := store.NewFilesystemStore( - nRepo.tufRepoPath, - "metadata", - "json", - "", - ) - if err != nil { - return nil, err - } - nRepo.fileStore = fileStore - - return nRepo, nil -} - // Initialize creates a new repository by using rootKey as the root Key for the // TUF repository. func (r *NotaryRepository) Initialize(rootKeyID string) error { diff --git a/client/repo.go b/client/repo.go new file mode 100644 index 0000000000..4e7f2a406c --- /dev/null +++ b/client/repo.go @@ -0,0 +1,50 @@ +// +build !pkcs11 + +package client + +import ( + "net/http" + "path/filepath" + + "github.com/docker/notary/cryptoservice" + "github.com/docker/notary/keystoremanager" + "github.com/docker/notary/pkg/passphrase" + "github.com/docker/notary/tuf/store" +) + +// NewNotaryRepository is a helper method that returns a new notary repository. +// It takes the base directory under where all the trust files will be stored +// (usually ~/.docker/trust/). +func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, + passphraseRetriever passphrase.Retriever) (*NotaryRepository, error) { + + keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, passphraseRetriever) + if err != nil { + return nil, err + } + + cryptoService := cryptoservice.NewCryptoService(gun, keyStoreManager.KeyStore) + + nRepo := &NotaryRepository{ + gun: gun, + baseDir: baseDir, + baseURL: baseURL, + tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), + CryptoService: cryptoService, + roundTrip: rt, + KeyStoreManager: keyStoreManager, + } + + fileStore, err := store.NewFilesystemStore( + nRepo.tufRepoPath, + "metadata", + "json", + "", + ) + if err != nil { + return nil, err + } + nRepo.fileStore = fileStore + + return nRepo, nil +} diff --git a/client/repo_pkcs11.go b/client/repo_pkcs11.go new file mode 100644 index 0000000000..ab59f39585 --- /dev/null +++ b/client/repo_pkcs11.go @@ -0,0 +1,52 @@ +// +build pkcs11 + +package client + +import ( + "net/http" + "path/filepath" + + "github.com/docker/notary/cryptoservice" + "github.com/docker/notary/keystoremanager" + "github.com/docker/notary/pkg/passphrase" + "github.com/docker/notary/signer/api" + "github.com/docker/notary/tuf/store" +) + +// NewNotaryRepository is a helper method that returns a new notary repository. +// It takes the base directory under where all the trust files will be stored +// (usually ~/.docker/trust/). +func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, + passphraseRetriever passphrase.Retriever) (*NotaryRepository, error) { + + keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, passphraseRetriever) + if err != nil { + return nil, err + } + + yubiKeyStore := api.NewYubiKeyStore() + cryptoService := cryptoservice.NewCryptoService(gun, yubiKeyStore, keyStoreManager.KeyStore) + + nRepo := &NotaryRepository{ + gun: gun, + baseDir: baseDir, + baseURL: baseURL, + tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), + CryptoService: cryptoService, + roundTrip: rt, + KeyStoreManager: keyStoreManager, + } + + fileStore, err := store.NewFilesystemStore( + nRepo.tufRepoPath, + "metadata", + "json", + "", + ) + if err != nil { + return nil, err + } + nRepo.fileStore = fileStore + + return nRepo, nil +} diff --git a/signer/api/ecdsa_hardware_crypto_service.go b/signer/api/ecdsa_hardware_crypto_service.go index b025900d0f..a76d21e8b1 100644 --- a/signer/api/ecdsa_hardware_crypto_service.go +++ b/signer/api/ecdsa_hardware_crypto_service.go @@ -1,4 +1,5 @@ // +build pkcs11 + package api import ( From 53ed60ed897ab9d3fcdbeb5c68497643786f6d43 Mon Sep 17 00:00:00 2001 From: Diogo Monica Date: Fri, 30 Oct 2015 15:38:57 -0700 Subject: [PATCH 04/85] Adding mandatory touch for signatures Signed-off-by: David Lawrence Signed-off-by: Diogo Monica (github: endophage) --- signer/api/ecdsa_hardware_crypto_service.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/signer/api/ecdsa_hardware_crypto_service.go b/signer/api/ecdsa_hardware_crypto_service.go index a76d21e8b1..25ce36f85a 100644 --- a/signer/api/ecdsa_hardware_crypto_service.go +++ b/signer/api/ecdsa_hardware_crypto_service.go @@ -124,6 +124,10 @@ func addECDSAKey(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, privKey data.Pri pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID), pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}), pkcs11.NewAttribute(pkcs11.CKA_VALUE, ecdsaPrivKeyD), + // 1 is touch enabled + // 2 is pin once + // 4 is pin always + pkcs11.NewAttribute(pkcs11.CKA_VENDOR_DEFINED, 3), } fmt.Println("About to import the certificate") From 07f00651522a7eea97f7f0836b1ae194291bdde2 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 30 Oct 2015 16:23:56 -0700 Subject: [PATCH 05/85] ask for pin when signing Signed-off-by: David Lawrence (github: endophage) --- client/client.go | 48 ++++++++++++- cmd/notary/cert.go | 15 ++++- cmd/notary/keys.go | 50 ++++++++++++-- keystoremanager/import_export.go | 14 ++-- keystoremanager/keystoremanager.go | 11 +-- keystoremanager/keystoremanager_test.go | 13 +++- signer/api/ecdsa_hardware_crypto_service.go | 75 +++++++++++++++++---- 7 files changed, 184 insertions(+), 42 deletions(-) diff --git a/client/client.go b/client/client.go index 6bd95d7f82..eb43189a00 100644 --- a/client/client.go +++ b/client/client.go @@ -24,7 +24,9 @@ import ( "github.com/docker/notary/tuf/store" ) -const maxSize = 5 << 20 +const ( + maxSize = 5 << 20 +) func init() { data.SetDefaultExpiryTimes( @@ -96,6 +98,50 @@ func NewTarget(targetName string, targetPath string) (*Target, error) { return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length}, nil } +// NewNotaryRepository is a helper method that returns a new notary repository. +// It takes the base directory under where all the trust files will be stored +// (usually ~/.docker/trust/). +func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, + passphraseRetriever passphrase.Retriever) (*NotaryRepository, error) { + + keysPath := filepath.Join(baseDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, passphraseRetriever) + if err != nil { + return nil, err + } + + keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, fileKeyStore) + if err != nil { + return nil, err + } + + yubiKeyStore := api.NewYubiKeyStore(passphraseRetriever) + cryptoService := cryptoservice.NewCryptoService(gun, yubiKeyStore, fileKeyStore) + + nRepo := &NotaryRepository{ + gun: gun, + baseDir: baseDir, + baseURL: baseURL, + tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), + CryptoService: cryptoService, + roundTrip: rt, + KeyStoreManager: keyStoreManager, + } + + fileStore, err := store.NewFilesystemStore( + nRepo.tufRepoPath, + "metadata", + "json", + "", + ) + if err != nil { + return nil, err + } + nRepo.fileStore = fileStore + + return nRepo, nil +} + // Initialize creates a new repository by using rootKey as the root Key for the // TUF repository. func (r *NotaryRepository) Initialize(rootKeyID string) error { diff --git a/cmd/notary/cert.go b/cmd/notary/cert.go index 5b76397515..21223bcf8a 100644 --- a/cmd/notary/cert.go +++ b/cmd/notary/cert.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "os" + "path/filepath" "time" "github.com/docker/notary/keystoremanager" @@ -55,7 +56,12 @@ func certRemove(cmd *cobra.Command, args []string) { parseConfig() - keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, retriever) + keysPath := filepath.Join(trustDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + fatalf("failed to create private key store in directory: %s", keysPath) + } + keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, fileKeyStore) if err != nil { fatalf("failed to create a new truststore manager with directory: %s", trustDir) } @@ -119,7 +125,12 @@ func certList(cmd *cobra.Command, args []string) { parseConfig() - keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, retriever) + keysPath := filepath.Join(trustDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + fatalf("failed to create private key store in directory: %s", keysPath) + } + keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, fileKeyStore) if err != nil { fatalf("failed to create a new truststore manager with directory: %s", trustDir) } diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 04db681393..7cc2297e48 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -11,6 +11,7 @@ import ( notaryclient "github.com/docker/notary/client" "github.com/docker/notary/keystoremanager" "github.com/docker/notary/pkg/passphrase" + "github.com/docker/notary/trustmanager" "github.com/spf13/cobra" ) @@ -111,7 +112,12 @@ func keysRemoveKey(cmd *cobra.Command, args []string) { parseConfig() - keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, retriever) + keysPath := filepath.Join(trustDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + fatalf("failed to create private key store in directory: %s", keysPath) + } + keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, fileKeyStore) if err != nil { fatalf("failed to create a new truststore manager with directory: %s", trustDir) } @@ -167,7 +173,12 @@ func keysList(cmd *cobra.Command, args []string) { parseConfig() - keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, retriever) + keysPath := filepath.Join(trustDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + fatalf("failed to create private key store in directory: %s", keysPath) + } + keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, fileKeyStore) if err != nil { fatalf("failed to create a new truststore manager with directory: %s", trustDir) } @@ -220,7 +231,12 @@ func keysGenerateRootKey(cmd *cobra.Command, args []string) { parseConfig() - keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, retriever) + keysPath := filepath.Join(trustDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + fatalf("failed to create private key store in directory: %s", keysPath) + } + keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, fileKeyStore) if err != nil { fatalf("failed to create a new truststore manager with directory: %s", trustDir) } @@ -244,7 +260,12 @@ func keysExport(cmd *cobra.Command, args []string) { parseConfig() - keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, retriever) + keysPath := filepath.Join(trustDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + fatalf("failed to create private key store in directory: %s", keysPath) + } + keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, fileKeyStore) if err != nil { fatalf("failed to create a new truststore manager with directory: %s", trustDir) } @@ -287,7 +308,12 @@ func keysExportRoot(cmd *cobra.Command, args []string) { parseConfig() - keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, retriever) + keysPath := filepath.Join(trustDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + fatalf("failed to create private key store in directory: %s", keysPath) + } + keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, fileKeyStore) if err != nil { fatalf("failed to create a new truststore manager with directory: %s", trustDir) } @@ -322,7 +348,12 @@ func keysImport(cmd *cobra.Command, args []string) { parseConfig() - keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, retriever) + keysPath := filepath.Join(trustDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + fatalf("failed to create private key store in directory: %s", keysPath) + } + keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, fileKeyStore) if err != nil { fatalf("failed to create a new truststore manager with directory: %s", trustDir) } @@ -351,7 +382,12 @@ func keysImportRoot(cmd *cobra.Command, args []string) { parseConfig() - keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, retriever) + keysPath := filepath.Join(trustDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + fatalf("failed to create private key store in directory: %s", keysPath) + } + keyStoreManager, err := keystoremanager.NewKeyStoreManager(trustDir, fileKeyStore) if err != nil { fatalf("failed to create a new truststore manager with directory: %s", trustDir) } diff --git a/keystoremanager/import_export.go b/keystoremanager/import_export.go index ce099f738f..6fd4735e3e 100644 --- a/keystoremanager/import_export.go +++ b/keystoremanager/import_export.go @@ -60,7 +60,7 @@ func (km *KeyStoreManager) ExportRootKeyReencrypt(dest io.Writer, keyID string, tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") defer os.RemoveAll(tempBaseDir) - tempKeysPath := filepath.Join(tempBaseDir, privDir) + tempKeysPath := filepath.Join(tempBaseDir, PrivDir) tempKeyStore, err := trustmanager.NewKeyFileStore(tempKeysPath, newPassphraseRetriever) if err != nil { return err @@ -178,7 +178,7 @@ func (km *KeyStoreManager) ExportAllKeys(dest io.Writer, newPassphraseRetriever defer os.RemoveAll(tempBaseDir) // Create temporary keystore to use as a staging area - tempKeysPath := filepath.Join(tempBaseDir, privDir) + tempKeysPath := filepath.Join(tempBaseDir, PrivDir) tempKeyStore, err := trustmanager.NewKeyFileStore(tempKeysPath, newPassphraseRetriever) if err != nil { return err @@ -190,7 +190,7 @@ func (km *KeyStoreManager) ExportAllKeys(dest io.Writer, newPassphraseRetriever zipWriter := zip.NewWriter(dest) - if err := addKeysToArchive(zipWriter, tempKeyStore, privDir); err != nil { + if err := addKeysToArchive(zipWriter, tempKeyStore, PrivDir); err != nil { return err } @@ -224,14 +224,14 @@ func (km *KeyStoreManager) ImportKeysZip(zipReader zip.Reader) error { // Note that using / as a separator is okay here - the zip // package guarantees that the separator will be / - if strings.HasPrefix(fNameTrimmed, privDir) { + if strings.HasPrefix(fNameTrimmed, PrivDir) { if fNameTrimmed[len(fNameTrimmed)-5:] == "_root" { if err = checkRootKeyIsEncrypted(fileBytes); err != nil { rc.Close() return err } } - keyName := strings.TrimPrefix(fNameTrimmed, privDir) + keyName := strings.TrimPrefix(fNameTrimmed, PrivDir) newKeys[keyName] = fileBytes } else { // This path inside the zip archive doesn't look like a @@ -283,7 +283,7 @@ func (km *KeyStoreManager) ExportKeysByGUN(dest io.Writer, gun string, passphras defer os.RemoveAll(tempBaseDir) // Create temporary keystore to use as a staging area - tempKeysPath := filepath.Join(tempBaseDir, privDir) + tempKeysPath := filepath.Join(tempBaseDir, PrivDir) tempKeyStore, err := trustmanager.NewKeyFileStore(tempKeysPath, passphraseRetriever) if err != nil { return err @@ -299,7 +299,7 @@ func (km *KeyStoreManager) ExportKeysByGUN(dest io.Writer, gun string, passphras return ErrNoKeysFoundForGUN } - if err := addKeysToArchive(zipWriter, tempKeyStore, privDir); err != nil { + if err := addKeysToArchive(zipWriter, tempKeyStore, PrivDir); err != nil { return err } diff --git a/keystoremanager/keystoremanager.go b/keystoremanager/keystoremanager.go index 58cb5b6bc3..09f336de0b 100644 --- a/keystoremanager/keystoremanager.go +++ b/keystoremanager/keystoremanager.go @@ -10,7 +10,6 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/docker/notary/pkg/passphrase" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" @@ -26,7 +25,7 @@ type KeyStoreManager struct { const ( trustDir = "trusted_certificates" - privDir = "private" + PrivDir = "private" rsaRootKeySize = 4096 // Used for new root keys ) @@ -56,13 +55,7 @@ func (err ErrRootRotationFail) Error() string { // NewKeyStoreManager returns an initialized KeyStoreManager, or an error // if it fails to create the KeyFileStores or load certificates -func NewKeyStoreManager(baseDir string, passphraseRetriever passphrase.Retriever) (*KeyStoreManager, error) { - keysPath := filepath.Join(baseDir, privDir) - keyStore, err := trustmanager.NewKeyFileStore(keysPath, passphraseRetriever) - if err != nil { - return nil, err - } - +func NewKeyStoreManager(baseDir string, keyStore *trustmanager.KeyFileStore) (*KeyStoreManager, error) { trustPath := filepath.Join(baseDir, trustDir) // Load all CAs that aren't expired and don't use SHA1 diff --git a/keystoremanager/keystoremanager_test.go b/keystoremanager/keystoremanager_test.go index 075689c6f0..bae645bdac 100644 --- a/keystoremanager/keystoremanager_test.go +++ b/keystoremanager/keystoremanager_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "io/ioutil" "os" + "path/filepath" "testing" "text/template" @@ -121,8 +122,12 @@ func TestValidateRoot(t *testing.T) { defer os.RemoveAll(tempBaseDir) assert.NoError(t, err, "failed to create a temporary directory: %s", err) + keysPath := filepath.Join(tempBaseDir, PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, passphraseRetriever) + assert.NoError(t, err) + // Create a FileStoreManager - keyStoreManager, err := NewKeyStoreManager(tempBaseDir, passphraseRetriever) + keyStoreManager, err := NewKeyStoreManager(tempBaseDir, fileKeyStore) assert.NoError(t, err) // Execute our template @@ -229,8 +234,12 @@ func filestoreWithTwoCerts(t *testing.T, gun, keyAlg string) ( tempBaseDir, err := ioutil.TempDir("", "notary-test-") assert.NoError(t, err, "failed to create a temporary directory: %s", err) + keysPath := filepath.Join(tempBaseDir, PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, passphraseRetriever) + assert.NoError(t, err) + // Create a FileStoreManager - keyStoreManager, err := NewKeyStoreManager(tempBaseDir, passphraseRetriever) + keyStoreManager, err := NewKeyStoreManager(tempBaseDir, fileKeyStore) assert.NoError(t, err) certs := make([]*x509.Certificate, 2) diff --git a/signer/api/ecdsa_hardware_crypto_service.go b/signer/api/ecdsa_hardware_crypto_service.go index 25ce36f85a..8c37feaea3 100644 --- a/signer/api/ecdsa_hardware_crypto_service.go +++ b/signer/api/ecdsa_hardware_crypto_service.go @@ -15,13 +15,17 @@ import ( "math/big" "github.com/Sirupsen/logrus" + "github.com/docker/notary/pkg/passphrase" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "github.com/miekg/pkcs11" ) -const pkcs11Lib = "/usr/local/lib/libykcs11.so" -const USER_PIN = "123456" +const ( + pkcs11Lib = "/usr/local/lib/libykcs11.so" + USER_PIN = "123456" + SO_USER_PIN = "010203040506070801020304050607080102030405060708" +) // Hardcoded yubikey PKCS11 ID var YUBIKEY_ROOT_KEY_ID = []byte{2} @@ -29,14 +33,18 @@ var YUBIKEY_ROOT_KEY_ID = []byte{2} // YubiPrivateKey represents a private key inside of a yubikey type YubiPrivateKey struct { data.ECDSAPublicKey + passRetriever passphrase.Retriever } type YubikeySigner struct { YubiPrivateKey } -func NewYubiPrivateKey(pubKey data.ECDSAPublicKey) *YubiPrivateKey { - return &YubiPrivateKey{ECDSAPublicKey: pubKey} +func NewYubiPrivateKey(pubKey data.ECDSAPublicKey, passRetriever passphrase.Retriever) *YubiPrivateKey { + return &YubiPrivateKey{ + ECDSAPublicKey: pubKey, + passRetriever: passRetriever, + } } func (ys *YubikeySigner) Public() crypto.PublicKey { @@ -72,7 +80,7 @@ func (y *YubiPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts } defer cleanup(ctx, session) - sig, err := sign(ctx, session, YUBIKEY_ROOT_KEY_ID, msg) + sig, err := sign(ctx, session, YUBIKEY_ROOT_KEY_ID, y.passRetriever, msg) if err != nil { return nil, fmt.Errorf("failed to sign using Yubikey: %v", err) } @@ -81,11 +89,11 @@ func (y *YubiPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts } // addECDSAKey adds a key to the yubikey -func addECDSAKey(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, privKey data.PrivateKey, pkcs11KeyID []byte) error { +func addECDSAKey(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, privKey data.PrivateKey, pkcs11KeyID []byte, passRetriever passphrase.Retriever) error { logrus.Debugf("Got into add key with key: %s\n", privKey.ID()) // TODO(diogo): Figure out CKU_SO with yubikey - err := ctx.Login(session, pkcs11.CKU_SO, "010203040506070801020304050607080102030405060708") + err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SO_USER_PIN) if err != nil { return err } @@ -206,8 +214,8 @@ func getECDSAKey(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, pkcs11KeyID []by } // Sign returns a signature for a given signature request -func sign(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, pkcs11KeyID, payload []byte) ([]byte, error) { - err := ctx.Login(session, pkcs11.CKU_USER, USER_PIN) +func sign(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever passphrase.Retriever, payload []byte) ([]byte, error) { + err := login(ctx, session, passRetriever, pkcs11.CKU_USER, USER_PIN) if err != nil { return nil, fmt.Errorf("error logging in: %v", err) } @@ -253,10 +261,12 @@ func sign(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, pkcs11KeyID, payload [] return sig[:], nil } -type YubiKeyStore struct{} +type YubiKeyStore struct { + passRetriever passphrase.Retriever +} -func NewYubiKeyStore() *YubiKeyStore { - return &YubiKeyStore{} +func NewYubiKeyStore(passphraseRetriever passphrase.Retriever) *YubiKeyStore { + return &YubiKeyStore{passRetriever: passphraseRetriever} } func (s *YubiKeyStore) ListKeys() map[string]string { @@ -285,7 +295,7 @@ func (s *YubiKeyStore) AddKey(keyID, alias string, privKey data.PrivateKey) erro } defer cleanup(ctx, session) - return addECDSAKey(ctx, session, privKey, YUBIKEY_ROOT_KEY_ID) + return addECDSAKey(ctx, session, privKey, YUBIKEY_ROOT_KEY_ID, s.passRetriever) } func (s *YubiKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) { @@ -303,7 +313,7 @@ func (s *YubiKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) { if pubKey.ID() != keyID { return nil, "", fmt.Errorf("expected root key: %s, but found: %s\n", keyID, pubKey.ID()) } - privKey := NewYubiPrivateKey(*pubKey) + privKey := NewYubiPrivateKey(*pubKey, s.passRetriever) if privKey == nil { return nil, "", errors.New("could not initialize new YubiPrivateKey") } @@ -367,3 +377,40 @@ func SetupHSMEnv(libraryPath string) (*pkcs11.Ctx, pkcs11.SessionHandle, error) return p, session, nil } + +func login(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, passRetriever passphrase.Retriever, userFlag uint, defaultPassw string) error { + // try default password + err := ctx.Login(session, userFlag, defaultPassw) + if err == nil { + return nil + } + + // default failed, ask user for password + for attempts := 0; ; attempts++ { + var ( + giveup bool + err error + user string + ) + if userFlag == pkcs11.CKU_SO { + user = "SO Pin" + } else { + user = "Pin" + } + passwd, giveup, err := passRetriever(user, "yubikey", false, attempts) + // Check if the passphrase retriever got an error or if it is telling us to give up + if giveup || err != nil { + return trustmanager.ErrPasswordInvalid{} + } + if attempts > 2 { + return trustmanager.ErrAttemptsExceeded{} + } + + // Try to convert PEM encoded bytes back to a PrivateKey using the passphrase + err = ctx.Login(session, userFlag, passwd) + if err == nil { + return nil + } + } + return nil +} From b59225297a2c70ab190fea63c1186011d4e3a113 Mon Sep 17 00:00:00 2001 From: Diogo Monica Date: Fri, 30 Oct 2015 17:05:33 -0700 Subject: [PATCH 06/85] Fixing makefile Signed-off-by: Diogo Monica Signed-off-by: David Lawrence Signed-off-by: Diogo Monica (github: endophage) --- Makefile | 13 +++++++------ client/repo.go | 11 +++++++++-- client/repo_pkcs11.go | 12 ++++++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 6516d5d4a4..745789ef39 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ GO_LDFLAGS=-ldflags "-w $(CTIMEVAR)" GO_LDFLAGS_STATIC=-ldflags "-w $(CTIMEVAR) -extldflags -static" GOOSES = darwin freebsd linux GOARCHS = amd64 +NOTARY_BUILDFLAGS="pkcs11" GO_EXC = go NOTARYDIR := /go/src/github.com/docker/notary @@ -49,15 +50,15 @@ version/version.go: ${PREFIX}/bin/notary-server: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" - @godep go build -o $@ ${GO_LDFLAGS} ./cmd/notary-server + @godep go build -tags ${NOTARY_BUILDFLAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-server ${PREFIX}/bin/notary: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" - @godep go build -o $@ ${GO_LDFLAGS} ./cmd/notary + @godep go build -tags ${NOTARY_BUILDFLAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary ${PREFIX}/bin/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" - @godep go build -o $@ ${GO_LDFLAGS} ./cmd/notary-signer + @godep go build -tags ${NOTARY_BUILDFLAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-signer vet: go_version @echo "+ $@" @@ -73,16 +74,16 @@ lint: build: go_version @echo "+ $@" - @go build -v ${GO_LDFLAGS} ./... + @go build -tags ${NOTARY_BUILDFLAGS} -v ${GO_LDFLAGS} ./... test: OPTS = test: go_version @echo "+ $@ $(OPTS)" - go test $(OPTS) ./... + go test -tags ${NOTARY_BUILDFLAGS} $(OPTS) ./... test-full: vet lint @echo "+ $@" - go test -v ./... + go test -tags ${NOTARY_BUILDFLAGS} -v ./... protos: @protoc --go_out=plugins=grpc:. proto/*.proto diff --git a/client/repo.go b/client/repo.go index 4e7f2a406c..35820d09a1 100644 --- a/client/repo.go +++ b/client/repo.go @@ -3,12 +3,14 @@ package client import ( + "fmt" "net/http" "path/filepath" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/keystoremanager" "github.com/docker/notary/pkg/passphrase" + "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/store" ) @@ -16,9 +18,14 @@ import ( // It takes the base directory under where all the trust files will be stored // (usually ~/.docker/trust/). func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, - passphraseRetriever passphrase.Retriever) (*NotaryRepository, error) { + retriever passphrase.Retriever) (*NotaryRepository, error) { + keysPath := filepath.Join(baseDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) + if err != nil { + return nil, fmt.Errorf("failed to create private key store in directory: %s", keysPath) + } - keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, passphraseRetriever) + keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, fileKeyStore) if err != nil { return nil, err } diff --git a/client/repo_pkcs11.go b/client/repo_pkcs11.go index ab59f39585..45c91cf0e2 100644 --- a/client/repo_pkcs11.go +++ b/client/repo_pkcs11.go @@ -3,6 +3,7 @@ package client import ( + "fmt" "net/http" "path/filepath" @@ -10,6 +11,7 @@ import ( "github.com/docker/notary/keystoremanager" "github.com/docker/notary/pkg/passphrase" "github.com/docker/notary/signer/api" + "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/store" ) @@ -17,14 +19,16 @@ import ( // It takes the base directory under where all the trust files will be stored // (usually ~/.docker/trust/). func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, - passphraseRetriever passphrase.Retriever) (*NotaryRepository, error) { + retriever passphrase.Retriever) (*NotaryRepository, error) { - keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, passphraseRetriever) + keysPath := filepath.Join(baseDir, keystoremanager.PrivDir) + fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, retriever) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create private key store in directory: %s", keysPath) } - yubiKeyStore := api.NewYubiKeyStore() + keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, fileKeyStore) + yubiKeyStore := api.NewYubiKeyStore(retriever) cryptoService := cryptoservice.NewCryptoService(gun, yubiKeyStore, keyStoreManager.KeyStore) nRepo := &NotaryRepository{ From af1bf0c1d5bbebf97517cd1357a3a763af3e5c20 Mon Sep 17 00:00:00 2001 From: Diogo Monica Date: Fri, 30 Oct 2015 18:09:13 -0700 Subject: [PATCH 07/85] Removing debug adding pcs11 to makefiles Signed-off-by: Diogo Monica Signed-off-by: David Lawrence Signed-off-by: Diogo Monica (github: endophage) --- Dockerfile.server | 1 + Dockerfile.signer | 1 + signer/api/ecdsa_hardware_crypto_service.go | 7 ------- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Dockerfile.server b/Dockerfile.server index 9e78872d62..be430f778b 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -15,6 +15,7 @@ COPY . /go/src/github.com/docker/notary WORKDIR /go/src/${NOTARYPKG} RUN go install \ + -tags pkcs11 \ -ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \ ${NOTARYPKG}/cmd/notary-server diff --git a/Dockerfile.signer b/Dockerfile.signer index 1d82a99eb2..e4400bdbb0 100644 --- a/Dockerfile.signer +++ b/Dockerfile.signer @@ -32,6 +32,7 @@ WORKDIR /go/src/${NOTARYPKG} # Install notary-signer RUN go install \ + -tags pkcs11 \ -ldflags "-w -X ${NOTARYPKG}/version.GitCommit=`git rev-parse --short HEAD` -X ${NOTARYPKG}/version.NotaryVersion=`cat NOTARY_VERSION`" \ ${NOTARYPKG}/cmd/notary-signer diff --git a/signer/api/ecdsa_hardware_crypto_service.go b/signer/api/ecdsa_hardware_crypto_service.go index 8c37feaea3..8029241d82 100644 --- a/signer/api/ecdsa_hardware_crypto_service.go +++ b/signer/api/ecdsa_hardware_crypto_service.go @@ -99,7 +99,6 @@ func addECDSAKey(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, privKey data.Pri } defer ctx.Logout(session) - fmt.Println("Parsing EC private key") // Create an ecdsa.PrivateKey out of the private key bytes ecdsaPrivKey, err := x509.ParseECPrivateKey(privKey.Private()) if err != nil { @@ -118,7 +117,6 @@ func addECDSAKey(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, privKey data.Pri if err != nil { return fmt.Errorf("failed to create the certificate: %v", err) } - fmt.Println("Got a certificate!") certTemplate := []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), @@ -138,14 +136,11 @@ func addECDSAKey(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, privKey data.Pri pkcs11.NewAttribute(pkcs11.CKA_VENDOR_DEFINED, 3), } - fmt.Println("About to import the certificate") - _, err = ctx.CreateObject(session, certTemplate) if err != nil { return fmt.Errorf("error importing: %v", err) } - fmt.Println("About to import the private key") _, err = ctx.CreateObject(session, privateKeyTemplate) if err != nil { return fmt.Errorf("error importing: %v", err) @@ -340,7 +335,6 @@ func (s *YubiKeyStore) ImportKey(pemBytes []byte, keyID string) error { } func cleanup(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) { - fmt.Println("Tearing down yubikey session") ctx.CloseSession(session) ctx.Finalize() ctx.Destroy() @@ -348,7 +342,6 @@ func cleanup(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) { // SetupHSMEnv is a method that depends on the existences func SetupHSMEnv(libraryPath string) (*pkcs11.Ctx, pkcs11.SessionHandle, error) { - fmt.Println("Setting up Yubikey") p := pkcs11.New(libraryPath) if p == nil { From 913c5ef0336fddc2656555749268eeb9856964a6 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 30 Oct 2015 20:15:31 -0700 Subject: [PATCH 08/85] add build tag files for pkcs11 dlopen lib Signed-off-by: Jessica Frazelle Signed-off-by: David Lawrence Signed-off-by: Jessica Frazelle (github: endophage) --- signer/api/ecdsa_hardware_crypto_service.go | 1 - signer/api/pkcs11_darwin.go | 12 ++++++++++++ signer/api/pkcs11_linux.go | 12 ++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 signer/api/pkcs11_darwin.go create mode 100644 signer/api/pkcs11_linux.go diff --git a/signer/api/ecdsa_hardware_crypto_service.go b/signer/api/ecdsa_hardware_crypto_service.go index 8029241d82..4b68240f0a 100644 --- a/signer/api/ecdsa_hardware_crypto_service.go +++ b/signer/api/ecdsa_hardware_crypto_service.go @@ -22,7 +22,6 @@ import ( ) const ( - pkcs11Lib = "/usr/local/lib/libykcs11.so" USER_PIN = "123456" SO_USER_PIN = "010203040506070801020304050607080102030405060708" ) diff --git a/signer/api/pkcs11_darwin.go b/signer/api/pkcs11_darwin.go new file mode 100644 index 0000000000..eba8a4ee2c --- /dev/null +++ b/signer/api/pkcs11_darwin.go @@ -0,0 +1,12 @@ +// +build pkcs11,darwin + +package api + +var ( + pkcs11Lib = "/usr/local/lib/libykcs11.dylib" +) + +func init() { + // TODO(diogomonica): all the crap for darwin to configure + // the variable pkcs11 to find the right one in the right dir +} diff --git a/signer/api/pkcs11_linux.go b/signer/api/pkcs11_linux.go new file mode 100644 index 0000000000..acade89b11 --- /dev/null +++ b/signer/api/pkcs11_linux.go @@ -0,0 +1,12 @@ +// +build pkcs11,linux + +package api + +var ( + pkcs11Lib = "/usr/local/lib/libykcs11.so" +) + +func init() { + // TODO(diogomonica): all the crap for linux to configure + // the variable pkcs11 to find the right one in the right dir +} From 5f21ebd1853d293c5cf328188331627612e2e114 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 30 Oct 2015 20:53:10 -0700 Subject: [PATCH 09/85] Add pkcs11 build tags Add build tags and a check in Makefile to be sure you do not import pkcs11 lib somewhere where it should not be. This will ensure docker import and integration will continue to work. Signed-off-by: Jessica Frazelle Signed-off-by: David Lawrence Signed-off-by: Jessica Frazelle (github: endophage) --- Makefile | 29 +++++++++------ cmd/notary-signer/main.go | 2 ++ cmd/notary-signer/main_test.go | 2 ++ signer/api/api_pkcs11_test.go | 43 +++++++++++++++++++++++ signer/api/api_test.go | 35 ------------------ signer/api/rsa_hardware_crypto_service.go | 2 ++ signer/keys/keys.go | 2 ++ 7 files changed, 69 insertions(+), 46 deletions(-) create mode 100644 signer/api/api_pkcs11_test.go diff --git a/Makefile b/Makefile index 745789ef39..e09918c1a7 100644 --- a/Makefile +++ b/Makefile @@ -8,17 +8,25 @@ NOTARY_VERSION := $(shell cat NOTARY_VERSION) GITCOMMIT := $(shell git rev-parse --short HEAD) GITUNTRACKEDCHANGES := $(shell git status --porcelain --untracked-files=no) ifneq ($(GITUNTRACKEDCHANGES),) - GITCOMMIT := $(GITCOMMIT)-dirty +GITCOMMIT := $(GITCOMMIT)-dirty endif CTIMEVAR=-X $(NOTARY_PKG)/version.GitCommit='$(GITCOMMIT)' -X $(NOTARY_PKG)/version.NotaryVersion='$(NOTARY_VERSION)' GO_LDFLAGS=-ldflags "-w $(CTIMEVAR)" GO_LDFLAGS_STATIC=-ldflags "-w $(CTIMEVAR) -extldflags -static" GOOSES = darwin freebsd linux GOARCHS = amd64 -NOTARY_BUILDFLAGS="pkcs11" +NOTARY_BUILDTAGS="pkcs11" GO_EXC = go NOTARYDIR := /go/src/github.com/docker/notary +# check to be sure pkcs11 lib is always imported with a build tag +GO_LIST_PKCS11 := $(shell go list -e -f '{{join .Deps "\n"}}' ./... | xargs go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | grep -q pkcs11) +ifeq ($(GO_LIST_PKCS11),) +$(info pkcs11 import was not found anywhere without a build tag, yay) +else +$(error You are importing pkcs11 somewhere and not using a build tag) +endif + # go cover test variables COVERDIR=.cover COVERPROFILE=$(COVERDIR)/cover.out @@ -27,7 +35,7 @@ PKGS = $(shell go list ./... | tr '\n' ' ') GO_VERSION = $(shell go version | awk '{print $$3}') -.PHONY: clean all fmt vet lint build test binaries cross cover docker-images +.PHONY: clean all fmt vet lint build test binaries cross cover docker-images notary-dockerfile .DELETE_ON_ERROR: cover .DEFAULT: default @@ -50,15 +58,15 @@ version/version.go: ${PREFIX}/bin/notary-server: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" - @godep go build -tags ${NOTARY_BUILDFLAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-server + @godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-server ${PREFIX}/bin/notary: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" - @godep go build -tags ${NOTARY_BUILDFLAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary + @godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary ${PREFIX}/bin/notary-signer: NOTARY_VERSION $(shell find . -type f -name '*.go') @echo "+ $@" - @godep go build -tags ${NOTARY_BUILDFLAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-signer + @godep go build -tags ${NOTARY_BUILDTAGS} -o $@ ${GO_LDFLAGS} ./cmd/notary-signer vet: go_version @echo "+ $@" @@ -74,21 +82,20 @@ lint: build: go_version @echo "+ $@" - @go build -tags ${NOTARY_BUILDFLAGS} -v ${GO_LDFLAGS} ./... + @go build -tags ${NOTARY_BUILDTAGS} -v ${GO_LDFLAGS} ./... test: OPTS = test: go_version @echo "+ $@ $(OPTS)" - go test -tags ${NOTARY_BUILDFLAGS} $(OPTS) ./... + go test -tags ${NOTARY_BUILDTAGS} $(OPTS) ./... test-full: vet lint @echo "+ $@" - go test -tags ${NOTARY_BUILDFLAGS} -v ./... + go test -tags ${NOTARY_BUILDTAGS} -v ./... protos: @protoc --go_out=plugins=grpc:. proto/*.proto - # This allows coverage for a package to come from tests in different package. # Requires that the following: # go get github.com/wadey/gocovmerge; go install github.com/wadey/gocovmerge @@ -112,7 +119,7 @@ cover: gen-cover @go tool cover -html="$(COVERPROFILE)" # Codecov knows how to merge multiple coverage files -ci: OPTS = -race -coverpkg "$(shell ./coverpkg.sh $(1) $(NOTARY_PKG))" +ci: OPTS = -tags ${NOTARY_BUILDTAGS} -race -coverpkg "$(shell ./coverpkg.sh $(1) $(NOTARY_PKG))" GO_EXC := godep go ci: gen-cover @gocovmerge $(shell ls -1 $(COVERDIR)/* | tr "\n" " ") > $(COVERPROFILE) diff --git a/cmd/notary-signer/main.go b/cmd/notary-signer/main.go index a904ba76ae..5b16c622fa 100644 --- a/cmd/notary-signer/main.go +++ b/cmd/notary-signer/main.go @@ -1,3 +1,5 @@ +// +build pkcs11 + package main import ( diff --git a/cmd/notary-signer/main_test.go b/cmd/notary-signer/main_test.go index 15b2c2ad1e..0864708f78 100644 --- a/cmd/notary-signer/main_test.go +++ b/cmd/notary-signer/main_test.go @@ -1,3 +1,5 @@ +// +build pkcs11 + package main import ( diff --git a/signer/api/api_pkcs11_test.go b/signer/api/api_pkcs11_test.go new file mode 100644 index 0000000000..f73091e916 --- /dev/null +++ b/signer/api/api_pkcs11_test.go @@ -0,0 +1,43 @@ +// +build pkcs11 + +package api_test + +import ( + "os" + "testing" + + "github.com/miekg/pkcs11" +) + +func SetupHSMEnv(t *testing.T) (*pkcs11.Ctx, pkcs11.SessionHandle) { + var libPath = "/usr/local/lib/softhsm/libsofthsm2.so" + if _, err := os.Stat(libPath); err != nil { + t.Skipf("Skipping test. Library path: %s does not exist", libPath) + } + + p := pkcs11.New(libPath) + + if p == nil { + t.Fatalf("Failed to init library") + } + + if err := p.Initialize(); err != nil { + t.Fatalf("Initialize error %s\n", err.Error()) + } + + slots, err := p.GetSlotList(true) + if err != nil { + t.Fatalf("Failed to list HSM slots %s", err) + } + + session, err := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) + if err != nil { + t.Fatalf("Failed to Start Session with HSM %s", err) + } + + if err = p.Login(session, pkcs11.CKU_USER, "1234"); err != nil { + t.Fatalf("User PIN %s\n", err.Error()) + } + + return p, session +} diff --git a/signer/api/api_test.go b/signer/api/api_test.go index 4fcf24ea23..729a3aaee7 100644 --- a/signer/api/api_test.go +++ b/signer/api/api_test.go @@ -7,7 +7,6 @@ import ( "io/ioutil" "net/http" "net/http/httptest" - "os" "strings" "testing" @@ -16,7 +15,6 @@ import ( "github.com/docker/notary/signer/api" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" - "github.com/miekg/pkcs11" "github.com/stretchr/testify/assert" pb "github.com/docker/notary/proto" @@ -32,39 +30,6 @@ var ( passphraseRetriever = func(string, string, bool, int) (string, bool, error) { return "passphrase", false, nil } ) -func SetupHSMEnv(t *testing.T) (*pkcs11.Ctx, pkcs11.SessionHandle) { - var libPath = "/usr/local/lib/softhsm/libsofthsm2.so" - if _, err := os.Stat(libPath); err != nil { - t.Skipf("Skipping test. Library path: %s does not exist", libPath) - } - - p := pkcs11.New(libPath) - - if p == nil { - t.Fatalf("Failed to init library") - } - - if err := p.Initialize(); err != nil { - t.Fatalf("Initialize error %s\n", err.Error()) - } - - slots, err := p.GetSlotList(true) - if err != nil { - t.Fatalf("Failed to list HSM slots %s", err) - } - - session, err := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) - if err != nil { - t.Fatalf("Failed to Start Session with HSM %s", err) - } - - if err = p.Login(session, pkcs11.CKU_USER, "1234"); err != nil { - t.Fatalf("User PIN %s\n", err.Error()) - } - - return p, session -} - func setup(cryptoServices signer.CryptoServiceIndex) { server = httptest.NewServer(api.Handlers(cryptoServices)) deleteKeyBaseURL = fmt.Sprintf("%s/delete", server.URL) diff --git a/signer/api/rsa_hardware_crypto_service.go b/signer/api/rsa_hardware_crypto_service.go index 8e673be341..640f2891d4 100644 --- a/signer/api/rsa_hardware_crypto_service.go +++ b/signer/api/rsa_hardware_crypto_service.go @@ -1,3 +1,5 @@ +// +build pkcs11 + package api import ( diff --git a/signer/keys/keys.go b/signer/keys/keys.go index 80396c6939..6403c217e2 100644 --- a/signer/keys/keys.go +++ b/signer/keys/keys.go @@ -1,3 +1,5 @@ +// +build pkcs11 + package keys import ( From 8902c8c0e99125ea08a10befbc5cf3ac2c0f4ab5 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 30 Oct 2015 21:23:55 -0700 Subject: [PATCH 10/85] fix go lint Signed-off-by: Jessica Frazelle Signed-off-by: David Lawrence Signed-off-by: Jessica Frazelle (github: endophage) --- keystoremanager/keystoremanager.go | 3 ++- tuf/data/keys.go | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/keystoremanager/keystoremanager.go b/keystoremanager/keystoremanager.go index 09f336de0b..6b61f1c009 100644 --- a/keystoremanager/keystoremanager.go +++ b/keystoremanager/keystoremanager.go @@ -24,7 +24,8 @@ type KeyStoreManager struct { } const ( - trustDir = "trusted_certificates" + trustDir = "trusted_certificates" + // PrivDir is the name of the private directory PrivDir = "private" rsaRootKeySize = 4096 // Used for new root keys ) diff --git a/tuf/data/keys.go b/tuf/data/keys.go index 22a1fb786c..63582db054 100644 --- a/tuf/data/keys.go +++ b/tuf/data/keys.go @@ -486,18 +486,22 @@ func (k UnknownPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOp return nil, errors.New("Unknown key type, cannot sign.") } +// SignatureAlgorithm returns the SigAlgorithm for a ECDSAPrivateKey func (k ECDSAPrivateKey) SignatureAlgorithm() SigAlgorithm { return ECDSASignature } +// SignatureAlgorithm returns the SigAlgorithm for a RSAPrivateKey func (k RSAPrivateKey) SignatureAlgorithm() SigAlgorithm { return RSAPSSSignature } +// SignatureAlgorithm returns the SigAlgorithm for a ED25519PrivateKey func (k ED25519PrivateKey) SignatureAlgorithm() SigAlgorithm { return EDDSASignature } +// SignatureAlgorithm returns the SigAlgorithm for an UnknownPrivateKey func (k UnknownPrivateKey) SignatureAlgorithm() SigAlgorithm { return "" } From 68992ddaf5bca3f0123eab6c2ae302eb727f6c2c Mon Sep 17 00:00:00 2001 From: Diogo Monica Date: Sun, 1 Nov 2015 11:57:56 -0800 Subject: [PATCH 11/85] Resolving rebase conflicts Signed-off-by: David Lawrence Signed-off-by: Diogo Monica (github: endophage) --- client/client.go | 44 -------------------------------------------- 1 file changed, 44 deletions(-) diff --git a/client/client.go b/client/client.go index eb43189a00..ec91fbd272 100644 --- a/client/client.go +++ b/client/client.go @@ -98,50 +98,6 @@ func NewTarget(targetName string, targetPath string) (*Target, error) { return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length}, nil } -// NewNotaryRepository is a helper method that returns a new notary repository. -// It takes the base directory under where all the trust files will be stored -// (usually ~/.docker/trust/). -func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, - passphraseRetriever passphrase.Retriever) (*NotaryRepository, error) { - - keysPath := filepath.Join(baseDir, keystoremanager.PrivDir) - fileKeyStore, err := trustmanager.NewKeyFileStore(keysPath, passphraseRetriever) - if err != nil { - return nil, err - } - - keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, fileKeyStore) - if err != nil { - return nil, err - } - - yubiKeyStore := api.NewYubiKeyStore(passphraseRetriever) - cryptoService := cryptoservice.NewCryptoService(gun, yubiKeyStore, fileKeyStore) - - nRepo := &NotaryRepository{ - gun: gun, - baseDir: baseDir, - baseURL: baseURL, - tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), - CryptoService: cryptoService, - roundTrip: rt, - KeyStoreManager: keyStoreManager, - } - - fileStore, err := store.NewFilesystemStore( - nRepo.tufRepoPath, - "metadata", - "json", - "", - ) - if err != nil { - return nil, err - } - nRepo.fileStore = fileStore - - return nRepo, nil -} - // Initialize creates a new repository by using rootKey as the root Key for the // TUF repository. func (r *NotaryRepository) Initialize(rootKeyID string) error { From 22244fff659886ca7f68b41d8f694b163ef4cddb Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Tue, 3 Nov 2015 10:57:56 -0800 Subject: [PATCH 12/85] improve password challenge messaging when using yubikey Signed-off-by: David Lawrence (github: endophage) --- pkg/passphrase/passphrase.go | 4 +++- signer/api/ecdsa_hardware_crypto_service.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/passphrase/passphrase.go b/pkg/passphrase/passphrase.go index 9359e0efc0..b00368fa8e 100644 --- a/pkg/passphrase/passphrase.go +++ b/pkg/passphrase/passphrase.go @@ -125,8 +125,10 @@ func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]s if createNew { fmt.Fprintf(out, "Enter passphrase for new %s key with id %s: ", displayAlias, keyName) + } else if displayAlias == "yubikey" { + fmt.Fprintf(out, "Enter the %s for the attached Yubikey: ", keyName) } else { - fmt.Fprintf(out, "Enter key passphrase for %s key with id %s: ", displayAlias, keyName) + fmt.Fprintf(out, "Enter passphrase for %s key with id %s: ", displayAlias, keyName) } passphrase, err := stdin.ReadBytes('\n') diff --git a/signer/api/ecdsa_hardware_crypto_service.go b/signer/api/ecdsa_hardware_crypto_service.go index 4b68240f0a..8da450e7d6 100644 --- a/signer/api/ecdsa_hardware_crypto_service.go +++ b/signer/api/ecdsa_hardware_crypto_service.go @@ -387,7 +387,7 @@ func login(ctx *pkcs11.Ctx, session pkcs11.SessionHandle, passRetriever passphra if userFlag == pkcs11.CKU_SO { user = "SO Pin" } else { - user = "Pin" + user = "User Pin" } passwd, giveup, err := passRetriever(user, "yubikey", false, attempts) // Check if the passphrase retriever got an error or if it is telling us to give up From bc0c0d4ea1d2dc36a696ada0b460b946044f165f Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Tue, 3 Nov 2015 10:28:56 -0800 Subject: [PATCH 13/85] health check will never be able to get auth token so remove RootHandler wrapper Signed-off-by: David Lawrence (github: endophage) --- server/server.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/server/server.go b/server/server.go index 66935da786..ca56f359c3 100644 --- a/server/server.go +++ b/server/server.go @@ -105,11 +105,8 @@ func RootHandler(ac auth.AccessController, ctx context.Context, trust signed.Cry prometheus.InstrumentHandlerWithOpts( prometheusOpts("DeleteTuf"), hand(handlers.DeleteHandler, "push", "pull"))) - r.Methods("GET").Path("/_notary_server/health").Handler(hand( - func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - health.StatusHandler(w, r) - return nil - })) + + r.Methods("GET").Path("/_notary_server/health").HandlerFunc(health.StatusHandler) r.Methods("GET").Path("/_notary_server/metrics").Handler(prometheus.Handler()) r.Methods("GET", "POST", "PUT", "HEAD", "DELETE").Path("/{other:.*}").Handler(hand(utils.NotFoundHandler)) From 519a2ccbe8e24136a2b2a9803a716659fc6acd85 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Mon, 2 Nov 2015 14:18:30 -0800 Subject: [PATCH 14/85] removing all errors that aren't in use, fixing one place in memorystore that was using a different errorcode to all other stores, pushing errors into appropriate packages Signed-off-by: David Lawrence (github: endophage) --- client/client.go | 3 +- tuf/client/errors.go | 70 ---------------------------- tuf/data/roles.go | 19 ++++++-- tuf/errors/errors.go | 98 ---------------------------------------- tuf/keys/db.go | 3 +- tuf/signed/errors.go | 11 +++++ tuf/signed/sign.go | 3 +- tuf/store/memorystore.go | 3 +- tuf/tuf.go | 11 ++--- 9 files changed, 34 insertions(+), 187 deletions(-) delete mode 100644 tuf/errors/errors.go diff --git a/client/client.go b/client/client.go index ec91fbd272..b029e4c569 100644 --- a/client/client.go +++ b/client/client.go @@ -18,7 +18,6 @@ import ( "github.com/docker/notary/tuf" tufclient "github.com/docker/notary/tuf/client" "github.com/docker/notary/tuf/data" - tuferrors "github.com/docker/notary/tuf/errors" "github.com/docker/notary/tuf/keys" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/store" @@ -174,7 +173,7 @@ func (r *NotaryRepository) Initialize(rootKeyID string) error { if err != nil { logrus.Debug("Error on InitRoot: ", err.Error()) switch err.(type) { - case tuferrors.ErrInsufficientSignatures, trustmanager.ErrPasswordInvalid: + case signed.ErrInsufficientSignatures, trustmanager.ErrPasswordInvalid: default: return err } diff --git a/tuf/client/errors.go b/tuf/client/errors.go index 0fb1a8d00c..037b3df00b 100644 --- a/tuf/client/errors.go +++ b/tuf/client/errors.go @@ -1,16 +1,9 @@ package client import ( - "errors" "fmt" ) -// Simple client errors -var ( - ErrNoRootKeys = errors.New("tuf: no root keys found in local meta store") - ErrInsufficientKeys = errors.New("tuf: insufficient keys to meet threshold") -) - // ErrChecksumMismatch - a checksum failed verification type ErrChecksumMismatch struct { role string @@ -29,69 +22,6 @@ func (e ErrMissingMeta) Error() string { return fmt.Sprintf("tuf: sha256 checksum required for %s", e.role) } -// ErrMissingRemoteMetadata - remote didn't have requested metadata -type ErrMissingRemoteMetadata struct { - Name string -} - -func (e ErrMissingRemoteMetadata) Error() string { - return fmt.Sprintf("tuf: missing remote metadata %s", e.Name) -} - -// ErrDownloadFailed - a download failed -type ErrDownloadFailed struct { - File string - Err error -} - -func (e ErrDownloadFailed) Error() string { - return fmt.Sprintf("tuf: failed to download %s: %s", e.File, e.Err) -} - -// ErrDecodeFailed - couldn't parse a download -type ErrDecodeFailed struct { - File string - Err error -} - -func (e ErrDecodeFailed) Error() string { - return fmt.Sprintf("tuf: failed to decode %s: %s", e.File, e.Err) -} - -func isDecodeFailedWithErr(err, expected error) bool { - e, ok := err.(ErrDecodeFailed) - if !ok { - return false - } - return e.Err == expected -} - -// ErrNotFound - didn't find a file -type ErrNotFound struct { - File string -} - -func (e ErrNotFound) Error() string { - return fmt.Sprintf("tuf: file not found: %s", e.File) -} - -// IsNotFound - check if an error is an ErrNotFound type -func IsNotFound(err error) bool { - _, ok := err.(ErrNotFound) - return ok -} - -// ErrWrongSize - the size is wrong -type ErrWrongSize struct { - File string - Actual int64 - Expected int64 -} - -func (e ErrWrongSize) Error() string { - return fmt.Sprintf("tuf: unexpected file size: %s (expected %d bytes, got %d bytes)", e.File, e.Expected, e.Actual) -} - // ErrCorruptedCache - local data is incorrect type ErrCorruptedCache struct { file string diff --git a/tuf/data/roles.go b/tuf/data/roles.go index d2d0063814..0e6541c978 100644 --- a/tuf/data/roles.go +++ b/tuf/data/roles.go @@ -3,8 +3,6 @@ package data import ( "fmt" "strings" - - "github.com/docker/notary/tuf/errors" ) // Canonical base role names @@ -26,6 +24,17 @@ var ValidRoles = map[string]string{ CanonicalTimestampRole: CanonicalTimestampRole, } +// ErrInvalidRole represents an error regarding a role. Typically +// something like a role for which sone of the public keys were +// not found in the TUF repo. +type ErrInvalidRole struct { + Role string +} + +func (e ErrInvalidRole) Error() string { + return fmt.Sprintf("tuf: invalid role %s", e.Role) +} + // SetValidRoles is a utility function to override some or all of the roles func SetValidRoles(rs map[string]string) { // iterate ValidRoles @@ -106,13 +115,13 @@ type Role struct { // NewRole creates a new Role object from the given parameters func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []string) (*Role, error) { if len(paths) > 0 && len(pathHashPrefixes) > 0 { - return nil, errors.ErrInvalidRole{} + return nil, ErrInvalidRole{Role: name} } if threshold < 1 { - return nil, errors.ErrInvalidRole{} + return nil, ErrInvalidRole{Role: name} } if !ValidRole(name) { - return nil, errors.ErrInvalidRole{} + return nil, ErrInvalidRole{Role: name} } return &Role{ RootRole: RootRole{ diff --git a/tuf/errors/errors.go b/tuf/errors/errors.go deleted file mode 100644 index 763be52fb4..0000000000 --- a/tuf/errors/errors.go +++ /dev/null @@ -1,98 +0,0 @@ -package errors - -import ( - "errors" - "fmt" - "time" -) - -// ErrInitNotAllowed - repo has already been initialized -var ErrInitNotAllowed = errors.New("tuf: repository already initialized") - -// ErrMissingMetadata - cannot find the file meta being requested. -// Specifically, could not find the FileMeta object in the expected -// location. -type ErrMissingMetadata struct { - Name string -} - -func (e ErrMissingMetadata) Error() string { - return fmt.Sprintf("tuf: missing metadata %s", e.Name) -} - -// ErrFileNotFound - could not find a file -type ErrFileNotFound struct { - Path string -} - -func (e ErrFileNotFound) Error() string { - return fmt.Sprintf("tuf: file not found %s", e.Path) -} - -// ErrInsufficientKeys - did not have enough keys to sign when requested -type ErrInsufficientKeys struct { - Name string -} - -func (e ErrInsufficientKeys) Error() string { - return fmt.Sprintf("tuf: insufficient keys to sign %s", e.Name) -} - -// ErrInsufficientSignatures - do not have enough signatures on a piece of -// metadata -type ErrInsufficientSignatures struct { - Name string - Err error -} - -func (e ErrInsufficientSignatures) Error() string { - return fmt.Sprintf("tuf: insufficient signatures for %s: %s", e.Name, e.Err) -} - -// ErrInvalidRole - role is wrong. Typically we're missing the public keys for it -type ErrInvalidRole struct { - Role string -} - -func (e ErrInvalidRole) Error() string { - return fmt.Sprintf("tuf: invalid role %s", e.Role) -} - -// ErrInvalidExpires - the expiry time for a metadata file is invalid -type ErrInvalidExpires struct { - Expires time.Time -} - -func (e ErrInvalidExpires) Error() string { - return fmt.Sprintf("tuf: invalid expires: %s", e.Expires) -} - -// ErrKeyNotFound - could not find a given key on a role -type ErrKeyNotFound struct { - Role string - KeyID string -} - -func (e ErrKeyNotFound) Error() string { - return fmt.Sprintf(`tuf: no key with id "%s" exists for the %s role`, e.KeyID, e.Role) -} - -// ErrNotEnoughKeys - there are not enough keys to ever meet the signature threshold -type ErrNotEnoughKeys struct { - Role string - Keys int - Threshold int -} - -func (e ErrNotEnoughKeys) Error() string { - return fmt.Sprintf("tuf: %s role has insufficient keys for threshold (has %d keys, threshold is %d)", e.Role, e.Keys, e.Threshold) -} - -// ErrPassphraseRequired - a passphrase is needed and wasn't provided -type ErrPassphraseRequired struct { - Role string -} - -func (e ErrPassphraseRequired) Error() string { - return fmt.Sprintf("tuf: a passphrase is required to access the encrypted %s keys file", e.Role) -} diff --git a/tuf/keys/db.go b/tuf/keys/db.go index 30afc8d1dc..2e27d052cd 100644 --- a/tuf/keys/db.go +++ b/tuf/keys/db.go @@ -12,7 +12,6 @@ var ( ErrExists = errors.New("tuf: key already in db") ErrWrongID = errors.New("tuf: key id mismatch") ErrInvalidKey = errors.New("tuf: invalid key") - ErrInvalidRole = errors.New("tuf: invalid role") ErrInvalidKeyID = errors.New("tuf: invalid key id") ErrInvalidThreshold = errors.New("tuf: invalid role threshold") ) @@ -42,7 +41,7 @@ func (db *KeyDB) AddKey(k data.PublicKey) { // role must have already been added. func (db *KeyDB) AddRole(r *data.Role) error { if !data.ValidRole(r.Name) { - return ErrInvalidRole + return data.ErrInvalidRole{Role: r.Name} } if r.Threshold < 1 { return ErrInvalidThreshold diff --git a/tuf/signed/errors.go b/tuf/signed/errors.go index 0c92f3465e..c042e60685 100644 --- a/tuf/signed/errors.go +++ b/tuf/signed/errors.go @@ -4,6 +4,17 @@ import ( "fmt" ) +// ErrInsufficientSignatures - do not have enough signatures on a piece of +// metadata +type ErrInsufficientSignatures struct { + Name string + Err error +} + +func (e ErrInsufficientSignatures) Error() string { + return fmt.Sprintf("tuf: insufficient signatures for %s: %s", e.Name, e.Err) +} + // ErrExpired indicates a piece of metadata has expired type ErrExpired struct { Role string diff --git a/tuf/signed/sign.go b/tuf/signed/sign.go index c33aa075d4..99987a2fb5 100644 --- a/tuf/signed/sign.go +++ b/tuf/signed/sign.go @@ -16,7 +16,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/notary/tuf/data" - "github.com/docker/notary/tuf/errors" "github.com/docker/notary/tuf/utils" ) @@ -68,7 +67,7 @@ func Sign(service CryptoService, s *data.Signed, keys ...data.PublicKey) error { } } if len(signatures) < 1 { - return errors.ErrInsufficientSignatures{ + return ErrInsufficientSignatures{ Name: fmt.Sprintf("Cryptoservice failed to produce any signatures for keys with IDs: %v", keyIDs), Err: nil, } diff --git a/tuf/store/memorystore.go b/tuf/store/memorystore.go index 89284a80f2..0e17889891 100644 --- a/tuf/store/memorystore.go +++ b/tuf/store/memorystore.go @@ -6,7 +6,6 @@ import ( "io" "github.com/docker/notary/tuf/data" - "github.com/docker/notary/tuf/errors" "github.com/docker/notary/tuf/utils" ) @@ -76,7 +75,7 @@ func (m *memoryStore) WalkStagedTargets(paths []string, targetsFn targetsWalkFun for _, path := range paths { dat, ok := m.files[path] if !ok { - return errors.ErrFileNotFound{Path: path} + return ErrMetaNotFound{} } meta, err := data.NewFileMeta(bytes.NewReader(dat), "sha256") if err != nil { diff --git a/tuf/tuf.go b/tuf/tuf.go index 4f7a5ffa59..33dd9448a8 100644 --- a/tuf/tuf.go +++ b/tuf/tuf.go @@ -13,7 +13,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/notary/tuf/data" - "github.com/docker/notary/tuf/errors" "github.com/docker/notary/tuf/keys" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/utils" @@ -163,12 +162,12 @@ func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error { // A new, empty, targets file will be created for the new role. func (tr *Repo) UpdateDelegations(role *data.Role, keys []data.PublicKey, before string) error { if !role.IsDelegation() || !role.IsValid() { - return errors.ErrInvalidRole{} + return data.ErrInvalidRole{Role: role.Name} } parent := filepath.Dir(role.Name) p, ok := tr.Targets[parent] if !ok { - return errors.ErrInvalidRole{} + return data.ErrInvalidRole{Role: role.Name} } for _, k := range keys { if !utils.StrSliceContains(role.KeyIDs, k.ID()) { @@ -225,7 +224,7 @@ func (tr *Repo) InitRoot(consistent bool) error { for _, r := range data.ValidRoles { role := tr.keysDB.GetRole(r) if role == nil { - return errors.ErrInvalidRole{} + return data.ErrInvalidRole{Role: data.CanonicalRootRole} } rootRoles[r] = &role.RootRole for _, kid := range role.KeyIDs { @@ -404,7 +403,7 @@ func (tr Repo) FindTarget(path string) *data.FileMeta { func (tr *Repo) AddTargets(role string, targets data.Files) (data.Files, error) { t, ok := tr.Targets[role] if !ok { - return targets, errors.ErrInvalidRole{Role: role} + return targets, data.ErrInvalidRole{Role: role} } invalid := make(data.Files) for path, target := range targets { @@ -428,7 +427,7 @@ func (tr *Repo) AddTargets(role string, targets data.Files) (data.Files, error) func (tr *Repo) RemoveTargets(role string, targets ...string) error { t, ok := tr.Targets[role] if !ok { - return errors.ErrInvalidRole{Role: role} + return data.ErrInvalidRole{Role: role} } for _, path := range targets { From 2a9e163bd28f90368fd54a38ccdc0ee126f149c1 Mon Sep 17 00:00:00 2001 From: Ying Li Date: Wed, 4 Nov 2015 13:39:23 -0800 Subject: [PATCH 15/85] Fixed cryptoservice.Create to call keyStore.AddKey with a GUN only if it is not a root role. Updated the cryptoservice tests to test all key algorithms, all roles, and cryptoservices without a GUN. This then also found bugs in cryptoservice.GetKey, cryptoservice.RemoveKey, and cryptoservice.GetPrivateKey, which weren't really being exercised previously. Signed-off-by: Ying Li Signed-off-by: David Lawrence Signed-off-by: Ying Li (github: endophage) --- cryptoservice/certificate_test.go | 2 +- cryptoservice/crypto_service.go | 65 ++++++------ cryptoservice/crypto_service_test.go | 148 ++++++++++++++++++++++----- 3 files changed, 159 insertions(+), 56 deletions(-) diff --git a/cryptoservice/certificate_test.go b/cryptoservice/certificate_test.go index 94b0eae8be..a88fa8754b 100644 --- a/cryptoservice/certificate_test.go +++ b/cryptoservice/certificate_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestUnlockedSigner(t *testing.T) { +func TestGenerateCertificate(t *testing.T) { privKey, err := trustmanager.GenerateECDSAKey(rand.Reader) assert.NoError(t, err, "could not generate key") diff --git a/cryptoservice/crypto_service.go b/cryptoservice/crypto_service.go index ec0971c34e..61030af0d0 100644 --- a/cryptoservice/crypto_service.go +++ b/cryptoservice/crypto_service.go @@ -53,8 +53,15 @@ func (ccs *CryptoService) Create(role, algorithm string) (data.PublicKey, error) logrus.Debugf("generated new %s key for role: %s and keyID: %s", algorithm, role, privKey.ID()) // Store the private key into our keystore with the name being: /GUN/ID.key with an alias of role + var keyPath string + if role == data.CanonicalRootRole { + keyPath = privKey.ID() + } else { + keyPath = filepath.Join(ccs.gun, privKey.ID()) + } + for _, ks := range ccs.keyStores { - err = ks.AddKey(filepath.Join(ccs.gun, privKey.ID()), role, privKey) + err = ks.AddKey(keyPath, role, privKey) if err == nil { return data.PublicKeyFromPrivate(privKey), nil } @@ -66,40 +73,47 @@ func (ccs *CryptoService) Create(role, algorithm string) (data.PublicKey, error) } -// GetPrivateKey returns a private key by ID +// GetPrivateKey returns a private key by ID. It tries to get the key first +// without a GUN (in which case it's a root key). If that fails, try to get +// the key with the GUN (non-root key). +// If that fails, then we don't have the key. func (ccs *CryptoService) GetPrivateKey(keyID string) (k data.PrivateKey, id string, err error) { + keyPaths := []string{keyID, filepath.Join(ccs.gun, keyID)} for _, ks := range ccs.keyStores { - k, id, err = ks.GetKey(keyID) - if k == nil || err != nil { - continue + for _, keyPath := range keyPaths { + k, id, err = ks.GetKey(keyPath) + if err != nil { + continue + } + return } - return } return // returns whatever the final values were } // GetKey returns a key by ID func (ccs *CryptoService) GetKey(keyID string) data.PublicKey { - for _, ks := range ccs.keyStores { - k, _, err := ks.GetKey(keyID) - if k == nil || err != nil { - continue - } - return data.PublicKeyFromPrivate(k) - + privKey, _, err := ccs.GetPrivateKey(keyID) + if err != nil { + return nil } - return nil // returns whatever the final values were + return data.PublicKeyFromPrivate(privKey) } // RemoveKey deletes a key by ID func (ccs *CryptoService) RemoveKey(keyID string) (err error) { + keyPaths := []string{keyID, filepath.Join(ccs.gun, keyID)} for _, ks := range ccs.keyStores { - e := ks.RemoveKey(keyID) - if e != nil { - err = e + for _, keyPath := range keyPaths { + _, _, err = ks.GetKey(keyPath) + if err != nil { + continue + } + err = ks.RemoveKey(keyPath) + return } } - return // returns last error if any + return // returns whatever the final values were } // Sign returns the signatures for the payload with a set of keyIDs. It ignores @@ -110,25 +124,18 @@ func (ccs *CryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signatur for _, keyid := range keyIDs { keyName := keyid - // Try to get the key first without a GUN (in which case it's a root - // key). If that fails, try to get the key with the GUN (non-root - // key). If that fails, then we don't have the key. privKey, _, err := ccs.GetPrivateKey(keyName) if err != nil { logrus.Debugf("error attempting to retrieve private key: %s, %v", keyid, err) - keyName = filepath.Join(ccs.gun, keyid) - privKey, _, err = ccs.GetPrivateKey(keyName) - if err != nil { - logrus.Debugf("error attempting to retrieve key ID: %s, %v", keyid, err) - return nil, err - } + continue } sigAlgo := privKey.SignatureAlgorithm() sig, err := privKey.Sign(rand.Reader, payload, nil) if err != nil { - logrus.Debugf("ignoring error attempting to %s sign with keyID: %s, %v", privKey.Algorithm(), keyid, err) - return nil, err + logrus.Debugf("ignoring error attempting to %s sign with keyID: %s, %v", + privKey.Algorithm(), keyid, err) + continue } logrus.Debugf("appending %s signature with Key ID: %s", privKey.Algorithm(), keyid) diff --git a/cryptoservice/crypto_service_test.go b/cryptoservice/crypto_service_test.go index 4ed3682a79..832197a542 100644 --- a/cryptoservice/crypto_service_test.go +++ b/cryptoservice/crypto_service_test.go @@ -1,51 +1,147 @@ package cryptoservice import ( - "github.com/stretchr/testify/assert" + "fmt" + "runtime" "testing" + "github.com/stretchr/testify/assert" + "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" ) -func TestCryptoService(t *testing.T) { - testCryptoService(t, data.ECDSAKey, signed.ECDSAVerifier{}) - testCryptoService(t, data.ED25519Key, signed.Ed25519Verifier{}) - if !testing.Short() { - testCryptoService(t, data.RSAKey, signed.RSAPSSVerifier{}) - } +var algoToSigType = map[string]data.SigAlgorithm{ + data.ECDSAKey: data.ECDSASignature, + data.ED25519Key: data.EDDSASignature, + data.RSAKey: data.RSAPSSSignature, } var passphraseRetriever = func(string, string, bool, int) (string, bool, error) { return "", false, nil } -func testCryptoService(t *testing.T, keyAlgo string, verifier signed.Verifier) { - content := []byte("this is a secret") +type CryptoServiceTester struct { + cryptoServiceFactory func() *CryptoService + role string + keyAlgo string +} - keyStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) - cryptoService := NewCryptoService("", keyStore) +// asserts that created key exists +func (c CryptoServiceTester) TestCreateAndGetKey(t *testing.T) { + cryptoService := c.cryptoServiceFactory() // Test Create - tufKey, err := cryptoService.Create("", keyAlgo) - assert.NoError(t, err, "error creating key") - - // Test Sign - signatures, err := cryptoService.Sign([]string{tufKey.ID()}, content) - assert.NoError(t, err, "signing failed") - assert.Len(t, signatures, 1, "wrong number of signatures") - - err = verifier.Verify(tufKey, signatures[0].Signature, content) - assert.NoError(t, err, "verification failed for %s key type", keyAlgo) + tufKey, err := cryptoService.Create(c.role, c.keyAlgo) + assert.NoError(t, err, c.errorMsg("error creating key")) // Test GetKey retrievedKey := cryptoService.GetKey(tufKey.ID()) - assert.Equal(t, tufKey.Public(), retrievedKey.Public(), "retrieved key didn't match") + assert.NotNil(t, retrievedKey, + c.errorMsg("Could not find key ID %s", tufKey.ID())) + assert.Equal(t, tufKey.Public(), retrievedKey.Public(), + c.errorMsg("retrieved public key didn't match")) - assert.Nil(t, cryptoService.GetKey("boguskeyid"), "non-nil result for bogus keyid") + // Test GetPrivateKey + retrievedKey, alias, err := cryptoService.GetPrivateKey(tufKey.ID()) + assert.NoError(t, err) + assert.Equal(t, tufKey.ID(), retrievedKey.ID(), + c.errorMsg("retrieved private key didn't have the right ID")) + assert.Equal(t, c.role, alias) +} + +// asserts that getting key fails for a non-existent key +func (c CryptoServiceTester) TestGetNonexistentKey(t *testing.T) { + cryptoService := c.cryptoServiceFactory() + + assert.Nil(t, cryptoService.GetKey("boguskeyid"), + c.errorMsg("non-nil result for bogus keyid")) + + _, _, err := cryptoService.GetPrivateKey("boguskeyid") + assert.NotNil(t, err) +} + +// asserts that signing with a created key creates a valid signature +func (c CryptoServiceTester) TestSignWithKey(t *testing.T) { + cryptoService := c.cryptoServiceFactory() + content := []byte("this is a secret") + + tufKey, err := cryptoService.Create(c.role, c.keyAlgo) + assert.NoError(t, err, c.errorMsg("error creating key")) + + // Test Sign + signatures, err := cryptoService.Sign([]string{tufKey.ID()}, content) + assert.NoError(t, err, c.errorMsg("signing failed")) + assert.Len(t, signatures, 1, c.errorMsg("wrong number of signatures")) + + verifier, ok := signed.Verifiers[algoToSigType[c.keyAlgo]] + assert.True(t, ok, c.errorMsg("Unknown verifier for algorithm")) + + err = verifier.Verify(tufKey, signatures[0].Signature, content) + assert.NoError(t, err, + c.errorMsg("verification failed for %s key type", c.keyAlgo)) +} + +// asserts that removing key that exists succeeds +func (c CryptoServiceTester) TestRemoveCreatedKey(t *testing.T) { + cryptoService := c.cryptoServiceFactory() + + tufKey, err := cryptoService.Create(c.role, c.keyAlgo) + assert.NoError(t, err, c.errorMsg("error creating key")) + assert.NotNil(t, cryptoService.GetKey(tufKey.ID())) // Test RemoveKey err = cryptoService.RemoveKey(tufKey.ID()) - assert.NoError(t, err, "could not remove key") - retrievedKey = cryptoService.GetKey(tufKey.ID()) - assert.Nil(t, retrievedKey, "remove didn't work") + assert.NoError(t, err, c.errorMsg("could not remove key")) + retrievedKey := cryptoService.GetKey(tufKey.ID()) + assert.Nil(t, retrievedKey, c.errorMsg("remove didn't work")) +} + +// Prints out an error message with information about the key algorithm, +// role, and test name. Ideally we could generate different tests given +// data, without having to put for loops in one giant test function, but +// that involves a lot of boilerplate. So as a compromise, everything will +// still be run in for loops in one giant test function, but we can at +// least provide an error message stating what data/helper test function +// failed. +func (c CryptoServiceTester) errorMsg(message string, args ...interface{}) string { + pc := make([]uintptr, 10) // at least 1 entry needed + runtime.Callers(2, pc) // the caller of errorMsg + f := runtime.FuncForPC(pc[0]) + return fmt.Sprintf("%s (role: %s, keyAlgo: %s): %s", f.Name(), c.role, + c.keyAlgo, fmt.Sprintf(message, args...)) +} + +func testCryptoService(t *testing.T, gun string) { + getCryptoService := func() *CryptoService { + return NewCryptoService( + gun, trustmanager.NewKeyMemoryStore(passphraseRetriever)) + } + roles := []string{ + data.CanonicalRootRole, + data.CanonicalTargetsRole, + data.CanonicalSnapshotRole, + data.CanonicalTimestampRole, + } + + for _, role := range roles { + for algo := range algoToSigType { + cst := CryptoServiceTester{ + cryptoServiceFactory: getCryptoService, + role: role, + keyAlgo: algo, + } + cst.TestCreateAndGetKey(t) + cst.TestGetNonexistentKey(t) + cst.TestSignWithKey(t) + cst.TestRemoveCreatedKey(t) + } + } +} + +func TestCryptoServiceWithNonEmptyGUN(t *testing.T) { + testCryptoService(t, "org/repo") +} + +func TestCryptoServiceWithEmptyGUN(t *testing.T) { + testCryptoService(t, "") } From 5b6f64de4bd3a381246100f1f53c99f899aac98c Mon Sep 17 00:00:00 2001 From: Ying Li Date: Thu, 5 Nov 2015 10:46:02 -0800 Subject: [PATCH 16/85] Refactor notary client tests. Move common code out into helper functions, and split up the bigger tests into tests that specifically test adding targets, getting changelists, publishing, and listing, as opposed to having two giant tests instead. Also depend more on existing functions in the code (such as NotaryRepository.GetChangelists and the server ServerMux), rather than reimplementing them in the tests. Signed-off-by: Ying Li Signed-off-by: David Lawrence Signed-off-by: Ying Li (github: endophage) --- client/client_root_validation_test.go | 67 +-- client/client_test.go | 575 ++++++++++++-------------- 2 files changed, 265 insertions(+), 377 deletions(-) diff --git a/client/client_root_validation_test.go b/client/client_root_validation_test.go index 1bc8763c99..752adbe4bc 100644 --- a/client/client_root_validation_test.go +++ b/client/client_root_validation_test.go @@ -1,12 +1,8 @@ package client import ( - "encoding/json" - "fmt" "io/ioutil" - "net/http" "os" - "path/filepath" "testing" "github.com/Sirupsen/logrus" @@ -37,17 +33,10 @@ func validateRootSuccessfully(t *testing.T, rootType string) { gun := "docker.com/notary" - ts, mux := createTestServer(t) + ts, mux := simpleTestServer(t) defer ts.Close() - repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) - assert.NoError(t, err, "error creating repository: %s", err) - - rootKeyID, err := repo.KeyStoreManager.GenRootKey(rootType) - assert.NoError(t, err, "error generating root key: %s", err) - - err = repo.Initialize(rootKeyID) - assert.NoError(t, err, "error creating repository: %s", err) + repo, _ := initializeRepo(t, rootType, tempBaseDir, gun, ts.URL) // tests need to manually boostrap timestamp as client doesn't generate it err = repo.tufRepo.InitTimestamp() @@ -58,50 +47,7 @@ func validateRootSuccessfully(t *testing.T, rootType string) { allCerts := repo.KeyStoreManager.TrustedCertificateStore().GetCertificates() assert.Len(t, allCerts, 1) - // Now test ListTargets. In preparation, we need to expose some signed - // metadata files on the internal HTTP server. - tempKey, err := data.UnmarshalPrivateKey([]byte(timestampECDSAKeyJSON)) - assert.NoError(t, err) - - repo.KeyStoreManager.KeyStore.AddKey(filepath.Join(filepath.FromSlash(gun), tempKey.ID()), "root", tempKey) - - // Because ListTargets will clear this - savedTUFRepo := repo.tufRepo - - rootJSONFile := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "metadata", "root.json") - rootFileBytes, err := ioutil.ReadFile(rootJSONFile) - - signedTargets, err := savedTUFRepo.SignTargets("targets", data.DefaultExpires("targets")) - assert.NoError(t, err) - - signedSnapshot, err := savedTUFRepo.SignSnapshot(data.DefaultExpires("snapshot")) - assert.NoError(t, err) - - signedTimestamp, err := savedTUFRepo.SignTimestamp(data.DefaultExpires("timestamp")) - assert.NoError(t, err) - - mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json", func(w http.ResponseWriter, r *http.Request) { - assert.NoError(t, err) - fmt.Fprint(w, string(rootFileBytes)) - }) - - mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.json", func(w http.ResponseWriter, r *http.Request) { - timestampJSON, _ := json.Marshal(signedTimestamp) - fmt.Fprint(w, string(timestampJSON)) - }) - - mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot.json", func(w http.ResponseWriter, r *http.Request) { - snapshotJSON, _ := json.Marshal(signedSnapshot) - fmt.Fprint(w, string(snapshotJSON)) - }) - - mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets.json", func(w http.ResponseWriter, r *http.Request) { - targetsJSON, _ := json.Marshal(signedTargets) - fmt.Fprint(w, string(targetsJSON)) - }) - - _, err = repo.ListTargets() - assert.NoError(t, err) + fakeServerData(t, repo, mux) // // Test TOFUS logic. We remove all certs and expect a new one to be added after ListTargets @@ -126,13 +72,16 @@ func validateRootSuccessfully(t *testing.T, rootType string) { assert.Len(t, repo.KeyStoreManager.TrustedCertificateStore().GetCertificates(), 0) // Add a previously generated certificate with CN=docker.com/notary - err = repo.KeyStoreManager.TrustedCertificateStore().AddCertFromFile("../fixtures/self-signed_docker.com-notary.crt") + err = repo.KeyStoreManager.TrustedCertificateStore().AddCertFromFile( + "../fixtures/self-signed_docker.com-notary.crt") assert.NoError(t, err) // This list targets is expected to fail, since there already exists a certificate // in the store for the dnsName docker.com/notary, so TOFUS doesn't apply _, err = repo.ListTargets() if assert.Error(t, err, "An error was expected") { - assert.Equal(t, err, &keystoremanager.ErrValidationFail{Reason: "failed to validate data with current trusted certificates"}) + assert.Equal(t, err, &keystoremanager.ErrValidationFail{ + Reason: "failed to validate data with current trusted certificates", + }) } } diff --git a/client/client_test.go b/client/client_test.go index eb2864ff52..156d640ccf 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,6 +1,7 @@ package client import ( + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -11,15 +12,14 @@ import ( "strings" "testing" - "github.com/docker/distribution/registry/api/v2" + "github.com/Sirupsen/logrus" + ctxu "github.com/docker/distribution/context" "github.com/docker/notary/client/changelist" "github.com/docker/notary/cryptoservice" - "github.com/docker/notary/server/handlers" + "github.com/docker/notary/server" "github.com/docker/notary/server/storage" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" - "github.com/docker/notary/utils" - "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "golang.org/x/net/context" ) @@ -28,7 +28,7 @@ const timestampKeyJSON = `{"keytype":"rsa","keyval":{"public":"MIIBIjANBgkqhkiG9 const timestampECDSAKeyJSON = ` {"keytype":"ecdsa","keyval":{"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgl3rzMPMEKhS1k/AX16MM4PdidpjJr+z4pj0Td+30QnpbOIARgpyR1PiFztU8BZlqG3cUazvFclr2q/xHvfrqw==","private":"MHcCAQEEIDqtcdzU7H3AbIPSQaxHl9+xYECt7NpK7B1+6ep5cv9CoAoGCCqGSM49AwEHoUQDQgAEgl3rzMPMEKhS1k/AX16MM4PdidpjJr+z4pj0Td+30QnpbOIARgpyR1PiFztU8BZlqG3cUazvFclr2q/xHvfrqw=="}}` -func createTestServer(t *testing.T) (*httptest.Server, *http.ServeMux) { +func simpleTestServer(t *testing.T) (*httptest.Server, *http.ServeMux) { mux := http.NewServeMux() // TUF will request /v2/docker.com/notary/_trust/tuf/timestamp.key // Return a canned timestamp.key @@ -43,6 +43,41 @@ func createTestServer(t *testing.T) (*httptest.Server, *http.ServeMux) { return ts, mux } +func fullTestServer(t *testing.T) *httptest.Server { + // Set up server + ctx := context.WithValue( + context.Background(), "metaStore", storage.NewMemStorage()) + + // Do not pass one of the const KeyAlgorithms here as the value! Passing a + // string is in itself good test that we are handling it correctly as we + // will be receiving a string from the configuration. + ctx = context.WithValue(ctx, "keyAlgorithm", "ecdsa") + + // Eat the logs instead of spewing them out + var b bytes.Buffer + l := logrus.New() + l.Out = &b + ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) + + cryptoService := cryptoservice.NewCryptoService( + "", trustmanager.NewKeyMemoryStore(passphraseRetriever)) + return httptest.NewServer(server.RootHandler(nil, ctx, cryptoService)) +} + +func initializeRepo(t *testing.T, rootType, tempBaseDir, gun, url string) (*NotaryRepository, string) { + repo, err := NewNotaryRepository( + tempBaseDir, gun, url, http.DefaultTransport, passphraseRetriever) + assert.NoError(t, err, "error creating repo: %s", err) + + rootKeyID, err := repo.KeyStoreManager.GenRootKey(rootType) + assert.NoError(t, err, "error generating root key: %s", err) + + err = repo.Initialize(rootKeyID) + assert.NoError(t, err, "error creating repository: %s", err) + + return repo, rootKeyID +} + // TestInitRepo runs through the process of initializing a repository and makes // sure the repository looks correct on disk. // We test this with both an RSA and ECDSA root key @@ -61,17 +96,10 @@ func testInitRepo(t *testing.T, rootType string) { assert.NoError(t, err, "failed to create a temporary directory: %s", err) - ts, _ := createTestServer(t) + ts, _ := simpleTestServer(t) defer ts.Close() - repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) - assert.NoError(t, err, "error creating repo: %s", err) - - rootKeyID, err := repo.KeyStoreManager.GenRootKey(rootType) - assert.NoError(t, err, "error generating root key: %s", err) - - err = repo.Initialize(rootKeyID) - assert.NoError(t, err, "error creating repository: %s", err) + repo, rootKeyID := initializeRepo(t, rootType, tempBaseDir, gun, ts.URL) // Inspect contents of the temporary directory expectedDirs := []string{ @@ -165,19 +193,32 @@ func testInitRepo(t *testing.T, rootType string) { } } -// TestAddListTarget adds a target to the repo and confirms that the changelist -// is updated correctly. Then it calls ListTargets and checks the return value. -// Using ListTargets involves serving signed metadata files over the test's -// internal HTTP server. +// TestAddTarget adds a target to the repo and confirms that the changelist +// is updated correctly. // We test this with both an RSA and ECDSA root key -func TestAddListTarget(t *testing.T) { - testAddListTarget(t, data.ECDSAKey) +func TestAddTarget(t *testing.T) { + testAddTarget(t, data.ECDSAKey) if !testing.Short() { - testAddListTarget(t, data.RSAKey) + testAddTarget(t, data.RSAKey) } } -func testAddListTarget(t *testing.T, rootType string) { +func addTarget(t *testing.T, repo *NotaryRepository, targetName, targetFile string) *Target { + target, err := NewTarget(targetName, targetFile) + assert.NoError(t, err, "error creating target") + err = repo.AddTarget(target) + assert.NoError(t, err, "error adding target") + return target +} + +// calls GetChangelist and gets the actual changes out +func getChanges(t *testing.T, repo *NotaryRepository) []changelist.Change { + changeList, err := repo.GetChangelist() + assert.NoError(t, err) + return changeList.List() +} + +func testAddTarget(t *testing.T, rootType string) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) @@ -186,151 +227,172 @@ func testAddListTarget(t *testing.T, rootType string) { gun := "docker.com/notary" - ts, mux := createTestServer(t) + ts, _ := simpleTestServer(t) defer ts.Close() - repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) - assert.NoError(t, err, "error creating repository: %s", err) + repo, _ := initializeRepo(t, rootType, tempBaseDir, gun, ts.URL) - rootKeyID, err := repo.KeyStoreManager.GenRootKey(rootType) - assert.NoError(t, err, "error generating root key: %s", err) - - err = repo.Initialize(rootKeyID) + // tests need to manually boostrap timestamp as client doesn't generate it + err = repo.tufRepo.InitTimestamp() assert.NoError(t, err, "error creating repository: %s", err) + assert.Len(t, getChanges(t, repo), 0, "should start with zero changes") + + // Add fixtures/intermediate-ca.crt as a target. There's no particular + // reason for using this file except that it happens to be available as + // a fixture. + addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") + changes := getChanges(t, repo) + assert.Len(t, changes, 1, "wrong number of changes files found") + + for _, c := range changes { // there is only one + assert.EqualValues(t, changelist.ActionCreate, c.Action()) + assert.Equal(t, "targets", c.Scope()) + assert.Equal(t, "target", c.Type()) + assert.Equal(t, "latest", c.Path()) + assert.NotEmpty(t, c.Content()) + } + + // Create a second target + addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") + changes = getChanges(t, repo) + assert.Len(t, changes, 2, "wrong number of changelist files found") + + newFileFound := false + for _, c := range changes { + if c.Path() != "latest" { + assert.EqualValues(t, changelist.ActionCreate, c.Action()) + assert.Equal(t, "targets", c.Scope()) + assert.Equal(t, "target", c.Type()) + assert.Equal(t, "current", c.Path()) + assert.NotEmpty(t, c.Content()) + + newFileFound = true + } + } + assert.True(t, newFileFound, "second changelist file not found") +} + +// TestListTarget fakes serving signed metadata files over the test's +// internal HTTP server to ensure that ListTargets returns the correct number +// of listed targets. +// We test this with both an RSA and ECDSA root key +func TestListTarget(t *testing.T) { + testListEmptyTargets(t, data.ECDSAKey) + testListTarget(t, data.ECDSAKey) + if !testing.Short() { + testListEmptyTargets(t, data.RSAKey) + testListTarget(t, data.RSAKey) + } +} + +func testListEmptyTargets(t *testing.T, rootType string) { + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) + + assert.NoError(t, err, "failed to create a temporary directory: %s", err) + + gun := "docker.com/notary" + + ts := fullTestServer(t) + defer ts.Close() + + repo, _ := initializeRepo(t, rootType, tempBaseDir, gun, ts.URL) // tests need to manually boostrap timestamp as client doesn't generate it err = repo.tufRepo.InitTimestamp() assert.NoError(t, err, "error creating repository: %s", err) - // Add fixtures/intermediate-ca.crt as a target. There's no particular reason - // for using this file except that it happens to be available as - // a fixture. - latestTarget, err := NewTarget("latest", "../fixtures/intermediate-ca.crt") - assert.NoError(t, err, "error creating target") - err = repo.AddTarget(latestTarget) - assert.NoError(t, err, "error adding target") + _, err = repo.ListTargets() + assert.Error(t, err) // no trust data +} - // Look for the changelist file - changelistDirPath := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "changelist") +// reads data from the repository in order to fake data being served via +// the ServeMux. +func fakeServerData(t *testing.T, repo *NotaryRepository, mux *http.ServeMux) { + tempKey, err := data.UnmarshalPrivateKey([]byte(timestampECDSAKeyJSON)) + assert.NoError(t, err) - changelistDir, err := os.Open(changelistDirPath) - assert.NoError(t, err, "could not open changelist directory") + savedTUFRepo := repo.tufRepo // in case this is overwritten - fileInfos, err := changelistDir.Readdir(0) - assert.NoError(t, err, "could not read changelist directory") + repo.KeyStoreManager.KeyStore.AddKey( + filepath.Join(filepath.FromSlash(repo.gun), tempKey.ID()), + "nonroot", tempKey) - // Should only be one file in the directory - assert.Len(t, fileInfos, 1, "wrong number of changelist files found") + rootJSONFile := filepath.Join(repo.baseDir, "tuf", + filepath.FromSlash(repo.gun), "metadata", "root.json") + rootFileBytes, err := ioutil.ReadFile(rootJSONFile) - clName := fileInfos[0].Name() - raw, err := ioutil.ReadFile(filepath.Join(changelistDirPath, clName)) - assert.NoError(t, err, "could not read changelist file %s", clName) + signedTargets, err := savedTUFRepo.SignTargets( + "targets", data.DefaultExpires("targets")) + assert.NoError(t, err) - c := &changelist.TufChange{} - err = json.Unmarshal(raw, c) - assert.NoError(t, err, "could not unmarshal changelist file %s", clName) + signedSnapshot, err := savedTUFRepo.SignSnapshot( + data.DefaultExpires("snapshot")) + assert.NoError(t, err) - assert.EqualValues(t, changelist.ActionCreate, c.Actn) - assert.Equal(t, "targets", c.Role) - assert.Equal(t, "target", c.ChangeType) - assert.Equal(t, "latest", c.ChangePath) - assert.NotEmpty(t, c.Data) + signedTimestamp, err := savedTUFRepo.SignTimestamp( + data.DefaultExpires("timestamp")) + assert.NoError(t, err) - changelistDir.Close() + mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json", + func(w http.ResponseWriter, r *http.Request) { + assert.NoError(t, err) + fmt.Fprint(w, string(rootFileBytes)) + }) - // Create a second target - currentTarget, err := NewTarget("current", "../fixtures/intermediate-ca.crt") - assert.NoError(t, err, "error creating target") - err = repo.AddTarget(currentTarget) - assert.NoError(t, err, "error adding target") + mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.json", + func(w http.ResponseWriter, r *http.Request) { + timestampJSON, _ := json.Marshal(signedTimestamp) + fmt.Fprint(w, string(timestampJSON)) + }) - changelistDir, err = os.Open(changelistDirPath) - assert.NoError(t, err, "could not open changelist directory") + mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot.json", + func(w http.ResponseWriter, r *http.Request) { + snapshotJSON, _ := json.Marshal(signedSnapshot) + fmt.Fprint(w, string(snapshotJSON)) + }) - // There should now be a second file in the directory - fileInfos, err = changelistDir.Readdir(0) - assert.NoError(t, err, "could not read changelist directory") + mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets.json", + func(w http.ResponseWriter, r *http.Request) { + targetsJSON, _ := json.Marshal(signedTargets) + fmt.Fprint(w, string(targetsJSON)) + }) +} - assert.Len(t, fileInfos, 2, "wrong number of changelist files found") +func testListTarget(t *testing.T, rootType string) { + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) - newFileFound := false - for _, fileInfo := range fileInfos { - if fileInfo.Name() != clName { - clName2 := fileInfo.Name() - raw, err := ioutil.ReadFile(filepath.Join(changelistDirPath, clName2)) - assert.NoError(t, err, "could not read changelist file %s", clName2) + assert.NoError(t, err, "failed to create a temporary directory: %s", err) - c := &changelist.TufChange{} - err = json.Unmarshal(raw, c) - assert.NoError(t, err, "could not unmarshal changelist file %s", clName2) + gun := "docker.com/notary" - assert.EqualValues(t, changelist.ActionCreate, c.Actn) - assert.Equal(t, "targets", c.Role) - assert.Equal(t, "target", c.ChangeType) - assert.Equal(t, "current", c.ChangePath) - assert.NotEmpty(t, c.Data) + ts, mux := simpleTestServer(t) + defer ts.Close() - newFileFound = true - break - } - } + repo, _ := initializeRepo(t, rootType, tempBaseDir, gun, ts.URL) - assert.True(t, newFileFound, "second changelist file not found") + // tests need to manually boostrap timestamp as client doesn't generate it + err = repo.tufRepo.InitTimestamp() + assert.NoError(t, err, "error creating repository: %s", err) - changelistDir.Close() - - // Now test ListTargets. In preparation, we need to expose some signed - // metadata files on the internal HTTP server. + latestTarget := addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") + currentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") // Apply the changelist. Normally, this would be done by Publish // load the changelist for this repo - cl, err := changelist.NewFileChangelist(filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "changelist")) + cl, err := changelist.NewFileChangelist( + filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "changelist")) assert.NoError(t, err, "could not open changelist") // apply the changelist to the repo err = applyChangelist(repo.tufRepo, cl) assert.NoError(t, err, "could not apply changelist") - tempKey, err := data.UnmarshalPrivateKey([]byte(timestampECDSAKeyJSON)) - assert.NoError(t, err) - - repo.KeyStoreManager.KeyStore.AddKey(filepath.Join(filepath.FromSlash(gun), tempKey.ID()), "nonroot", tempKey) - - // Because ListTargets will clear this - savedTUFRepo := repo.tufRepo - - rootJSONFile := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "metadata", "root.json") - rootFileBytes, err := ioutil.ReadFile(rootJSONFile) - - signedTargets, err := savedTUFRepo.SignTargets("targets", data.DefaultExpires("targets")) - assert.NoError(t, err) - - signedSnapshot, err := savedTUFRepo.SignSnapshot(data.DefaultExpires("snapshot")) - assert.NoError(t, err) - - signedTimestamp, err := savedTUFRepo.SignTimestamp(data.DefaultExpires("timestamp")) - assert.NoError(t, err) - - mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json", func(w http.ResponseWriter, r *http.Request) { - assert.NoError(t, err) - fmt.Fprint(w, string(rootFileBytes)) - }) - - mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.json", func(w http.ResponseWriter, r *http.Request) { - timestampJSON, _ := json.Marshal(signedTimestamp) - fmt.Fprint(w, string(timestampJSON)) - }) - - mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot.json", func(w http.ResponseWriter, r *http.Request) { - snapshotJSON, _ := json.Marshal(signedSnapshot) - fmt.Fprint(w, string(snapshotJSON)) - }) - - mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets.json", func(w http.ResponseWriter, r *http.Request) { - targetsJSON, _ := json.Marshal(signedTargets) - fmt.Fprint(w, string(targetsJSON)) - }) + fakeServerData(t, repo, mux) targets, err := repo.ListTargets() assert.NoError(t, err) @@ -376,17 +438,10 @@ func testValidateRootKey(t *testing.T, rootType string) { gun := "docker.com/notary" - ts, _ := createTestServer(t) + ts, _ := simpleTestServer(t) defer ts.Close() - repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) - assert.NoError(t, err, "error creating repository: %s", err) - - rootKeyID, err := repo.KeyStoreManager.GenRootKey(rootType) - assert.NoError(t, err, "error generating root key: %s", err) - - err = repo.Initialize(rootKeyID) - assert.NoError(t, err, "error creating repository: %s", err) + initializeRepo(t, rootType, tempBaseDir, gun, ts.URL) rootJSONFile := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "metadata", "root.json") @@ -419,6 +474,57 @@ func testValidateRootKey(t *testing.T, rootType string) { } } +// TestGetChangelist ensures that the changelist returned matches the changes +// added. +// We test this with both an RSA and ECDSA root key +func TestGetChangelist(t *testing.T) { + testGetChangelist(t, data.ECDSAKey) + if !testing.Short() { + testGetChangelist(t, data.RSAKey) + } +} + +func testGetChangelist(t *testing.T, rootType string) { + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) + + assert.NoError(t, err, "failed to create a temporary directory: %s", err) + + gun := "docker.com/notary" + ts, _ := simpleTestServer(t) + + repo, _ := initializeRepo(t, rootType, tempBaseDir, gun, ts.URL) + assert.Len(t, getChanges(t, repo), 0, "No changes should be in changelist yet") + + // Create 2 targets + addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") + addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") + + // Test loading changelist + chgs := getChanges(t, repo) + assert.Len(t, chgs, 2, "Wrong number of changes returned from changelist") + + changes := make(map[string]changelist.Change) + for _, ch := range chgs { + changes[ch.Path()] = ch + } + + currentChange := changes["current"] + assert.NotNil(t, currentChange, "Expected changelist to contain a change for path 'current'") + assert.EqualValues(t, changelist.ActionCreate, currentChange.Action()) + assert.Equal(t, "targets", currentChange.Scope()) + assert.Equal(t, "target", currentChange.Type()) + assert.Equal(t, "current", currentChange.Path()) + + latestChange := changes["latest"] + assert.NotNil(t, latestChange, "Expected changelist to contain a change for path 'latest'") + assert.EqualValues(t, changelist.ActionCreate, latestChange.Action()) + assert.Equal(t, "targets", latestChange.Scope()) + assert.Equal(t, "target", latestChange.Type()) + assert.Equal(t, "latest", latestChange.Path()) +} + // TestPublish creates a repo, instantiates a notary server, and publishes // the repo to the server. // We test this with both an RSA and ECDSA root key @@ -437,148 +543,19 @@ func testPublish(t *testing.T, rootType string) { assert.NoError(t, err, "failed to create a temporary directory: %s", err) gun := "docker.com/notary" + ts := fullTestServer(t) - // Set up server - ctx := context.WithValue(context.Background(), "metaStore", storage.NewMemStorage()) + repo, _ := initializeRepo(t, rootType, tempBaseDir, gun, ts.URL) - // Do not pass one of the const KeyAlgorithms here as the value! Passing a - // string is in itself good test that we are handling it correctly as we will - // be receiving a string from the configuration. - ctx = context.WithValue(ctx, "keyAlgorithm", "ecdsa") - - hand := utils.RootHandlerFactory(nil, ctx, - cryptoservice.NewCryptoService("", trustmanager.NewKeyMemoryStore(passphraseRetriever))) - - r := mux.NewRouter() - r.Methods("POST").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/").Handler(hand(handlers.AtomicUpdateHandler, "push", "pull")) - r.Methods("GET").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/{tufRole:(root|targets|snapshot)}.json").Handler(hand(handlers.GetHandler, "pull")) - r.Methods("GET").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/timestamp.json").Handler(hand(handlers.GetTimestampHandler, "pull")) - r.Methods("GET").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/timestamp.key").Handler(hand(handlers.GetTimestampKeyHandler, "push", "pull")) - //r.Methods("POST").Path("/v2/{imageName:" + server.RepositoryNameRegexp + "}/_trust/tuf/{tufRole:(root|targets|timestamp|snapshot)}.json").Handler(hand(handlers.UpdateHandler, "push", "pull")) - r.Methods("DELETE").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/").Handler(hand(handlers.DeleteHandler, "push", "pull")) - - ts := httptest.NewServer(r) - - repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) - assert.NoError(t, err, "error creating repository: %s", err) - - rootKeyID, err := repo.KeyStoreManager.GenRootKey(rootType) - assert.NoError(t, err, "error generating root key: %s", err) - - err = repo.Initialize(rootKeyID) - assert.NoError(t, err, "error creating repository: %s", err) - - // Add fixtures/intermediate-ca.crt as a target. There's no particular reason - // for using this file except that it happens to be available as - // a fixture. - latestTarget, err := NewTarget("latest", "../fixtures/intermediate-ca.crt") - assert.NoError(t, err, "error creating target") - err = repo.AddTarget(latestTarget) - assert.NoError(t, err, "error adding target") - - // Look for the changelist file - changelistDirPath := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "changelist") - - changelistDir, err := os.Open(changelistDirPath) - assert.NoError(t, err, "could not open changelist directory") - - fileInfos, err := changelistDir.Readdir(0) - assert.NoError(t, err, "could not read changelist directory") - - // Should only be one file in the directory - assert.Len(t, fileInfos, 1, "wrong number of changelist files found") - - clName := fileInfos[0].Name() - raw, err := ioutil.ReadFile(filepath.Join(changelistDirPath, clName)) - assert.NoError(t, err, "could not read changelist file %s", clName) - - c := &changelist.TufChange{} - err = json.Unmarshal(raw, c) - assert.NoError(t, err, "could not unmarshal changelist file %s", clName) - - assert.EqualValues(t, changelist.ActionCreate, c.Actn) - assert.Equal(t, "targets", c.Role) - assert.Equal(t, "target", c.ChangeType) - assert.Equal(t, "latest", c.ChangePath) - assert.NotEmpty(t, c.Data) - - changelistDir.Close() - - // Create a second target - currentTarget, err := NewTarget("current", "../fixtures/intermediate-ca.crt") - assert.NoError(t, err, "error creating target") - err = repo.AddTarget(currentTarget) - assert.NoError(t, err, "error adding target") - - changelistDir, err = os.Open(changelistDirPath) - assert.NoError(t, err, "could not open changelist directory") - - // There should now be a second file in the directory - fileInfos, err = changelistDir.Readdir(0) - assert.NoError(t, err, "could not read changelist directory") - - assert.Len(t, fileInfos, 2, "wrong number of changelist files found") - - newFileFound := false - for _, fileInfo := range fileInfos { - if fileInfo.Name() != clName { - clName2 := fileInfo.Name() - raw, err := ioutil.ReadFile(filepath.Join(changelistDirPath, clName2)) - assert.NoError(t, err, "could not read changelist file %s", clName2) - - c := &changelist.TufChange{} - err = json.Unmarshal(raw, c) - assert.NoError(t, err, "could not unmarshal changelist file %s", clName2) - - assert.EqualValues(t, changelist.ActionCreate, c.Actn) - assert.Equal(t, "targets", c.Role) - assert.Equal(t, "target", c.ChangeType) - assert.Equal(t, "current", c.ChangePath) - assert.NotEmpty(t, c.Data) - - newFileFound = true - break - } - } - - assert.True(t, newFileFound, "second changelist file not found") - - changelistDir.Close() - - // Test loading changelist - changes := make(map[string]changelist.Change) - cl, err := repo.GetChangelist() - assert.NoError(t, err, "could not get changelist for repo") - - assert.Len(t, cl.List(), 2, "Wrong number of changes returned from changelist") - for _, ch := range cl.List() { - changes[ch.Path()] = ch - } - - currentChange := changes["current"] - assert.NotNil(t, currentChange, "Expected changelist to contain a change for path 'current'") - assert.EqualValues(t, changelist.ActionCreate, currentChange.Action()) - assert.Equal(t, "targets", currentChange.Scope()) - assert.Equal(t, "target", currentChange.Type()) - assert.Equal(t, "current", currentChange.Path()) - - latestChange := changes["latest"] - assert.NotNil(t, latestChange, "Expected changelist to contain a change for path 'latest'") - assert.EqualValues(t, changelist.ActionCreate, latestChange.Action()) - assert.Equal(t, "targets", latestChange.Scope()) - assert.Equal(t, "target", latestChange.Type()) - assert.Equal(t, "latest", latestChange.Path()) + // Create 2 targets + latestTarget := addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") + currentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt") + assert.Len(t, getChanges(t, repo), 2, "wrong number of changelist files found") // Now test Publish err = repo.Publish() assert.NoError(t, err) - - changelistDir, err = os.Open(changelistDirPath) - assert.NoError(t, err, "could not open changelist directory") - fileInfos, err = changelistDir.Readdir(0) - assert.NoError(t, err, "could not read changelist directory") - // Should only be one file in the directory - assert.Len(t, fileInfos, 0, "wrong number of changelist files found") + assert.Len(t, getChanges(t, repo), 0, "wrong number of changelist files found") // Create a new repo and pull from the server tempBaseDir2, err := ioutil.TempDir("", "notary-test-") @@ -624,45 +601,13 @@ func TestRotate(t *testing.T) { gun := "docker.com/notary" - // Set up server - ctx := context.WithValue(context.Background(), "metaStore", storage.NewMemStorage()) + ts := fullTestServer(t) - // Do not pass one of the const KeyAlgorithms here as the value! Passing a - // string is in itself good test that we are handling it correctly as we will - // be receiving a string from the configuration. - ctx = context.WithValue(ctx, "keyAlgorithm", "ecdsa") + repo, _ := initializeRepo(t, data.ECDSAKey, tempBaseDir, gun, ts.URL) - hand := utils.RootHandlerFactory(nil, ctx, - cryptoservice.NewCryptoService("", trustmanager.NewKeyMemoryStore(passphraseRetriever))) - - r := mux.NewRouter() - r.Methods("POST").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/").Handler(hand(handlers.AtomicUpdateHandler, "push", "pull")) - r.Methods("GET").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/{tufRole:(root|targets|snapshot)}.json").Handler(hand(handlers.GetHandler, "pull")) - r.Methods("GET").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/timestamp.json").Handler(hand(handlers.GetTimestampHandler, "pull")) - r.Methods("GET").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/timestamp.key").Handler(hand(handlers.GetTimestampKeyHandler, "push", "pull")) - //r.Methods("POST").Path("/v2/{imageName:" + server.RepositoryNameRegexp + "}/_trust/tuf/{tufRole:(root|targets|timestamp|snapshot)}.json").Handler(hand(handlers.UpdateHandler, "push", "pull")) - r.Methods("DELETE").Path("/v2/{imageName:" + v2.RepositoryNameRegexp.String() + "}/_trust/tuf/").Handler(hand(handlers.DeleteHandler, "push", "pull")) - - ts := httptest.NewServer(r) - - repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) - assert.NoError(t, err, "error creating repository: %s", err) - - rootKeyID, err := repo.KeyStoreManager.GenRootKey(data.ECDSAKey) - assert.NoError(t, err, "error generating root key: %s", err) - - err = repo.Initialize(rootKeyID) - assert.NoError(t, err, "error creating repository: %s", err) - - // Add fixtures/intermediate-ca.crt as a target. There's no particular reason - // for using this file except that it happens to be available as - // a fixture. // Adding a target will allow us to confirm the repository is still valid after // rotating the keys. - latestTarget, err := NewTarget("latest", "../fixtures/intermediate-ca.crt") - assert.NoError(t, err, "error creating target") - err = repo.AddTarget(latestTarget) - assert.NoError(t, err, "error adding target") + addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt") // Publish err = repo.Publish() @@ -694,12 +639,6 @@ func TestRotate(t *testing.T) { assert.NotEqual(t, snapshotKeyIDs[0], newSnapshotKeyIDs[0]) // Confirm changelist dir empty after publishing changes - // Look for the changelist file - changelistDirPath := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "changelist") - changelistDir, err := os.Open(changelistDirPath) - assert.NoError(t, err, "could not open changelist directory") - fileInfos, err := changelistDir.Readdir(0) - assert.NoError(t, err, "could not read changelist directory") - // Should only be one file in the directory - assert.Len(t, fileInfos, 0, "wrong number of changelist files found") + changes := getChanges(t, repo) + assert.Len(t, changes, 0, "wrong number of changelist files found") } From 24cca5d6ccebde4e6633d18aea5e54f30e741c99 Mon Sep 17 00:00:00 2001 From: Ying Li Date: Wed, 4 Nov 2015 16:43:51 -0800 Subject: [PATCH 17/85] Update the cobra dependency and its subdependencies (pflag, blackfriday, sanitized_anchor_name, and go-md2man). Signed-off-by: Ying Li Signed-off-by: David Lawrence Signed-off-by: Ying Li (github: endophage) --- Godeps/Godeps.json | 18 +- .../cpuguy83/go-md2man/md2man/md2man.go | 19 + .../cpuguy83/go-md2man/md2man/roff.go | 269 ++++ .../russross/blackfriday/.gitignore | 8 + .../russross/blackfriday/.travis.yml | 17 + .../russross/blackfriday/LICENSE.txt | 29 + .../github.com/russross/blackfriday/README.md | 246 +++ .../github.com/russross/blackfriday/block.go | 1389 +++++++++++++++++ .../github.com/russross/blackfriday/html.go | 948 +++++++++++ .../github.com/russross/blackfriday/inline.go | 1103 +++++++++++++ .../github.com/russross/blackfriday/latex.go | 332 ++++ .../russross/blackfriday/markdown.go | 919 +++++++++++ .../russross/blackfriday/smartypants.go | 398 +++++ .../sanitized_anchor_name/.travis.yml | 10 + .../shurcooL/sanitized_anchor_name/README.md | 31 + .../shurcooL/sanitized_anchor_name/main.go | 29 + .../src/github.com/spf13/cobra/.travis.yml | 3 +- .../src/github.com/spf13/cobra/README.md | 92 +- .../spf13/cobra/bash_completions.go | 95 +- .../spf13/cobra/bash_completions_test.go | 82 - .../src/github.com/spf13/cobra/cobra.go | 70 +- .../github.com/spf13/cobra/cobra/cmd/add.go | 125 ++ .../spf13/cobra/cobra/cmd/helpers.go | 342 ++++ .../github.com/spf13/cobra/cobra/cmd/init.go | 219 +++ .../spf13/cobra/cobra/cmd/licenses.go | 435 ++++++ .../github.com/spf13/cobra/cobra/cmd/root.go | 84 + .../src/github.com/spf13/cobra/cobra/main.go | 20 + .../src/github.com/spf13/cobra/cobra_test.go | 942 ----------- .../src/github.com/spf13/cobra/command.go | 409 +++-- .../github.com/spf13/cobra/command_test.go | 90 -- .../src/github.com/spf13/cobra/doc_util.go | 34 + .../src/github.com/spf13/cobra/man_docs.go | 213 +++ .../src/github.com/spf13/cobra/man_docs.md | 25 + .../src/github.com/spf13/cobra/md_docs.go | 33 +- .../github.com/spf13/cobra/md_docs_test.go | 67 - .../src/github.com/spf13/pflag/.travis.yml | 10 + .../src/github.com/spf13/pflag/README.md | 87 +- .../src/github.com/spf13/pflag/bool.go | 40 +- .../src/github.com/spf13/pflag/bool_test.go | 164 -- .../src/github.com/spf13/pflag/count.go | 97 ++ .../src/github.com/spf13/pflag/duration.go | 25 +- .../github.com/spf13/pflag/example_test.go | 77 - .../src/github.com/spf13/pflag/export_test.go | 29 - .../src/github.com/spf13/pflag/flag.go | 385 +++-- .../src/github.com/spf13/pflag/flag_test.go | 658 -------- .../src/github.com/spf13/pflag/float32.go | 25 +- .../src/github.com/spf13/pflag/float64.go | 21 +- .../src/github.com/spf13/pflag/golangflag.go | 97 ++ .../src/github.com/spf13/pflag/int.go | 21 +- .../src/github.com/spf13/pflag/int32.go | 25 +- .../src/github.com/spf13/pflag/int64.go | 21 +- .../src/github.com/spf13/pflag/int8.go | 25 +- .../src/github.com/spf13/pflag/int_slice.go | 128 ++ .../src/github.com/spf13/pflag/ip.go | 30 +- .../src/github.com/spf13/pflag/ipmask.go | 48 +- .../src/github.com/spf13/pflag/ipnet.go | 100 ++ .../src/github.com/spf13/pflag/string.go | 21 +- .../github.com/spf13/pflag/string_slice.go | 111 ++ .../src/github.com/spf13/pflag/uint.go | 25 +- .../src/github.com/spf13/pflag/uint16.go | 25 +- .../src/github.com/spf13/pflag/uint32.go | 25 +- .../src/github.com/spf13/pflag/uint64.go | 25 +- .../src/github.com/spf13/pflag/uint8.go | 25 +- .../src/github.com/spf13/pflag/verify/all.sh | 69 + .../github.com/spf13/pflag/verify/gofmt.sh | 19 + .../github.com/spf13/pflag/verify/golint.sh | 15 + 66 files changed, 9119 insertions(+), 2499 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/cpuguy83/go-md2man/md2man/md2man.go create mode 100644 Godeps/_workspace/src/github.com/cpuguy83/go-md2man/md2man/roff.go create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/.gitignore create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/LICENSE.txt create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/README.md create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/block.go create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/html.go create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/inline.go create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/latex.go create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/markdown.go create mode 100644 Godeps/_workspace/src/github.com/russross/blackfriday/smartypants.go create mode 100644 Godeps/_workspace/src/github.com/shurcooL/sanitized_anchor_name/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/shurcooL/sanitized_anchor_name/README.md create mode 100644 Godeps/_workspace/src/github.com/shurcooL/sanitized_anchor_name/main.go delete mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/bash_completions_test.go create mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/cobra/cmd/add.go create mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/cobra/cmd/helpers.go create mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/cobra/cmd/init.go create mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/cobra/cmd/licenses.go create mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/cobra/cmd/root.go create mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/cobra/main.go delete mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/cobra_test.go delete mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/command_test.go create mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/doc_util.go create mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/man_docs.go create mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/man_docs.md delete mode 100644 Godeps/_workspace/src/github.com/spf13/cobra/md_docs_test.go delete mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/bool_test.go create mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/count.go delete mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/example_test.go delete mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/export_test.go delete mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/flag_test.go create mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/golangflag.go create mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/int_slice.go create mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/ipnet.go create mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/string_slice.go create mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/verify/all.sh create mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/verify/gofmt.sh create mode 100644 Godeps/_workspace/src/github.com/spf13/pflag/verify/golint.sh diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 010cab1c36..a51051fa94 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -35,6 +35,11 @@ "ImportPath": "github.com/bugsnag/panicwrap", "Rev": "e5f9854865b9778a45169fc249e99e338d4d6f27" }, + { + "ImportPath": "github.com/cpuguy83/go-md2man/md2man", + "Comment": "v1.0.4", + "Rev": "71acacd42f85e5e82f70a55327789582a5200a90" + }, { "ImportPath": "github.com/docker/distribution/context", "Comment": "v2.0.0-353-gfed58bd", @@ -197,13 +202,22 @@ "ImportPath": "github.com/prometheus/procfs", "Rev": "b1afdc266f54247f5dc725544f5d351a8661f502" }, + { + "ImportPath": "github.com/russross/blackfriday", + "Comment": "v1.3", + "Rev": "8cec3a854e68dba10faabbe31c089abf4a3e57a6" + }, + { + "ImportPath": "github.com/shurcooL/sanitized_anchor_name", + "Rev": "244f5ac324cb97e1987ef901a0081a77bfd8e845" + }, { "ImportPath": "github.com/spf13/cast", "Rev": "4d07383ffe94b5e5a6fa3af9211374a4507a0184" }, { "ImportPath": "github.com/spf13/cobra", - "Rev": "312092086bed4968099259622145a0c9ae280064" + "Rev": "2e6a42892123dda608922f8af8ce85c3bff19575" }, { "ImportPath": "github.com/spf13/jwalterweatherman", @@ -211,7 +225,7 @@ }, { "ImportPath": "github.com/spf13/pflag", - "Rev": "5644820622454e71517561946e3d94b9f9db6842" + "Rev": "08b1a584251b5b62f458943640fc8ebd4d50aaa5" }, { "ImportPath": "github.com/spf13/viper", diff --git a/Godeps/_workspace/src/github.com/cpuguy83/go-md2man/md2man/md2man.go b/Godeps/_workspace/src/github.com/cpuguy83/go-md2man/md2man/md2man.go new file mode 100644 index 0000000000..8f44fa1550 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cpuguy83/go-md2man/md2man/md2man.go @@ -0,0 +1,19 @@ +package md2man + +import ( + "github.com/russross/blackfriday" +) + +func Render(doc []byte) []byte { + renderer := RoffRenderer(0) + extensions := 0 + extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS + extensions |= blackfriday.EXTENSION_TABLES + extensions |= blackfriday.EXTENSION_FENCED_CODE + extensions |= blackfriday.EXTENSION_AUTOLINK + extensions |= blackfriday.EXTENSION_SPACE_HEADERS + extensions |= blackfriday.EXTENSION_FOOTNOTES + extensions |= blackfriday.EXTENSION_TITLEBLOCK + + return blackfriday.Markdown(doc, renderer, extensions) +} diff --git a/Godeps/_workspace/src/github.com/cpuguy83/go-md2man/md2man/roff.go b/Godeps/_workspace/src/github.com/cpuguy83/go-md2man/md2man/roff.go new file mode 100644 index 0000000000..4478786b7b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cpuguy83/go-md2man/md2man/roff.go @@ -0,0 +1,269 @@ +package md2man + +import ( + "bytes" + "fmt" + "html" + "strings" + + "github.com/russross/blackfriday" +) + +type roffRenderer struct{} + +func RoffRenderer(flags int) blackfriday.Renderer { + return &roffRenderer{} +} + +func (r *roffRenderer) GetFlags() int { + return 0 +} + +func (r *roffRenderer) TitleBlock(out *bytes.Buffer, text []byte) { + out.WriteString(".TH ") + + splitText := bytes.Split(text, []byte("\n")) + for i, line := range splitText { + line = bytes.TrimPrefix(line, []byte("% ")) + if i == 0 { + line = bytes.Replace(line, []byte("("), []byte("\" \""), 1) + line = bytes.Replace(line, []byte(")"), []byte("\" \""), 1) + } + line = append([]byte("\""), line...) + line = append(line, []byte("\" ")...) + out.Write(line) + } + + out.WriteString(" \"\"\n") +} + +func (r *roffRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) { + out.WriteString("\n.PP\n.RS\n\n.nf\n") + escapeSpecialChars(out, text) + out.WriteString("\n.fi\n.RE\n") +} + +func (r *roffRenderer) BlockQuote(out *bytes.Buffer, text []byte) { + out.WriteString("\n.PP\n.RS\n") + out.Write(text) + out.WriteString("\n.RE\n") +} + +func (r *roffRenderer) BlockHtml(out *bytes.Buffer, text []byte) { + out.Write(text) +} + +func (r *roffRenderer) Header(out *bytes.Buffer, text func() bool, level int, id string) { + marker := out.Len() + + switch { + case marker == 0: + // This is the doc header + out.WriteString(".TH ") + case level == 1: + out.WriteString("\n\n.SH ") + case level == 2: + out.WriteString("\n.SH ") + default: + out.WriteString("\n.SS ") + } + + if !text() { + out.Truncate(marker) + return + } +} + +func (r *roffRenderer) HRule(out *bytes.Buffer) { + out.WriteString("\n.ti 0\n\\l'\\n(.lu'\n") +} + +func (r *roffRenderer) List(out *bytes.Buffer, text func() bool, flags int) { + marker := out.Len() + out.WriteString(".IP ") + if flags&blackfriday.LIST_TYPE_ORDERED != 0 { + out.WriteString("\\(bu 2") + } else { + out.WriteString("\\n+[step" + string(flags) + "]") + } + out.WriteString("\n") + if !text() { + out.Truncate(marker) + return + } + +} + +func (r *roffRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) { + out.WriteString("\n\\item ") + out.Write(text) +} + +func (r *roffRenderer) Paragraph(out *bytes.Buffer, text func() bool) { + marker := out.Len() + out.WriteString("\n.PP\n") + if !text() { + out.Truncate(marker) + return + } + if marker != 0 { + out.WriteString("\n") + } +} + +// TODO: This might now work +func (r *roffRenderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { + out.WriteString(".TS\nallbox;\n") + + out.Write(header) + out.Write(body) + out.WriteString("\n.TE\n") +} + +func (r *roffRenderer) TableRow(out *bytes.Buffer, text []byte) { + if out.Len() > 0 { + out.WriteString("\n") + } + out.Write(text) + out.WriteString("\n") +} + +func (r *roffRenderer) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { + if out.Len() > 0 { + out.WriteString(" ") + } + out.Write(text) + out.WriteString(" ") +} + +// TODO: This is probably broken +func (r *roffRenderer) TableCell(out *bytes.Buffer, text []byte, align int) { + if out.Len() > 0 { + out.WriteString("\t") + } + out.Write(text) + out.WriteString("\t") +} + +func (r *roffRenderer) Footnotes(out *bytes.Buffer, text func() bool) { + +} + +func (r *roffRenderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { + +} + +func (r *roffRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) { + out.WriteString("\n\\[la]") + out.Write(link) + out.WriteString("\\[ra]") +} + +func (r *roffRenderer) CodeSpan(out *bytes.Buffer, text []byte) { + out.WriteString("\\fB\\fC") + escapeSpecialChars(out, text) + out.WriteString("\\fR") +} + +func (r *roffRenderer) DoubleEmphasis(out *bytes.Buffer, text []byte) { + out.WriteString("\\fB") + out.Write(text) + out.WriteString("\\fP") +} + +func (r *roffRenderer) Emphasis(out *bytes.Buffer, text []byte) { + out.WriteString("\\fI") + out.Write(text) + out.WriteString("\\fP") +} + +func (r *roffRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { +} + +func (r *roffRenderer) LineBreak(out *bytes.Buffer) { + out.WriteString("\n.br\n") +} + +func (r *roffRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { + r.AutoLink(out, link, 0) +} + +func (r *roffRenderer) RawHtmlTag(out *bytes.Buffer, tag []byte) { + out.Write(tag) +} + +func (r *roffRenderer) TripleEmphasis(out *bytes.Buffer, text []byte) { + out.WriteString("\\s+2") + out.Write(text) + out.WriteString("\\s-2") +} + +func (r *roffRenderer) StrikeThrough(out *bytes.Buffer, text []byte) { +} + +func (r *roffRenderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { + +} + +func (r *roffRenderer) Entity(out *bytes.Buffer, entity []byte) { + out.WriteString(html.UnescapeString(string(entity))) +} + +func processFooterText(text []byte) []byte { + text = bytes.TrimPrefix(text, []byte("% ")) + newText := []byte{} + textArr := strings.Split(string(text), ") ") + + for i, w := range textArr { + if i == 0 { + w = strings.Replace(w, "(", "\" \"", 1) + w = fmt.Sprintf("\"%s\"", w) + } else { + w = fmt.Sprintf(" \"%s\"", w) + } + newText = append(newText, []byte(w)...) + } + newText = append(newText, []byte(" \"\"")...) + + return newText +} + +func (r *roffRenderer) NormalText(out *bytes.Buffer, text []byte) { + escapeSpecialChars(out, text) +} + +func (r *roffRenderer) DocumentHeader(out *bytes.Buffer) { +} + +func (r *roffRenderer) DocumentFooter(out *bytes.Buffer) { +} + +func needsBackslash(c byte) bool { + for _, r := range []byte("-_&\\~") { + if c == r { + return true + } + } + return false +} + +func escapeSpecialChars(out *bytes.Buffer, text []byte) { + for i := 0; i < len(text); i++ { + // directly copy normal characters + org := i + + for i < len(text) && !needsBackslash(text[i]) { + i++ + } + if i > org { + out.Write(text[org:i]) + } + + // escape a character + if i >= len(text) { + break + } + out.WriteByte('\\') + out.WriteByte(text[i]) + } +} diff --git a/Godeps/_workspace/src/github.com/russross/blackfriday/.gitignore b/Godeps/_workspace/src/github.com/russross/blackfriday/.gitignore new file mode 100644 index 0000000000..75623dcccb --- /dev/null +++ b/Godeps/_workspace/src/github.com/russross/blackfriday/.gitignore @@ -0,0 +1,8 @@ +*.out +*.swp +*.8 +*.6 +_obj +_test* +markdown +tags diff --git a/Godeps/_workspace/src/github.com/russross/blackfriday/.travis.yml b/Godeps/_workspace/src/github.com/russross/blackfriday/.travis.yml new file mode 100644 index 0000000000..4dd7a1c591 --- /dev/null +++ b/Godeps/_workspace/src/github.com/russross/blackfriday/.travis.yml @@ -0,0 +1,17 @@ +# Travis CI (http://travis-ci.org/) is a continuous integration service for +# open source projects. This file configures it to run unit tests for +# blackfriday. + +language: go + +go: + - 1.2 + - 1.3 + - 1.4 + +install: + - go get -d -t -v ./... + - go build -v ./... + +script: + - go test -v ./... diff --git a/Godeps/_workspace/src/github.com/russross/blackfriday/LICENSE.txt b/Godeps/_workspace/src/github.com/russross/blackfriday/LICENSE.txt new file mode 100644 index 0000000000..2885af3602 --- /dev/null +++ b/Godeps/_workspace/src/github.com/russross/blackfriday/LICENSE.txt @@ -0,0 +1,29 @@ +Blackfriday is distributed under the Simplified BSD License: + +> Copyright © 2011 Russ Ross +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions +> are met: +> +> 1. Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> +> 2. Redistributions in binary form must reproduce the above +> copyright notice, this list of conditions and the following +> disclaimer in the documentation and/or other materials provided with +> the distribution. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +> POSSIBILITY OF SUCH DAMAGE. diff --git a/Godeps/_workspace/src/github.com/russross/blackfriday/README.md b/Godeps/_workspace/src/github.com/russross/blackfriday/README.md new file mode 100644 index 0000000000..52e3b25b10 --- /dev/null +++ b/Godeps/_workspace/src/github.com/russross/blackfriday/README.md @@ -0,0 +1,246 @@ +Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) +=========== + +Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It +is paranoid about its input (so you can safely feed it user-supplied +data), it is fast, it supports common extensions (tables, smart +punctuation substitutions, etc.), and it is safe for all utf-8 +(unicode) input. + +HTML output is currently supported, along with Smartypants +extensions. An experimental LaTeX output engine is also included. + +It started as a translation from C of [Sundown][3]. + + +Installation +------------ + +Blackfriday is compatible with Go 1. If you are using an older +release of Go, consider using v1.1 of blackfriday, which was based +on the last stable release of Go prior to Go 1. You can find it as a +tagged commit on github. + +With Go 1 and git installed: + + go get github.com/russross/blackfriday + +will download, compile, and install the package into your `$GOPATH` +directory hierarchy. Alternatively, you can achieve the same if you +import it into a project: + + import "github.com/russross/blackfriday" + +and `go get` without parameters. + +Usage +----- + +For basic usage, it is as simple as getting your input into a byte +slice and calling: + + output := blackfriday.MarkdownBasic(input) + +This renders it with no extensions enabled. To get a more useful +feature set, use this instead: + + output := blackfriday.MarkdownCommon(input) + +### Sanitize untrusted content + +Blackfriday itself does nothing to protect against malicious content. If you are +dealing with user-supplied markdown, we recommend running blackfriday's output +through HTML sanitizer such as +[Bluemonday](https://github.com/microcosm-cc/bluemonday). + +Here's an example of simple usage of blackfriday together with bluemonday: + +``` go +import ( + "github.com/microcosm-cc/bluemonday" + "github.com/russross/blackfriday" +) + +// ... +unsafe := blackfriday.MarkdownCommon(input) +html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) +``` + +### Custom options + +If you want to customize the set of options, first get a renderer +(currently either the HTML or LaTeX output engines), then use it to +call the more general `Markdown` function. For examples, see the +implementations of `MarkdownBasic` and `MarkdownCommon` in +`markdown.go`. + +You can also check out `blackfriday-tool` for a more complete example +of how to use it. Download and install it using: + + go get github.com/russross/blackfriday-tool + +This is a simple command-line tool that allows you to process a +markdown file using a standalone program. You can also browse the +source directly on github if you are just looking for some example +code: + +* + +Note that if you have not already done so, installing +`blackfriday-tool` will be sufficient to download and install +blackfriday in addition to the tool itself. The tool binary will be +installed in `$GOPATH/bin`. This is a statically-linked binary that +can be copied to wherever you need it without worrying about +dependencies and library versions. + + +Features +-------- + +All features of Sundown are supported, including: + +* **Compatibility**. The Markdown v1.0.3 test suite passes with + the `--tidy` option. Without `--tidy`, the differences are + mostly in whitespace and entity escaping, where blackfriday is + more consistent and cleaner. + +* **Common extensions**, including table support, fenced code + blocks, autolinks, strikethroughs, non-strict emphasis, etc. + +* **Safety**. Blackfriday is paranoid when parsing, making it safe + to feed untrusted user input without fear of bad things + happening. The test suite stress tests this and there are no + known inputs that make it crash. If you find one, please let me + know and send me the input that does it. + + NOTE: "safety" in this context means *runtime safety only*. In order to + protect yourself agains JavaScript injection in untrusted content, see + [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). + +* **Fast processing**. It is fast enough to render on-demand in + most web applications without having to cache the output. + +* **Thread safety**. You can run multiple parsers in different + goroutines without ill effect. There is no dependence on global + shared state. + +* **Minimal dependencies**. Blackfriday only depends on standard + library packages in Go. The source code is pretty + self-contained, so it is easy to add to any project, including + Google App Engine projects. + +* **Standards compliant**. Output successfully validates using the + W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. + + +Extensions +---------- + +In addition to the standard markdown syntax, this package +implements the following extensions: + +* **Intra-word emphasis supression**. The `_` character is + commonly used inside words when discussing code, so having + markdown interpret it as an emphasis command is usually the + wrong thing. Blackfriday lets you treat all emphasis markers as + normal characters when they occur inside a word. + +* **Tables**. Tables can be created by drawing them in the input + using a simple syntax: + + ``` + Name | Age + --------|------ + Bob | 27 + Alice | 23 + ``` + +* **Fenced code blocks**. In addition to the normal 4-space + indentation to mark code blocks, you can explicitly mark them + and supply a language (to make syntax highlighting simple). Just + mark it like this: + + ``` go + func getTrue() bool { + return true + } + ``` + + You can use 3 or more backticks to mark the beginning of the + block, and the same number to mark the end of the block. + +* **Autolinking**. Blackfriday can find URLs that have not been + explicitly marked as links and turn them into links. + +* **Strikethrough**. Use two tildes (`~~`) to mark text that + should be crossed out. + +* **Hard line breaks**. With this extension enabled (it is off by + default in the `MarkdownBasic` and `MarkdownCommon` convenience + functions), newlines in the input translate into line breaks in + the output. + +* **Smart quotes**. Smartypants-style punctuation substitution is + supported, turning normal double- and single-quote marks into + curly quotes, etc. + +* **LaTeX-style dash parsing** is an additional option, where `--` + is translated into `–`, and `---` is translated into + `—`. This differs from most smartypants processors, which + turn a single hyphen into an ndash and a double hyphen into an + mdash. + +* **Smart fractions**, where anything that looks like a fraction + is translated into suitable HTML (instead of just a few special + cases like most smartypant processors). For example, `4/5` + becomes `4⁄5`, which renders as + 4⁄5. + + +Other renderers +--------------- + +Blackfriday is structured to allow alternative rendering engines. Here +are a few of note: + +* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): + provides a GitHub Flavored Markdown renderer with fenced code block + highlighting, clickable header anchor links. + + It's not customizable, and its goal is to produce HTML output + equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), + except the rendering is performed locally. + +* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, + but for markdown. + +* LaTeX output: renders output as LaTeX. This is currently part of the + main Blackfriday repository, but may be split into its own project + in the future. If you are interested in owning and maintaining the + LaTeX output component, please be in touch. + + It renders some basic documents, but is only experimental at this + point. In particular, it does not do any inline escaping, so input + that happens to look like LaTeX code will be passed through without + modification. + + +Todo +---- + +* More unit testing +* Improve unicode support. It does not understand all unicode + rules (about what constitutes a letter, a punctuation symbol, + etc.), so it may fail to detect word boundaries correctly in + some instances. It is safe on all utf-8 input. + + +License +------- + +[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) + + + [1]: http://daringfireball.net/projects/markdown/ "Markdown" + [2]: http://golang.org/ "Go Language" + [3]: https://github.com/vmg/sundown "Sundown" diff --git a/Godeps/_workspace/src/github.com/russross/blackfriday/block.go b/Godeps/_workspace/src/github.com/russross/blackfriday/block.go new file mode 100644 index 0000000000..1f300b334d --- /dev/null +++ b/Godeps/_workspace/src/github.com/russross/blackfriday/block.go @@ -0,0 +1,1389 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Functions to parse block-level elements. +// + +package blackfriday + +import ( + "bytes" + + "github.com/shurcooL/sanitized_anchor_name" +) + +// Parse block-level data. +// Note: this function and many that it calls assume that +// the input buffer ends with a newline. +func (p *parser) block(out *bytes.Buffer, data []byte) { + if len(data) == 0 || data[len(data)-1] != '\n' { + panic("block input is missing terminating newline") + } + + // this is called recursively: enforce a maximum depth + if p.nesting >= p.maxNesting { + return + } + p.nesting++ + + // parse out one block-level construct at a time + for len(data) > 0 { + // prefixed header: + // + // # Header 1 + // ## Header 2 + // ... + // ###### Header 6 + if p.isPrefixHeader(data) { + data = data[p.prefixHeader(out, data):] + continue + } + + // block of preformatted HTML: + // + //
+ // ... + //
+ if data[0] == '<' { + if i := p.html(out, data, true); i > 0 { + data = data[i:] + continue + } + } + + // title block + // + // % stuff + // % more stuff + // % even more stuff + if p.flags&EXTENSION_TITLEBLOCK != 0 { + if data[0] == '%' { + if i := p.titleBlock(out, data, true); i > 0 { + data = data[i:] + continue + } + } + } + + // blank lines. note: returns the # of bytes to skip + if i := p.isEmpty(data); i > 0 { + data = data[i:] + continue + } + + // indented code block: + // + // func max(a, b int) int { + // if a > b { + // return a + // } + // return b + // } + if p.codePrefix(data) > 0 { + data = data[p.code(out, data):] + continue + } + + // fenced code block: + // + // ``` go + // func fact(n int) int { + // if n <= 1 { + // return n + // } + // return n * fact(n-1) + // } + // ``` + if p.flags&EXTENSION_FENCED_CODE != 0 { + if i := p.fencedCode(out, data, true); i > 0 { + data = data[i:] + continue + } + } + + // horizontal rule: + // + // ------ + // or + // ****** + // or + // ______ + if p.isHRule(data) { + p.r.HRule(out) + var i int + for i = 0; data[i] != '\n'; i++ { + } + data = data[i:] + continue + } + + // block quote: + // + // > A big quote I found somewhere + // > on the web + if p.quotePrefix(data) > 0 { + data = data[p.quote(out, data):] + continue + } + + // table: + // + // Name | Age | Phone + // ------|-----|--------- + // Bob | 31 | 555-1234 + // Alice | 27 | 555-4321 + if p.flags&EXTENSION_TABLES != 0 { + if i := p.table(out, data); i > 0 { + data = data[i:] + continue + } + } + + // an itemized/unordered list: + // + // * Item 1 + // * Item 2 + // + // also works with + or - + if p.uliPrefix(data) > 0 { + data = data[p.list(out, data, 0):] + continue + } + + // a numbered/ordered list: + // + // 1. Item 1 + // 2. Item 2 + if p.oliPrefix(data) > 0 { + data = data[p.list(out, data, LIST_TYPE_ORDERED):] + continue + } + + // definition lists: + // + // Term 1 + // : Definition a + // : Definition b + // + // Term 2 + // : Definition c + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.dliPrefix(data) > 0 { + data = data[p.list(out, data, LIST_TYPE_DEFINITION):] + continue + } + } + + // anything else must look like a normal paragraph + // note: this finds underlined headers, too + data = data[p.paragraph(out, data):] + } + + p.nesting-- +} + +func (p *parser) isPrefixHeader(data []byte) bool { + if data[0] != '#' { + return false + } + + if p.flags&EXTENSION_SPACE_HEADERS != 0 { + level := 0 + for level < 6 && data[level] == '#' { + level++ + } + if data[level] != ' ' { + return false + } + } + return true +} + +func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int { + level := 0 + for level < 6 && data[level] == '#' { + level++ + } + i := skipChar(data, level, ' ') + end := skipUntilChar(data, i, '\n') + skip := end + id := "" + if p.flags&EXTENSION_HEADER_IDS != 0 { + j, k := 0, 0 + // find start/end of header id + for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { + } + for k = j + 1; k < end && data[k] != '}'; k++ { + } + // extract header id iff found + if j < end && k < end { + id = string(data[j+2 : k]) + end = j + skip = k + 1 + for end > 0 && data[end-1] == ' ' { + end-- + } + } + } + for end > 0 && data[end-1] == '#' { + if isBackslashEscaped(data, end-1) { + break + } + end-- + } + for end > 0 && data[end-1] == ' ' { + end-- + } + if end > i { + if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = sanitized_anchor_name.Create(string(data[i:end])) + } + work := func() bool { + p.inline(out, data[i:end]) + return true + } + p.r.Header(out, work, level, id) + } + return skip +} + +func (p *parser) isUnderlinedHeader(data []byte) int { + // test of level 1 header + if data[0] == '=' { + i := skipChar(data, 1, '=') + i = skipChar(data, i, ' ') + if data[i] == '\n' { + return 1 + } else { + return 0 + } + } + + // test of level 2 header + if data[0] == '-' { + i := skipChar(data, 1, '-') + i = skipChar(data, i, ' ') + if data[i] == '\n' { + return 2 + } else { + return 0 + } + } + + return 0 +} + +func (p *parser) titleBlock(out *bytes.Buffer, data []byte, doRender bool) int { + if data[0] != '%' { + return 0 + } + splitData := bytes.Split(data, []byte("\n")) + var i int + for idx, b := range splitData { + if !bytes.HasPrefix(b, []byte("%")) { + i = idx // - 1 + break + } + } + + data = bytes.Join(splitData[0:i], []byte("\n")) + p.r.TitleBlock(out, data) + + return len(data) +} + +func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int { + var i, j int + + // identify the opening tag + if data[0] != '<' { + return 0 + } + curtag, tagfound := p.htmlFindTag(data[1:]) + + // handle special cases + if !tagfound { + // check for an HTML comment + if size := p.htmlComment(out, data, doRender); size > 0 { + return size + } + + // check for an
tag + if size := p.htmlHr(out, data, doRender); size > 0 { + return size + } + + // no special case recognized + return 0 + } + + // look for an unindented matching closing tag + // followed by a blank line + found := false + /* + closetag := []byte("\n") + j = len(curtag) + 1 + for !found { + // scan for a closing tag at the beginning of a line + if skip := bytes.Index(data[j:], closetag); skip >= 0 { + j += skip + len(closetag) + } else { + break + } + + // see if it is the only thing on the line + if skip := p.isEmpty(data[j:]); skip > 0 { + // see if it is followed by a blank line/eof + j += skip + if j >= len(data) { + found = true + i = j + } else { + if skip := p.isEmpty(data[j:]); skip > 0 { + j += skip + found = true + i = j + } + } + } + } + */ + + // if not found, try a second pass looking for indented match + // but not if tag is "ins" or "del" (following original Markdown.pl) + if !found && curtag != "ins" && curtag != "del" { + i = 1 + for i < len(data) { + i++ + for i < len(data) && !(data[i-1] == '<' && data[i] == '/') { + i++ + } + + if i+2+len(curtag) >= len(data) { + break + } + + j = p.htmlFindEnd(curtag, data[i-1:]) + + if j > 0 { + i += j - 1 + found = true + break + } + } + } + + if !found { + return 0 + } + + // the end of the block has been found + if doRender { + // trim newlines + end := i + for end > 0 && data[end-1] == '\n' { + end-- + } + p.r.BlockHtml(out, data[:end]) + } + + return i +} + +// HTML comment, lax form +func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int { + if data[0] != '<' || data[1] != '!' || data[2] != '-' || data[3] != '-' { + return 0 + } + + i := 5 + + // scan for an end-of-comment marker, across lines if necessary + for i < len(data) && !(data[i-2] == '-' && data[i-1] == '-' && data[i] == '>') { + i++ + } + i++ + + // no end-of-comment marker + if i >= len(data) { + return 0 + } + + // needs to end with a blank line + if j := p.isEmpty(data[i:]); j > 0 { + size := i + j + if doRender { + // trim trailing newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + p.r.BlockHtml(out, data[:end]) + } + return size + } + + return 0 +} + +// HR, which is the only self-closing block tag considered +func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int { + if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { + return 0 + } + if data[3] != ' ' && data[3] != '/' && data[3] != '>' { + // not an
tag after all; at least not a valid one + return 0 + } + + i := 3 + for data[i] != '>' && data[i] != '\n' { + i++ + } + + if data[i] == '>' { + i++ + if j := p.isEmpty(data[i:]); j > 0 { + size := i + j + if doRender { + // trim newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + p.r.BlockHtml(out, data[:end]) + } + return size + } + } + + return 0 +} + +func (p *parser) htmlFindTag(data []byte) (string, bool) { + i := 0 + for isalnum(data[i]) { + i++ + } + key := string(data[:i]) + if blockTags[key] { + return key, true + } + return "", false +} + +func (p *parser) htmlFindEnd(tag string, data []byte) int { + // assume data[0] == '<' && data[1] == '/' already tested + + // check if tag is a match + closetag := []byte("") + if !bytes.HasPrefix(data, closetag) { + return 0 + } + i := len(closetag) + + // check that the rest of the line is blank + skip := 0 + if skip = p.isEmpty(data[i:]); skip == 0 { + return 0 + } + i += skip + skip = 0 + + if i >= len(data) { + return i + } + + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + return i + } + if skip = p.isEmpty(data[i:]); skip == 0 { + // following line must be blank + return 0 + } + + return i + skip +} + +func (p *parser) isEmpty(data []byte) int { + // it is okay to call isEmpty on an empty buffer + if len(data) == 0 { + return 0 + } + + var i int + for i = 0; i < len(data) && data[i] != '\n'; i++ { + if data[i] != ' ' && data[i] != '\t' { + return 0 + } + } + return i + 1 +} + +func (p *parser) isHRule(data []byte) bool { + i := 0 + + // skip up to three spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // look at the hrule char + if data[i] != '*' && data[i] != '-' && data[i] != '_' { + return false + } + c := data[i] + + // the whole line must be the char or whitespace + n := 0 + for data[i] != '\n' { + switch { + case data[i] == c: + n++ + case data[i] != ' ': + return false + } + i++ + } + + return n >= 3 +} + +func (p *parser) isFencedCode(data []byte, syntax **string, oldmarker string) (skip int, marker string) { + i, size := 0, 0 + skip = 0 + + // skip up to three spaces + for i < len(data) && i < 3 && data[i] == ' ' { + i++ + } + if i >= len(data) { + return + } + + // check for the marker characters: ~ or ` + if data[i] != '~' && data[i] != '`' { + return + } + + c := data[i] + + // the whole line must be the same char or whitespace + for i < len(data) && data[i] == c { + size++ + i++ + } + + if i >= len(data) { + return + } + + // the marker char must occur at least 3 times + if size < 3 { + return + } + marker = string(data[i-size : i]) + + // if this is the end marker, it must match the beginning marker + if oldmarker != "" && marker != oldmarker { + return + } + + if syntax != nil { + syn := 0 + i = skipChar(data, i, ' ') + + if i >= len(data) { + return + } + + syntaxStart := i + + if data[i] == '{' { + i++ + syntaxStart++ + + for i < len(data) && data[i] != '}' && data[i] != '\n' { + syn++ + i++ + } + + if i >= len(data) || data[i] != '}' { + return + } + + // strip all whitespace at the beginning and the end + // of the {} block + for syn > 0 && isspace(data[syntaxStart]) { + syntaxStart++ + syn-- + } + + for syn > 0 && isspace(data[syntaxStart+syn-1]) { + syn-- + } + + i++ + } else { + for i < len(data) && !isspace(data[i]) { + syn++ + i++ + } + } + + language := string(data[syntaxStart : syntaxStart+syn]) + *syntax = &language + } + + i = skipChar(data, i, ' ') + if i >= len(data) || data[i] != '\n' { + return + } + + skip = i + 1 + return +} + +func (p *parser) fencedCode(out *bytes.Buffer, data []byte, doRender bool) int { + var lang *string + beg, marker := p.isFencedCode(data, &lang, "") + if beg == 0 || beg >= len(data) { + return 0 + } + + var work bytes.Buffer + + for { + // safe to assume beg < len(data) + + // check for the end of the code block + fenceEnd, _ := p.isFencedCode(data[beg:], nil, marker) + if fenceEnd != 0 { + beg += fenceEnd + break + } + + // copy the current line + end := skipUntilChar(data, beg, '\n') + 1 + + // did we reach the end of the buffer without a closing marker? + if end >= len(data) { + return 0 + } + + // verbatim copy to the working buffer + if doRender { + work.Write(data[beg:end]) + } + beg = end + } + + syntax := "" + if lang != nil { + syntax = *lang + } + + if doRender { + p.r.BlockCode(out, work.Bytes(), syntax) + } + + return beg +} + +func (p *parser) table(out *bytes.Buffer, data []byte) int { + var header bytes.Buffer + i, columns := p.tableHeader(&header, data) + if i == 0 { + return 0 + } + + var body bytes.Buffer + + for i < len(data) { + pipes, rowStart := 0, i + for ; data[i] != '\n'; i++ { + if data[i] == '|' { + pipes++ + } + } + + if pipes == 0 { + i = rowStart + break + } + + // include the newline in data sent to tableRow + i++ + p.tableRow(&body, data[rowStart:i], columns, false) + } + + p.r.Table(out, header.Bytes(), body.Bytes(), columns) + + return i +} + +// check if the specified position is preceded by an odd number of backslashes +func isBackslashEscaped(data []byte, i int) bool { + backslashes := 0 + for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' { + backslashes++ + } + return backslashes&1 == 1 +} + +func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns []int) { + i := 0 + colCount := 1 + for i = 0; data[i] != '\n'; i++ { + if data[i] == '|' && !isBackslashEscaped(data, i) { + colCount++ + } + } + + // doesn't look like a table header + if colCount == 1 { + return + } + + // include the newline in the data sent to tableRow + header := data[:i+1] + + // column count ignores pipes at beginning or end of line + if data[0] == '|' { + colCount-- + } + if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) { + colCount-- + } + + columns = make([]int, colCount) + + // move on to the header underline + i++ + if i >= len(data) { + return + } + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + i = skipChar(data, i, ' ') + + // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 + // and trailing | optional on last column + col := 0 + for data[i] != '\n' { + dashes := 0 + + if data[i] == ':' { + i++ + columns[col] |= TABLE_ALIGNMENT_LEFT + dashes++ + } + for data[i] == '-' { + i++ + dashes++ + } + if data[i] == ':' { + i++ + columns[col] |= TABLE_ALIGNMENT_RIGHT + dashes++ + } + for data[i] == ' ' { + i++ + } + + // end of column test is messy + switch { + case dashes < 3: + // not a valid column + return + + case data[i] == '|' && !isBackslashEscaped(data, i): + // marker found, now skip past trailing whitespace + col++ + i++ + for data[i] == ' ' { + i++ + } + + // trailing junk found after last column + if col >= colCount && data[i] != '\n' { + return + } + + case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount: + // something else found where marker was required + return + + case data[i] == '\n': + // marker is optional for the last column + col++ + + default: + // trailing junk found after last column + return + } + } + if col != colCount { + return + } + + p.tableRow(out, header, columns, true) + size = i + 1 + return +} + +func (p *parser) tableRow(out *bytes.Buffer, data []byte, columns []int, header bool) { + i, col := 0, 0 + var rowWork bytes.Buffer + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + + for col = 0; col < len(columns) && i < len(data); col++ { + for data[i] == ' ' { + i++ + } + + cellStart := i + + for (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { + i++ + } + + cellEnd := i + + // skip the end-of-cell marker, possibly taking us past end of buffer + i++ + + for cellEnd > cellStart && data[cellEnd-1] == ' ' { + cellEnd-- + } + + var cellWork bytes.Buffer + p.inline(&cellWork, data[cellStart:cellEnd]) + + if header { + p.r.TableHeaderCell(&rowWork, cellWork.Bytes(), columns[col]) + } else { + p.r.TableCell(&rowWork, cellWork.Bytes(), columns[col]) + } + } + + // pad it out with empty columns to get the right number + for ; col < len(columns); col++ { + if header { + p.r.TableHeaderCell(&rowWork, nil, columns[col]) + } else { + p.r.TableCell(&rowWork, nil, columns[col]) + } + } + + // silently ignore rows with too many cells + + p.r.TableRow(out, rowWork.Bytes()) +} + +// returns blockquote prefix length +func (p *parser) quotePrefix(data []byte) int { + i := 0 + for i < 3 && data[i] == ' ' { + i++ + } + if data[i] == '>' { + if data[i+1] == ' ' { + return i + 2 + } + return i + 1 + } + return 0 +} + +// parse a blockquote fragment +func (p *parser) quote(out *bytes.Buffer, data []byte) int { + var raw bytes.Buffer + beg, end := 0, 0 + for beg < len(data) { + end = beg + for data[end] != '\n' { + end++ + } + end++ + + if pre := p.quotePrefix(data[beg:]); pre > 0 { + // skip the prefix + beg += pre + } else if p.isEmpty(data[beg:]) > 0 && + (end >= len(data) || + (p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0)) { + // blockquote ends with at least one blank line + // followed by something without a blockquote prefix + break + } + + // this line is part of the blockquote + raw.Write(data[beg:end]) + beg = end + } + + var cooked bytes.Buffer + p.block(&cooked, raw.Bytes()) + p.r.BlockQuote(out, cooked.Bytes()) + return end +} + +// returns prefix length for block code +func (p *parser) codePrefix(data []byte) int { + if data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { + return 4 + } + return 0 +} + +func (p *parser) code(out *bytes.Buffer, data []byte) int { + var work bytes.Buffer + + i := 0 + for i < len(data) { + beg := i + for data[i] != '\n' { + i++ + } + i++ + + blankline := p.isEmpty(data[beg:i]) > 0 + if pre := p.codePrefix(data[beg:i]); pre > 0 { + beg += pre + } else if !blankline { + // non-empty, non-prefixed line breaks the pre + i = beg + break + } + + // verbatim copy to the working buffeu + if blankline { + work.WriteByte('\n') + } else { + work.Write(data[beg:i]) + } + } + + // trim all the \n off the end of work + workbytes := work.Bytes() + eol := len(workbytes) + for eol > 0 && workbytes[eol-1] == '\n' { + eol-- + } + if eol != len(workbytes) { + work.Truncate(eol) + } + + work.WriteByte('\n') + + p.r.BlockCode(out, work.Bytes(), "") + + return i +} + +// returns unordered list item prefix +func (p *parser) uliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // need a *, +, or - followed by a space + if (data[i] != '*' && data[i] != '+' && data[i] != '-') || + data[i+1] != ' ' { + return 0 + } + return i + 2 +} + +// returns ordered list item prefix +func (p *parser) oliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // count the digits + start := i + for data[i] >= '0' && data[i] <= '9' { + i++ + } + + // we need >= 1 digits followed by a dot and a space + if start == i || data[i] != '.' || data[i+1] != ' ' { + return 0 + } + return i + 2 +} + +// returns definition list item prefix +func (p *parser) dliPrefix(data []byte) int { + i := 0 + + // need a : followed by a spaces + if data[i] != ':' || data[i+1] != ' ' { + return 0 + } + for data[i] == ' ' { + i++ + } + return i + 2 +} + +// parse ordered or unordered list block +func (p *parser) list(out *bytes.Buffer, data []byte, flags int) int { + i := 0 + flags |= LIST_ITEM_BEGINNING_OF_LIST + work := func() bool { + for i < len(data) { + skip := p.listItem(out, data[i:], &flags) + i += skip + + if skip == 0 || flags&LIST_ITEM_END_OF_LIST != 0 { + break + } + flags &= ^LIST_ITEM_BEGINNING_OF_LIST + } + return true + } + + p.r.List(out, work, flags) + return i +} + +// Parse a single list item. +// Assumes initial prefix is already removed if this is a sublist. +func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int { + // keep track of the indentation of the first line + itemIndent := 0 + for itemIndent < 3 && data[itemIndent] == ' ' { + itemIndent++ + } + + i := p.uliPrefix(data) + if i == 0 { + i = p.oliPrefix(data) + } + if i == 0 { + i = p.dliPrefix(data) + // reset definition term flag + if i > 0 { + *flags &= ^LIST_TYPE_TERM + } + } + if i == 0 { + // if in defnition list, set term flag and continue + if *flags&LIST_TYPE_DEFINITION != 0 { + *flags |= LIST_TYPE_TERM + } else { + return 0 + } + } + + // skip leading whitespace on first line + for data[i] == ' ' { + i++ + } + + // find the end of the line + line := i + for i > 0 && data[i-1] != '\n' { + i++ + } + + // get working buffer + var raw bytes.Buffer + + // put the first line into the working buffer + raw.Write(data[line:i]) + line = i + + // process the following lines + containsBlankLine := false + sublist := 0 + +gatherlines: + for line < len(data) { + i++ + + // find the end of this line + for data[i-1] != '\n' { + i++ + } + + // if it is an empty line, guess that it is part of this item + // and move on to the next line + if p.isEmpty(data[line:i]) > 0 { + containsBlankLine = true + line = i + continue + } + + // calculate the indentation + indent := 0 + for indent < 4 && line+indent < i && data[line+indent] == ' ' { + indent++ + } + + chunk := data[line+indent : i] + + // evaluate how this line fits in + switch { + // is this a nested list item? + case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || + p.oliPrefix(chunk) > 0 || + p.dliPrefix(chunk) > 0: + + if containsBlankLine { + *flags |= LIST_ITEM_CONTAINS_BLOCK + } + + // to be a nested list, it must be indented more + // if not, it is the next item in the same list + if indent <= itemIndent { + break gatherlines + } + + // is this the first item in the nested list? + if sublist == 0 { + sublist = raw.Len() + } + + // is this a nested prefix header? + case p.isPrefixHeader(chunk): + // if the header is not indented, it is not nested in the list + // and thus ends the list + if containsBlankLine && indent < 4 { + *flags |= LIST_ITEM_END_OF_LIST + break gatherlines + } + *flags |= LIST_ITEM_CONTAINS_BLOCK + + // anything following an empty line is only part + // of this item if it is indented 4 spaces + // (regardless of the indentation of the beginning of the item) + case containsBlankLine && indent < 4: + if *flags&LIST_TYPE_DEFINITION != 0 && i < len(data)-1 { + // is the next item still a part of this list? + next := i + for data[next] != '\n' { + next++ + } + for next < len(data)-1 && data[next] == '\n' { + next++ + } + if i < len(data)-1 && data[i] != ':' && data[next] != ':' { + *flags |= LIST_ITEM_END_OF_LIST + } + } else { + *flags |= LIST_ITEM_END_OF_LIST + } + break gatherlines + + // a blank line means this should be parsed as a block + case containsBlankLine: + raw.WriteByte('\n') + *flags |= LIST_ITEM_CONTAINS_BLOCK + } + + // if this line was preceeded by one or more blanks, + // re-introduce the blank into the buffer + if containsBlankLine { + containsBlankLine = false + raw.WriteByte('\n') + + } + + // add the line into the working buffer without prefix + raw.Write(data[line+indent : i]) + + line = i + } + + rawBytes := raw.Bytes() + + // render the contents of the list item + var cooked bytes.Buffer + if *flags&LIST_ITEM_CONTAINS_BLOCK != 0 && *flags&LIST_TYPE_TERM == 0 { + // intermediate render of block item, except for definition term + if sublist > 0 { + p.block(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) + } else { + p.block(&cooked, rawBytes) + } + } else { + // intermediate render of inline item + if sublist > 0 { + p.inline(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) + } else { + p.inline(&cooked, rawBytes) + } + } + + // render the actual list item + cookedBytes := cooked.Bytes() + parsedEnd := len(cookedBytes) + + // strip trailing newlines + for parsedEnd > 0 && cookedBytes[parsedEnd-1] == '\n' { + parsedEnd-- + } + p.r.ListItem(out, cookedBytes[:parsedEnd], *flags) + + return line +} + +// render a single paragraph that has already been parsed out +func (p *parser) renderParagraph(out *bytes.Buffer, data []byte) { + if len(data) == 0 { + return + } + + // trim leading spaces + beg := 0 + for data[beg] == ' ' { + beg++ + } + + // trim trailing newline + end := len(data) - 1 + + // trim trailing spaces + for end > beg && data[end-1] == ' ' { + end-- + } + + work := func() bool { + p.inline(out, data[beg:end]) + return true + } + p.r.Paragraph(out, work) +} + +func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { + // prev: index of 1st char of previous line + // line: index of 1st char of current line + // i: index of cursor/end of current line + var prev, line, i int + + // keep going until we find something to mark the end of the paragraph + for i < len(data) { + // mark the beginning of the current line + prev = line + current := data[i:] + line = i + + // did we find a blank line marking the end of the paragraph? + if n := p.isEmpty(current); n > 0 { + // did this blank line followed by a definition list item? + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if i < len(data)-1 && data[i+1] == ':' { + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + } + } + + p.renderParagraph(out, data[:i]) + return i + n + } + + // an underline under some text marks a header, so our paragraph ended on prev line + if i > 0 { + if level := p.isUnderlinedHeader(current); level > 0 { + // render the paragraph + p.renderParagraph(out, data[:prev]) + + // ignore leading and trailing whitespace + eol := i - 1 + for prev < eol && data[prev] == ' ' { + prev++ + } + for eol > prev && data[eol-1] == ' ' { + eol-- + } + + // render the header + // this ugly double closure avoids forcing variables onto the heap + work := func(o *bytes.Buffer, pp *parser, d []byte) func() bool { + return func() bool { + pp.inline(o, d) + return true + } + }(out, p, data[prev:eol]) + + id := "" + if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = sanitized_anchor_name.Create(string(data[prev:eol])) + } + + p.r.Header(out, work, level, id) + + // find the end of the underline + for data[i] != '\n' { + i++ + } + return i + } + } + + // if the next line starts a block of HTML, then the paragraph ends here + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + if data[i] == '<' && p.html(out, current, false) > 0 { + // rewind to before the HTML block + p.renderParagraph(out, data[:i]) + return i + } + } + + // if there's a prefixed header or a horizontal rule after this, paragraph is over + if p.isPrefixHeader(current) || p.isHRule(current) { + p.renderParagraph(out, data[:i]) + return i + } + + // if there's a definition list item, prev line is a definition term + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.dliPrefix(current) != 0 { + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + } + } + + // if there's a list after this, paragraph is over + if p.flags&EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK != 0 { + if p.uliPrefix(current) != 0 || + p.oliPrefix(current) != 0 || + p.quotePrefix(current) != 0 || + p.codePrefix(current) != 0 { + p.renderParagraph(out, data[:i]) + return i + } + } + + // otherwise, scan to the beginning of the next line + for data[i] != '\n' { + i++ + } + i++ + } + + p.renderParagraph(out, data[:i]) + return i +} diff --git a/Godeps/_workspace/src/github.com/russross/blackfriday/html.go b/Godeps/_workspace/src/github.com/russross/blackfriday/html.go new file mode 100644 index 0000000000..264aae5870 --- /dev/null +++ b/Godeps/_workspace/src/github.com/russross/blackfriday/html.go @@ -0,0 +1,948 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// +// HTML rendering backend +// +// + +package blackfriday + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" +) + +// Html renderer configuration options. +const ( + HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks + HTML_SKIP_STYLE // skip embedded