Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@ build:ubsan --copt="-DUNDEFINED_SANITIZER=1"
build:ubsan --copt="-fno-sanitize=function" --copt="-fno-sanitize=vptr"
build:ubsan --linkopt="-fsanitize=undefined"
build:ubsan --test_env="UBSAN_OPTIONS=log_path=/tmp/san_out"

# libFuzzer + ASan for fuzz targets under //Testing/Fuzzing/...
# Mirrors the prototype's --config=fuzz configuration; layered on top of
# the shared san-common block (also used by --config=asan/tsan/ubsan).
build:fuzz --config=san-common
build:fuzz --@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
bazel_dep(name = "protobuf", version = "33.6")
bazel_dep(name = "rules_apple", version = "4.3.3")
bazel_dep(name = "rules_cc", version = "0.2.17")
bazel_dep(name = "rules_fuzzing", version = "0.6.0")
bazel_dep(name = "rules_shell", version = "0.6.1")
bazel_dep(name = "rules_swift", version = "2.9.0")
bazel_dep(name = "xxhash", version = "0.8.3.bcr.1")
Expand Down
1 change: 1 addition & 0 deletions Source/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,7 @@ test_suite(
"//Source/common/cel:CELTest",
"//Source/common/faa:unit_tests",
"//Source/common/ne:unit_tests",
"//Source/common/verifyinghasher:unit_tests",
],
visibility = ["//:santa_package_group"],
)
Expand Down
210 changes: 210 additions & 0 deletions Source/common/verifyinghasher/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
load("@rules_cc//cc:defs.bzl", "objc_library")
load("//:helper.bzl", "santa_unit_test")

# Default visibility scoped to:
# - this subpackage's own tests
# - the CLI in Testing/OneOffs
# - the fuzz harnesses in Testing/Fuzzing
# The public facade target (:VerifyingHasher, added in Task 13) gets
Comment thread
mlw marked this conversation as resolved.
Outdated
# an explicit per-target visibility list that includes
# //Source/santad:__subpackages__. Internal targets remain
# package-private to santad to enforce that only the facade is consumed
# outside this subpackage.
package(
default_visibility = [
"//Source/common/verifyinghasher:__pkg__",
"//Testing/Fuzzing:__pkg__",
"//Testing/OneOffs:__pkg__",
],
)

licenses(["notice"])

objc_library(
name = "FileReader",
srcs = ["FileReader.mm"],
hdrs = ["FileReader.h"],
)

objc_library(
name = "MemoryFileReader",
hdrs = ["MemoryFileReader.h"],
deps = [":FileReader"],
)

objc_library(
name = "CountingMemoryFileReader",
hdrs = ["CountingMemoryFileReader.h"],
deps = [":FileReader"],
)

santa_unit_test(
name = "FileReaderTest",
srcs = ["FileReaderTest.mm"],
deps = [
":FileReader",
":MemoryFileReader",
"//Source/common:ScopedFile",
],
)

objc_library(
name = "HashTraits",
hdrs = ["HashTraits.h"],
)

santa_unit_test(
name = "HashTraitsTest",
srcs = ["HashTraitsTest.mm"],
deps = [":HashTraits"],
)

objc_library(
name = "UninitBuffer",
hdrs = ["UninitBuffer.h"],
)

santa_unit_test(
name = "UninitBufferTest",
srcs = ["UninitBufferTest.mm"],
deps = [":UninitBuffer"],
)

objc_library(
name = "HeaderParser",
srcs = ["HeaderParser.mm"],
hdrs = ["HeaderParser.h"],
)

santa_unit_test(
name = "HeaderParserTest",
srcs = ["HeaderParserTest.mm"],
deps = [
":FileReader",
":HeaderParser",
":MemoryFileReader",
"//Source/common:ScopedFile",
],
)

objc_library(
name = "CodeSignatureParser",
srcs = ["CodeSignatureParser.mm"],
hdrs = ["CodeSignatureParser.h"],
deps = [":HashTraits"],
)

santa_unit_test(
name = "CodeSignatureParserTest",
srcs = ["CodeSignatureParserTest.mm"],
deps = [
":CodeSignatureParser",
"//Source/common:ScopedFile",
],
)

objc_library(
name = "PageVerifier",
hdrs = ["PageVerifier.h"],
deps = [":HashTraits"],
)

santa_unit_test(
name = "PageVerifierTest",
srcs = ["PageVerifierTest.mm"],
deps = [":PageVerifier"],
)

objc_library(
name = "VerifyingHasherCore",
srcs = ["VerifyingHasherCore.mm"],
hdrs = ["VerifyingHasherCore.h"],
deps = [
":CodeSignatureParser",
":FileReader",
":HashTraits",
":HeaderParser",
":PageVerifier",
":UninitBuffer",
],
)

