diff --git a/client/client_root_validation_test.go b/client/client_root_validation_test.go new file mode 100644 index 0000000000..e563989521 --- /dev/null +++ b/client/client_root_validation_test.go @@ -0,0 +1,269 @@ +package client + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/Sirupsen/logrus" + "github.com/docker/notary/keystoremanager" + "github.com/endophage/gotuf/data" + "github.com/stretchr/testify/assert" +) + +type SignedRSARootTemplate struct { + RootPem string +} + +const validPEMEncodedRSARoot = `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZLekNDQXhXZ0F3SUJBZ0lRUnlwOVFxY0pmZDNheXFkaml6OHhJREFMQmdrcWhraUc5dzBCQVFzd09ERWEKTUJnR0ExVUVDaE1SWkc5amEyVnlMbU52YlM5dWIzUmhjbmt4R2pBWUJnTlZCQU1URVdSdlkydGxjaTVqYjIwdgpibTkwWVhKNU1CNFhEVEUxTURjeE56QTJNelF5TTFvWERURTNNRGN4TmpBMk16UXlNMW93T0RFYU1CZ0dBMVVFCkNoTVJaRzlqYTJWeUxtTnZiUzl1YjNSaGNua3hHakFZQmdOVkJBTVRFV1J2WTJ0bGNpNWpiMjB2Ym05MFlYSjUKTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFvUWZmcnpzWW5zSDh2R2Y0Smg1NQpDajV3cmpVR3pEL3NIa2FGSHB0ako2VG9KR0p2NXlNQVB4enlJbnU1c0lvR0xKYXBuWVZCb0FVMFlnSTlxbEFjCllBNlN4YVN3Z202cnB2bW5sOFFuMHFjNmdlcjNpbnBHYVVKeWxXSHVQd1drdmNpbVFBcUhaeDJkUXRMN2c2a3AKcm1LZVRXcFdvV0x3M0pvQVVaVVZoWk1kNmEyMlpML0R2QXcrSHJvZ2J6NFhleWFoRmI5SUg0MDJ6UHhONnZnYQpKRUZURjBKaTFqdE5nME1vNHBiOVNIc01zaXcrTFpLN1NmZkhWS1B4dmQyMW0vYmlObXdzZ0V4QTNVOE9PRzhwCnV5Z2ZhY3lzNWM4K1pyWCtaRkcvY3Z3S3owazYvUWZKVTQwczZNaFh3NUMyV3R0ZFZtc0c5LzdyR0ZZakhvSUoKd2VEeXhnV2s3dnhLelJKSS91bjdjYWdESWFRc0tySlFjQ0hJR0ZSbHBJUjVUd1g3dmwzUjdjUm5jckRSTVZ2YwpWU0VHMmVzeGJ3N2p0eklwL3lwblZSeGNPbnk3SXlweWpLcVZlcVo2SGd4WnRUQlZyRjFPL2FIbzJrdmx3eVJTCkF1czRrdmg2ejMranpUbTlFemZYaVBRelk5QkVrNWdPTHhoVzlyYzZVaGxTK3BlNWxrYU4vSHlxeS9sUHVxODkKZk1yMnJyN2xmNVdGZEZuemU2V05ZTUFhVzdkTkE0TkUwZHlENTM0MjhaTFh4TlZQTDRXVTY2R2FjNmx5blE4bApyNXRQc1lJRlh6aDZGVmFSS0dRVXRXMWh6OWVjTzZZMjdSaDJKc3lpSXhnVXFrMm9veEU2OXVONDJ0K2R0cUtDCjFzOEcvN1Z0WThHREFMRkxZVG56THZzQ0F3RUFBYU0xTURNd0RnWURWUjBQQVFIL0JBUURBZ0NnTUJNR0ExVWQKSlFRTU1Bb0dDQ3NHQVFVRkJ3TURNQXdHQTFVZEV3RUIvd1FDTUFBd0N3WUpLb1pJaHZjTkFRRUxBNElDQVFCTQpPbGwzRy9YQno4aWRpTmROSkRXVWgrNXczb2ptd2FuclRCZENkcUVrMVdlbmFSNkR0Y2ZsSng2WjNmL213VjRvCmIxc2tPQVgxeVg1UkNhaEpIVU14TWljei9RMzhwT1ZlbEdQclduYzNUSkIrVktqR3lIWGxRRFZrWkZiKzQrZWYKd3RqN0huZ1hoSEZGRFNnam0zRWRNbmR2Z0RRN1NRYjRza09uQ05TOWl5WDdlWHhoRkJDWm1aTCtIQUxLQmoyQgp5aFY0SWNCRHFtcDUwNHQxNHJ4OS9KdnR5MGRHN2ZZN0k1MWdFUXBtNFMwMkpNTDV4dlRtMXhmYm9XSWhaT0RJCnN3RUFPK2VrQm9GSGJTMVE5S01QaklBdzNUckNISDh4OFhacTV6c1l0QUMxeVpIZENLYTI2YVdkeTU2QTllSGoKTzFWeHp3bWJOeVhSZW5WdUJZUCswd3IzSFZLRkc0Sko0WlpwTlp6UVcvcHFFUGdoQ1RKSXZJdWVLNjUyQnlVYwovL3N2K25YZDVmMTlMZUVTOXBmMGwyNTNORGFGWlBiNmFlZ0tmcXVXaDhxbFFCbVVRMkd6YVRMYnRtTmQyOE02Clc3aUw3dGtLWmUxWm5CejlSS2d0UHJEampXR1pJbmpqY09VOEV0VDRTTHE3a0NWRG1QczVNRDh2YUFtOTZKc0UKam1MQzNVdS80azdIaURZWDBpMG1PV2tGalpRTWRWYXRjSUY1RlBTcHB3c1NiVzhRaWRuWHQ1NFV0d3RGREVQegpscGpzN3liZVFFNzFKWGNNWm5WSUs0YmpSWHNFRlBJOThScElsRWRlZGJTVWRZQW5jTE5KUlQ3SFpCTVBHU3daCjBQTkp1Z2xubHIzc3JWemRXMWR6MnhRamR2THd4eTZtTlVGNnJiUUJXQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K` + +const validCAPEMEncodeRSARoot = `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlHTXpDQ0JCdWdBd0lCQWdJQkFUQU5CZ2txaGtpRzl3MEJBUXNGQURCZk1Rc3dDUVlEVlFRR0V3SlZVekVMDQpNQWtHQTFVRUNBd0NRMEV4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEekFOQmdOVkJBb01Ca1J2DQpZMnRsY2pFYU1CZ0dBMVVFQXd3UlRtOTBZWEo1SUZSbGMzUnBibWNnUTBFd0hoY05NVFV3TnpFMk1EUXlOVEF6DQpXaGNOTWpVd056RXpNRFF5TlRBeldqQmZNUm93R0FZRFZRUUREQkZPYjNSaGNua2dWR1Z6ZEdsdVp5QkRRVEVMDQpNQWtHQTFVRUJoTUNWVk14RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEekFOQmdOVkJBb01Ca1J2DQpZMnRsY2pFTE1Ba0dBMVVFQ0F3Q1EwRXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUtBb0lDDQpBUUN3VlZENHBLN3o3cFhQcEpiYVoxSGc1ZVJYSWNhWXRiRlBDbk4waXF5OUhzVkVHbkVuNUJQTlNFc3VQK20wDQo1TjBxVlY3REdiMVNqaWxvTFhEMXFERHZoWFdrK2dpUzlwcHFQSFBMVlBCNGJ2enNxd0RZcnRwYnFrWXZPMFlLDQowU0wza3hQWFVGZGxrRmZndTB4amxjem0yUGhXRzNKZDhhQXRzcEwvTCtWZlBBMTNKVWFXeFNMcHVpMUluOHJoDQpnQXlRVEs2UTRPZjZHYkpZVG5BSGI1OVVvTFhTekI1QWZxaVVxNkw3bkVZWUtvUGZsUGJSQUlXTC9VQm0wYytIDQpvY21zNzA2UFlwbVBTMlJRdjNpT0dtbm45aEVWcDNQNmpxN1dBZXZiQTRhWUd4NUVzYlZ0WUFCcUpCYkZXQXV3DQp3VEdSWW16bjBNajBlVE1nZTl6dFlCMi8yc3hkVGU2dWhtRmdwVVhuZ0RxSkk1TzlOM3pQZnZsRUltQ2t5M0hNDQpqSm9MN2c1c21xWDlvMVArRVNMaDBWWnpoaDdJRFB6UVRYcGNQSVMvNnowbDIyUUdrSy8xTjFQYUFEYVVIZExMDQp2U2F2M3kyQmFFbVB2ZjJma1pqOHlQNWVZZ2k3Q3c1T05oSExEWUhGY2w5Wm0veXdtZHhISkVUejluZmdYbnNXDQpITnhEcXJrQ1ZPNDZyL3U2clNyVXQ2aHIzb2RkSkc4czhKbzA2ZWFydzZYVTNNek0rM2dpd2tLMFNTTTN1UlBxDQo0QXNjUjFUditFMzFBdU9BbWpxWVFvVDI5Yk1JeG9TemVsamovWW5lZHdqVzQ1cFd5YzNKb0hhaWJEd3ZXOVVvDQpHU1pCVnk0aHJNL0ZhN1hDV3YxV2ZITlcxZ0R3YUxZd0RubDVqRm1SQnZjZnVRSURBUUFCbzRINU1JSDJNSUdSDQpCZ05WSFNNRWdZa3dnWWFBRkhVTTFVM0U0V3lMMW52RmQrZFBZOGY0TzJoWm9XT2tZVEJmTVFzd0NRWURWUVFHDQpFd0pWVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhGakFVQmdOVkJBY01EVk5oYmlCR2NtRnVZMmx6WTI4eER6QU5CZ05WDQpCQW9NQmtSdlkydGxjakVhTUJnR0ExVUVBd3dSVG05MFlYSjVJRlJsYzNScGJtY2dRMEdDQ1FEQ2VETGJlbUlUDQpTekFTQmdOVkhSTUJBZjhFQ0RBR0FRSC9BZ0VBTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGDQpCUWNEQVRBT0JnTlZIUThCQWY4RUJBTUNBVVl3SFFZRFZSME9CQllFRkhlNDhoY0JjQXAwYlVWbFR4WGVSQTRvDQpFMTZwTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElDQVFBV1V0QVBkVUZwd1JxK04xU3pHVWVqU2lrZU1HeVBac2NaDQpKQlVDbWhab0Z1ZmdYR2JMTzVPcGNSTGFWM1hkYTB0LzVQdGRHTVNFemN6ZW9aSFdrbkR0dys3OU9CaXR0UFBqDQpTaDFvRkR1UG8zNVI3ZVA2MjRsVUNjaC9JblpDcGhUYUx4OW9ETEdjYUszYWlsUTl3akJkS2RsQmw4S05LSVpwDQphMTNhUDVyblNtMkp2YSt0WHkveWkzQlNkczNkR0Q4SVRLWnlJLzZBRkh4R3ZPYnJESUJwbzRGRi96Y1dYVkRqDQpwYU9teHBsUnRNNEhpdG0rc1hHdmZxSmU0eDVEdU9YT25QclQzZEh2UlQ2dlNaVW9Lb2J4TXFtUlRPY3JPSVBhDQpFZU1wT29ic2hPUnVSbnRNRFl2dmdPM0Q2cDZpY2lEVzJWcDlONnJkTWRmT1dFUU44SlZXdkI3SXhSSGs5cUtKDQp2WU9XVmJjekF0MHFwTXZYRjNQWExqWmJVTTBrbk9kVUtJRWJxUDRZVWJnZHp4NlJ0Z2lpWTkzMEFqNnRBdGNlDQowZnBnTmx2ak1ScFNCdVdUbEFmTk5qRy9ZaG5kTXo5dUk2OFRNZkZwUjNQY2dWSXYzMGtydy85VnpvTGkyRHBlDQpvdzZEckdPNm9pK0RoTjc4UDRqWS9POVVjelpLMnJvWkwxT2k1UDBSSXhmMjNVWkM3eDFEbGNOM25CcjRzWVN2DQpyQng0Y0ZUTU5wd1UrbnpzSWk0ZGpjRkRLbUpkRU95ak1ua1AydjBMd2U3eXZLMDhwWmRFdSswemJycTE3a3VlDQpYcFhMYzdLNjhRQjE1eXh6R3lsVTVyUnd6bUMvWXNBVnlFNGVvR3U4UHhXeHJFUnZIYnk0QjhZUDB2QWZPcmFMDQpsS21YbEs0ZFRnPT0NCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0NCg==` + +const validIntermediateAndCertRSA = `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlHTXpDQ0JCdWdBd0lCQWdJQkFUQU5CZ2txaGtpRzl3MEJBUXNGQURCZk1Rc3dDUVlEVlFRR0V3SlZVekVMDQpNQWtHQTFVRUNBd0NRMEV4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEekFOQmdOVkJBb01Ca1J2DQpZMnRsY2pFYU1CZ0dBMVVFQXd3UlRtOTBZWEo1SUZSbGMzUnBibWNnUTBFd0hoY05NVFV3TnpFMk1EUXlOVEF6DQpXaGNOTWpVd056RXpNRFF5TlRBeldqQmZNUm93R0FZRFZRUUREQkZPYjNSaGNua2dWR1Z6ZEdsdVp5QkRRVEVMDQpNQWtHQTFVRUJoTUNWVk14RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEekFOQmdOVkJBb01Ca1J2DQpZMnRsY2pFTE1Ba0dBMVVFQ0F3Q1EwRXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUtBb0lDDQpBUUN3VlZENHBLN3o3cFhQcEpiYVoxSGc1ZVJYSWNhWXRiRlBDbk4waXF5OUhzVkVHbkVuNUJQTlNFc3VQK20wDQo1TjBxVlY3REdiMVNqaWxvTFhEMXFERHZoWFdrK2dpUzlwcHFQSFBMVlBCNGJ2enNxd0RZcnRwYnFrWXZPMFlLDQowU0wza3hQWFVGZGxrRmZndTB4amxjem0yUGhXRzNKZDhhQXRzcEwvTCtWZlBBMTNKVWFXeFNMcHVpMUluOHJoDQpnQXlRVEs2UTRPZjZHYkpZVG5BSGI1OVVvTFhTekI1QWZxaVVxNkw3bkVZWUtvUGZsUGJSQUlXTC9VQm0wYytIDQpvY21zNzA2UFlwbVBTMlJRdjNpT0dtbm45aEVWcDNQNmpxN1dBZXZiQTRhWUd4NUVzYlZ0WUFCcUpCYkZXQXV3DQp3VEdSWW16bjBNajBlVE1nZTl6dFlCMi8yc3hkVGU2dWhtRmdwVVhuZ0RxSkk1TzlOM3pQZnZsRUltQ2t5M0hNDQpqSm9MN2c1c21xWDlvMVArRVNMaDBWWnpoaDdJRFB6UVRYcGNQSVMvNnowbDIyUUdrSy8xTjFQYUFEYVVIZExMDQp2U2F2M3kyQmFFbVB2ZjJma1pqOHlQNWVZZ2k3Q3c1T05oSExEWUhGY2w5Wm0veXdtZHhISkVUejluZmdYbnNXDQpITnhEcXJrQ1ZPNDZyL3U2clNyVXQ2aHIzb2RkSkc4czhKbzA2ZWFydzZYVTNNek0rM2dpd2tLMFNTTTN1UlBxDQo0QXNjUjFUditFMzFBdU9BbWpxWVFvVDI5Yk1JeG9TemVsamovWW5lZHdqVzQ1cFd5YzNKb0hhaWJEd3ZXOVVvDQpHU1pCVnk0aHJNL0ZhN1hDV3YxV2ZITlcxZ0R3YUxZd0RubDVqRm1SQnZjZnVRSURBUUFCbzRINU1JSDJNSUdSDQpCZ05WSFNNRWdZa3dnWWFBRkhVTTFVM0U0V3lMMW52RmQrZFBZOGY0TzJoWm9XT2tZVEJmTVFzd0NRWURWUVFHDQpFd0pWVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhGakFVQmdOVkJBY01EVk5oYmlCR2NtRnVZMmx6WTI4eER6QU5CZ05WDQpCQW9NQmtSdlkydGxjakVhTUJnR0ExVUVBd3dSVG05MFlYSjVJRlJsYzNScGJtY2dRMEdDQ1FEQ2VETGJlbUlUDQpTekFTQmdOVkhSTUJBZjhFQ0RBR0FRSC9BZ0VBTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGDQpCUWNEQVRBT0JnTlZIUThCQWY4RUJBTUNBVVl3SFFZRFZSME9CQllFRkhlNDhoY0JjQXAwYlVWbFR4WGVSQTRvDQpFMTZwTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElDQVFBV1V0QVBkVUZwd1JxK04xU3pHVWVqU2lrZU1HeVBac2NaDQpKQlVDbWhab0Z1ZmdYR2JMTzVPcGNSTGFWM1hkYTB0LzVQdGRHTVNFemN6ZW9aSFdrbkR0dys3OU9CaXR0UFBqDQpTaDFvRkR1UG8zNVI3ZVA2MjRsVUNjaC9JblpDcGhUYUx4OW9ETEdjYUszYWlsUTl3akJkS2RsQmw4S05LSVpwDQphMTNhUDVyblNtMkp2YSt0WHkveWkzQlNkczNkR0Q4SVRLWnlJLzZBRkh4R3ZPYnJESUJwbzRGRi96Y1dYVkRqDQpwYU9teHBsUnRNNEhpdG0rc1hHdmZxSmU0eDVEdU9YT25QclQzZEh2UlQ2dlNaVW9Lb2J4TXFtUlRPY3JPSVBhDQpFZU1wT29ic2hPUnVSbnRNRFl2dmdPM0Q2cDZpY2lEVzJWcDlONnJkTWRmT1dFUU44SlZXdkI3SXhSSGs5cUtKDQp2WU9XVmJjekF0MHFwTXZYRjNQWExqWmJVTTBrbk9kVUtJRWJxUDRZVWJnZHp4NlJ0Z2lpWTkzMEFqNnRBdGNlDQowZnBnTmx2ak1ScFNCdVdUbEFmTk5qRy9ZaG5kTXo5dUk2OFRNZkZwUjNQY2dWSXYzMGtydy85VnpvTGkyRHBlDQpvdzZEckdPNm9pK0RoTjc4UDRqWS9POVVjelpLMnJvWkwxT2k1UDBSSXhmMjNVWkM3eDFEbGNOM25CcjRzWVN2DQpyQng0Y0ZUTU5wd1UrbnpzSWk0ZGpjRkRLbUpkRU95ak1ua1AydjBMd2U3eXZLMDhwWmRFdSswemJycTE3a3VlDQpYcFhMYzdLNjhRQjE1eXh6R3lsVTVyUnd6bUMvWXNBVnlFNGVvR3U4UHhXeHJFUnZIYnk0QjhZUDB2QWZPcmFMDQpsS21YbEs0ZFRnPT0NCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0NCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQ0KTUlJRlZ6Q0NBeitnQXdJQkFnSUJBekFOQmdrcWhraUc5dzBCQVFzRkFEQmZNUm93R0FZRFZRUUREQkZPYjNSaA0KY25rZ1ZHVnpkR2x1WnlCRFFURUxNQWtHQTFVRUJoTUNWVk14RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJseg0KWTI4eER6QU5CZ05WQkFvTUJrUnZZMnRsY2pFTE1Ba0dBMVVFQ0F3Q1EwRXdIaGNOTVRVd056RTJNRFF5TlRVdw0KV2hjTk1UWXdOekUxTURReU5UVXdXakJnTVJzd0dRWURWUVFEREJKelpXTjFjbVV1WlhoaGJYQnNaUzVqYjIweA0KQ3pBSkJnTlZCQVlUQWxWVE1SWXdGQVlEVlFRSERBMVRZVzRnUm5KaGJtTnBjMk52TVE4d0RRWURWUVFLREFaRQ0KYjJOclpYSXhDekFKQmdOVkJBZ01Ba05CTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQw0KQVFFQW1MWWlZQ1RBV0pCV0F1eFpMcVZtVjRGaVVkR2dFcW9RdkNiTjczekYvbVFmaHEwQ0lUbzZ4U3hzMVFpRw0KRE96VXRrcHpYenppU2o0SjUrZXQ0SmtGbGVlRUthTWNIYWRlSXNTbEhHdlZ0WER2OTNvUjN5ZG1mWk8rVUxSVQ0KOHhIbG9xY0xyMUtyT1AxZGFMZmRNUmJhY3RkNzVVUWd2dzlYVHNkZU1WWDVBbGljU0VOVktWK0FRWHZWcHY4UA0KVDEwTVN2bEJGYW00cmVYdVkvU2tlTWJJYVc1cEZ1NkFRdjNabWZ0dDJ0YTBDQjlrYjFtWWQrT0tydThIbm5xNQ0KYUp3NlIzR2hQMFRCZDI1UDFQa2lTeE0yS0dZWlprMFcvTlpxTEs5L0xURktUTkN2N1ZqQ2J5c1ZvN0h4Q1kwYg0KUWUvYkRQODJ2N1NuTHRiM2Fab2dmdmE0SFFJREFRQUJvNElCR3pDQ0FSY3dnWWdHQTFVZEl3U0JnREIrZ0JSMw0KdVBJWEFYQUtkRzFGWlU4VjNrUU9LQk5lcWFGanBHRXdYekVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTQ0KQWtOQk1SWXdGQVlEVlFRSERBMVRZVzRnUm5KaGJtTnBjMk52TVE4d0RRWURWUVFLREFaRWIyTnJaWEl4R2pBWQ0KQmdOVkJBTU1FVTV2ZEdGeWVTQlVaWE4wYVc1bklFTkJnZ0VCTUF3R0ExVWRFd0VCL3dRQ01BQXdIUVlEVlIwbA0KQkJZd0ZBWUlLd1lCQlFVSEF3SUdDQ3NHQVFVRkJ3TUJNQTRHQTFVZER3RUIvd1FFQXdJRm9EQXVCZ05WSFJFRQ0KSnpBbGdoSnpaV04xY21VdVpYaGhiWEJzWlM1amIyMkNDV3h2WTJGc2FHOXpkSWNFZndBQUFUQWRCZ05WSFE0RQ0KRmdRVURQRDRDYVhSYnU1UUJiNWU4eThvZHZUcVc0SXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnSUJBSk95bG1jNA0KbjdKNjRHS3NQL3hoVWRLS1Y5L0tEK3VmenBLYnJMSW9qV243clR5ZTcwdlkwT2pRRnVPWGM1NHlqTVNJTCsvNQ0KbWxOUTdZL2ZKUzh4ZEg3OUVSKzRuV011RDJlY2lMbnNMZ2JZVWs0aGl5Ynk4LzVWKy9ZcVBlQ3BQQ242VEpSSw0KYTBFNmxWL1VqWEpkcmlnSnZKb05PUjhaZ3RFWi9RUGdqSkVWVXNnNDdkdHF6c0RwZ2VTOGRjanVNV3BaeFAwMg0KcWF2RkxEalNGelZIKzJENk90eTFEUXBsbS8vM1hhUlhoMjNkT0NQOHdqL2J4dm5WVG9GV3Mrek80dVQxTEYvUw0KS1hDTlFvZWlHeFdIeXpyWEZWVnRWbkM5RlNOejBHZzIvRW0xdGZSZ3ZoVW40S0xKY3ZaVzlvMVI3VlZDWDBMMQ0KMHgwZnlLM1ZXZVdjODZhNWE2ODFhbUtaU0ViakFtSVZaRjl6T1gwUE9EQzhveSt6cU9QV2EwV0NsNEs2ekRDNg0KMklJRkJCTnk1MFpTMmlPTjZSWTZtRTdObUE3OGdja2Y0MTVjcUlWcmxvWUpiYlREZXBmaFRWMjE4U0xlcHBoNA0KdUdiMi9zeGtsZkhPWUUrcnBIY2lpYld3WHJ3bE9ESmFYdXpYRmhwbFVkL292ZHVqQk5BSUhrQmZ6eStZNnoycw0KYndaY2ZxRDROSWIvQUdoSXlXMnZidnU0enNsRHAxTUVzTG9hTytTemlyTXpreU1CbEtSdDEyMHR3czRFa1VsbQ0KL1FoalNVb1pwQ0FzeTVDL3BWNCtieDBTeXNOZC9TK2tLYVJaYy9VNlkzWllCRmhzekxoN0phTFhLbWs3d0huRQ0KcmdnbTZvejRML0d5UFdjL0ZqZm5zZWZXS00yeUMzUURoanZqDQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo=` + +const signedRSARootTemplate = `{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2016-07-16T23:34:13.389129622-07:00","keys":{"1fc4fdc38f66558658c5c59b67f1716bdc6a74ef138b023ae5931db69f51d670":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8nIgzLigo5D47dWQe1IUjzHXxvyx0j/OL16VQymuloWsgVDxxT6+mH3CeviMAs+/McnEPE9exnm6SQGR5x3XMw=="}},"23c29cc372109c819e081bc953b7657d05e3f968f03c21d0d75ea457590f3d14":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEClUFVWkc85OQScfTQRS02VaLIEaeCmxdwYS/hcTLVoTxlFfRfs7HyalTwXGAGO79XZZS+koE6s8D0xGcCJQkLQ=="}},"49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292":{"keytype":"rsa-x509","keyval":{"private":null,"public":"{{.RootPem}}"}},"e3a5a4fdaf11ea1ec58f5efed6f3639b39cd4cfa1418c8b55c9a8c2447ace5d9":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgl3rzMPMEKhS1k/AX16MM4PdidpjJr+z4pj0Td+30QnpbOIARgpyR1PiFztU8BZlqG3cUazvFclr2q/xHvfrqw=="}}},"roles":{"root":{"keyids":["49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292"],"threshold":1},"snapshot":{"keyids":["23c29cc372109c819e081bc953b7657d05e3f968f03c21d0d75ea457590f3d14"],"threshold":1},"targets":{"keyids":["1fc4fdc38f66558658c5c59b67f1716bdc6a74ef138b023ae5931db69f51d670"],"threshold":1},"timestamp":{"keyids":["e3a5a4fdaf11ea1ec58f5efed6f3639b39cd4cfa1418c8b55c9a8c2447ace5d9"],"threshold":1}},"version":2},"signatures":[{"keyid":"49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292","method":"rsapss","sig":"625670b428f4dbc5dcdb7f8a1df8fa828bc5118ea131b0573b91d3db417423965e20f6f597b3a6923122c29f591eeb02958f907aa88fd42161fc07c26efe2d2da9ea3245cfb66f14249d6131956af3a72889e077d8f4038fd93ac75844ac7cef3d46cecd3456d814241453191642c301dd3aa47f259b1c8f2ce85d91513ca884fbb010b2ff86d02067ef117eb2083cfce0c65d70b357e41f66cc68bd03b80396887d20467103c520a08d7c498a6ffdf61e4994aa33fc2cd86fa48842c68135a1719fa03a517b02b33f0ce95c0bf358a9cbd8d6e804e869fb1bca29adee8d3a87dd469df0eb4dc0dab30cbd84f0a8a2d8ea85ff77ed3b036f961875cf532e68b420326d63b0da1219df92639a1d7683a0dce909382b6cd7c0082f5b00545a54fc3253f54dfedff348e71a38746804651c8e0b0b19585392ffa42698b878329ee8ef2eece83ae8a23df26feec62f2a843c522784d70fff1dcf04ead5ad5791b6160b8a2eeacca0a67d14159c6a70ced1f839016b86dea72050976f87d69c437dc429eaf57ea071b3a2d20145a8f2b0ed42483e6f9e405b4601ba4016f4801f3324901804b67b798767c9cafe28daca0091bbbfcff8d80a63dff3a667cbdb181c36c94b70035c48e3ad862252eb2a9833d8bd1b469fd32c3f4f1013e27b708d59bcbb9d94c91fe15c889473f9a365c6116d2989387c2cf74dbe8be67faafef69279"}]}` + +// TestValidateRoot 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 +func TestValidateRoot(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + validateRootSuccessfully(t, data.RSAKey) +} + +func validateRootSuccessfully(t *testing.T, rootType data.KeyAlgorithm) { + // 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, mux := createTestServer(t) + defer ts.Close() + + repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport) + assert.NoError(t, err, "error creating repository: %s", err) + + rootKeyID, err := repo.KeyStoreManager.GenRootKey(rootType.String(), "passphrase") + assert.NoError(t, err, "error generating root key: %s", err) + + rootCryptoService, err := repo.KeyStoreManager.GetRootCryptoService(rootKeyID, "passphrase") + assert.NoError(t, err, "error retrieving root key: %s", err) + + err = repo.Initialize(rootCryptoService) + assert.NoError(t, err, "error creating repository: %s", err) + + // Initialize is supposed to have created new certificate for this repository + // Lets check for it and store it for later use + 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. + var tempKey data.TUFKey + json.Unmarshal([]byte(timestampECDSAKeyJSON), &tempKey) + + repo.KeyStoreManager.NonRootKeyStore().AddKey(filepath.Join(filepath.FromSlash(gun), tempKey.ID()), &tempKey) + + mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json", func(w http.ResponseWriter, r *http.Request) { + rootJSONFile := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "metadata", "root.json") + rootFileBytes, err := ioutil.ReadFile(rootJSONFile) + assert.NoError(t, err) + fmt.Fprint(w, string(rootFileBytes)) + }) + + // Because ListTargets will clear this + savedTUFRepo := repo.tufRepo + + mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.json", func(w http.ResponseWriter, r *http.Request) { + signedTimestamp, err := savedTUFRepo.SignTimestamp(data.DefaultExpires("timestamp"), nil) + assert.NoError(t, err) + 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) { + signedSnapshot, err := savedTUFRepo.SignSnapshot(data.DefaultExpires("snapshot"), nil) + assert.NoError(t, err) + 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) { + signedTargets, err := savedTUFRepo.SignTargets("targets", data.DefaultExpires("targets"), nil) + assert.NoError(t, err) + targetsJSON, _ := json.Marshal(signedTargets) + fmt.Fprint(w, string(targetsJSON)) + }) + + _, err = repo.ListTargets() + assert.NoError(t, err) + + // + // Test TOFUS logic. We remove all certs and expect a new one to be added after ListTargets + // + err = repo.KeyStoreManager.TrustedCertificateStore().RemoveAll() + assert.NoError(t, err) + assert.Len(t, repo.KeyStoreManager.TrustedCertificateStore().GetCertificates(), 0) + + // This list targets is expected to succeed and the certificate store to have the new certificate + _, err = repo.ListTargets() + assert.NoError(t, err) + assert.Len(t, repo.KeyStoreManager.TrustedCertificateStore().GetCertificates(), 1) + + // + // Test certificate mismatch logic. We remove all certs, and a different cert to the + // same CN, and expect ValidateRoot to fail + // + + // First, remove all certs + err = repo.KeyStoreManager.TrustedCertificateStore().RemoveAll() + assert.NoError(t, err) + 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") + 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) + } + + // + // Test CA fallback logic. + // If we already have a certificate that matches a CN and get presented a new + // certificate for that same CN and that certificate is signed by a CA we + // know about, then we trust it but don't remove the previous pinning. + // + + // First, remove all certs and trusted CAs + err = repo.KeyStoreManager.TrustedCertificateStore().RemoveAll() + assert.NoError(t, err) + assert.Len(t, repo.KeyStoreManager.TrustedCertificateStore().GetCertificates(), 0) + err = repo.KeyStoreManager.TrustedCAStore().RemoveAll() + assert.NoError(t, err) + assert.Len(t, repo.KeyStoreManager.TrustedCAStore().GetCertificates(), 0) + + // Add a trusted root-ca + err = repo.KeyStoreManager.TrustedCAStore().AddCertFromFile("../fixtures/root-ca.crt") + assert.NoError(t, err) + + // Add a previously generated certificate with CN=secure.example.com + err = repo.KeyStoreManager.TrustedCertificateStore().AddCertFromFile("../fixtures/self-signed_secure.example.com.crt") + assert.NoError(t, err) + + // Create a manual rootSigner with a valid intermediate + leaf cert + var testSignedRoot data.Signed + var signedRootBytes bytes.Buffer + + // Execute our template + templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) + templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validIntermediateAndCertRSA}) + + // Unmarshal our signedroot + json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) + + // + // This call to ValidateRoot will suceed in getting to the TUF validation, since + // we are usign a valid PEM encoded certificate chain of intermediate + leaf cert + // that are signed by a trusted root authority and the leaf cert has a correct CN. + // It will, however, fail to validate, because this is has an invalid TUF signature + // + err = repo.KeyStoreManager.ValidateRoot(&testSignedRoot, "secure.example.com") + if assert.Error(t, err, "An error was expected") { + assert.Equal(t, err, errors.New("tuf: valid signatures did not meet threshold")) + } +} + +func TestValidateRootWithInvalidData(t *testing.T) { + var testSignedRoot data.Signed + var signedRootBytes bytes.Buffer + + // 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) + + // Create a FileStoreManager + keyStoreManager, err := keystoremanager.NewKeyStoreManager(tempBaseDir) + assert.NoError(t, err) + + // Execute our template + templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) + templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot}) + + // Unmarshal our signedroot + json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) + + // + // This call to ValidateRoot will succeed since we are usign a valid PEM + // encoded certificate, and have no other certificates for this CN + // + err = keyStoreManager.ValidateRoot(&testSignedRoot, "docker.com/notary") + assert.NoError(t, err) + + // + // This call to ValidateRoot will fail since we are passing in a dnsName that + // doesn't match the CN of the certificate. + // + err = keyStoreManager.ValidateRoot(&testSignedRoot, "diogomonica.com/notary") + if assert.Error(t, err, "An error was expected") { + assert.Equal(t, err, keystoremanager.ErrValidationFail) + } + + // + // This call to ValidateRoot will fail since we are passing an unparsable RootSigned + // + // Execute our template deleting the old buffer first + signedRootBytes.Reset() + templ, _ = template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) + templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: "------ ABSOLUTELY NOT A PEM -------"}) + // Unmarshal our signedroot + json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) + + err = keyStoreManager.ValidateRoot(&testSignedRoot, "docker.com/notary") + assert.Error(t, err, "illegal base64 data at input byte") + + // + // This call to ValidateRoot will fail since we are passing an invalid PEM cert + // + // Execute our template deleting the old buffer first + signedRootBytes.Reset() + templ, _ = template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) + templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: "LS0tLS1CRUdJTiBDRVJU"}) + // Unmarshal our signedroot + json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) + + err = keyStoreManager.ValidateRoot(&testSignedRoot, "docker.com/notary") + if assert.Error(t, err, "An error was expected") { + assert.Equal(t, err, keystoremanager.ErrValidationFail) + } + + // + // This call to ValidateRoot will fail since we are passing only CA certificate + // This will fail due to the lack of a leaf certificate + // + // Execute our template deleting the old buffer first + signedRootBytes.Reset() + templ, _ = template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) + templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validCAPEMEncodeRSARoot}) + // Unmarshal our signedroot + json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) + + err = keyStoreManager.ValidateRoot(&testSignedRoot, "docker.com/notary") + if assert.Error(t, err, "An error was expected") { + assert.Equal(t, err, keystoremanager.ErrValidationFail) + } +} diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index cc7831e03f..62b9e7a47c 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -64,7 +64,7 @@ func keysRemove(cmd *cobra.Command, args []string) { gunOrID := args[0] // Try to retrieve the ID from the CA store. - cert, err := caStore.GetCertificateByKeyID(gunOrID) + cert, err := caStore.GetCertificateByCertID(gunOrID) if err == nil { fmt.Printf("Removing: ") printCert(cert) @@ -78,7 +78,7 @@ func keysRemove(cmd *cobra.Command, args []string) { } // Try to retrieve the ID from the Certificate store. - cert, err = certificateStore.GetCertificateByKeyID(gunOrID) + cert, err = certificateStore.GetCertificateByCertID(gunOrID) if err == nil { fmt.Printf("Removing: ") printCert(cert) @@ -216,12 +216,12 @@ func keysGenerate(cmd *cobra.Command, args []string) { func printCert(cert *x509.Certificate) { timeDifference := cert.NotAfter.Sub(time.Now()) - keyID, err := trustmanager.FingerprintCert(cert) + certID, err := trustmanager.FingerprintCert(cert) if err != nil { fatalf("could not fingerprint certificate: %v", err) } - fmt.Printf("%s %s (expires in: %v days)\n", cert.Subject.CommonName, keyID, math.Floor(timeDifference.Hours()/24)) + fmt.Printf("%s %s (expires in: %v days)\n", cert.Subject.CommonName, certID, math.Floor(timeDifference.Hours()/24)) } func printKey(keyPath string) { diff --git a/fixtures/self-signed_docker.com-notary.crt b/fixtures/self-signed_docker.com-notary.crt new file mode 100644 index 0000000000..901a3de8e3 --- /dev/null +++ b/fixtures/self-signed_docker.com-notary.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBpDCCAUqgAwIBAgIRAIquZ7lRJj1Um030Kd7GFXgwCgYIKoZIzj0EAwIwODEa +MBgGA1UEChMRZG9ja2VyLmNvbS9ub3RhcnkxGjAYBgNVBAMTEWRvY2tlci5jb20v +bm90YXJ5MB4XDTE1MDcxNzAwMzE1NFoXDTE3MDcxNjAwMzE1NFowODEaMBgGA1UE +ChMRZG9ja2VyLmNvbS9ub3RhcnkxGjAYBgNVBAMTEWRvY2tlci5jb20vbm90YXJ5 +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjnnozttLzYgIN5fL8ZwYbsMig0pj +HSNupVTPjDIrLUYUnoQfG6IQ0E2BMixEGnI/A9WreeXP2oz06LZ4SROMQqM1MDMw +DgYDVR0PAQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQC +MAAwCgYIKoZIzj0EAwIDSAAwRQIgT9cxottjza9BBQcMsoB/Uf2JYXWgSkp9QMXT +8mG4mMICIQDMYWFdgn5u8nDeThJ+bG8Lu5nIGb/NWEOFtU0xQv913Q== +-----END CERTIFICATE----- + diff --git a/fixtures/self-signed_secure.example.com.crt b/fixtures/self-signed_secure.example.com.crt new file mode 100755 index 0000000000..c657270e15 --- /dev/null +++ b/fixtures/self-signed_secure.example.com.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBqDCCAU6gAwIBAgIRAM1vKVhmZuWcrogc3ASBaZUwCgYIKoZIzj0EAwIwOjEb +MBkGA1UEChMSc2VjdXJlLmV4YW1wbGUuY29tMRswGQYDVQQDExJzZWN1cmUuZXhh +bXBsZS5jb20wHhcNMTUwNzE3MDU1NTIzWhcNMTcwNzE2MDU1NTIzWjA6MRswGQYD +VQQKExJzZWN1cmUuZXhhbXBsZS5jb20xGzAZBgNVBAMTEnNlY3VyZS5leGFtcGxl +LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI556M7bS82ICDeXy/GcGG7D +IoNKYx0jbqVUz4wyKy1GFJ6EHxuiENBNgTIsRBpyPwPVq3nlz9qM9Oi2eEkTjEKj +NTAzMA4GA1UdDwEB/wQEAwIAoDATBgNVHSUEDDAKBggrBgEFBQcDAzAMBgNVHRMB +Af8EAjAAMAoGCCqGSM49BAMCA0gAMEUCIER2XCkQ8dUWBZEUeT5kABg7neiHPtSL +VVE6bJxu2sxlAiEAkRG6u1ieXKGl38gUkCn75Yvo9nOSLdh0gtxUUcOXvUc= +-----END CERTIFICATE----- diff --git a/keystoremanager/keystoremanager.go b/keystoremanager/keystoremanager.go index b1e43b5f9b..f90dde1bec 100644 --- a/keystoremanager/keystoremanager.go +++ b/keystoremanager/keystoremanager.go @@ -3,8 +3,6 @@ package keystoremanager import ( "crypto/rand" "crypto/x509" - "encoding/json" - "encoding/pem" "errors" "fmt" "path/filepath" @@ -36,6 +34,15 @@ const ( rsaRootKeySize = 4096 // Used for new root keys ) +var ( + // ErrValidationFail is returned when there is no trusted certificate in any of the + // root keys available in the roots.json + ErrValidationFail = errors.New("could not validate the path to a trusted root") + // ErrRootRotationFail is returned when we fail to do a full root key rotation + // by either failing to add the new root certificate, or delete the old ones + ErrRootRotationFail = errors.New("could not rotate trust to a new trusted root") +) + // NewKeyStoreManager returns an initialized KeyStoreManager, or an error // if it fails to create the KeyFileStores or load certificates func NewKeyStoreManager(baseDir string) (*KeyStoreManager, error) { @@ -166,79 +173,166 @@ attempts to validate the certificate by first checking for an exact match on the certificate store, and subsequently trying to find a valid chain on the trustedCAStore. -When this is being used with a notary repository, the dnsName parameter should -be the GUN associated with the repository. +Currently this method operates on a Trust On First Use (TOFU) model: if we +have never seen a certificate for a particular CN, we trust it. If later we see +a different certificate for that certificate, we return an ErrValidationFailed error. -Example TUF Content for root role: -"roles" : { - "root" : { - "threshold" : 1, - "keyids" : [ - "e6da5c303d572712a086e669ecd4df7b785adfc844e0c9a7b1f21a7dfc477a38" - ] - }, - ... -} +Note that since we only allow trust data to be downloaded over an HTTPS channel +we are using the current web-of-trust to validate the first download of the certificate +adding an extra layer of security over the normal (SSH style) trust model. +We shall call this: TOFUS. -Example TUF Content for root key: -"e6da5c303d572712a086e669ecd4df7b785adfc844e0c9a7b1f21a7dfc477a38" : { - "keytype" : "RSA", - "keyval" : { - "private" : "", - "public" : "Base64-encoded, PEM encoded x509 Certificate" - } -} +ValidateRoot also supports root key rotation, trusting a new certificate that has +been included in the roots.json, and removing trust in the old one. */ func (km *KeyStoreManager) ValidateRoot(root *data.Signed, dnsName string) error { - rootSigned := &data.Root{} - err := json.Unmarshal(root.Signed, rootSigned) + logrus.Debugf("entered ValidateRoot with dns: %s", dnsName) + rootSigned, err := data.RootFromSigned(root) if err != nil { return err } - certs := make(map[string]data.PublicKey) - for _, keyID := range rootSigned.Roles["root"].KeyIDs { - // TODO(dlaw): currently assuming only one cert contained in - // public key entry. Need to fix when we want to pass in chains. - k, _ := pem.Decode([]byte(rootSigned.Keys[keyID].Public())) - decodedCerts, err := x509.ParseCertificates(k.Bytes) + // validKeys will store all the keys that were considered valid either by + // direct certificate match, or CA chain path + validKeys := make(map[string]data.PublicKey) + + // allCerts will keep a list of all leafCerts that were found, and is used + // to aid on root certificate rotation + allCerts := make(map[string]*x509.Certificate) + + // Before we loop through all root keys available, make sure any exist + rootRoles, ok := rootSigned.Signed.Roles["root"] + if !ok { + return errors.New("no root roles found in tuf metadata") + } + + logrus.Debugf("found the following root keys in roots.json: %v", rootRoles.KeyIDs) + // Iterate over every keyID for the root role inside of roots.json + for _, keyID := range rootRoles.KeyIDs { + // Decode all the x509 certificates that were bundled with this + // Specific root key + decodedCerts, err := trustmanager.LoadCertBundleFromPEM([]byte(rootSigned.Signed.Keys[keyID].Public())) if err != nil { logrus.Debugf("error while parsing root certificate with keyID: %s, %v", keyID, err) continue } - // TODO(diogo): Assuming that first certificate is the leaf-cert. Need to - // iterate over all decodedCerts and find a non-CA one (should be the last). - leafCert := decodedCerts[0] + // Get all non-CA certificates in the decoded certificates + leafCerts := trustmanager.GetLeafCerts(decodedCerts) + + // If we got no leaf certificates or we got more than one, fail + if len(leafCerts) != 1 { + logrus.Debugf("wasn't able to find a leaf certificate in the chain of keyID: %s", keyID) + continue + } + + // Get the ID of the leaf certificate + leafCert := leafCerts[0] leafID, err := trustmanager.FingerprintCert(leafCert) if err != nil { logrus.Debugf("error while fingerprinting root certificate with keyID: %s, %v", keyID, err) continue } - // Check to see if there is an exact match of this certificate. - // Checking the CommonName is not required since ID is calculated over - // Cert.Raw. It's included to prevent breaking logic with changes of how the - // ID gets computed. - _, err = km.trustedCertificateStore.GetCertificateByKeyID(leafID) - if err == nil && leafCert.Subject.CommonName == dnsName { - certs[keyID] = rootSigned.Keys[keyID] + // Validate that this leaf certificate has a CN that matches the exact gun + if leafCert.Subject.CommonName != dnsName { + logrus.Debugf("error leaf certificate CN: %s doesn't match the given dns name: %s", leafCert.Subject.CommonName, dnsName) + continue } - // Check to see if this leafCertificate has a chain to one of the Root CAs - // of our CA Store. - certList := []*x509.Certificate{leafCert} - err = trustmanager.Verify(km.trustedCAStore, dnsName, certList) + // Add all the valid leafs to the certificates map so we can refer to them later + allCerts[leafID] = leafCert + + // Retrieve all the trusted certificates that match this dns Name + certsForCN, err := km.trustedCertificateStore.GetCertificatesByCN(dnsName) + if err != nil { + // If the error that we get back is different than ErrNoCertificatesFound + // we couldn't check if there are any certificates with this CN already + // trusted. Let's take the conservative approach and not trust this key + if _, ok := err.(*trustmanager.ErrNoCertificatesFound); !ok { + logrus.Debugf("error retrieving certificates for: %s, %v", dnsName, err) + continue + } + } + + // If there are no certificates with this CN, lets TOFUS! + // Note that this logic should only exist in docker 1.8 + if len(certsForCN) == 0 { + km.trustedCertificateStore.AddCert(leafCert) + certsForCN = append(certsForCN, leafCert) + logrus.Debugf("using TOFUS on %s with keyID: %s", dnsName, leafID) + } + + // Iterate over all known certificates for this CN and see if any are trusted + for _, cert := range certsForCN { + // Check to see if there is an exact match of this certificate. + certID, err := trustmanager.FingerprintCert(cert) + if err == nil && certID == leafID { + validKeys[keyID] = rootSigned.Signed.Keys[keyID] + logrus.Debugf("found an exact match for %s with keyID: %s", dnsName, keyID) + } + } + + // Check to see if this leafCertificate has a chain to one of the Root + // CAs of our CA Store. + err = trustmanager.Verify(km.trustedCAStore, dnsName, decodedCerts) if err == nil { - certs[keyID] = rootSigned.Keys[keyID] + validKeys[keyID] = rootSigned.Signed.Keys[keyID] + logrus.Debugf("found a CA path for %s with keyID: %s", dnsName, keyID) } } - if len(certs) < 1 { - return errors.New("could not validate the path to a trusted root") + if len(validKeys) < 1 { + logrus.Debugf("wasn't able to trust any of the root keys") + return ErrValidationFail } - _, err = signed.VerifyRoot(root, 0, certs, 1) + // TODO(david): change hardcoded minversion on TUF. + newRootKey, err := signed.VerifyRoot(root, 0, validKeys, 1) + if err != nil { + return err + } - return err + // VerifyRoot returns a non-nil value if there is a root key rotation happening. + // If this happens, we should replace the old root of trust with the new one + if newRootKey != nil { + logrus.Debugf("got a new root key to rotate to: %s", newRootKey.ID()) + + // Retrieve the certificate associated with the new root key and trust it + newRootKeyCert, ok := allCerts[newRootKey.ID()] + // Paranoid check for the certificate still being in the map + if !ok { + logrus.Debugf("error while retrieving new root certificate with keyID: %s, %v", newRootKey.ID(), err) + return ErrRootRotationFail + } + + // Add the new root certificate to our certificate store + err := km.trustedCertificateStore.AddCert(newRootKeyCert) + if err != nil { + // Ignore the error if the certificate already exists + if _, ok := err.(*trustmanager.ErrCertExists); !ok { + logrus.Debugf("error while adding new root certificate with keyID: %s, %v", newRootKey.ID(), err) + return ErrRootRotationFail + } + logrus.Debugf("root certificate already exists in keystore: %s", newRootKey.ID()) + } + + // Remove the new root certificate from the certificate mapping so we + // can remove trust from all of the remaining ones + delete(allCerts, newRootKey.ID()) + + // Iterate over all old valid certificates and remove them, essentially + // finishing the rotation of the currently trusted root certificate + for _, cert := range allCerts { + err := km.trustedCertificateStore.RemoveCert(cert) + if err != nil { + logrus.Debugf("error while removing old root certificate: %v", err) + return ErrRootRotationFail + } + logrus.Debugf("removed trust from old root certificate") + } + } + + logrus.Debugf("Root validation succeeded") + return nil } diff --git a/trustmanager/keyfilestore.go b/trustmanager/keyfilestore.go index 0b6c471e41..65d2837026 100644 --- a/trustmanager/keyfilestore.go +++ b/trustmanager/keyfilestore.go @@ -162,3 +162,8 @@ func listKeys(s LimitedFileStore) []string { } return keyIDList } + +// RemoveKey removes the key from the keyfilestore +func (s *KeyFileStore) RemoveKey(name string) error { + return s.Remove(name) +} diff --git a/trustmanager/keyfilestore_test.go b/trustmanager/keyfilestore_test.go index 55cca4968b..05e1b50b54 100644 --- a/trustmanager/keyfilestore_test.go +++ b/trustmanager/keyfilestore_test.go @@ -251,3 +251,53 @@ func TestGetDecryptedWithInvalidPassphrase(t *testing.T) { t.Fatalf("expected error while decrypting the content due to invalid passphrase") } } + +func TestRemoveKey(t *testing.T) { + testName := "docker.com/notary/root" + testExt := "key" + + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + if err != nil { + t.Fatalf("failed to create a temporary directory: %v", err) + } + defer os.RemoveAll(tempBaseDir) + + // Since we're generating this manually we need to add the extension '.' + expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt) + + // Create our store + store, err := NewKeyFileStore(tempBaseDir) + if err != nil { + t.Fatalf("failed to create new key filestore: %v", err) + } + + privKey, err := GenerateRSAKey(rand.Reader, 512) + if err != nil { + t.Fatalf("could not generate private key: %v", err) + } + + // Call the AddKey function + err = store.AddKey(testName, privKey) + if err != nil { + t.Fatalf("failed to add file to store: %v", err) + } + + // Check to see if file exists + _, err = ioutil.ReadFile(expectedFilePath) + if err != nil { + t.Fatalf("expected file not found: %v", err) + } + + // Call remove key + err = store.RemoveKey(testName) + if err != nil { + t.Fatalf("unable to remove key: %v", err) + } + + // Check to see if file still exists + _, err = ioutil.ReadFile(expectedFilePath) + if err == nil { + t.Fatalf("file should not exist %s", expectedFilePath) + } +} diff --git a/trustmanager/x509filestore.go b/trustmanager/x509filestore.go index 553565356b..a09ee1185e 100644 --- a/trustmanager/x509filestore.go +++ b/trustmanager/x509filestore.go @@ -50,14 +50,14 @@ func newX509FileStore(directory string, validate func(*x509.Certificate) bool) ( } // AddCert creates a filename for a given cert and adds a certificate with that name -func (s X509FileStore) AddCert(cert *x509.Certificate) error { +func (s *X509FileStore) AddCert(cert *x509.Certificate) error { if cert == nil { return errors.New("adding nil Certificate to X509Store") } // Check if this certificate meets our validation criteria if !s.validate.Validate(cert) { - return errors.New("certificate validation failed") + return &ErrCertValidation{} } // Attempt to write the certificate to the file if err := s.addNamedCert(cert); err != nil { @@ -69,16 +69,16 @@ func (s X509FileStore) AddCert(cert *x509.Certificate) error { // addNamedCert allows adding a certificate while controling the filename it gets // stored under. If the file does not exist on disk, saves it. -func (s X509FileStore) addNamedCert(cert *x509.Certificate) error { - fileName, keyID, err := fileName(cert) +func (s *X509FileStore) addNamedCert(cert *x509.Certificate) error { + fileName, certID, err := fileName(cert) if err != nil { return err } - logrus.Debug("Adding cert with keyID: ", keyID) - // Validate if we already loaded this certificate before - if _, ok := s.fingerprintMap[keyID]; ok { - return errors.New("certificate already in the store") + logrus.Debug("Adding cert with certID: ", certID) + // Validate if we already added this certificate before + if _, ok := s.fingerprintMap[certID]; ok { + return &ErrCertExists{} } // Convert certificate to PEM @@ -98,28 +98,28 @@ func (s X509FileStore) addNamedCert(cert *x509.Certificate) error { } // We wrote the certificate succcessfully, add it to our in-memory storage - s.fingerprintMap[keyID] = cert - s.fileMap[keyID] = fileName + s.fingerprintMap[certID] = cert + s.fileMap[certID] = fileName - name := string(cert.RawSubject) - s.nameMap[name] = append(s.nameMap[name], keyID) + name := string(cert.Subject.CommonName) + s.nameMap[name] = append(s.nameMap[name], certID) return nil } // RemoveCert removes a certificate from a X509FileStore. -func (s X509FileStore) RemoveCert(cert *x509.Certificate) error { +func (s *X509FileStore) RemoveCert(cert *x509.Certificate) error { if cert == nil { return errors.New("removing nil Certificate from X509Store") } - keyID, err := fingerprintCert(cert) + certID, err := fingerprintCert(cert) if err != nil { return err } - delete(s.fingerprintMap, keyID) - filename := s.fileMap[keyID] - delete(s.fileMap, keyID) + delete(s.fingerprintMap, certID) + filename := s.fileMap[certID] + delete(s.fileMap, certID) name := string(cert.RawSubject) @@ -127,7 +127,7 @@ func (s X509FileStore) RemoveCert(cert *x509.Certificate) error { fpList := s.nameMap[name] newfpList := fpList[:0] for _, x := range fpList { - if x != keyID { + if x != certID { newfpList = append(newfpList, x) } } @@ -141,6 +141,20 @@ func (s X509FileStore) RemoveCert(cert *x509.Certificate) error { return nil } +// RemoveAll removes all the certificates from the store +func (s *X509FileStore) RemoveAll() error { + for _, filename := range s.fileMap { + if err := s.fileStore.Remove(filename); err != nil { + return err + } + } + s.fileMap = make(map[CertID]string) + s.fingerprintMap = make(map[CertID]*x509.Certificate) + s.nameMap = make(map[string][]CertID) + + return nil +} + // AddCertFromPEM adds the first certificate that it finds in the byte[], returning // an error if no Certificates are found func (s X509FileStore) AddCertFromPEM(pemBytes []byte) error { @@ -152,7 +166,7 @@ func (s X509FileStore) AddCertFromPEM(pemBytes []byte) error { } // AddCertFromFile tries to adds a X509 certificate to the store given a filename -func (s X509FileStore) AddCertFromFile(filename string) error { +func (s *X509FileStore) AddCertFromFile(filename string) error { cert, err := LoadCertFromFile(filename) if err != nil { return err @@ -162,7 +176,7 @@ func (s X509FileStore) AddCertFromFile(filename string) error { } // GetCertificates returns an array with all of the current X509 Certificates. -func (s X509FileStore) GetCertificates() []*x509.Certificate { +func (s *X509FileStore) GetCertificates() []*x509.Certificate { certs := make([]*x509.Certificate, len(s.fingerprintMap)) i := 0 for _, v := range s.fingerprintMap { @@ -174,7 +188,7 @@ func (s X509FileStore) GetCertificates() []*x509.Certificate { // GetCertificatePool returns an x509 CertPool loaded with all the certificates // in the store. -func (s X509FileStore) GetCertificatePool() *x509.CertPool { +func (s *X509FileStore) GetCertificatePool() *x509.CertPool { pool := x509.NewCertPool() for _, v := range s.fingerprintMap { @@ -183,25 +197,52 @@ func (s X509FileStore) GetCertificatePool() *x509.CertPool { return pool } -// GetCertificateByKeyID returns the certificate that matches a certain keyID or error -func (s X509FileStore) GetCertificateByKeyID(keyID string) (*x509.Certificate, error) { +// GetCertificateByCertID returns the certificate that matches a certain certID +func (s *X509FileStore) GetCertificateByCertID(certID string) (*x509.Certificate, error) { + return s.getCertificateByCertID(CertID(certID)) +} + +// getCertificateByCertID returns the certificate that matches a certain certID +func (s *X509FileStore) getCertificateByCertID(certID CertID) (*x509.Certificate, error) { // If it does not look like a hex encoded sha256 hash, error - if len(keyID) != 64 { + if len(certID) != 64 { return nil, errors.New("invalid Subject Key Identifier") } // Check to see if this subject key identifier exists - if cert, ok := s.fingerprintMap[CertID(keyID)]; ok { + if cert, ok := s.fingerprintMap[CertID(certID)]; ok { return cert, nil } - return nil, errors.New("certificate not found in Key Store") + return nil, &ErrNoCertificatesFound{query: string(certID)} +} + +// GetCertificatesByCN returns all the certificates that match a specific +// CommonName +func (s *X509FileStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + if ids, ok := s.nameMap[cn]; ok { + for _, v := range ids { + cert, err := s.getCertificateByCertID(v) + if err != nil { + // This error should never happen. This would mean that we have + // an inconsistent X509FileStore + return nil, err + } + certs = append(certs, cert) + } + } + if len(certs) == 0 { + return nil, &ErrNoCertificatesFound{query: cn} + } + + return certs, nil } // GetVerifyOptions returns VerifyOptions with the certificates within the KeyStore // as part of the roots list. This never allows the use of system roots, returning // an error if there are no root CAs. -func (s X509FileStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) { +func (s *X509FileStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) { // If we have no Certificates loaded return error (we don't want to rever to using // system CAs). if len(s.fingerprintMap) == 0 { @@ -217,10 +258,10 @@ func (s X509FileStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, err } func fileName(cert *x509.Certificate) (string, CertID, error) { - keyID, err := fingerprintCert(cert) + certID, err := fingerprintCert(cert) if err != nil { return "", "", err } - return path.Join(cert.Subject.CommonName, string(keyID)), keyID, nil + return path.Join(cert.Subject.CommonName, string(certID)), certID, nil } diff --git a/trustmanager/x509filestore_test.go b/trustmanager/x509filestore_test.go index 484dbc9761..a90a0a194a 100644 --- a/trustmanager/x509filestore_test.go +++ b/trustmanager/x509filestore_test.go @@ -135,6 +135,52 @@ func TestRemoveCertX509FileStore(t *testing.T) { } } +func TestRemoveAllX509FileStore(t *testing.T) { + tempDir, err := ioutil.TempDir("", "cert-test") + if err != nil { + t.Fatal(err) + } + + // Add three certificates to store + store, _ := NewX509FileStore(tempDir) + certFiles := [3]string{"../fixtures/root-ca.crt", + "../fixtures/intermediate-ca.crt", + "../fixtures/secure.example.com.crt"} + for _, file := range certFiles { + b, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("couldn't load fixture: %v", err) + } + var block *pem.Block + block, _ = pem.Decode(b) + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("couldn't parse certificate: %v", err) + } + err = store.AddCert(cert) + if err != nil { + t.Fatalf("failed to load certificate: %v", err) + } + } + + // Number of certificates should be 3 since we added the cert + numCerts := len(store.GetCertificates()) + if numCerts != 3 { + t.Fatalf("unexpected number of certificates in store: %d", numCerts) + } + + // Remove the cert from the store + err = store.RemoveAll() + if err != nil { + t.Fatalf("failed to remove all certificates: %v", err) + } + // Number of certificates should be 0 since we added and removed the cert + numCerts = len(store.GetCertificates()) + if numCerts != 0 { + t.Fatalf("unexpected number of certificates in store: %d", numCerts) + } +} func TestInexistentGetCertificateByKeyIDX509FileStore(t *testing.T) { tempDir, err := ioutil.TempDir("", "cert-test") if err != nil { @@ -146,7 +192,7 @@ func TestInexistentGetCertificateByKeyIDX509FileStore(t *testing.T) { t.Fatalf("failed to load certificate from file: %v", err) } - _, err = store.GetCertificateByKeyID("4d06afd30b8bed131d2a84c97d00b37f422021598bfae34285ce98e77b708b5a") + _, err = store.GetCertificateByCertID("4d06afd30b8bed131d2a84c97d00b37f422021598bfae34285ce98e77b708b5a") if err == nil { t.Fatalf("no error returned for inexistent certificate") } @@ -181,7 +227,7 @@ func TestGetCertificateByKeyIDX509FileStore(t *testing.T) { } // Tries to retrieve cert by Subject Key IDs - _, err = store.GetCertificateByKeyID(keyID) + _, err = store.GetCertificateByCertID(keyID) if err != nil { t.Fatalf("expected certificate in store: %s", keyID) } diff --git a/trustmanager/x509memstore.go b/trustmanager/x509memstore.go index f6bcb9d796..55666c0976 100644 --- a/trustmanager/x509memstore.go +++ b/trustmanager/x509memstore.go @@ -3,6 +3,8 @@ package trustmanager import ( "crypto/x509" "errors" + + "github.com/Sirupsen/logrus" ) // X509MemStore implements X509Store as an in-memory object with no persistence @@ -23,7 +25,7 @@ func NewX509MemStore() *X509MemStore { } } -// NewX509FilteredMemStore returns a new X509FileStore that validates certificates +// NewX509FilteredMemStore returns a new X509Memstore that validates certificates // that are added. func NewX509FilteredMemStore(validate func(*x509.Certificate) bool) *X509MemStore { s := &X509MemStore{ @@ -37,45 +39,48 @@ func NewX509FilteredMemStore(validate func(*x509.Certificate) bool) *X509MemStor } // AddCert adds a certificate to the store -func (s X509MemStore) AddCert(cert *x509.Certificate) error { +func (s *X509MemStore) AddCert(cert *x509.Certificate) error { if cert == nil { return errors.New("adding nil Certificate to X509Store") } if !s.validate.Validate(cert) { - return errors.New("certificate failed validation") + return &ErrCertValidation{} } - keyID, err := fingerprintCert(cert) + certID, err := fingerprintCert(cert) if err != nil { return err } - s.fingerprintMap[keyID] = cert + logrus.Debug("Adding cert with certID: ", certID) + + // In this store we overwrite the certificate if it already exists + s.fingerprintMap[certID] = cert name := string(cert.RawSubject) - s.nameMap[name] = append(s.nameMap[name], keyID) + s.nameMap[name] = append(s.nameMap[name], certID) return nil } // RemoveCert removes a certificate from a X509MemStore. -func (s X509MemStore) RemoveCert(cert *x509.Certificate) error { +func (s *X509MemStore) RemoveCert(cert *x509.Certificate) error { if cert == nil { return errors.New("removing nil Certificate to X509Store") } - keyID, err := fingerprintCert(cert) + certID, err := fingerprintCert(cert) if err != nil { return err } - delete(s.fingerprintMap, keyID) + delete(s.fingerprintMap, certID) name := string(cert.RawSubject) // Filter the fingerprint out of this name entry fpList := s.nameMap[name] newfpList := fpList[:0] for _, x := range fpList { - if x != keyID { + if x != certID { newfpList = append(newfpList, x) } } @@ -84,8 +89,20 @@ func (s X509MemStore) RemoveCert(cert *x509.Certificate) error { return nil } +// RemoveAll removes all the certificates from the store +func (s *X509MemStore) RemoveAll() error { + + for _, cert := range s.fingerprintMap { + if err := s.RemoveCert(cert); err != nil { + return err + } + } + + return nil +} + // AddCertFromPEM adds a certificate to the store from a PEM blob -func (s X509MemStore) AddCertFromPEM(pemBytes []byte) error { +func (s *X509MemStore) AddCertFromPEM(pemBytes []byte) error { cert, err := LoadCertFromPEM(pemBytes) if err != nil { return err @@ -94,7 +111,7 @@ func (s X509MemStore) AddCertFromPEM(pemBytes []byte) error { } // AddCertFromFile tries to adds a X509 certificate to the store given a filename -func (s X509MemStore) AddCertFromFile(originFilname string) error { +func (s *X509MemStore) AddCertFromFile(originFilname string) error { cert, err := LoadCertFromFile(originFilname) if err != nil { return err @@ -104,7 +121,7 @@ func (s X509MemStore) AddCertFromFile(originFilname string) error { } // GetCertificates returns an array with all of the current X509 Certificates. -func (s X509MemStore) GetCertificates() []*x509.Certificate { +func (s *X509MemStore) GetCertificates() []*x509.Certificate { certs := make([]*x509.Certificate, len(s.fingerprintMap)) i := 0 for _, v := range s.fingerprintMap { @@ -116,7 +133,7 @@ func (s X509MemStore) GetCertificates() []*x509.Certificate { // GetCertificatePool returns an x509 CertPool loaded with all the certificates // in the store. -func (s X509MemStore) GetCertificatePool() *x509.CertPool { +func (s *X509MemStore) GetCertificatePool() *x509.CertPool { pool := x509.NewCertPool() for _, v := range s.fingerprintMap { @@ -125,25 +142,52 @@ func (s X509MemStore) GetCertificatePool() *x509.CertPool { return pool } -// GetCertificateByKeyID returns the certificate that matches a certain keyID or error -func (s X509MemStore) GetCertificateByKeyID(keyID string) (*x509.Certificate, error) { +// GetCertificateByCertID returns the certificate that matches a certain certID +func (s *X509MemStore) GetCertificateByCertID(certID string) (*x509.Certificate, error) { + return s.getCertificateByCertID(CertID(certID)) +} + +// getCertificateByCertID returns the certificate that matches a certain certID or error +func (s *X509MemStore) getCertificateByCertID(certID CertID) (*x509.Certificate, error) { // If it does not look like a hex encoded sha256 hash, error - if len(keyID) != 64 { + if len(certID) != 64 { return nil, errors.New("invalid Subject Key Identifier") } // Check to see if this subject key identifier exists - if cert, ok := s.fingerprintMap[CertID(keyID)]; ok { + if cert, ok := s.fingerprintMap[CertID(certID)]; ok { return cert, nil } - return nil, errors.New("certificate not found in Key Store") + return nil, &ErrNoCertificatesFound{query: string(certID)} +} + +// GetCertificatesByCN returns all the certificates that match a specific +// CommonName +func (s *X509MemStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + if ids, ok := s.nameMap[cn]; ok { + for _, v := range ids { + cert, err := s.getCertificateByCertID(v) + if err != nil { + // This error should never happen. This would mean that we have + // an inconsistent X509MemStore + return nil, err + } + certs = append(certs, cert) + } + } + if len(certs) == 0 { + return nil, &ErrNoCertificatesFound{query: cn} + } + + return certs, nil } // GetVerifyOptions returns VerifyOptions with the certificates within the KeyStore // as part of the roots list. This never allows the use of system roots, returning // an error if there are no root CAs. -func (s X509MemStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) { +func (s *X509MemStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) { // If we have no Certificates loaded return error (we don't want to rever to using // system CAs). if len(s.fingerprintMap) == 0 { diff --git a/trustmanager/x509memstore_test.go b/trustmanager/x509memstore_test.go index 9cb46cd5c9..e2470bb2c3 100644 --- a/trustmanager/x509memstore_test.go +++ b/trustmanager/x509memstore_test.go @@ -106,14 +106,55 @@ func TestRemoveCert(t *testing.T) { } } -func TestInexistentGetCertificateByKeyID(t *testing.T) { +func TestRemoveAllX509MemStore(t *testing.T) { + // Add three certificates to store + store := NewX509MemStore() + certFiles := [3]string{"../fixtures/root-ca.crt", + "../fixtures/intermediate-ca.crt", + "../fixtures/secure.example.com.crt"} + for _, file := range certFiles { + b, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("couldn't load fixture: %v", err) + } + var block *pem.Block + block, _ = pem.Decode(b) + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("couldn't parse certificate: %v", err) + } + err = store.AddCert(cert) + if err != nil { + t.Fatalf("failed to load certificate: %v", err) + } + } + + // Number of certificates should be 3 since we added the cert + numCerts := len(store.GetCertificates()) + if numCerts != 3 { + t.Fatalf("unexpected number of certificates in store: %d", numCerts) + } + + // Remove the cert from the store + err := store.RemoveAll() + if err != nil { + t.Fatalf("failed to remove all certificates: %v", err) + } + // Number of certificates should be 0 since we added and removed the cert + numCerts = len(store.GetCertificates()) + if numCerts != 0 { + t.Fatalf("unexpected number of certificates in store: %d", numCerts) + } +} +func TestInexistentGetCertificateByCertID(t *testing.T) { store := NewX509MemStore() err := store.AddCertFromFile("../fixtures/root-ca.crt") if err != nil { t.Fatalf("failed to load certificate from file: %v", err) } - _, err = store.GetCertificateByKeyID("4d06afd30b8bed131d2a84c97d00b37f422021598bfae34285ce98e77b708b5a") + _, err = store.GetCertificateByCertID("4d06afd30b8bed131d2a84c97d00b37f422021598bfae34285ce98e77b708b5a") if err == nil { t.Fatalf("no error returned for inexistent certificate") } @@ -138,15 +179,15 @@ func TestGetCertificateByKeyID(t *testing.T) { t.Fatalf("failed to load certificate from PEM: %v", err) } - keyID, err := FingerprintCert(cert) + certID, err := FingerprintCert(cert) if err != nil { t.Fatalf("failed to fingerprint the certificate: %v", err) } // Tries to retrieve cert by Subject Key IDs - _, err = store.GetCertificateByKeyID(keyID) + _, err = store.GetCertificateByCertID(certID) if err != nil { - t.Fatalf("expected certificate in store: %s", keyID) + t.Fatalf("expected certificate in store: %s", certID) } } diff --git a/trustmanager/x509store.go b/trustmanager/x509store.go index 85307b5fce..05aaaba248 100644 --- a/trustmanager/x509store.go +++ b/trustmanager/x509store.go @@ -8,13 +8,47 @@ import ( const certExtension string = "crt" +// ErrNoCertificatesFound is returned when no certificates are found for a +// GetCertificatesBy* +type ErrNoCertificatesFound struct { + query string +} + +// ErrNoCertificatesFound is returned when no certificates are found for a +// GetCertificatesBy* +func (err ErrNoCertificatesFound) Error() string { + return fmt.Sprintf("error, no certificates found in the keystore match: %s", err.query) +} + +// ErrCertValidation is returned when a certificate doesn't pass the store specific +// validations +type ErrCertValidation struct { +} + +// ErrCertValidation is returned when a certificate doesn't pass the store specific +// validations +func (err ErrCertValidation) Error() string { + return fmt.Sprintf("store-specific certificate validations failed") +} + +// ErrCertExists is returned when a Certificate already exists in the key store +type ErrCertExists struct { +} + +// ErrCertExists is returned when a Certificate already exists in the key store +func (err ErrCertExists) Error() string { + return fmt.Sprintf("certificate already in the store") +} + // X509Store is the interface for all X509Stores type X509Store interface { AddCert(cert *x509.Certificate) error AddCertFromPEM(pemCerts []byte) error AddCertFromFile(filename string) error RemoveCert(cert *x509.Certificate) error - GetCertificateByKeyID(keyID string) (*x509.Certificate, error) + RemoveAll() error + GetCertificateByCertID(certID string) (*x509.Certificate, error) + GetCertificatesByCN(cn string) ([]*x509.Certificate, error) GetCertificates() []*x509.Certificate GetCertificatePool() *x509.CertPool GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) diff --git a/trustmanager/x509utils.go b/trustmanager/x509utils.go index 6b8acb6f1e..6b55509237 100644 --- a/trustmanager/x509utils.go +++ b/trustmanager/x509utils.go @@ -127,25 +127,66 @@ func loadCertsFromDir(s *X509FileStore) { } } -// LoadCertFromFile tries to adds a X509 certificate to the store given a filename +// LoadCertFromFile loads the first certificate from the file provided. The +// data is expected to be PEM Encoded and contain one of more certificates +// with PEM type "CERTIFICATE" func LoadCertFromFile(filename string) (*x509.Certificate, error) { - // TODO(diogo): handle multiple certificates in one file. + certs, err := LoadCertBundleFromFile(filename) + if err != nil { + return nil, err + } + return certs[0], nil +} + +// LoadCertBundleFromFile loads certificates from the []byte provided. The +// data is expected to be PEM Encoded and contain one of more certificates +// with PEM type "CERTIFICATE" +func LoadCertBundleFromFile(filename string) ([]*x509.Certificate, error) { b, err := ioutil.ReadFile(filename) if err != nil { return nil, err } + + return LoadCertBundleFromPEM(b) +} + +// LoadCertBundleFromPEM loads certificates from the []byte provided. The +// data is expected to be PEM Encoded and contain one of more certificates +// with PEM type "CERTIFICATE" +func LoadCertBundleFromPEM(pemBytes []byte) ([]*x509.Certificate, error) { + certificates := []*x509.Certificate{} var block *pem.Block - block, b = pem.Decode(b) - for ; block != nil; block, b = pem.Decode(b) { + block, pemBytes = pem.Decode(pemBytes) + for ; block != nil; block, pemBytes = pem.Decode(pemBytes) { if block.Type == "CERTIFICATE" { cert, err := x509.ParseCertificate(block.Bytes) - if err == nil { - return cert, nil + if err != nil { + return nil, err } + certificates = append(certificates, cert) + } else { + return nil, fmt.Errorf("invalid pem block type: %s", block.Type) } } - return nil, errors.New("could not load certificate from file") + if len(certificates) == 0 { + return nil, fmt.Errorf("no valid certificates found") + } + + return certificates, nil +} + +// GetLeafCerts parses a list of x509 Certificates and returns all of them +// that aren't CA +func GetLeafCerts(certs []*x509.Certificate) []*x509.Certificate { + var leafCerts []*x509.Certificate + for _, cert := range certs { + if cert.IsCA { + continue + } + leafCerts = append(leafCerts, cert) + } + return leafCerts } // ParsePEMPrivateKey returns a data.PrivateKey from a PEM encoded private key. It