mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
Adds parsing for two cert-chain shapes that were previously opaque:
- X.509 extension 1.2.840.113635.100.6.1.15 (Apple IMG4 manifest cert
spec): the MANP/OBJP constraint groups that bound which manifest
properties a signing cert is permitted to vouch for. Constraints are
pinned values, [0] required-any, or [1] optional-any.
- IM4C (Image4 Certificate): the non-X.509 cert format used by
peripheral firmware tickets (Yonkers/Savage). Decodes version, raw
EC public key, ECDSA signature, and the same CRTP constraint groups.
Also:
- analyzeSignature now identifies DER-encoded ECDSA curves by parsing
SEQUENCE{r,s} and inferring P-256/384/521 from integer width
- isPrintableString rejects invalid UTF-8 — RuneError is IsPrint()==true
so binary nonces (e.g. ynon=0xAA*16) were rendering as garbage glyphs
Closes #1170
241 lines
6.5 KiB
Go
241 lines
6.5 KiB
Go
package img4
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/asn1"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// oidAppleImg4ManifestCertSpec is the X.509 extension OID 1.2.840.113635.100.6.1.15.
|
|
// It carries the IMG4 manifest property constraints that the certificate is permitted to sign.
|
|
var oidAppleImg4ManifestCertSpec = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 6, 1, 15}
|
|
|
|
const (
|
|
tagCRTP = 1129469008 // CRTP - Certificate Properties (IM4C constraint container)
|
|
tagPUBK = 1347764811 // PUBK - Public Key (IM4C signing key)
|
|
)
|
|
|
|
// ConstraintKind describes how a constraint binds a manifest property.
|
|
type ConstraintKind int
|
|
|
|
const (
|
|
// ConstraintPinned: property must equal the encoded concrete value.
|
|
ConstraintPinned ConstraintKind = iota
|
|
// ConstraintRequired: property must be present (any value). Encoded as [0] EXPLICIT NULL.
|
|
ConstraintRequired
|
|
// ConstraintOptional: property may be present (any value). Encoded as [1] EXPLICIT NULL.
|
|
ConstraintOptional
|
|
)
|
|
|
|
// Constraint is a single 4cc property constraint from an IMG4 cert spec.
|
|
type Constraint struct {
|
|
Name string
|
|
Kind ConstraintKind
|
|
Value any // populated when Kind == ConstraintPinned
|
|
}
|
|
|
|
// ConstraintGroup is a named SET of constraints (MANP or OBJP).
|
|
type ConstraintGroup struct {
|
|
Name string
|
|
Constraints []Constraint
|
|
}
|
|
|
|
// IM4C is an Image4 Certificate — a non-X.509 cert format used by some
|
|
// peripheral firmware tickets (e.g. Yonkers/Savage). It carries the same
|
|
// constraint groups as the X.509 1.2.840.113635.100.6.1.15 extension,
|
|
// plus a raw public key and a signature over the body.
|
|
type IM4C struct {
|
|
Version int
|
|
Constraints []ConstraintGroup
|
|
PublicKey []byte
|
|
Signature []byte
|
|
}
|
|
|
|
// parseIM4C parses the cert-chain bytes of an IM4M when they hold an IM4C
|
|
// rather than an X.509 chain. The input is the IM4M CertChain RawValue's
|
|
// inner bytes — i.e. one SEQUENCE { "IM4C", INT, SET{ CRTP, PUBK }, sig }.
|
|
func parseIM4C(data []byte) (*IM4C, error) {
|
|
var body struct {
|
|
Tag string `asn1:"ia5"`
|
|
Version int
|
|
Props asn1.RawValue `asn1:"set"`
|
|
Signature []byte
|
|
}
|
|
if _, err := asn1.Unmarshal(data, &body); err != nil {
|
|
return nil, err
|
|
}
|
|
if body.Tag != "IM4C" {
|
|
return nil, fmt.Errorf("not an IM4C: tag=%q", body.Tag)
|
|
}
|
|
|
|
im4c := &IM4C{
|
|
Version: body.Version,
|
|
Signature: body.Signature,
|
|
}
|
|
|
|
rest := body.Props.Bytes
|
|
for len(rest) > 0 {
|
|
var entry asn1.RawValue
|
|
next, err := asn1.Unmarshal(rest, &entry)
|
|
if err != nil {
|
|
break
|
|
}
|
|
rest = next
|
|
if entry.Class != asn1.ClassPrivate {
|
|
continue
|
|
}
|
|
switch entry.Tag {
|
|
case tagCRTP:
|
|
var crtp struct {
|
|
Name string `asn1:"ia5"`
|
|
Set asn1.RawValue
|
|
}
|
|
if _, err := asn1.Unmarshal(entry.Bytes, &crtp); err == nil {
|
|
im4c.Constraints = parseConstraintGroups(crtp.Set.Bytes)
|
|
}
|
|
case tagPUBK:
|
|
// PUBK wraps an OCTET STRING directly (no SEQUENCE).
|
|
var pk []byte
|
|
if _, err := asn1.Unmarshal(entry.Bytes, &pk); err == nil {
|
|
im4c.PublicKey = pk
|
|
}
|
|
}
|
|
}
|
|
|
|
return im4c, nil
|
|
}
|
|
|
|
// parseConstraintGroups walks a SET of priv-tagged groups (MANP, OBJP, ...).
|
|
// Used for both the X.509 extension value and the IM4C CRTP body.
|
|
func parseConstraintGroups(setBytes []byte) []ConstraintGroup {
|
|
var groups []ConstraintGroup
|
|
rest := setBytes
|
|
for len(rest) > 0 {
|
|
var entry asn1.RawValue
|
|
next, err := asn1.Unmarshal(rest, &entry)
|
|
if err != nil {
|
|
break
|
|
}
|
|
rest = next
|
|
if entry.Class != asn1.ClassPrivate {
|
|
continue
|
|
}
|
|
var grp struct {
|
|
Name string `asn1:"ia5"`
|
|
Set asn1.RawValue
|
|
}
|
|
if _, err := asn1.Unmarshal(entry.Bytes, &grp); err != nil {
|
|
continue
|
|
}
|
|
groups = append(groups, ConstraintGroup{
|
|
Name: grp.Name,
|
|
Constraints: parseConstraints(grp.Set.Bytes),
|
|
})
|
|
}
|
|
return groups
|
|
}
|
|
|
|
// parseConstraints walks the inner SET of priv-tagged property constraints.
|
|
func parseConstraints(setBytes []byte) []Constraint {
|
|
var out []Constraint
|
|
rest := setBytes
|
|
for len(rest) > 0 {
|
|
var entry asn1.RawValue
|
|
next, err := asn1.Unmarshal(rest, &entry)
|
|
if err != nil {
|
|
break
|
|
}
|
|
rest = next
|
|
if entry.Class != asn1.ClassPrivate {
|
|
continue
|
|
}
|
|
var prop struct {
|
|
Name string `asn1:"ia5"`
|
|
Value asn1.RawValue
|
|
}
|
|
if _, err := asn1.Unmarshal(entry.Bytes, &prop); err != nil {
|
|
continue
|
|
}
|
|
c := Constraint{Name: prop.Name}
|
|
// Context-tagged NULL is a wildcard: [0] required-any, [1] optional-any.
|
|
if prop.Value.Class == asn1.ClassContextSpecific {
|
|
if prop.Value.Tag == 1 {
|
|
c.Kind = ConstraintOptional
|
|
} else {
|
|
c.Kind = ConstraintRequired
|
|
}
|
|
} else {
|
|
c.Kind = ConstraintPinned
|
|
c.Value = ParsePropertyValueWithTag(prop.Value, entry.Tag)
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// extractCertConstraints pulls the IMG4 constraint extension from a parsed X.509 cert.
|
|
func extractCertConstraints(cert *x509.Certificate) []ConstraintGroup {
|
|
for _, ext := range cert.Extensions {
|
|
if !ext.Id.Equal(oidAppleImg4ManifestCertSpec) {
|
|
continue
|
|
}
|
|
// The extension value is a SET wrapping the priv-tagged groups.
|
|
var set asn1.RawValue
|
|
if _, err := asn1.Unmarshal(ext.Value, &set); err != nil {
|
|
return nil
|
|
}
|
|
return parseConstraintGroups(set.Bytes)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// constraintGroupsToMap flattens groups into a JSON-friendly map.
|
|
// Wildcards become "*" (required) or "?" (optional); pinned values pass through.
|
|
func constraintGroupsToMap(groups []ConstraintGroup) map[string]map[string]any {
|
|
out := make(map[string]map[string]any, len(groups))
|
|
for _, g := range groups {
|
|
props := make(map[string]any, len(g.Constraints))
|
|
for _, c := range g.Constraints {
|
|
switch c.Kind {
|
|
case ConstraintRequired:
|
|
props[c.Name] = "*"
|
|
case ConstraintOptional:
|
|
props[c.Name] = "?"
|
|
default:
|
|
props[c.Name] = c.Value
|
|
}
|
|
}
|
|
out[g.Name] = props
|
|
}
|
|
return out
|
|
}
|
|
|
|
// writeConstraints renders constraint groups into sb at the given base indent.
|
|
func writeConstraints(sb *strings.Builder, groups []ConstraintGroup, indent string) {
|
|
for _, g := range groups {
|
|
label := g.Name
|
|
switch g.Name {
|
|
case "MANP":
|
|
label = "MANP (Manifest Properties)"
|
|
case "OBJP":
|
|
label = "OBJP (Object Properties)"
|
|
}
|
|
fmt.Fprintf(sb, "%s%s:\n", indent, colorSubField(label))
|
|
for _, c := range g.Constraints {
|
|
name := colorField(c.Name)
|
|
if long, ok := PropertyFourCCs[c.Name]; ok && !strings.EqualFold(c.Name, long) {
|
|
name = fmt.Sprintf("%s (%s)", name, colorLongName(long))
|
|
}
|
|
switch c.Kind {
|
|
case ConstraintRequired:
|
|
fmt.Fprintf(sb, "%s %s: *\n", indent, name)
|
|
case ConstraintOptional:
|
|
fmt.Fprintf(sb, "%s %s: * (optional)\n", indent, name)
|
|
default:
|
|
fmt.Fprintf(sb, "%s %s: %s\n", indent, name, FormatPropertyValue(c.Value))
|
|
}
|
|
}
|
|
}
|
|
}
|