# Single source of truth for the multi-CD ad-hoc-signed test fixture.
# Testing/Fuzzing references this via cross-package filegroup rather than
# holding its own copy.
filegroup(
name = "hw_universal_fixture",
srcs = ["testdata/hw_universal"],
visibility = [
"//Source/common/verifyinghasher:__pkg__",
"//Testing/Fuzzing:__pkg__",
],
)

# Team-signed counterpart used to exercise positive drift detection
# (kMatchSidTidDrift requires a non-empty team_id). Generated via:
# cp testdata/hw_universal testdata/hw_team_signed
# codesign --force --sign <ApplicationDevelopmentIdentity> testdata/hw_team_signed
# Update kHwTeamSignedSigningID + kHwTeamSignedTeamID in VerifyingHasherTest.mm
# if regenerated with a different identity.
filegroup(
name = "hw_team_signed_fixture",
srcs = ["testdata/hw_team_signed"],
visibility = ["//Source/common/verifyinghasher:__pkg__"],
)

santa_unit_test(
name = "VerifyingHasherCoreTest",
srcs = ["VerifyingHasherCoreTest.mm"],
structured_resources = [":hw_universal_fixture"],
deps = [
":CountingMemoryFileReader",
":MemoryFileReader",
":VerifyingHasherCore",
"//Source/common:ScopedFile",
],
)

objc_library(
name = "VerifyingHasher",
srcs = ["VerifyingHasher.mm"],
hdrs = ["VerifyingHasher.h"],
visibility = [
"//Source/common/verifyinghasher:__pkg__",
"//Source/santad:__subpackages__",
"//Testing/Fuzzing:__pkg__",
"//Testing/OneOffs:__pkg__",
],
deps = [
":CodeSignatureParser",
":FileReader",
":VerifyingHasherCore",
],
)

santa_unit_test(
name = "VerifyingHasherTest",
srcs = ["VerifyingHasherTest.mm"],
structured_resources = [
":hw_team_signed_fixture",
":hw_universal_fixture",
],
deps = [
":VerifyingHasher",
"//Source/common:ScopedFile",
],
)

test_suite(
name = "unit_tests",
tests = [
":CodeSignatureParserTest",
":FileReaderTest",
":HashTraitsTest",
":HeaderParserTest",
":PageVerifierTest",
":UninitBufferTest",
":VerifyingHasherCoreTest",
":VerifyingHasherTest",
],
)
66 changes: 66 additions & 0 deletions Source/common/verifyinghasher/CodeSignatureParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/// Copyright 2026 North Pole Security, Inc.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.

#ifndef SANTA_COMMON_VERIFYINGHASHER_CODESIGNATUREPARSER_H
#define SANTA_COMMON_VERIFYINGHASHER_CODESIGNATUREPARSER_H

#include <sys/cdefs.h>

__BEGIN_DECLS
#include <Kernel/kern/cs_blobs.h>
__END_DECLS

#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>

namespace santa {

// `slot_hashes` is a non-owning view into the input blob passed to
// ParseCodeSignature(); the caller must keep that blob alive for as long
// as ParsedCodeDirectory is used. `cdhash` is a fixed-size in-line array,
// owned and self-contained. VerifyingHasherCore holds parsed_cd_ alongside the
// cs_blob_buf_ that backs the view, so they share the verifier's lifetime.
struct ParsedCodeDirectory {
uint8_t hash_type = 0; // CS_HASHTYPE_*
uint8_t hash_size = 0; // 20, 32, or 48
uint8_t compare_size = 0; // hash_size, except 20 for SHA-256-TRUNCATED
Comment thread
mlw marked this conversation as resolved.
Outdated
uint32_t page_size = 0;
uint64_t code_limit = 0;
uint32_t page_count = 0;
std::span<const uint8_t> slot_hashes; // page_count * hash_size bytes
// 20-byte truncated cdhash of this CodeDirectory blob, computed using
// its own hashType (matches xnu's cs_cd_hash and es_cdhash_t).
uint8_t cdhash[CS_CDHASH_LEN] = {};
std::string
identifier; // signing identifier from CD identOffset (empty if absent)
std::string team_id; // team id from CD teamOffset (empty if absent or
// pre-CS_SUPPORTSTEAMID)
};

// Parse a CS_SuperBlob already pread'd into memory. `slice_size` is used
// for the codeLimit-fits-in-slice validation. Returns true on success;
// false sets `err` to a diagnostic string.
//
// Selects the strongest available CD by hashType:
// SHA-384 > SHA-256 > SHA-256-TRUNCATED > SHA-1
// Skips alternates of unsupported types.
bool ParseCodeSignature(std::span<const uint8_t> blob, uint64_t slice_size,
ParsedCodeDirectory& out, std::string& err);

} // namespace santa

#endif // SANTA_COMMON_VERIFYINGHASHER_CODESIGNATUREPARSER_H
Loading
Loading