Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ ficheros más grandes del repositorio (hasta 32 KB).

`ancla-y-paginas-en-el-puente` · `arrastre-bajo-el-sandbox` · `barra-de-titulo-en-linux` ·
`ca-en-los-almacenes-de-confianza` · `ca-nss-navegador-abierto` · `campos-de-firma-vacios` ·
`contrato-protocolo-afirma` · `coordenadas-recuadro-pades` · `dnie-en-flatpak` · `dnie-en-linux` · `dnie-en-windows` ·
`contrato-protocolo-afirma` · `coordenadas-recuadro-pades` · `dnie-en-flatpak` · `dnie-en-linux` · `dnie-en-macos` · `dnie-en-windows` ·
`exclusion-afirma-ui-utils` · `filtros-sede-unmeasured` · `firma-visible-trifasica` ·
`flathub-libreria-nativa` · `flatpak-canal-unico` · `glibc-libreria-nativa` ·
`graalvm-libawt-shared` · `i18next-y-el-po` · `native-image-postfirma` ·
Expand Down
128 changes: 128 additions & 0 deletions docs/research/card-probes/keychain_token_probe.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Mide si un binario con firma ad hoc y sin perfil de aprovisionamiento lista y usa las identidades de un token del sistema de macOS (#2002, ID-540).
//
// Uso:
// swiftc -O keychain_token_probe.swift -o keychain_token_probe
// codesign --force --sign - keychain_token_probe
// ./keychain_token_probe [--list-only] [--index N]
// Empieza con la tarjeta PIV o la YubiKey dentro. Firma un resumen SHA-256 una sola vez, con la identidad N del token (0 por defecto);
// el PIN lo pide macOS y la sonda no reintenta nunca. No imprime certificados, nombres, números de serie ni el sufijo del token.

import CryptoKit
import Foundation
import Security

struct Query {
let name: String
let extra: [CFString: Any]
}

let queries = [
Query(name: "llavero clásico", extra: [:]),
Query(name: "grupo de tokens", extra: [kSecAttrAccessGroup: kSecAttrAccessGroupToken]),
Query(name: "grupo de tokens + llavero de protección de datos",
extra: [kSecAttrAccessGroup: kSecAttrAccessGroupToken, kSecUseDataProtectionKeychain: true]),
]

func describe(_ status: OSStatus) -> String {
let message = SecCopyErrorMessageString(status, nil) as String? ?? "?"
return "\(status) (\(message))"
}

func tokenKind(_ tokenID: String) -> String {
String(tokenID.split(separator: ":").first ?? "")
}

func printSigningInfo() {
var code: SecCode?
var staticCode: SecStaticCode?
var info: CFDictionary?
guard SecCodeCopySelf([], &code) == errSecSuccess, let code,
SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode,
SecCodeCopySigningInformation(staticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &info) == errSecSuccess,
let info = info as? [CFString: Any]
else {
print("firma del binario: no se pudo leer")
return
}
let flags = (info[kSecCodeInfoFlags] as? UInt32) ?? 0
let adhoc = flags & SecCodeSignatureFlags.adhoc.rawValue != 0
let entitlements = (info[kSecCodeInfoEntitlementsDict] as? [String: Any])?.keys.sorted() ?? []
print("firma del binario: ad hoc=\(adhoc), equipo=\(info[kSecCodeInfoTeamIdentifier] as? String ?? "ninguno"), "
+ "entitlements=\(entitlements.isEmpty ? "ninguno" : entitlements.joined(separator: ","))")
}

func tokenIdentities(_ query: Query) -> [[CFString: Any]] {
var attributes: [CFString: Any] = [
kSecClass: kSecClassIdentity,
kSecMatchLimit: kSecMatchLimitAll,
kSecReturnRef: true,
kSecReturnAttributes: true,
]
attributes.merge(query.extra) { _, new in new }
var result: CFTypeRef?
let started = Date()
let status = SecItemCopyMatching(attributes as CFDictionary, &result)
let elapsed = String(format: "%.2f", Date().timeIntervalSince(started))
let items = (result as? [[CFString: Any]]) ?? []
let fromToken = items.filter { $0[kSecAttrTokenID] != nil }
let kinds = Set(fromToken.compactMap { ($0[kSecAttrTokenID] as? String).map(tokenKind) }).sorted()
print("consulta «\(query.name)»: estado=\(describe(status)), identidades=\(items.count), "
+ "de token=\(fromToken.count) \(kinds), \(elapsed)s")
return fromToken
}

func algorithm(for key: SecKey) -> SecKeyAlgorithm? {
let attributes = SecKeyCopyAttributes(key) as? [CFString: Any] ?? [:]
let type = attributes[kSecAttrKeyType] as? String
let bits = attributes[kSecAttrKeySizeInBits] as? Int ?? 0
print("clave: tipo=\(type == (kSecAttrKeyTypeRSA as String) ? "RSA" : type == (kSecAttrKeyTypeECSECPrimeRandom as String) ? "EC" : type ?? "?"), bits=\(bits)")
if type == (kSecAttrKeyTypeRSA as String) { return .rsaSignatureDigestPKCS1v15SHA256 }
if type == (kSecAttrKeyTypeECSECPrimeRandom as String) { return .ecdsaSignatureDigestX962SHA256 }
return nil
}

func signOnce(with item: [CFString: Any]) -> Int32 {
guard let ref = item[kSecValueRef] else { print("la identidad no trae referencia"); return 2 }
let identity = ref as! SecIdentity
var privateKey: SecKey?
var certificate: SecCertificate?
let keyStatus = SecIdentityCopyPrivateKey(identity, &privateKey)
let certStatus = SecIdentityCopyCertificate(identity, &certificate)
print("clave privada: \(describe(keyStatus)); certificado: \(describe(certStatus))")
guard let privateKey, let certificate, let publicKey = SecCertificateCopyKey(certificate) else { return 2 }
guard let algorithm = algorithm(for: privateKey) else { print("tipo de clave no previsto"); return 2 }
print("admite el algoritmo: \(SecKeyIsAlgorithmSupported(privateKey, .sign, algorithm))")

let digest = Data(SHA256.hash(data: Data("rfirma #2002".utf8)))
var error: Unmanaged<CFError>?
print("firmando: macOS pedirá el PIN")
let started = Date()
let signature = SecKeyCreateSignature(privateKey, algorithm, digest as CFData, &error) as Data?
let elapsed = String(format: "%.2f", Date().timeIntervalSince(started))
guard let signature else {
let cfError = error!.takeRetainedValue()
print("firma: fallo dominio=\(CFErrorGetDomain(cfError)!) código=\(CFErrorGetCode(cfError)), \(elapsed)s")
return 1
}
let verified = SecKeyVerifySignature(publicKey, algorithm, digest as CFData, signature as CFData, nil)
print("firma: \(signature.count) bytes, verifica con el certificado=\(verified), \(elapsed)s")
return verified ? 0 : 1
}

let arguments = CommandLine.arguments
let listOnly = arguments.contains("--list-only")
let index = arguments.firstIndex(of: "--index").flatMap { Int(arguments[safe: $0 + 1] ?? "") } ?? 0

extension Array {
subscript(safe position: Int) -> Element? { indices.contains(position) ? self[position] : nil }
}

print("macOS \(ProcessInfo.processInfo.operatingSystemVersionString)")
printSigningInfo()
let found = queries.map(tokenIdentities)
guard !listOnly else { exit(0) }
guard let candidates = found.first(where: { !$0.isEmpty }), let chosen = candidates[safe: index] else {
print("ninguna consulta devuelve la identidad \(index) de un token: no se firma")
exit(1)
}
exit(signOnce(with: chosen))
35 changes: 35 additions & 0 deletions docs/research/dnie-en-macos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# El DNIe y las tarjetas en macOS

Mediciones con tarjetas reales en macOS, contraparte de
[`dnie-en-windows.md`](dnie-en-windows.md), para el
[spec de macOS](https://github.com/sgomez/rfirma/issues/2000). Las sondas las ejecuta el
titular con las reglas de ID-485, y su salida no trae certificados, nombres ni números de
serie.

## Una app con firma ad hoc y las identidades de un token del sistema

Si un binario firmado ad hoc, sin equipo, sin entitlements y sin perfil de
aprovisionamiento —como rFirma sin cuenta de Apple Developer— puede listar las
identidades de un token del sistema y firmar con ellas. Sin el DNIe: una tarjeta PIV o una
YubiKey, que macOS lee con su `pivtoken`. De esto depende ID-532 (ID-540).

La sonda es [`card-probes/keychain_token_probe.swift`](card-probes/keychain_token_probe.swift):
tres consultas de identidades (llavero clásico, grupo `com.apple.token` y este con el
llavero de protección de datos) y una sola firma de un resumen SHA-256 con
`SecKeyCreateSignature`, verificada con la clave pública del certificado. El PIN lo pide
macOS; la sonda no reintenta.

### Resultado

**No se ha medido.** El titular no tiene una tarjeta PIV ni una YubiKey: su llave FIDO
no ofrece PIV y macOS no crea ningún token con ella, y el DNIe no lo reconoce macOS sin un
controlador de terceros, que el spec deja fuera de esta sonda. Las alternativas sin
tarjeta —una PIV emulada con `vsmartcard` y jCardSim, una JavaCard con un applet PIV o un
token propio de CryptoTokenKit— cuestan más que lo que ID-532 aporta: una tarjeta que no
es el DNIe, que entre los usuarios de rFirma es marginal.

### Conclusión para ID-532

**ID-532 cae por decisión, no por medición.** Las tarjetas entran solo por PKCS#11 y el
Almacén de macOS se queda con los certificados de software. Quien tenga una tarjeta PIV
puede reabrirlo ejecutando la sonda, primero con `--list-only`.
Loading