Repository: smallstep/crypto (go)
Issue: Add support for ASN.1 BOOLEAN type
### What would you like to be added

As the title suggests, it would be nice to be able to do `asn1Enc "bool:false"` `asn1Enc "bool:true"`

### Why this is needed

Encoding a BOOLEAN value is a useful primitive to have, to add to strings and numbers
Pull request description: Allow to encode boolean types
### Description

This commit allows the use of the "bool:" or "boolean:" prefix to ASN.1 encode boolean values like true or false.

Fixes #340


Patch:
diff --git a/x509util/extensions.go b/x509util/extensions.go
index 6e64990..61fd0a9 100644
--- a/x509util/extensions.go
+++ b/x509util/extensions.go
@@ -495,8 +495,8 @@ func parseFieldParameters(str string) (p asn1Params) {
 			p.Type = part
 			params = append(params, part)
 		// types that are parsed from the string.
-		// int and oid are not a type that can be set in a tag.
-		case "int", "oid":
+		// int, oid, and bool are not a type that can be set in a tag.
+		case "int", "oid", "bool", "boolean":
 			p.Type = part
 		// types parsed from the string as a time
 		case "utc", "generalized":
@@ -574,6 +574,12 @@ func marshalValue(value, params string) ([]byte, error) {
 			}
 		}
 		return asn1.MarshalWithParams(t, p.Params)
+	case "bool", "boolean":
+		b, err := strconv.ParseBool(value)
+		if err != nil {
+			return nil, errors.Wrap(err, "invalid bool value")
+		}
+		return asn1.MarshalWithParams(b, p.Params)
 	default: // if it's an unknown type, default to printable
 		if !isPrintableString(value, true, true) {
 			return nil, fmt.Errorf("invalid printable value")


Test patch:
diff --git a/x509util/extensions_test.go b/x509util/extensions_test.go
index b2b8d0b..0e364bc 100644
--- a/x509util/extensions_test.go
+++ b/x509util/extensions_test.go
@@ -370,6 +370,12 @@ func TestSubjectAlternativeName_RawValue(t *testing.T) {
 		{"otherName whitespaces", fields{"1.2.3.4", ",,printable:abc1234", nil}, asn1.RawValue{
 			FullBytes: append([]byte{160, 16, 6, 3, 42, 3, 4, 160, 9, 19, 7}, []byte("abc1234")...),
 		}, false},
+		{"otherName bool:true", fields{"1.2.3.4", "bool:true", nil}, asn1.RawValue{
+			FullBytes: []byte{160, 10, 6, 3, 42, 3, 4, 160, 3, 1, 1, 255},
+		}, false},
+		{"otherName boolean:false", fields{"1.2.3.4", "boolean:false", nil}, asn1.RawValue{
+			FullBytes: []byte{160, 10, 6, 3, 42, 3, 4, 160, 3, 1, 1, 0},
+		}, false},
 		{"fail dn", fields{"dn", "1234", nil}, asn1.RawValue{}, true},
 		{"fail x400Address", fields{"x400Address", "1234", nil}, asn1.RawValue{}, true},
 		{"fail ediPartyName", fields{"ediPartyName", "1234", nil}, asn1.RawValue{}, true},
diff --git a/x509util/options_test.go b/x509util/options_test.go
index c7b6dc0..067d132 100644
--- a/x509util/options_test.go
+++ b/x509util/options_test.go
@@ -79,7 +79,7 @@ func TestWithTemplate(t *testing.T) {
 		{"id": "1.2.3.4", "value": {{ asn1Enc (first .Insecure.CR.DNSNames) | toJson }}},
 		{"id": "1.2.3.5", "value": {{ asn1Marshal (first .Insecure.CR.DNSNames) | toJson }}},
 		{"id": "1.2.3.6", "value": {{ asn1Seq (asn1Enc (first .Insecure.CR.DNSNames)) (asn1Enc "int:123456") | toJson }}},
-		{"id": "1.2.3.7", "value": {{ asn1Set (asn1Marshal (first .Insecure.CR.DNSNames) "utf8") (asn1Enc "int:123456") | toJson }}}
+		{"id": "1.2.3.7", "value": {{ asn1Set (asn1Marshal (first .Insecure.CR.DNSNames) "utf8") (asn1Enc "bool:true") | toJson }}}
 	]
 }`
 
@@ -181,7 +181,7 @@ func TestWithTemplate(t *testing.T) {
 		{"id": "1.2.3.4", "value": "Ewdmb28uY29t"},
 		{"id": "1.2.3.5", "value": "Ewdmb28uY29t"},
 		{"id": "1.2.3.6", "value": "MA4TB2Zvby5jb20CAwHiQA=="},
-		{"id": "1.2.3.7", "value": "MQ4MB2Zvby5jb20CAwHiQA=="}
+		{"id": "1.2.3.7", "value": "MQwMB2Zvby5jb20BAf8="}
 	]
 }`),
 		}, false},
@@ -340,9 +340,11 @@ func Test_asn1Encode(t *testing.T) {
 		{"ok generalized", args{"generalized:" + now.Format(time.RFC3339)}, mustMarshal(t, now, "generalized"), false},
 		{"ok int", args{"int:1234"}, mustMarshal(t, 1234, ""), false},
 		{"ok numeric", args{"numeric:1234"}, mustMarshal(t, "1234", "numeric"), false},
+		{"ok bool", args{"bool:true"}, mustMarshal(t, true, ""), false},
 		{"ok raw", args{"raw:" + mustMarshal(t, 1234, "")}, mustMarshal(t, 1234, ""), false},
 		{"fail numeric", args{"numeric:not-a-number"}, "", true},
 		{"fail time", args{"utc:not-a-time"}, "", true},
+		{"fail bool", args{"bool:untrue"}, "", true},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -375,6 +377,7 @@ func Test_asn1Marshal(t *testing.T) {
 		{"ok time", args{now, nil}, mustMarshal(t, now, "utc"), false},
 		{"ok seq", args{[]any{"string", 1234}, nil}, mustMarshal(t, []any{"string", 1234}, ""), false},
 		{"ok set", args{[]any{"string", 1234}, []string{"set"}}, mustMarshal(t, []any{"string", 1234}, "set"), false},
+		{"ok bool", args{false, nil}, mustMarshal(t, false, ""), false},
 		{"fail numeric", args{"string", []string{"numeric"}}, "", true},
 	}
 	for _, tt := range tests {