diff --git a/internal/certs/cert_store_darwin.go b/internal/certs/cert_store_darwin.go index 97a79fa5..faafb448 100644 --- a/internal/certs/cert_store_darwin.go +++ b/internal/certs/cert_store_darwin.go @@ -23,71 +23,200 @@ package certs #import #import +// Memory management rules: +// Foundation object (Objective-C prefixed with `NS`) get ARC (Automatic Reference Counting), and do not need to be released manually. +// Core Foundation objects (C), prefixed with need to be released manually using CFRelease() unless: +// - They're obtained using a CF method containing the word Get (a.k.a. the Get Rule). +// - They're obtained using toll-free bridging from a Foundation Object (using the __bridge keyword). -int installTrustedCert(char const *bytes, unsigned long long length) { - if (length == 0) { - return errSecInvalidData; - } - - NSData *der = [NSData dataWithBytes:bytes length:length]; - - // Step 1. Import the certificate in the keychain. - SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der); - NSDictionary* addQuery = @{ - (id)kSecValueRef: (__bridge id) cert, - (id)kSecClass: (id)kSecClassCertificate, - }; - - OSStatus status = SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL); - if ((errSecSuccess != status) && (errSecDuplicateItem != status)) { - CFRelease(cert); - return status; - } - - // Step 2. Set the trust for the certificate. - SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL - NSDictionary *trustSettings = @{ - (id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot], - (id)kSecTrustSettingsPolicy: (__bridge id) policy, - }; - status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings)); - CFRelease(policy); - CFRelease(cert); - - return status; +//**************************************************************************************************************************************************** +/// \brief Create a certificate object from DER-encoded data. +/// +/// \return The certifcation. The caller is responsible for releasing the object using CFRelease. +/// \return NULL if data is not a valid DER-encoded certificate. +//**************************************************************************************************************************************************** +SecCertificateRef certFromData(char const* data, uint64_t length) { + NSData *der = [NSData dataWithBytes:data length:length]; + return SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der); } -int removeTrustedCert(char const *bytes, unsigned long long length) { - if (0 == length) { - return errSecInvalidData; - } +//**************************************************************************************************************************************************** +/// \brief Check if a certificate is in the user's keychain. +/// +/// \param[in] cert The certificate. +/// \return true iff the certificate is in the user's keychain. +//**************************************************************************************************************************************************** +bool _isCertificateInKeychain(SecCertificateRef const cert) { + NSDictionary *attrs = @{ + (id)kSecMatchItemList: @[(__bridge id)cert], + (id)kSecClass: (id)kSecClassCertificate, + (id)kSecReturnData: @YES + }; + return errSecSuccess == SecItemCopyMatching((__bridge CFDictionaryRef)attrs, NULL); +} - NSData *der = [NSData dataWithBytes: bytes length: length]; - SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der); +//**************************************************************************************************************************************************** +/// \brief Check if a certificate is in the user's keychain. +/// +/// \param[in] certData The certificate data in DER encoded format. +/// \param[in] certSize The size of the certData in bytes. +/// \return true iff the certificate is in the user's keychain. +//**************************************************************************************************************************************************** +bool isCertificateInKeychain(char const* certData, uint64_t certSize) { + return _isCertificateInKeychain(certFromData(certData, certSize)); +} - // Step 1. Unset the trust for the certificate. - SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); - NSDictionary * trustSettings = @{ - (id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified], - (id)kSecTrustSettingsPolicy: (__bridge id) policy, - }; - OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings)); - CFRelease(policy); - if (errSecSuccess != status) { - CFRelease(cert); - return status; - } - // Step 2. Remove the certificate from the keychain. - NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate, - (id)kSecMatchItemList: @[(__bridge id)cert], - (id)kSecMatchLimit: (id)kSecMatchLimitOne, - }; - status = SecItemDelete((__bridge CFDictionaryRef) query); +//**************************************************************************************************************************************************** +/// \brief Add a certificate to the user's keychain. +/// +/// \param[in] cert The certificate. +/// \return The status for the operation. +//**************************************************************************************************************************************************** +OSStatus _addCertificateToKeychain(SecCertificateRef const cert) { + NSDictionary* addQuery = @{ + (id)kSecValueRef: (__bridge id) cert, + (id)kSecClass: (id)kSecClassCertificate, + }; + return SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL); +} - CFRelease(cert); - return status; +//**************************************************************************************************************************************************** +/// \brief Add a certificate to the user's keychain. +/// +/// \param[in] certData The certificate data in DER encoded format. +/// \param[in] certSize The size of the certData in bytes. +/// \return The status for the operation. +//**************************************************************************************************************************************************** +OSStatus addCertificateToKeychain(char const* certData, uint64_t certSize) { + return _addCertificateToKeychain(certFromData(certData, certSize)); +} + +//**************************************************************************************************************************************************** +/// \brief Add a certificate to the user's keychain. +/// +/// \param[in] cert The certificate. +/// \return The status for the operation. +//**************************************************************************************************************************************************** +OSStatus _removeCertificateFromKeychain(SecCertificateRef const cert) { + NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate, + (id)kSecMatchItemList: @[(__bridge id)cert], + (id)kSecMatchLimit: (id)kSecMatchLimitOne, + }; + return SecItemDelete((__bridge CFDictionaryRef) query); +} + +//**************************************************************************************************************************************************** +/// \brief Add a certificate to the user's keychain. +/// +/// \param[in] certData The certificate data in DER encoded format. +/// \param[in] certSize The size of the certData in bytes. +/// \return The status for the operation. +//**************************************************************************************************************************************************** +OSStatus removeCertificateFromKeychain(char const* certData, uint64_t certSize) { + return _removeCertificateFromKeychain(certFromData(certData, certSize)); +} + +//**************************************************************************************************************************************************** +/// \brief Check if a certificate is trusted in the user's keychain. +/// +/// \param[in] cert The certificate. +/// \return true iff the certificate is trusted in the user's keychain. +//**************************************************************************************************************************************************** +bool _isCertificateTrusted(SecCertificateRef const cert) { + CFArrayRef trustSettings = NULL; + OSStatus status = SecTrustSettingsCopyTrustSettings(cert, kSecTrustSettingsDomainUser, &trustSettings); + if (status != errSecSuccess) { + return false; + } + CFIndex count = CFArrayGetCount(trustSettings); + bool result = false; + for (CFIndex index = 0; index < count; ++index) { + CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(trustSettings, index); + if (!dict) { + continue; + } + CFNumberRef num = (CFNumberRef)CFDictionaryGetValue(dict, kSecTrustSettingsResult); + int value; + if (num && CFNumberGetValue(num, kCFNumberSInt32Type, &value) && (value == kSecTrustSettingsResultTrustRoot)) { + result = true; + break; + } + } + CFRelease(trustSettings); + return result; +} + +//**************************************************************************************************************************************************** +/// \brief Check if a certificate is trusted in the user's keychain. +/// +/// \param[in] certData The certificate data in DER encoded format. +/// \param[in] certSize The size of the certData in bytes. +/// \return true iff the certificate is trusted in the user's keychain. +//**************************************************************************************************************************************************** +bool isCertificateTrusted(char const* certData, uint64_t certSize) { + return _isCertificateTrusted(certFromData(certData, certSize)); +} + +//**************************************************************************************************************************************************** +/// \brief Set the trust level for a certificate in the user's keychain. This call will trigger a security prompt. +/// +/// \param[in] cert The certificate. +/// \param[in] trustLevel The trust level. +/// \return The status for the operation. +//**************************************************************************************************************************************************** +OSStatus _setCertificateTrustLevel(SecCertificateRef const cert, int trustLevel) { + SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL + NSDictionary *trustSettings = @{ + (id)kSecTrustSettingsResult: [NSNumber numberWithInt:trustLevel], + (id)kSecTrustSettingsPolicy: (__bridge id) policy, + }; + OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings)); + CFRelease(policy); + return status; +} + +//**************************************************************************************************************************************************** +/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt. +/// +/// \param[in] cert The certificate. +/// \return The status for the operation. +//**************************************************************************************************************************************************** +OSStatus _setCertificateTrusted(SecCertificateRef cert) { + return _setCertificateTrustLevel(cert, kSecTrustSettingsResultTrustRoot); +} + +//**************************************************************************************************************************************************** +/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt. +/// +/// \param[in] certData The certificate data in DER encoded format. +/// \param[in] certSize The size of the certData in bytes. +/// \return The status for the operation. +//**************************************************************************************************************************************************** +OSStatus setCertificateTrusted(char const* certData, uint64_t certSize) { + return _setCertificateTrusted(certFromData(certData, certSize)); +} + +//**************************************************************************************************************************************************** +/// \brief Remove the trust level of a certificate in the user's keychain. +/// +/// \param[in] cert The certificate. +/// \return The status for the operation. +//**************************************************************************************************************************************************** +OSStatus _removeCertificateTrust(SecCertificateRef cert) { + return _setCertificateTrustLevel(cert, kSecTrustSettingsResultUnspecified); +} + +//**************************************************************************************************************************************************** +/// \brief Remove the trust level of a certificate in the user's keychain. +/// +/// \param[in] certData The certificate data in DER encoded format. +/// \param[in] certSize The size of the certData in bytes. +/// \return The status for the operation. +//**************************************************************************************************************************************************** +OSStatus removeCertificateTrust(char const* certData, uint64_t certSize) { + return _removeCertificateTrust(certFromData(certData, certSize)); } */ import "C" @@ -105,6 +234,10 @@ const ( errAuthorizationCanceled = -60006 ) +var ( + ErrUserCanceledCertificateInstall = errors.New("the user cancelled the authorization dialog") +) + // certPEMToDER converts a certificate in PEM format to DER format, which is the format required by Apple's Security framework. func certPEMToDER(certPEM []byte) ([]byte, error) { block, left := pem.Decode(certPEM) @@ -119,6 +252,116 @@ func certPEMToDER(certPEM []byte) ([]byte, error) { return block.Bytes, nil } +// wrapCGoCertCallReturningBool wrap call to a CGo function returning a bool. +// if the certificate is invalid the call will return false. +func wrapCGoCertCallReturningBool(certPEM []byte, fn func(*C.char, C.ulonglong) bool) bool { + certDER, err := certPEMToDER(certPEM) + if err != nil { + return false // error are ignored + } + + buffer := C.CBytes(certDER) + defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert + + return fn((*C.char)(buffer), C.ulonglong(len(certDER))) +} + +// wrapCGoCertCallReturningBool wrap call to a CGo function returning an error +func wrapCGoCertCallReturningError(certPEM []byte, fn func(*C.char, C.ulonglong) error) error { + certDER, err := certPEMToDER(certPEM) + if err != nil { + return err + } + + buffer := C.CBytes(certDER) + defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert + + return fn((*C.char)(buffer), C.ulonglong(len(certDER))) +} + +// isCertInKeychain returns true if the given certificate is stored in the user's keychain. +func isCertInKeychain(certPEM []byte) bool { + return wrapCGoCertCallReturningBool(certPEM, isCertInKeychainCGo) +} + +func isCertInKeychainCGo(buffer *C.char, size C.ulonglong) bool { + return bool(C.isCertificateInKeychain(buffer, size)) +} + +// addCertToKeychain adds a certificate to the user's keychain. +// Trying to add a certificate that is already in the keychain will result in an error. +func addCertToKeychain(certPEM []byte) error { + return wrapCGoCertCallReturningError(certPEM, addCertToKeychainCGo) +} + +func addCertToKeychainCGo(buffer *C.char, size C.ulonglong) error { + if errCode := C.addCertificateToKeychain(buffer, size); errCode != errSecSuccess { + return fmt.Errorf("could not add certificate to keychain (error %v)", errCode) + } + + return nil +} + +// removeCertFromKeychain removes a certificate from the user's keychain. +// Trying to remove a certificate that is not in the keychain will result in an error. +func removeCertFromKeychain(certPEM []byte) error { + return wrapCGoCertCallReturningError(certPEM, removeCertFromKeychainCGo) +} + +func removeCertFromKeychainCGo(buffer *C.char, size C.ulonglong) error { + if errCode := C.removeCertificateFromKeychain(buffer, size); errCode != errSecSuccess { + return fmt.Errorf("could not remove certificate from keychain (error %v)", errCode) + } + return nil +} + +// isCertTrusted check if a certificate is trusted in the user's keychain. +func isCertTrusted(certPEM []byte) bool { + return wrapCGoCertCallReturningBool(certPEM, isCertTrustedCGo) +} + +func isCertTrustedCGo(buffer *C.char, size C.ulonglong) bool { + return bool(C.isCertificateTrusted(buffer, size)) +} + +// setCertTrusted sets a certificate as trusted in the user's keychain. +// This function will trigger a security prompt from the system. +func setCertTrusted(certPEM []byte) error { + return wrapCGoCertCallReturningError(certPEM, setCertTrustedCGo) +} + +func setCertTrustedCGo(buffer *C.char, size C.ulonglong) error { + errCode := C.setCertificateTrusted(buffer, size) + switch errCode { + case errSecSuccess: + return nil + case errAuthorizationCanceled: + return ErrUserCanceledCertificateInstall + default: + return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode) + } +} + +// removeCertTrust remove the trust level of the certificated from the user's keychain. +// This function will trigger a security prompt from the system. +func removeCertTrust(certPEM []byte) error { + return wrapCGoCertCallReturningError(certPEM, removeCertTrustCGo) +} + +func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error { + errCode := C.removeCertificateTrust(buffer, size) + switch errCode { + case errSecSuccess: + return nil + case errAuthorizationCanceled: + return ErrUserCanceledCertificateInstall + default: + return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode) + } +} + +// installCert installs a certificate in the keychain. The certificate is added to the keychain and it is set as trusted. +// This function will trigger a security prompt from the system, unless the certificate is already trusted in the user keychain. func installCert(certPEM []byte) error { certDER, err := certPEMToDER(certPEM) if err != nil { @@ -127,18 +370,24 @@ func installCert(certPEM []byte) error { p := C.CBytes(certDER) defer C.free(unsafe.Pointer(p)) //nolint:unconvert + buffer := (*C.char)(p) + size := C.ulonglong(len(certDER)) - errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))) - switch errCode { - case errSecSuccess: - return nil - case errAuthorizationCanceled: - return fmt.Errorf("the user cancelled the authorization dialog") - default: - return fmt.Errorf("could not install certification into keychain (error %v)", errCode) + if !isCertInKeychainCGo(buffer, size) { + if err := addCertToKeychainCGo(buffer, size); err != nil { + return err + } } + + if !isCertTrustedCGo(buffer, size) { + return setCertTrustedCGo(buffer, size) + } + + return nil } +// uninstallCert uninstalls a certificate in the keychain. The certificate trust is removed and the certificated is deleted from the keychain. +// This function will trigger a security prompt from the system, unless the certificate is not trusted in the user keychain. func uninstallCert(certPEM []byte) error { certDER, err := certPEMToDER(certPEM) if err != nil { @@ -147,10 +396,32 @@ func uninstallCert(certPEM []byte) error { p := C.CBytes(certDER) defer C.free(unsafe.Pointer(p)) //nolint:unconvert + buffer := (*C.char)(p) + size := C.ulonglong(len(certDER)) - if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 { - return fmt.Errorf("could not install certificate from keychain (error %v)", errCode) + if isCertTrustedCGo(buffer, size) { + if err := removeCertTrustCGo(buffer, size); err != nil { + return err + } + } + + if isCertInKeychainCGo(buffer, size) { + return removeCertFromKeychainCGo(buffer, size) } return nil } + +func isCertInstalled(certPEM []byte) bool { + certDER, err := certPEMToDER(certPEM) + if err != nil { + return false + } + + p := C.CBytes(certDER) + defer C.free(unsafe.Pointer(p)) //nolint:unconvert + buffer := (*C.char)(p) + size := C.ulonglong(len(certDER)) + + return isCertInKeychainCGo(buffer, size) && isCertTrustedCGo(buffer, size) +} diff --git a/internal/certs/cert_store_darwin_test.go b/internal/certs/cert_store_darwin_test.go index 3b1d419f..2b7dda59 100644 --- a/internal/certs/cert_store_darwin_test.go +++ b/internal/certs/cert_store_darwin_test.go @@ -25,20 +25,73 @@ import ( "github.com/stretchr/testify/require" ) -// This test implies human interactions to enter password and is disabled by default. -func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused +func TestCertInKeychain(t *testing.T) { + // no trust settings change is performed, so this test will not trigger an OS security prompt. + certPEM := generatePEMCertificate(t) + require.False(t, isCertInKeychain(certPEM)) + require.NoError(t, addCertToKeychain(certPEM)) + require.True(t, isCertInKeychain(certPEM)) + require.Error(t, addCertToKeychain(certPEM)) + require.True(t, isCertInKeychain(certPEM)) + require.NoError(t, removeCertFromKeychain(certPEM)) + require.False(t, isCertInKeychain(certPEM)) + require.Error(t, removeCertFromKeychain(certPEM)) + require.False(t, isCertInKeychain(certPEM)) +} + +// This test require human interaction (macOS security prompts), and is disabled by default. +func TestCertificateTrust(t *testing.T) { + certPEM := generatePEMCertificate(t) + require.False(t, isCertTrusted(certPEM)) + require.NoError(t, addCertToKeychain(certPEM)) + require.NoError(t, setCertTrusted(certPEM)) + require.True(t, isCertTrusted(certPEM)) + require.NoError(t, removeCertTrust(certPEM)) + require.False(t, isCertTrusted(certPEM)) + require.NoError(t, removeCertFromKeychain(certPEM)) +} + +// This test require human interaction (macOS security prompts), and is disabled by default. +func TestInstallAndRemove(t *testing.T) { + certPEM := generatePEMCertificate(t) + + // fresh install + require.False(t, isCertInstalled(certPEM)) + require.NoError(t, installCert(certPEM)) + require.True(t, isCertInKeychain(certPEM)) + require.True(t, isCertTrusted(certPEM)) + require.True(t, isCertInstalled(certPEM)) + require.NoError(t, uninstallCert(certPEM)) + require.False(t, isCertInKeychain(certPEM)) + require.False(t, isCertTrusted(certPEM)) + require.False(t, isCertInstalled(certPEM)) + + // Install where certificate is already in Keychain, but not trusted. + require.NoError(t, addCertToKeychain(certPEM)) + require.False(t, isCertInstalled(certPEM)) + require.NoError(t, installCert(certPEM)) + require.True(t, isCertInstalled(certPEM)) + + // Install where certificate is already installed + require.NoError(t, installCert(certPEM)) + + // Remove when certificate is not trusted. + require.NoError(t, removeCertTrust(certPEM)) + require.NoError(t, uninstallCert(certPEM)) + require.False(t, isCertInstalled(certPEM)) + + // Remove when certificate has already been removed. + require.NoError(t, uninstallCert(certPEM)) + require.False(t, isCertTrusted(certPEM)) + require.False(t, isCertInKeychain(certPEM)) +} + +func generatePEMCertificate(t *testing.T) []byte { template, err := NewTLSTemplate() require.NoError(t, err) certPEM, _, err := GenerateCert(template) require.NoError(t, err) - require.Error(t, installCert([]byte{0})) // Cannot install an invalid cert. - require.Error(t, uninstallCert(certPEM)) // Cannot uninstall a cert that is not installed. - require.NoError(t, installCert(certPEM)) // Can install a valid cert. - require.NoError(t, installCert(certPEM)) // Can install an already installed cert. - require.NoError(t, uninstallCert(certPEM)) // Can uninstall an installed cert. - require.Error(t, uninstallCert(certPEM)) // Cannot uninstall an already uninstalled cert. - require.NoError(t, installCert(certPEM)) // Can reinstall an uninstalled cert. - require.NoError(t, uninstallCert(certPEM)) // Can uninstall a reinstalled cert. + return certPEM } diff --git a/internal/certs/installer.go b/internal/certs/installer.go index 25f09e3d..fd14054f 100644 --- a/internal/certs/installer.go +++ b/internal/certs/installer.go @@ -30,3 +30,7 @@ func (installer *Installer) InstallCert(certPEM []byte) error { func (installer *Installer) UninstallCert(certPEM []byte) error { return uninstallCert(certPEM) } + +func (installer *Installer) IsCertInstalled(certPEM []byte) bool { + return isCertInstalled(certPEM) +}